Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

Readiness dashboard

New

A compliance officer’s inspection-readiness screen: a lollipop month chart, headline figures, a prompt, nested report discs, a score gauge and an upload funnel.

readiness-dashboard/readiness-dashboard.tsx
Live · interactive

Inspection readiness

Nearly readyIf an inspector came today: 9 gaps to close, none past a legal deadline

Share of deals cleared before transfer, by month

±0.0 pts

October cleared a higher share than September

Cleared before transfer

0.0%

+3.0 pts

41 of 43 deals in October

Overdue items

0

−3 vs Sep

Both are owner registers

Training completed

0 of 34

3 agents due

Refresher course, 2026

Reports filed

Prepared by Corvane, filed by the officer

  • All reports17
  • TTR65%11
  • STR24%4
  • Match reports12%2
Restricted cases1 opencount only

Readiness score

0/100

Up 4 points since September

Client upload funnel

27% of uploads need a second request. Expired IDs are the usual cause.

The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.

When to use

  • One question a whole screen answers, here “if an inspector came today, how ready are we?”, with the verdict at the top and the evidence under it.
  • A month-by-month measure that people read as a change: the big figure compares the lit month with the one before, in points for a share and in units for a count.
  • A few related measures behind tabs, sharing one chart and one set of filters (office, period, year), so switching keeps the reader’s place.
  • A screen that mixes kinds of evidence: a trend, headline figures, a score against a pass mark, a breakdown by kind and a step-by-step funnel.
  • Where the next question is open-ended: the prompt lets the reader ask the data in words, with /September pointing at a month.

When not to use

  • A single figure or a single chart. Use a stat card or the chart on its own, not a whole screen.
  • Data people need to sort, filter by many fields or export row by row. Use a table, such as the records table.
  • Live operations that change by the second: the figures here are period totals that count in once.
  • More than six measures, or measures with nothing in common: tabs over one chart only work when every tab reads on the same axis of time.
  • A screen the reader edits: nothing here is input but the filters, the search and the prompt. Use a form or a workspace.

You can add this component with the shadcn CLI or by hand:

Into components/readiness-dashboard of any React project, with no setup:

Terminal
npx shadcn@latest add https://brunodelorence.com/r/readiness-dashboard.json

Either way it needs React 19 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.

Usage

compliance-page.tsx
import { ReadinessDashboard } from './readiness-dashboard';export function CompliancePage() {  return (    <ReadinessDashboard      user={{ name: 'Mira', role: 'Compliance officer' }}      defaultScope="Whole firm"      onBuildPack={() => console.log('Building the pack')}      autoplay={false}    />  );}

Each state below is a live instance, locked so you can study it.

The pieces it is built from, each with its measurements read live.

If an inspector came today, how ready would the firm be? Tabs pick the measure, the office, Weekly / Monthly / Yearly and the year pick the figures, and hovering a month lights its value while the big change on the left compares it with the one before. Under it, a prompt that draws /tokens as chips, reports filed as nested discs, the readiness score on a wide gauge, and the client upload funnel with its own tooltip. Every chart is coded, in light and dark.

  • Every chart is HTML and CSS but the gauge, one small SVG. Values move by transforms only: a stem is a full-height line scaled from its foot, a dot, a bar or a tooltip a full-height box slid down by (1 − share) of its own height, a disc scaled from the point it stands on. A new series glides in column after column (a 28 ms stagger), and nothing is laid out again.
  • The lollipop chart is a radio group: hovering a column lights it (its back, a white chip and a value pill over the dot), clicking or the arrow keys pick it, and the change on the left compares the lit column with the one before, in points for a share and in units for a count. Months not on record yet are outlined and skipped.
  • The scale is fitted to each series: the lowest value stands 0.7 of the range up its column and the highest leaves half a range for its pill, so every tab reads like the summary.
  • The report discs stand on one point and are sized by the square root of their counts, so their areas compare; each count sits in the band between its disc’s top and the next one’s.
  • The gauge is a 378-unit arc whose centre lies under the card, so it runs out of both sides: grey up to the pass mark, blue up to the score, hatched beyond. The figures count up over 1.1 s the first time the dashboard is on screen, and the gauge with them.
  • The prompt’s field has transparent text over a copy that draws its /tokens as chips (their padding is a box-shadow, so the copy keeps the field’s exact metrics), so the caret and the selection stay the browser’s.
  • It lays itself out from its own width with container queries: the full screen from 1180 px; then the chart’s words over it and two cards a row; the figures card under the chart below 860 px; an icon rail below 700 px; and below 560 px a top bar with a menu button, the tabs scrolling sideways.
  • While on screen and untouched, a demo pointer hovers three months, switches to Weekly and back, opens Training, types a question into the prompt and walks the funnel and the discs. Any real input hands it over; it comes back after 9 s of quiet.
  • Every colour is a light-dark() token on the root: a near-black frame with cards a step lighter by night, a pale grey frame with white cards by day, the same periwinkle blue for the data and a pink family for the reports.
