v1.0

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

PropTypeDefaultDescription
counts*Record<string, number>—