import * as React from "react" import { Circle, Minus, Triangle } from "lucide-react" import { cn } from "../../lib/utils" import { statusLabel, statusTone, type StatusTone } from "../../utils/fhirStatusVocabulary" import { Badge } from "./badge" // ------------------------------------------------------------------------------------------------ // / design decision D5 - a FHIR status, said once, in the product's voice, and encoded TWICE. // ------------------------------------------------------------------------------------------------ // THE SHAPE IS NOT DECORATION. D5: *"Colour alone fails for ~8% of men and this is a clinical // product."* A red dot and a green dot are the same dot to a deuteranopic reader, so every tone carries a // distinct SILHOUETTE as well as a distinct colour - filled circle - triangle - hollow ring - bar. Remove // the shape and this component stops doing the only thing it was built for. // // THE WORDS AND THE TONE BOTH COME FROM THE SHARED VOCABULARY, never from the caller. A surface that // passes its own label is how the product ends up with three words for one state - the exact failure card // 248 was raised about. `utils/fhirStatusVocabulary.ts` // // IT RENDERS NOTHING FOR A CODE WE HAVE NO WORDS FOR. `statusLabel` returns `undefined` there, and an // unmapped code must not reach a patient wearing the shape of a deliberate label - that is the original // defect (`Encounter [finished]`) arriving back through a fallback. Returning `null` makes the omission // the caller's problem at the call site, where it can be seen, rather than a silent bad label on screen. // - do not dress up "we don't know" as an answer. // // DO NOT add a `variant` or a `color` prop. The tone is derived from the status precisely so two // surfaces cannot disagree about how serious the same state is. // ------------------------------------------------------------------------------------------------ const TONE_PRESENTATION: Readonly> = { // Blue, the same role the product already uses for "this is live and normal" - not green, which would // read as an outcome on a thing that has not finished. live: { className: "text-primary", Icon: Circle, fill: true }, // amber-700 (`--warning`), chosen in index.css for its 5.02:1 contrast rather than its brightness. attention: { className: "text-warning", Icon: Triangle, fill: true }, // Deliberately MUTED. A visit that completed normally is the unremarkable case; drawing it as // loudly as a cancellation is how the statuses that matter stop being noticed. done: { className: "text-muted-foreground", Icon: Circle, fill: false }, cancelled: { className: "text-destructive", Icon: Minus, fill: false }, } export interface StatusProps extends Omit, "children"> { /** The RAW FHIR status code - `in-progress`, `entered-in-error`. Never a pre-formatted label. */ status: string | undefined /** * Hides the shape, leaving the word alone. For dense rows where the icon is noise - NOT a way to opt * out of D5 on a surface where the status carries weight, because the word alone is the thing that * fails for a colour-blind reader once anything else on the row is colour-coded. */ hideIcon?: boolean } function Status({ status, hideIcon = false, className, ...props }: StatusProps) { const label = statusLabel(status) // No words for it render nothing at all. See the header: a fallback here would reintroduce the bug. if (!label) return null const tone = statusTone(status) const presentation = tone ? TONE_PRESENTATION[tone] : undefined return ( {presentation && !hideIcon && ( ) } /** * A `Status` in a neutral chip - the shape the FHIR detail sheets were already using, minus the twelve * hand-rolled colour maps. * * WHAT IT REPLACES: each `Fhir*Display` carried its OWN `STATUS_STYLES` record of hardcoded hex pairs * (`{ bg: "#DCFCE7", text: "#116932" }`), applied as an INLINE STYLE, encoding the status in colour alone * and printing the raw code as the text. Twelve copies, twelve chances to disagree, none of them reading * from a token and none of them survivable by a colour-blind reader. * * The chip itself is deliberately NEUTRAL now: `Status` carries the colour and the shape, so a coloured * background would be a third encoding of the same fact competing with the two D5 asked for. * * Renders `null` when the status is absent or unmapped, so a caller cannot end up with an empty chip. */ function StatusBadge({ status, className, ...props }: StatusProps) { if (!statusLabel(status)) return null return ( ) } export { Status, StatusBadge, TONE_PRESENTATION }