PVR Tech Studio

Widgets

A library of 20+ ready-made dashboard widgets — KPI tiles, chart cards, feeds, heroes, and console-style sets — all token-driven and importable one-by-one.

10 min read
Updated July 15, 2026

The Widgets showcase (/widgets, sidebar Components → Widget, lazy-loaded) is a modern, interactive gallery of every widget in src/components/widgets/, arranged in labelled sections: Welcome heroes (three variants), a KPI tile gallery, Chart cards (donut, gauge, breakdown, area trend, stat list, weekly bars), Overview panels (report panel + mini calendar), Lists & activity, and Cloud & infrastructure (Google Cloud and Firebase console-style sets).

Every widget is fully data-driven via props — the page passes demo data, but the components carry no sample content of their own. Widgets are pure presentational cards: they render inside the shared Card (or their own token-styled shell), wrap themselves in a staggerItem motion variant so a surrounding Stagger grid cascades them in, and stay reduced-motion safe throughout.

Three cross-cutting rules apply everywhere:

Colors are tokens only

DOM/SVG widgets use semantic utilities (bg-primary, text-success, …) or currentColor; the canvas charts (chart.js) can't read CSS variables, so they get their colors from useChartTokens(), which reads the raw --* vars and re-reads on theme/skin change.

User-facing text is i18n keys

The page's labels live in the widgets namespace (214 keys, en + ja at parity); the components themselves take already-translated strings as props (only WidgetMenu translates its own items).

Widget headers are borderless

WidgetHeader sits inside the card's own padding with no divider band — the modern portlet look — and renders the WidgetMenu by default (menu={false} opts out).

Architecture & files

