Charts

Release Readiness

A four-state go/no-go board that rolls per-workstream checks, sign-offs and open blockers into one derived verdict, leads with the single number that reaches zero exactly when the release is clear, and refuses to call an unread board.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import {
  buildReleaseReadinessRollup,
  countOf,
  inspectReleaseReadinessData,
  joinList,
  readReleaseCutoff,
  type ChartReleaseReadinessData,
  type ReleaseMixKey,
  type ReleaseReadinessOrder,

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/chart-release-readiness.json

Prompt

Build a React + TypeScript + Tailwind "ChartReleaseReadiness" card (plain
HTML/CSS layout, no chart library) with zod. It answers one question — can we
ship? — and the whole design rests on a single rule: THE VERDICT IS DERIVED.
There is no verdict field on the payload and no prop that sets it. A card that
can be told "Go" independently of the rows printed under it will eventually
disagree with them, and the row is what a reader believes.

Contract
- One zod schema is the source of truth:
  { status: "loading" | "empty" | "error" | "ready"; title: string;
    caption?: string; message?: string;
    workstreams: {
      id: string; label: string;
      checks: { passed: number; failed: number; waiting: number };
      signOff: { state: "signed" | "pending" | "declined" | "unassigned";
                 owner?: string; role?: string; note?: string };
      blockers: { id: string; title: string;
                  severity: "critical" | "major" | "minor" }[];
    }[];
    cutoff?: string; asOf?: string; timeZone?: string }.
- `waiting` is not "skipped" and not "passed": it is a check that has not
  reported yet. Say so in the schema doc comment. Folding it into either
  neighbour is how a board talks a team into shipping on evidence it never
  received.
- `declined` is not `pending`: a refusal is a known bad answer, a missing
  signature is only an absence. `unassigned` is an outstanding signature with
  nobody named, which is worse to discover late.
- Only OPEN blockers belong in the array. A closed blocker is history, not a
  fact about readiness.
- `cutoff` and `asOf` are ISO instants and `timeZone` is the IANA zone they are
  read in (default "UTC"). Never read the clock inside the component: a card
  that calls Date.now() renders one number on the server and another in the
  browser, and stops being reproducible the moment anyone screenshots it.
- Component props = z.infer of the schema plus heroMetric ("open" | "cleared" |
  "pass-rate", default "open"), order ("worst-first" | "given", default
  "worst-first"), showMix (default true), onSelectWorkstream, onRetry,
  className and the native div props through forwardRef.
- Ship a pure module beside the schema: inspectReleaseReadinessData() for the
  structural pass, apportionPercent(), readReleaseCutoff() and
  buildReleaseReadinessRollup() returning per-row readings, totals, the mix,
  the deciding row and the verdict.

Behavior — the rule, which is the product
- Split every outstanding thing into two buckets. KNOWN BAD: failed checks,
  critical blockers, refused signatures. NOT IN YET: waiting checks, non-
  critical open blockers, missing or unassigned signatures.
- Verdict: no-go the moment the known-bad bucket is non-empty; hold when it is
  empty but the not-in-yet bucket is not; go only when both are empty. Apply
  the same split per row to give each workstream a state (blocked / waiting /
  clear) so the verdict is visibly the roll-up of the rows, not a parallel
  opinion.
- AN EMPTY BOARD IS NOT A GO. status="ready" with zero workstreams falls
  through to the zero state. Nothing assessed is not the same as nothing
  wrong, and a green light on an empty board is the worst thing this card
  could print.
- A BOARD THAT COULD NOT BE READ GETS NO VERDICT. Duplicate workstream ids,
  duplicate blocker ids inside one workstream, negative or non-finite counts:
  refuse in the contract and render the reason in the error branch. Every
  number here is a sum over ids, so a repeat is counted twice.
- Lead with ONE number, and pick the one that reaches zero exactly when the
  verdict turns green: the count of open items (known bad + not in yet). The
  big number and the verdict then say the same thing twice, which is what
  makes the card readable in a second.
- The deciding row is the worst row under one total order (state, then failing
  count, then criticals, then open items, then payload position). That order is
  also the default display order, so the row the verdict rests on is the top
  row, and the readout starts on it before anyone touches the card.
- Guard every denominator. A workstream with no automated gate at all is legal
  (0 of 0 checks) and must print "no checks configured" rather than NaN;
  heroMetric="pass-rate" on a board with no checks anywhere prints an em dash,
  never 0% or 100%.
- The four states are first-class branches of one bg-card panel: a pulsing
  silhouette of the ready layout (aria-hidden, plus an sr-only role="status"),
  the zero state, an error state carrying either the transport message or the
  contract issue plus a "Try again" button only when onRetry exists, and ready.

Rendering & styling
- Hierarchy comes from size and weight, not from boxing: one card, generous
  whitespace, a text-6xl tabular-nums numeral, a text-sm label under it, a
  text-xs muted caption under that, and the verdict as the single saturated
  focal element on the row.
- Three tones, one meaning each, used identically by the mix bar, the row bars,
  the row dots and the verdict pill: passed var(--chart-2), waiting
  var(--chart-3), failed var(--destructive). Colour is never the only
  encoding — every slice carries its count, every row carries its state as a
  word, and the verdict is a word before it is a colour.
- The mix bar's segments are flex items growing in proportion to their count,
  so they always fill the track exactly; per cent widths would inherit the
  rounding of the printed labels and leave a sliver of rail showing. A
  non-empty slice carries a minimum width so a single failure among hundreds
  survives on a narrow card.
- PER CENTS THAT PARTITION A WHOLE ARE APPORTIONED BY LARGEST REMAINDER: floor
  every exact share, hand the leftover points to the largest fractional parts,
  ties to the bigger count then the earlier key. Rounding each slice on its own
  prints 99 or 101. One deliberate departure: a slice holding at least one item
  never prints 0% — it is floored to 1% and the point comes off the largest
  slice, because "0% failed" beside a red segment is the one number a reader
  must not be able to dismiss. Say so in the footnote.
- DATES: format the cutoff from the same epoch value the countdown is measured
  on, with an explicit timeZone. Never slice the ISO string — an instant late
  in the UTC day is the next day in Tokyo and the previous day in Los Angeles,
  and the board would name a date the signers do not recognise. Measure the
  countdown as a difference of CIVIL DAYS: read the calendar fields in the zone
  and re-pack them through Date.UTC, whose differences are exact multiples of a
  day. Dividing raw millisecond gaps by 86400000 loses a day across a clock
  change and rounds "tomorrow morning" down to zero.
- Two row signals, two meanings, both visible: a muted wash says "the readout
  is reading this row" (pointer, focus or the deciding row), a left bar says
  "you pinned it" — the visible twin of aria-pressed. Draw the bar transparent
  in every other state so pinning cannot nudge the row sideways.
- Accessibility: rows are real buttons with aria-pressed for the pin. Their
  aria-label is built by concatenating the row's own visible strings and then
  adding detail, so the accessible name always CONTAINS the visible label
  (WCAG 2.5.3 Label in Name). Tab reaches every row; with one focused, ArrowUp
  / ArrowDown walk the list and Home / End jump to the ends, and only those
  keys are preventDefault-ed. The readout box is aria-hidden — it is the
  pointer's visible echo and every sentence in it is already a row's accessible
  name. Below everything, an sr-only WRAPPER DIV holds a real table (one row
  per workstream, with what stands in the way spelled out) plus a summary
  paragraph naming the verdict, the split and the deciding row. Put sr-only on
  the wrapper, never on the table: CSS width is only a lower bound for a table
  box, so width:1px does not hold one back.
- Motion: the only animation is the skeleton pulse and a colour transition on
  hover, both with motion-reduce variants. Nothing about reading the card
  depends on motion, and there are no timers, observers or rAF loops to leak.
- The readout reserves three lines whether or not the third is filled, so the
  footnote under it does not move as the pointer crosses the list.

Customization levers
- heroMetric picks the one big number: "open" (default, reaches 0 exactly when
  the verdict turns green), "cleared" (workstreams cleared, for a board whose
  unit is teams rather than items), "pass-rate" (the passed slice of the mix —
  the same whole per cent the legend prints, so the two cannot disagree).
- order="given" keeps the payload's sequence for boards where the order is
  itself meaningful, such as the stages of a release train; the default
  "worst-first" is the order a go/no-go meeting reads in.
- showMix={false} drops the release-wide bar and its legend for a dense tile;
  the per-row bars and every count stay.
- The bucket rule is the one thing worth re-pointing per organisation: move
  major blockers into the known-bad bucket for a stricter gate, or let an
  unassigned signature merely hold rather than block. Change it in the rollup,
  never per render, so the rows and the verdict cannot drift apart.
- Tone tokens: re-point passed / waiting / failed to any three semantic tokens;
  keep one meaning per tone, since the same three colour the bar, the rows and
  the verdict.
- Density: drop the mix legend first, the footnote second, the readout last —
  the readout is what makes the list explorable rather than merely printed.
- onSelectWorkstream fires alongside the card's own pin/unpin, for drilling
  into that workstream's checks in a panel beside the card. The pin is local
  and real on its own, so nothing breaks when the callback is omitted.

Concepts

  • Derived verdict — go / hold / no-go is recomputed from the rows on every render and cannot be supplied. The moment a verdict is a field, some code path sets it without touching the evidence, and the card starts printing a decision its own rows contradict. The corollary is that removing a workstream can change the answer, which is exactly what a reader expects a readiness board to do.
  • Blocking versus outstanding — the split that makes a hold different from a no-go. A failed check, a critical blocker and a refused signature are known bad answers; a waiting check, a lesser blocker and a missing signature are absences. Collapsing the two loses the only distinction a release manager acts on: whether to fix something or to chase someone.
  • Empty is not clear — a board with no workstreams renders its zero state instead of a green light. Both look like "nothing wrong" from the maths, and only one of them is; the zero state is the card refusing to let an unpopulated feed read as an approval.
  • Deciding row — one total order (state, then failures, then criticals, then open items, then payload position) picks the single row the verdict rests on. It is also the default sort, so the top row is the answer's cause, and the readout opens on it before anyone points at anything — the card explains itself untouched.
  • Pin, hover, focus — the readout listens to three sources with a fixed precedence: a pinned row wins, then the pointer, then keyboard focus, then the deciding row. A pin survives the pointer leaving, so a row can be held while the eye walks the rest of the list.
  • Civil-day countdown — "days left" is a difference of calendar days read in the release team's zone, not a millisecond gap divided by 86 400 000. The second form loses a day whenever the window crosses a clock change and rounds "tomorrow morning" down to zero, and the printed date comes from the same instant so the two can never disagree.

On This Page