PropTypeDefaultDescription
defaultTab'summary' | 'deals' | 'reports' | 'screening' | 'training' | 'gaps''summary'The measure the chart shows at first.
defaultPeriod'weekly' | 'monthly' | 'yearly''monthly'Weekly, monthly or yearly figures at first.
defaultScope'Whole firm' | 'Harbour office' | 'Old Town office''Whole firm'The office the figures are for, at first.
user{ name: string; role: string }{ name: 'Mira', role: 'Compliance officer' }Who is signed in, top right.
onBuildPack() => void—Called when Build inspection pack is pressed; the button then shows its own progress.
onTabChange(tab: TabId) => void—Called when the reader picks a tab, with its id.
onPeriodChange(period: Period) => void—Called when the reader picks Weekly, Monthly or Yearly.
onScopeChange(scope: Scope) => void—Called when the reader picks an office.
defaultStateReadinessDashboardState—Opens in this moment, held still (no count-in, no rise): hover and picked (a month column lit or picked, by its index), year, open ('office' | 'notifications' | 'breakdown' | 'search' | 'menu', a popover or, on a phone, the menu), query (the search’s text), prompt (the prompt’s text), ask ('closed' | 'thinking' | 'answered'), pack ('building' | 'ready', held until pressed again), ring (the report disc lit: 0 all, 1 TTR, 2 STR, 3 match reports), funnel (a funnel step lit, 0–3), reportsPeriod and funnelMonth. Turns the demo off; a popover it opens ignores presses outside the dashboard until the dashboard is used.
autoplaybooleantrueWhile on screen and untouched, a demo pointer shows the main interactions. Any real input stops it.
colorScheme'light' | 'dark' | 'system'—Light or dark colours, or the reader’s; without it, the page’s color-scheme.
classNamestring—Extra class for the root.

The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.