FileResponsibility
src/pages/ui/WidgetsPage.tsxThe /widgets gallery — composes every widget with demo data and i18n'd section headings.
src/components/widgets/index.tsThe barrel — import every widget and its exported row types from @/components/widgets.
src/components/widgets/primitives/WidgetHeader.tsxBorderless card header (title · subtitle · optional icon · action slot · ⋯ menu by default).
src/components/widgets/primitives/WidgetMenu.tsxThe ⋯ overflow Dropdown — presentational demo actions that fire toasts.
src/components/widgets/primitives/WidgetSection.tsxLabelled page section — icon-chip heading + description + a Stagger responsive grid.
src/components/widgets/primitives/TrendPill.tsxThe up/down percentage chip (sign drives arrow + success/danger tint).
src/components/widgets/primitives/Sparkline.tsxDependency-free SVG sparkline with an animated left→right clip-rect reveal; currentColor.
src/components/widgets/primitives/MiniBarChart.tsxWeekly highlighted bar chart (react-chartjs-2 Bar) — one solid bar, the rest ~35% alpha.
src/components/widgets/KpiTile.tsxThe KPI stat tile with eight variant styles.
src/components/widgets/DonutWidget.tsxchart.js Doughnut with a centered animated total, share rows, footer stats.
src/components/widgets/GaugeWidget.tsxSegmented half-doughnut gauge (chart.js) + tone banner + sub-score bars + bottom stat tiles.
src/components/widgets/AreaTrendWidget.tsxRecharts area chart with a period Dropdown, headline value, breakdown rows, footer stats.
src/components/widgets/BarStatWidget.tsxHeadline value + trend over a MiniBarChart with locale-aware weekday labels.
src/components/widgets/BreakdownWidget.tsxStacked proportion bar + legend + optional 12-bar activity strip + footer stats.
src/components/widgets/StatListChartWidget.tsxIcon stat rows over a trend sparkline, with footer stats.
src/components/widgets/ActivityFeedWidget.tsxActivity feed — colored icon rows with title, detail, timestamp.
src/components/widgets/NotificationsWidget.tsxAvatar notification rows with unread dots and a count Badge.
src/components/widgets/TimelineWidget.tsxVertical timeline — time, colored node, title + description.
src/components/widgets/PeopleListWidget.tsxPeople list — avatar + presence, badge, count/rating meta.
src/components/widgets/WelcomeWidget.tsxGreeting hero in three variants (gradient/panel/lines) sharing one vertical layout.
src/components/widgets/ReportPanelWidget.tsxRich full-width report — headline revenue, featured stats, highlight chips, team bars, goal footer.
src/components/widgets/MiniCalendarWidget.tsxMonth mini-calendar (reuses the calendar app's MiniCalendar) + upcoming-events list.
src/components/widgets/gcp/* (hues.ts + 4 cards)Google-Cloud-console set: service tile, quota meter, billing card, status list.
src/components/widgets/firebase/* (2 cards)Firebase-console set: metric tile + plan-usage card with a primary-gradient header.
src/hooks/useChartTokens.tsReads the raw token vars for canvas charts; builds the categorical series palette.

The route is registered lazily in src/routes.tsx:

const WidgetsPage = lazy(() => import('@/pages/ui/WidgetsPage').then((m) => ({default: m.WidgetsPage})))
// …
{path: '/widgets', element: <WidgetsPage />},

Usage

Import from the barrel and drop widgets into a Stagger grid (each widget carries its own staggerItem variant, so the grid cascades them in automatically):

import {KpiTile, DonutWidget, WidgetSection} from '@/components/widgets'
import {DollarSign} from 'lucide-react'
 
<WidgetSection title={t('chartsTitle')} subtitle={t('chartsSubtitle')} gridClassName="lg:grid-cols-3">
    <KpiTile variant="flat" tone="primary" icon={DollarSign} label={t('mRevenue')} value={48250} prefix="$" delta={12.4} spark={[12, 18, 14, 22, 19, 27, 24, 31]} />
    <DonutWidget
        title={t('donutTitle')}
        subtitle={t('donutSubtitle')}
        total={4820}
        totalLabel={t('donutTotal')}
        segments={[{label: t('segCity'), value: 2100}, {label: t('segAirport'), value: 1450}]}
    />
</WidgetSection>

WidgetSection provides the heading and the Stagger grid. All widget text arrives as props — pass t(…) results, never literals.

API / Props

All widgets share the pattern: an outer motion.div with variants={staggerItem} and h-full, a Card/token-styled shell, a WidgetHeader, and (where a footer exists) an mt-auto bottom-anchored stat row. Tones are the five semantic keys primary | success | warning | danger | info unless noted.

Primitives

WidgetHeader

PropTypeDefaultDescription
titleReactNodeThe card heading (truncated).
subtitleReactNodeSmall descriptor under the title.
actionReactNodeRight-aligned slot (filter, badge, "view all"…), shown before the ⋯ menu.
menubooleantrueRender the WidgetMenu ⋯ overflow. Pass menu={false} to opt out.
iconReactNodeLeading node before the title (e.g. a 3D illustration <img>).
toneToneWhen set, the title renders as an uppercase tinted label (the "engage" look).
classNamestringExtra classes on the header row.

WidgetMenu takes no props — it is the demo ⋯ Dropdown (Refresh / Export / View details / Fullscreen / Hide) whose items fire an info toast. Replace it with a real menu by passing your own action and menu={false} to WidgetHeader.

WidgetSectiontitle: string, subtitle: string, icon?: ReactNode (rendered in a primary-tinted chip), gridClassName?: string, children.

TrendPillvalue: number (signed percentage; sign drives the arrow and the success/danger tint), suffix?: string, className?.

Sparkline

PropTypeDefaultDescription
datanumber[]The series (auto-scaled to min/max).
variant'line' | 'area' | 'bar''area'Smoothed Catmull-Rom curve (line/area) or baseline-growing bars.
toneTone'primary'Sets the text-color class; the SVG draws with currentColor.
width / heightnumber120 / 40SVG viewBox only — the element scales to its container.

The line/area draw-in is an animated clip rect whose width grows from x=0 — strictly left→right, robust under preserveAspectRatio="none". Under reduced motion the full curve renders immediately.

MiniBarChartdata: number[], labels: string[], highlightIndex: number (the one bar drawn at full opacity; the rest at ~35% alpha), tone? (default 'primary'). A chart.js Bar with a staggered grow-in, disabled under reduced motion; colors from useChartTokens().

KPI tiles

KpiTile (KpiTileProps, exported)

PropTypeDefaultDescription
labelstringMetric name.
valuenumberCount-up headline via AnimatedNumber.
iconLucideIconMetric icon.
variantKpiVariant'flat'flat | minimal | gradient | glass | outline | chip | backgroundIcon | flip.
toneTone'primary'Accent tone for icon chip / gradient / dot.
deltanumberRenders a TrendPill.
hintstringTrendPill suffix (e.g. "vs last week").
sparknumber[]Renders an inline Sparkline (variant-dependent placement).
prefix / suffix / decimalsstring / string / numberPassed through to AnimatedNumber.
tintbooleanfalseSoft-tinted card surface — flat variant only.

Solid-fill variants (gradient, the flip back face) use the text-*-foreground token map — never text-white — so contrast holds on skins with dark foregrounds (e.g. Amber). flip is a CSS 3D hover-flip.

Chart cards

  • DonutWidgettitle, subtitle, total: number, totalLabel, prefix?, segments: DonutSegment[] ({label, value} — colored by cycling useChartTokens().series), footer?, tint?, icon?. chart.js Doughnut, cutout: '68%', centered AnimatedNumber total.
  • GaugeWidgettitle, subtitle, value (0–100), valueLabel, tone?: 'primary' | 'success' | 'warning' | 'info', segments? (arc tick count, default 15), bannerIcon, bannerText, stats, breakdown?. A chart.js half-doughnut of rounded ticks.
  • AreaTrendWidgettitle, subtitle, calloutLabel, periods: TrendPeriod[] (a period Dropdown switches the chart and headline), breakdownLabel?, breakdown?, footer?. The one Recharts widget; colors from useChartTokens().
  • BarStatWidgettitle, subtitle, value: string, delta: number, data: number[] (7 values), highlightIndex, tone?, breakdown?, stats?. Weekday labels via Intl — no i18n keys.
  • BreakdownWidgettitle, subtitle, rows: BreakdownRow[] (tone includes neutral), total?, unit?, delta?, activity?: number[], activityLabel?, footer?, tint?, icon?. DOM/token-only.
  • StatListChartWidgettitle, subtitle, rows: StatRow[], spark: number[], sparkTone?, sparkLabel?, footer?.

Lists & feeds

ComponentKey props
ActivityFeedWidgettitle, subtitle, items: ActivityItem[] ({icon, tone, title, text, time}), action?.
NotificationsWidgettitle, subtitle, items: NotificationItem[] ({name, avatar?, text, time, unread?}), count? (header Badge).
TimelineWidgettitle, subtitle, items: TimelineItem[] ({time, tone, title, desc}).
PeopleListWidgettitle, subtitle, people: Person[] ({name, avatar?, status?, subtitle, badge?, time?, count?, rating?}), action?, compact?, unit?.

Composite widgets

  • WelcomeWidgetgreeting, name, message, ctaLabel, onCta?, avatar?, variant?: 'gradient' | 'panel' | 'lines' (default gradient), dateLabel?, stats?. All three variants share one vertical layout (chip · avatar+greeting · message · stats · CTA) for consistent spacing and equal height. lines layers a HeroCanvas variant="lines" under a surface gradient scrim.
  • ReportPanelWidgettitle, subtitle, revenueLabel, revenue: number, revenuePrefix?, revenueDelta, compareLabel, stats: ReportStat[], highlights?, teamsLabel, teams: TeamBar[] (ranked Progress bars), goalLabel? + goalPercent? + goalCaption?.
  • MiniCalendarWidgettitle, subtitle, events: CalEvent[] ({date, title, time, tone} — dates dot the calendar, the first 3 list as "upcoming"), upcomingLabel?. Reuses the calendar app's MiniCalendar with the active i18n.language.

Google Cloud & Firebase sets

Accents in these sets use the GcpHue = 'primary' | 'info' type — see the skin rule.

ComponentKey props
GcpServiceTileicon, name, hue: GcpHue, value: number, unit, statusLabel, healthy? (drives the primary/info status dot).
GcpUsageMetertitle, subtitle, rows: QuotaRow[] ({label, used, limit, unit, hue} meters), footer?.
GcpBillingCardlabel, amount: number, delta, projectLabel + project, creditLabel + credit, breakdown? (bars cycle primary → info), forecast?.
GcpStatusCardtitle, subtitle, services: ServiceStatus[] ({name, region, health: 'ok' | 'warn' | 'down', healthLabel}ok/warn map to primary/info; only down keeps danger red).
FirebaseMetricTileicon, name, value: number, unit?, delta?, prefix?.
FirebaseUsageCardtitle, planLabel, rows: FirebaseUsageRow[], remainingLabel?, footer?. Primary-gradient header with text-primary-foreground.

Configuration & customization

The equal-height flex-fill chain

Grid cells stretch, but card content top-aligns — so a short card in a row of tall ones would show a dead gap at the bottom. Every widget therefore builds a flex-fill chain and bottom-anchors its footer:

<motion.div variants={staggerItem} className="h-full">   {/* the grid cell fills */}
    <Card className="flex h-full flex-col">              {/* the card fills the cell */}
        <CardBody className="flex flex-1 flex-col">      {/* the body absorbs the height */}
            <WidgetHeader />
            {/* …content; an inner chart area can take flex-1 too… */}
            <div className="mt-auto grid … border-t border-border pt-4">{/* footer pins to the bottom */}</div>
        </CardBody>
    </Card>
