StatusBar
Preview
Basic
Loading…
Preview
Code
ts
import StatusBar from '@/components/data/StatusBar';src/components/data/StatusBar.tsx
AI prompt
text
Build a status-distribution bar (a single stacked bar with a legend) component in React + TypeScript + Tailwind CSS. Plain divs, no chart library: there is one dimension here, and a stacked bar reads it at a glance.
## Look
- A house panel with `p-5 space-y-3`.
- Header row (`flex items-baseline justify-between`): the title "Queue by status" (10px semibold uppercase wide-tracking slate-500 / dark slate-400), and on the right the total in 10px slate-400 / dark slate-500: "1 request" or "N requests".
- The bar: `flex h-2 w-full overflow-hidden rounded-full` on a slate-100 / dark slate-800 track. One segment per status with a non-zero count, its width `count / total * 100%`, filled with the status colour. Zero-count statuses draw no segment.
- The legend below: `grid grid-cols-2 sm:grid-cols-4 gap-x-4 gap-y-1.5`. Each item is `flex items-center gap-1.5 text-[11px]`: a 6px dot, the label in slate-500 / dark slate-400 (truncating), and the count pushed right (`ml-auto`) in semibold tabular slate-700 / dark slate-200.
## Behaviour
- Four statuses in a fixed lifecycle order, whatever order the counts arrive in: Pending (amber-500), Submitted (violet-500), Approved (emerald-500), Rejected (rose-500). A missing key counts as 0, and keys that are not one of the four are ignored.
- Hover titles: a segment shows "Approved: 1204". A legend item shows the status's one-line hint, e.g. Pending: "Raised here and waiting for release. Nothing has been sent yet."
- Total 0: the bar is replaced by a 12px slate-400 paragraph, "Nothing in the queue. Statuses appear here as requests move through the lifecycle." The legend still shows, with all zeros.
## API
`counts: Record<string, number>`, keyed by status label ("Pending", "Submitted", "Approved", "Rejected").
## Demo
`{ Pending: 12, Submitted: 42, Approved: 1204, Rejected: 18 }`.
## House style (applies to everything above)
- Stack: React 19 + TypeScript + Tailwind CSS v4, icons from lucide-react. One self-contained file; default-export the component and named-export its types. `'use client'` if it has state, refs or handlers.
- Font Inter; palette indigo on slate. Primary accent indigo-600 (hover indigo-700, dark mode indigo-400). Body text slate-700 / dark slate-200; secondary slate-500 / dark slate-400.
- Dark mode is a `.dark` class on <html> (not prefers-color-scheme). Every colour needs its `dark:` pair.
- Compact admin scale: text-xs (12px) for controls and body, 10–11px for meta, rounded-lg (8px) controls, rounded-2xl (16px) cards.
- Card surface ("panel"): `bg-white/60 dark:bg-slate-800/60 backdrop-blur-xl border border-white/60 dark:border-slate-700/60 rounded-2xl shadow-lg`, on a soft slate gradient page background.
- Floating surfaces (dropdowns, popovers, menus) are OPAQUE: `bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-2xl shadow-lg`, no backdrop blur (it creates a stacking context that traps the popover's z-index). In-flow popovers are z-50; portalled overlays z-200.
- Text inputs and select triggers: `w-full px-3 py-2 text-xs rounded-lg border border-slate-300 dark:border-slate-700 bg-white/80 dark:bg-slate-900/60 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500/40 focus:border-indigo-500`.
- Field labels: 11px semibold slate-600. Section titles: 10px semibold uppercase wide-tracking slate-500.
- Primary button: indigo-600 fill, white 12px semibold text, rounded-lg, px-3 py-2, disabled at 50% opacity. Ghost button: slate-600 text, hover slate-100.
- Popovers close on outside click AND on Escape (listen to both; include the portalled panel's element in the outside-click check).
- Don't nest scroll containers around popovers: an ancestor with overflow hidden/auto clips an absolutely-positioned dropdown. Portal the panel to <body> when it must escape a scroller, and reposition it on scroll and resize.
- Accessible by default: visible focus rings, keyboard support that matches the WAI-ARIA pattern for the widget, `aria-label` on icon-only buttons, `min-w-0` so text truncates instead of overflowing.Source
tsx
/* Origin: bonus-adjustment (96S2), verbatim. */
import { RequestStatus, displayFor } from '@/lib/status';
/**
* The four-state lifecycle as one proportional bar plus a legend.
*
* A bar rather than a chart on purpose: there is one dimension here (how the
* open queue is distributed), and a stacked bar reads it at a glance without
* pulling a charting library into the bundle for a single figure.
*/
export default function StatusBar({ counts }: { counts: Record<string, number> }) {
const statuses = Object.values(RequestStatus);
const total = statuses.reduce((sum, s) => sum + (counts[s] ?? 0), 0);
return (
<div className="panel p-5 space-y-3">
<div className="flex items-baseline justify-between">
<h2 className="panel-title">Queue by status</h2>
<span className="text-[10px] text-slate-400 dark:text-slate-500">
{total} request{total === 1 ? '' : 's'}
</span>
</div>
{total === 0 ? (
<p className="text-xs text-slate-400 dark:text-slate-500">
Nothing in the queue. Statuses appear here as requests move through the lifecycle.
</p>
) : (
<div className="flex h-2 w-full overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800">
{statuses.map((status) => {
const count = counts[status] ?? 0;
if (count === 0) return null;
return (
<div
key={status}
title={`${displayFor(status).label}: ${count}`}
style={{ width: `${(count / total) * 100}%` }}
className={displayFor(status).dot}
/>
);
})}
</div>
)}
<ul className="grid grid-cols-2 sm:grid-cols-4 gap-x-4 gap-y-1.5">
{statuses.map((status) => {
const display = displayFor(status);
const count = counts[status] ?? 0;
return (
<li key={status} className="flex items-center gap-1.5 text-[11px]" title={display.hint}>
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${display.dot}`} />
<span className="text-slate-500 dark:text-slate-400 truncate">{display.label}</span>
<span className="ml-auto font-semibold tabular-nums text-slate-700 dark:text-slate-200">
{count}
</span>
</li>
);
})}
</ul>
</div>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
counts* | Record<string, number> | — |