readiness-dashboard.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';/* * Readiness dashboard: a compliance officer's screen for "if an inspector came today". An icon * rail, a search, the period controls and tabs over a month-by-month lollipop chart, a card of * headline figures, a prompt to ask the data a question, and three cards under it: reports filed * (nested discs), the readiness score (a gauge) and the client upload funnel. * * - Every chart is HTML and CSS but the gauge, one small SVG. Values move by transforms only: a *   stem is a full-height line scaled from its foot, a dot or a bar a full-height box slid down, *   a disc scaled from the point it stands on. A new series glides in, column after column. * - The tabs pick what the chart shows; the office, Weekly / Monthly / Yearly and the year pick *   which figures. Hovering a column lights it with its value; clicking or the arrow keys pick it, *   and the big change on the left compares it with the one before. * - The figures count up the first time the dashboard comes on screen (at once under reduced *   motion), and the stems, discs and bars rise from the floor. * - The prompt shows /tokens as chips: the field's own text is transparent over a copy of it that *   draws them, so the caret and the selection stay the browser's. * - While on screen and untouched, a demo pointer hovers the months, switches the period and a *   tab, types a question into the prompt and walks the funnel and the discs. Any real input *   hands it over to the reader; it starts again after a while. * - It lays itself out from its own width (container queries): the full screen from 1180 px, a
readiness-dashboard.module.css
/* Readiness dashboard (readiness-dashboard.tsx): a near-black app frame with a rail, cards a step   lighter, one periwinkle blue for the data and a pink family for the reports; by day a pale grey   frame with white cards and the same blue, a shade deeper.   Colours are the tokens below, light-dark(light, dark) where the two differ: the component takes   the color-scheme it inherits from the page (light unless the page says otherwise), or the one   its colorScheme prop sets (data-color-scheme).   Layout comes from the component's own width (container queries): the full screen from 1180 px,   then a narrower card of figures, two cards a row, an icon rail, and under 560 px a top bar with   a menu button and everything stacked.   Memory. Every value moves by a transform (stems scale, dots, bars and the funnel's pill slide,   discs scale), so changing a series never lays the page out; the demo pointer is one small layer. */.root {  --frame: #f3f3f5;  --panel: #fff;  --panel-2: #f5f5f7;  --control: #ececef;  --chip: #ececef;  --column: #ececf0;  --line: #e4e4e8;  --line-strong: #d6d6dc;
charts.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';/* * The dashboard's four charts, all HTML and CSS but the gauge (one small SVG): * * - LollipopChart: a stem and a dot per period. Each column is a radio of a group, so the arrows *   walk the months; hovering one lights it like the picked one, with its value in a pill over *   the dot. A stem is a full-height line scaled from its foot, a dot a full-height box slid down: *   both move by transforms, so a new series glides in without laying anything out. * - ReportRings: nested discs standing on one point, each sized by the square root of its count *   (so their areas compare), scaled from that point. * - ScoreGauge: a wide arc running out of its card: grey up to the pass mark, blue up to the *   score, hatched beyond. * - UploadFunnel: four steps as hatched bars, each a full-height box slid down; the picked or *   hovered step fills blue, with its count and share in a pill. */import { useId, type KeyboardEvent } from 'react';import {  FUNNEL_NOUNS,  FUNNEL_STEPS,  PASS_MARK,
picker.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';/* * A small select of the dashboard ("Whole firm", "Year 2026", "This year", "October"): a button * that opens a listbox under it. The list takes the focus while it is open: arrows, Home and End * move through it, Enter or Space picks, Escape closes it and gives the focus back to the button; * a press anywhere else or Tab closes it too. */import { useEffect, useId, useRef, useState, type KeyboardEvent } from 'react';import { icons } from './icons';import styles from './readiness-dashboard.module.css';export function Picker<T extends string>({  label,  value,  options,  onChange,  align = 'start',  part,  defaultOpen = false,}: {
data.ts
// Copyright (c) 2026 Bruno Delorence. MIT License./* * The dashboard's figures: an invented estate agency's compliance year, as its officer sees it. * Every series is written out or derived here, so the component only picks and draws. */export const PRODUCT = 'Corvane';export const MONTHS = [  'Jan',  'Feb',  'Mar',  'Apr',  'May',  'Jun',  'Jul',  'Aug',  'Sep',  'Oct',  'Nov',  'Dec',] as const;export const MONTH_NAMES = [
icons.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License./* * The dashboard's line icons, drawn on a 24-unit grid with round 1.6-unit strokes. They are * decorative (aria-hidden); their size comes from the stylesheet, their colour from currentColor. */import type { ReactNode } from 'react';function Icon({ children, className }: { children: ReactNode; className?: string }) {  return (    <svg      className={className}      viewBox="0 0 24 24"      fill="none"      stroke="currentColor"      strokeWidth="1.6"      strokeLinecap="round"      strokeLinejoin="round"      aria-hidden="true"      focusable="false"    >      {children}    </svg>
demo-pointer.tsx
'use client';import type { CSSProperties, Ref } from 'react';/* * The demo's pointer, and the steps that move it. A demo is a script of awaited steps: wait, or * glide the pointer somewhere. Stopping the demo (the reader takes over, or it scrolls away) ends * the step in progress at once: its promise rejects, which ends the script right there, and the * pointer fades out. * * Each glide is a curved flight played by the Web Animations API, so nothing runs frame by frame. * The points of the flight are handed to whoever wants to move along with it. The arrow and the hand are HTML boxes * that swap by their visibility: a change of style inside an SVG would lay the SVG out again. */export type Point = { x: number; y: number };/** A glide's path and timing, for an animation that plays along with it. */export type Flight = { points: Point[]; timing: KeyframeAnimationOptions };export type Demo = {  /** Resolves after `ms`. */  wait: (ms: number) => Promise<void>;  /** Glides the pointer to `to`, in the stage's own px, in `ms`; `along` gets the flight. */
use-autoplay.ts
'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) {  const ref = useRef<T>(null);  const [visible, setVisible] = useState(false);  const [reduced, setReduced] = useState(true);  const [taken, setTaken] = useState(false);  const timer = useRef(0);  useEffect(() => {    const element = ref.current;    if (!element) return;    let onScreen = false;    const update = () => setVisible(onScreen && !document.hidden);    const observer = new IntersectionObserver(      ([entry]) => {        onScreen = entry.isIntersecting;
  • Real landmarks and names: the rail is a navigation with aria-current on its section, the page a main with a heading, each card a section named by its title. On a phone the rail’s menu button is a disclosure.
  • The measures are tabs (arrows, Home and End) over a tab panel. The chart’s months and the funnel’s steps are radio groups with one tab stop, each column named with its value (“October: 95.3%”); months not on record yet are skipped.
  • The office, year, reports and funnel selects are listboxes opened from a button: arrows, Home, End, Enter and Escape, with the focus returned to the button. The search is a combobox with its results as a listbox.
  • Notifications, the score’s breakdown and the menu close on Escape or a press elsewhere. Period changes, a search pick, the pack’s progress and the prompt’s answer are announced in a polite live region.
  • The discs, the gauge and the dot grid are drawn for the eye; their figures are in the legend, the score and the grid’s label. Under reduced motion nothing counts or rises and the demo never plays.