</motion.div>

Follow this chain in any new widget: h-full wrapper → Card flex h-full flex-colCardBody flex flex-1 flex-colmt-auto on the footer/stat row. Where a card still reads short, add real content rather than leaving whitespace — that's why most widgets accept optional footer/breakdown/activity props.

Chart colors & useChartTokens()

The chart.js widgets (DonutWidget, GaugeWidget, MiniBarChart/BarStatWidget) render to a <canvas>, which cannot resolve CSS variables — so they call useChartTokens(), which reads the raw --primary/--info/--foreground/… values off <html> and re-reads via a MutationObserver on class/data-skin/data-oled. Every canvas chart therefore re-colors live on theme/skin switch. The Recharts AreaTrendWidget uses the same hook. The SVG Sparkline doesn't need it — it draws with currentColor. Canvas animations are all gated by useReducedMotion(). See Charts for the full useChartTokens story.

The skin rule

Color skins override only --primary and --infonot --success/--warning/--danger, which stay semantic. So:

  • Brand/decorative accents that should re-skin use only primary and info — the GcpHue type, the alternating breakdown dots, the Firebase gradient. GcpStatusCard maps ok/warn to primary/info; only a hard down keeps danger red.
  • True status meaning (TrendPill up/down, health down, warning banners) keeps success/warning/danger.
  • The multi-hue categorical palette (useChartTokens().series, used by DonutWidget) prefers a skin-provided --c1..--c6 palette (the Prism skin defines one); otherwise it anchors on the exact primary and hue-rotates around it so it stays distinct on every skin and in dark mode.

