Precautions Bar
StableFlagPatient identity · Clinical
This is read on the way through a door, so it is ordered by the action required rather than alphabetically, and required PPE is named rather than implied by a category — “contact precautions” is a label, “gown and gloves” is an instruction. Lapsed precautions are dropped rather than greyed, because a stale precaution on screen is how staff learn to ignore all of them. Behavioral flags describe the approach, not the person: “two staff for personal care” is actionable and carries no judgement.
pnpm dlx shadcn@latest add @oxygenui/precautions-barFirst install? Add "@oxygenui": "https://oxygenui.design/r/{name}.json" to the registries block of your components.json first — or skip the config and pass https://oxygenui.design/r/precautions-bar.json directly.
Preview
Every state, switchable.
Airborne precautions
N95 before entry, door closed
Contact precautions
Gown and gloves
Approach
Two staff for personal care
Fall risk
Bed low, call bell in reach
Airborne before contact before behavioral. The required PPE is named, not implied by a category.
Usage
Props are the FHIR resource.
import { PrecautionsBar } from "@/components/oxygen/precautions-bar";
<PrecautionsBar
flags={flags}
actionFor={(flag) => PPE_BY_CODE[flag.code?.coding?.[0]?.code ?? ""]}
/>Dependencies
- @oxygenui-design/fhir@^0.1.0
- lucide-react
- clsx
- tailwind-merge
| Prop | Type | Default |
|---|---|---|
| actionFor Maps a Flag to its required action. Without an entry, the precaution renders with its label alone rather than an invented instruction. | ((flag: Flag) => string | undefined) | undefined | — |
| asOf | Date | undefined | new Date() |
| className | string | undefined | — |
| flags | Flag[] | undefined | — |
| precautions Explicit precautions, for callers not modelling these as FHIR Flags. | PrecautionDisplay[] | undefined | — |
Guidance
When to use it, and when not to.
DECISION SURFACE / 6 RULES
Recommended context
Use it
- At the top of any chart, room display, or transport handover.
- With actionFor supplying the specific PPE or approach required.
- On ward and corridor displays, where it is read at distance.
Guardrails
Don't
- Rendering lapsed precautions greyed out rather than removing them.
- Behavioral labels that characterise a person rather than an approach.
- Relying on the icon alone \u2014 these are read on poor displays at distance.
Quality
What was tested, and what is still missing.
- Icon, text, and colour
- All three, always. These are read at distance and often in monochrome.
- Ordered by action
- Reading order follows urgency of required action, not the source order of the flags.
- Empty is stated
- No active precautions renders as a sentence rather than an empty bar.
Open gaps
Known limitations
- PPE mapping is caller-supplied; there is no built-in infection-control catalogue.
- Category mapping covers common codes and falls back to a neutral kind.
- Does not model precaution ordering rules beyond urgency of action.
Source
Exactly what lands in your repository.
Read directly from the published registry, so this can never drift from what the CLI installs.
Show sourceHide source160 lines
"use client";
/**
* PrecautionsBar — what staff must know before entering the room.
*
* This is read on the way through a door, so it is ordered by the action
* required rather than alphabetically, and the required PPE is named rather
* than implied by a category. "Contact precautions" is a label; "gown and
* gloves" is an instruction.
*
* Two things it does that a flag list does not:
*
* 1. Lapsed precautions are dropped, not greyed out. A stale precaution left
* on screen is how staff learn to ignore all of them, and the credibility
* of the whole bar depends on nothing false being on it.
* 2. Behavioral flags describe the approach, not the person. "Two staff for
* personal care" is actionable and carries no judgement; "aggressive" is
* a label that follows someone through the record and shapes their care
* for years. The component takes an approach string and will render a
* category without one rather than inventing a characterisation.
*/
import * as React from "react";
import { Biohazard, HandHelping, PersonStanding, ShieldAlert, Wind } from "lucide-react";
import { codeableText, flagCategory, isFlagActive, type Flag } from "@oxygenui-design/fhir";
import { cn } from "@/lib/utils";
export type PrecautionKind =
"isolation" | "airborne" | "fall" | "behavioral" | "mobility" | "other";
export interface PrecautionDisplay {
kind: PrecautionKind;
/** What the precaution is called. */
label: string;
/** What staff must actually do. The reason the bar exists. */
action?: string;
}
export interface PrecautionsBarProps {
flags?: Flag[];
/** Explicit precautions, for callers not modelling these as FHIR Flags. */
precautions?: PrecautionDisplay[];
/**
* Maps a Flag to its required action. Without an entry, the precaution
* renders with its label alone rather than an invented instruction.
*/
actionFor?: (flag: Flag) => string | undefined;
asOf?: Date;
className?: string;
}
const KIND_ICON: Record<PrecautionKind, React.ComponentType<{ className?: string }>> = {
isolation: Biohazard,
airborne: Wind,
fall: PersonStanding,
behavioral: ShieldAlert,
mobility: HandHelping,
other: ShieldAlert,
};
/** Written out in full — Tailwind cannot resolve a class built from a variable. */
const KIND_CLASS: Record<PrecautionKind, string> = {
isolation:
"border-[var(--ox-status-critical-border)] bg-[var(--ox-status-critical-bg)] text-[var(--ox-status-critical)]",
airborne:
"border-[var(--ox-status-critical-border)] bg-[var(--ox-status-critical-bg)] text-[var(--ox-status-critical)]",
fall: "border-[var(--ox-status-high-border)] bg-[var(--ox-status-high-bg)] text-[var(--ox-status-high)]",
behavioral: "border-[#ddd6fe] bg-[var(--ox-flag-restricted-bg)] text-[var(--ox-flag-restricted)]",
mobility:
"border-[var(--ox-status-low-border)] bg-[var(--ox-status-low-bg)] text-[var(--ox-status-low)]",
other: "border-[var(--ox-border-strong)] bg-[var(--ox-bg-muted)] text-[var(--ox-text-muted)]",
};
/** Ordered by how urgently it changes what someone does at the door. */
const KIND_ORDER: PrecautionKind[] = [
"airborne",
"isolation",
"behavioral",
"fall",
"mobility",
"other",
];
const CATEGORY_KIND: Record<string, PrecautionKind> = {
infection: "isolation",
airborne: "airborne",
safety: "fall",
behavioral: "behavioral",
mobility: "mobility",
};
function kindFor(flag: Flag): PrecautionKind {
const category = flagCategory(flag);
return (category && CATEGORY_KIND[category]) || "other";
}
export function PrecautionsBar({
flags,
precautions,
actionFor,
asOf = new Date(),
className,
}: PrecautionsBarProps) {
const fromFlags: PrecautionDisplay[] = (flags ?? [])
// Dropped, not greyed. A lapsed precaution on screen devalues every
// precaution beside it.
.filter((flag) => isFlagActive(flag, asOf))
.map((flag) => ({
kind: kindFor(flag),
label: codeableText(flag.code) ?? "Precaution",
action: actionFor?.(flag),
}));
const all = [...fromFlags, ...(precautions ?? [])].sort(
(a, b) => KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind),
);
if (!all.length) {
return (
<p className={cn("text-[length:var(--ox-text-xs)] text-[var(--ox-text-subtle)]", className)}>
No active precautions recorded
</p>
);
}
return (
<section
aria-label="Precautions"
className={cn("flex flex-wrap items-stretch gap-2", className)}
>
{all.map((precaution, index) => {
const Icon = KIND_ICON[precaution.kind];
return (
<div
key={`${precaution.label}-${index}`}
className={cn(
"flex items-center gap-2 rounded-[var(--ox-radius)] border px-2.5 py-1.5",
KIND_CLASS[precaution.kind],
)}
>
<Icon aria-hidden="true" className="size-4 shrink-0" />
<div className="min-w-0">
{/* Icon, text, and colour together — this is read at a distance,
in a corridor, often on a poor display. */}
<p className="text-[length:var(--ox-text-sm)] font-semibold leading-tight">
{precaution.label}
</p>
{precaution.action && (
<p className="text-[length:var(--ox-text-xs)] leading-tight opacity-90">
{precaution.action}
</p>
)}
</div>
</div>
);
})}
</section>
);
}