Troubleshooting

SymptomCause / fix
A chart keeps its old colors after a theme/skin switchThe color was hardcoded instead of read from useChartTokens() — literals never update.
Cards in a row have uneven heights / bottom gapsA link in the flex-fill chain is missing, or the row isn't a stretching grid.
Widgets don't animate inThey must sit inside a Stagger (e.g. via WidgetSection). Reduced motion also disables entrances.
The sparkline reveals from the center or renders partiallyKeep the animated clip-rect reveal — a stroke-dash pathLength breaks under preserveAspectRatio="none".
The ⋯ menu shows where it doesn't belongWidgetHeader renders WidgetMenu by default — pass menu={false}.
A GCP/Firebase accent doesn't change with the skinIt used success/warning/danger — skins don't override those. Use primary/info (GcpHue).

FAQ

Are these the same as StatCard or Panel? No. StatCard is a single dashboard stat primitive and Panel is the general collapsible portlet. The widgets library is a catalog of purpose-built dashboard cards with their own borderless WidgetHeader.

Which chart library do they use? chart.js (react-chartjs-2) for the donut, gauge, and weekly bars, plus Recharts for AreaTrendWidget and a dependency-free SVG Sparkline.

Do widgets fetch data? No — they are pure presentational components. Pass values from your API layer or state.

Can I use a widget outside the showcase? Yes — everything is exported from @/components/widgets. The home dashboards compose similar patterns.

Was this page helpful?