Internal & investor material. Sign in with your @qiri.ai Google account to continue.
Every published pharmacist-console component, documented from its own source: the guidance is the component's JSDoc, the props are its typed API, the snippet is its committed preview. Cool, clinical, decision-dense. Open the live library to build with them.
51 components, alphabetical. Guidance and props are read from the source at generation time; the drift gate checks the count.
console-v8/components/AnswerMarkdown.tsx

Renders a live Ask Qiri answer. The platform returns the answer as citation-sanitised **markdown** (headings, bold, lists, paragraphs) with inline `[n]` citation markers that map to sources[n-1] in the RHS panel. We render with react-markdown (React elements only — no innerHTML, so no XSS surface, unlike the trusted-only <Html> helper) and turn the `[n]` markers into superscript chips.
import { AnswerMarkdown } from 'qiri-console-v8';
<BotBubble>
<AnswerMarkdown
text={`**Yes — this pairing needs a counselling point, not a hold.** Atorvastatin and clarithromycin both compete for CYP3A4, so a short macrolide course can raise statin levels and the risk of myopathy [1].
For a 5-day course the pragmatic step is to **pause atorvastatin** while the patient takes clarithromycin and resume once finished [2]. Advise Margaret to report any unusual muscle pain or dark urine in the meantime.`}
/>
</BotBubble>console-v8/components/Avatar.tsx
Avatar — a person's identity chip: initials on the Qiri taupe gradient (or an image). Consolidates the five hand-coded initials-circles across the console into one component. Self-styled so it renders anywhere, in or out of the shell.
When to use: to represent a patient or staff member in a row, header or panel. Do: pass the full `name` — initials derive automatically. Don't: use it for brand marks (that's QiriMark).
| Prop | Type | Notes |
|---|---|---|
name | string | |
size? | number | Diameter in px. Default 34. |
src? | string |
import { Avatar } from 'qiri-console-v8';
<div style={wrap}>
<Avatar name="Margaret Chen" size={52} />
<Avatar name="David Nguyen" size={40} />
<Avatar name="Priya Sharma" size={34} />
<Avatar name="Thomas Wright" size={26} />
</div>console-v8/components/Badge.tsx

Badge — the console's status/label chip, wrapping the shared `.tag` classes (uppercase, pill, tonal). Replaces the several parallel badge treatments with one typed tone scale.
When to use: a short status or category label (a queue count, a script state, a role). Keep it one or two words. Renders anywhere: the `.tag` styles are global (they were per-view once; the seam is closed).
Tones: `neutral` (default grey), `cta` (accent), `success`, `amber` (caution), `danger` (critical), `info`.
| Prop | Type | Notes |
|---|---|---|
tone? | BadgeTone | |
children | ReactNode | |
className? | string |
import { Badge } from 'qiri-console-v8';
<div id="view-lite" style={{ padding: 24, background: 'var(--color-bg,#F2F0EB)', display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center' }}>
<Badge>Draft</Badge>
<Badge tone="cta">In review</Badge>
<Badge tone="success">Approved</Badge>
<Badge tone="amber">Needs check</Badge>
<Badge tone="danger">Blocked</Badge>
<Badge tone="info">PBS</Badge>
</div>console-v8/components/Button.tsx

Button — the console's action atom. A single pill button with the Qiri variant matrix, wrapping the shared `.btn*` classes so it is visually identical to every button already in the console while giving callers a typed, documented API.
When to use: any click that performs an action — submit, confirm, open, cancel. For navigation between views, use a nav item; for a subtle text action inside a dense row, prefer `variant="ghost"`.
Variants - `primary` — the one clear next step on a screen (charcoal ink fill). One per view. - `secondary` — an alternative or lower-emphasis action (outline). Pairs with primary. - `ghost` — a quiet action in a toolbar/row where chrome would be noise. - `danger` — a destructive confirm inside its own flow (filled red). Not for "override". - `success` — a positive terminal confirm (rare; e.g. "Approved"). Filled green. - `override` — going *against* a safety flag: a calm danger chip (faint wash, darkened label passing WCAG AA), so the deliberate act reads without traffic-lighting.
Do: keep one `primary` per view; put the verb on the label ("Sign off", not "OK"). Don't: use `danger` for a routine action, or stack two primaries.
| Prop | Type | Notes |
|---|---|---|
variant? | ButtonVariant | Emphasis + intent. Default `primary`. |
size? | ButtonSize | `sm` 28px · `md` 36px (default) · `lg` 44px. |
loading? | boolean | Show a spinner and disable interaction while an action is in flight. |
leftIcon? | ReactNode | |
rightIcon? | ReactNode | Icon after the label. |
children? | ReactNode |
import { Button } from 'qiri-console-v8';
<div style={wrap}>
<Col label="Emphasis & intent">
<Row>
<Button variant="primary">Sign off</Button>
<Button variant="secondary">Amend</Button>
<Button variant="ghost">Skip</Button>
<Button variant="danger">Delete script</Button>
<Button variant="success">Approved</Button>
<Button variant="override">Override safety flag</Button>
</Row>
</Col>
</div>console-v8/components/Checkbox.tsx

Checkbox: a labelled tick for an independent yes/no the user opts into (consent, "also print a label", filter facets). Wraps a NATIVE checkbox (kept focusable and announced) behind a drawn 18px box with the house check glyph, so forms and screen readers see a real input.
When to use: independent booleans, or several-of-many choices in a list. For a setting that takes effect immediately use Switch; for one-of-many use RadioGroup. Do: phrase the label as the positive state ("Notify the prescriber"). Don't: use a checkbox for an action (that is a Button).
| Prop | Type | Notes |
|---|---|---|
label | ReactNode | |
hint? | ReactNode | Smaller muted line under the label. |
import { Checkbox } from 'qiri-console-v8';
<div style={wrap}>
<Checkbox label="Also print a medicine label" defaultChecked />
<Checkbox label="Notify the prescriber" hint="Sends a secure message with the dispense record" />
<Checkbox label="Add to SMS reminders" defaultChecked hint="Patient has consented to reminders" />
<Checkbox label="Include in PBS claim" disabled hint="Unavailable: private script" />
</div>console-v8/components/pulse-parts.tsx

The "coming soon" placeholder card (`pl-soon`): teasers for expiring-stock and loyalty features not yet live.
import { ComingSoonCard } from 'qiri-console-v8';
<div style={{ padding: 24, background: 'var(--color-bg,#F2F0EB)', maxWidth: 420 }}>
<ComingSoonCard />
</div>console-v8/components/ConfirmDialog.tsx

ConfirmDialog — the "are you sure?" pattern, built on Modal + Button. Use it to gate a consequential or destructive action behind an explicit confirm.
When to use: deleting a record, discarding unsaved work, an irreversible send. For a routine save, don't confirm — just do it and toast. Set `danger` for a destructive confirm (red button). Do: name the action on the confirm button ("Delete script", not "OK").
| Prop | Type | Notes |
|---|---|---|
open? | boolean | |
title | ReactNode | |
message? | ReactNode | |
confirmLabel? | string | |
cancelLabel? | string | |
danger? | boolean | Style the confirm as destructive. |
onConfirm? | () => void | |
onCancel? | () => void |
import { ConfirmDialog } from 'qiri-console-v8';
<>
<style>{`.modal-scrim{position:static!important;inset:auto!important;min-height:auto!important;padding:32px!important}`}</style>
<ConfirmDialog open danger title="Delete this script?"
message="This removes the script from the queue. Amendments are audited, but a delete can't be undone."
confirmLabel="Delete script" onConfirm={() => {}} onCancel={() => {}} />
</>console-v8/components/console-context.tsx
Ids of signed-off scripts still animating out of the queue (the verdict-exit choreography): a departing script stays in `scripts`, so every count derived from the list still includes it, while its row renders collapsed+fading; removal (and only then the count change, then the toast) follows.
console-v8/components/ConsoleShell.tsx

Modals + toasts mount once
console-v8/components/ConsultStage.tsx

The live video surface, shared by the pharmacist room and the patient page. It is presentation only: the parent owns the WebRTC connection (lib/consult-call) and hands down the local + remote MediaStreams. This component renders them into the demo's existing video-stage chrome (status chips, self-view, control bar) and toggles the local mic/camera tracks in place.
| Prop | Type | Notes |
|---|---|---|
localStream | MediaStream | null | |
remoteStream | MediaStream | null | |
connected | boolean | |
waitingLabel | string | Caption shown over the stage until the remote feed is live. |
remoteLabel? | string | Small tag bottom-left (patient name / location), optional. |
ended? | boolean | |
recording? | boolean |
import { ConsultStage } from 'qiri-console-v8';
<ConsultStage
localStream={null}
remoteStream={null}
connected={false}
waitingLabel="Waiting for the patient to join…"
remoteLabel="Kiosk · Blacktown"
recording
onEnd={() => {}}
/>console-v8/components/CounterTransactions.tsx

Counter "Transactions" view — the bank-statement history of dispense payments + OTC sales at this site. Styled to match the console Records page (design-system .hist-toolbar + .fleet-table): search, a status filter, a Today/7-days/30-days period control, Export, and a sortable table with status pills and a printable receipt. No patient identity is shown. Lives inside the standalone counter platform, so it takes its auth context by prop rather than from the console provider.
import { CounterTransactions } from 'qiri-console-v8';
<CounterTransactions
idToken="preview" siteId="preview"
pharmacy={{ name: 'Qiri Pharmacy — Newtown', abn: '51 824 753 556' }}
eyebrow="Counter"
toast={() => {}}
/>console-v8/components/DataTable.tsx

DataTable — the console's list surface (`.fleet-table`): the same table the scripts queue, records, patients and transactions all use. Give it columns and rows; it handles the header, alignment and horizontal scroll.
When to use: any tabular list of records. For a single key/value read-out use a definition list instead. Do: right-align numeric columns. Don't: put actions anywhere but the last column.
Scope, honestly: this atom renders columns and rows with alignment and horizontal scroll, and that is all. Sorting, selection, pagination and sticky headers are the CALLER's job today (compose Pager for paging; surfaces sort their own data). If a richer grid earns its place, it extends this API rather than forking the table.
import { DataTable } from 'qiri-console-v8';
<div id="view-lite" style={{ padding: 24, background: 'var(--color-surface,#fff)' }}>
<DataTable
rows={rows}
columns={[
{ key: 'patient', header: 'Patient', render: (r: any) => (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 9 }}>
<Avatar name={r.patient} size={26} /> {r.patient}
</span>) },
{ key: 'drug', header: 'Medicine' },
{ key: 'status', header: 'Status', render: (r: any) => <Badge tone={r.tone}>{r.status}</Badge> },
]}
/>
</div>console-v8/components/DateField.tsx

DateField: a labelled date input in the TextField chrome. Wraps the NATIVE `input[type="date"]` deliberately: the browser's picker is keyboard accessible, localised (Australian users see their own format), and on a touch kiosk it opens the platform date wheel; a custom calendar popover would have to re-earn all of that.
When to use: any single date the user picks or types (script written date, date of birth, consult day). `min`/`max` pass straight through for range limits. Do: constrain with `min`/`max` where the domain has hard bounds. Don't: parse or display machine dates through this; those render as text via the mono rules.
| Prop | Type | Notes |
|---|---|---|
label | string | Uppercase field label (required for accessibility). |
hint? | ReactNode | Helper text under the field. |
error? | ReactNode |
import { DateField } from 'qiri-console-v8';
<div style={wrap}>
<DateField label="Script written" defaultValue="2026-07-08" />
<DateField label="Consult day" defaultValue="2026-07-15" hint="Bookings open 30 days ahead" min="2026-07-13" max="2026-08-12" />
<DateField label="Date of birth" defaultValue="2031-02-14" error="Date of birth can't be in the future" />
</div>console-v8/components/MedicineTypeahead.tsx

Dose field: a droplist of the matched medicine's registered strengths plus an "Other…" escape hatch to free-text any strength. Falls back to a plain text input when no medicine is matched, so it behaves exactly as before for unknown drugs. Strengths render in mono (machine data).
import { DoseField } from 'qiri-console-v8';
<div style={{ padding: 24, maxWidth: 280, background: 'var(--color-bg,#F2F0EB)' }}>
<DoseField entry={metformin} value="500 mg" onChange={() => {}} />
</div>console-v8/components/EmptyState.tsx

EmptyState — the "nothing here yet" pattern: a centred icon, a title, a line of guidance, and an optional next action. Turns a blank region into a designed, reassuring moment.
When to use: an empty queue, a cleared search, a not-yet-configured surface. Do: say what goes here and how to start it. Don't: leave a blank panel, and don't use it for an error (that's a different, tonal pattern).
| Prop | Type | Notes |
|---|---|---|
icon? | IconName | |
title | ReactNode | |
description? | ReactNode |
import { EmptyState } from 'qiri-console-v8';
<div style={{ padding: 12, background: 'var(--color-surface,#fff)', maxWidth: 460 }}>
<EmptyState icon="check" title="Queue's clear"
description="Every script has been signed off. New scripts land here as they arrive."
action={{ label: 'Add a script' }} />
</div>console-v8/components/pulse-parts.tsx

The "When the counter is busy" histogram (`pl-card`): sales per hour, 8a–8p, with a ghosted sample shape before any sales land.
import { HoursHistogram } from 'qiri-console-v8';
<div style={{ padding: 24, background: 'var(--color-bg,#F2F0EB)', maxWidth: 420 }}>
<HoursHistogram hours={hours} hasTxns />
</div>console-v8/components/Icon.tsx
Icon — the typed entry point to the Qiri icon set. Wraps the named SVG constants in `components/icons.ts` (Lucide base, 24×24, stroke-width 2, `currentColor`) so callers reach for a name with autocomplete instead of pasting raw `<svg>` markup.
When to use: any inline icon. Colour follows the surrounding text (`currentColor`); set `size` for anything other than the 16px default. Do: pick the nearest existing name. Don't: paste a raw `<svg>` — add it to `icons.ts` and give it a name so it's reusable and lint-clean.
| Prop | Type | Notes |
|---|---|---|
name | IconName | Which icon from the Qiri set. |
size? | number | Pixel size (width = height). Default 16. |
import { Icon } from 'qiri-console-v8';
<div style={{ padding: 24, background: 'var(--color-bg,#F2F0EB)', color: 'var(--color-ink,#272F38)',
display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(92px,1fr))', gap: 6 }}>
{names.map((n) => (
<div key={n} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8,
padding: '14px 6px', border: '1px solid var(--color-divider)', borderRadius: 10, background: 'var(--color-surface,#fff)' }}>
<Icon name={n} size={20} />
<span style={{ font: '11px var(--font-mono,monospace)', color: 'var(--color-ink-muted)' }}>{n}</span>
</div>
))}
</div>console-v8/components/InboxSurface.tsx

Inbox panel — eScripts patients sent from the Qiri app (clinical.patient_script_submission), rendered as a tab on the Scripts page. Loading one runs the normal eRx intake with the pharmacist's own credential (api.createFromToken → the platform screens it), links the resulting prescription_session_id, and clears it from the inbox. Pharmacist-in-the-loop from the first step; no patient ever calls the platform. The fetch, load, and dismiss logic lives in ScriptsSurface (which owns the tab + count); this panel is presentational.
import { InboxPanel } from 'qiri-console-v8';
<InboxPanel
subs={SUBS as any}
loaded
busyId="2"
error={null}
onLoad={() => {}}
onDismiss={() => {}}
/>console-v8/components/InlineEditField.tsx

Raw value used for editing.
| Prop | Type | Notes |
|---|---|---|
value | string | Raw value used for editing. |
display? | string | Formatted text for view mode (defaults to value). |
addLabel? | string | Muted call-to-action shown in view mode when empty, e.g. "Add DOB". |
mono? | boolean | |
kind? | 'text' | 'tel' | 'email' | 'select' | |
ariaLabel | string | Accessible label (the field name). |
onSave | (next: string) => Promise<void> |
import { InlineEditField } from 'qiri-console-v8';
<Row label="Preferred name">
<InlineEditField value="Margaret Chen" ariaLabel="Preferred name" onSave={noSave} />
</Row>console-v8/components/InteractionCard.tsx

Shared structured interaction card used by both the live Ask Qiri surface (AskQiriSurface) and the Records transcript replay (RecordsSurface), so the two never drift. A CRITICAL/CONTRAINDICATED interaction carries the design-system danger (red) shading; everything else stays on the neutral caution card.
import { InteractionCard } from 'qiri-console-v8';
<InteractionCard
summary="Two moderate interactions found across the patient’s active medicines. No hard contraindication — counsel and monitor."
rows={[
{
drugs: 'Warfarin + Amoxicillin',
severity: 'Moderate',
effect: 'Amoxicillin may potentiate the anticoagulant effect of warfarin, raising INR.',
management: 'Advise the patient to watch for bruising or bleeding; recommend an INR check within 3–5 days.',
},
{
drugs: 'Sertraline + Tramadol',
severity: 'Moderate',
effect: 'Additive serotonergic activity — small increased risk of serotonin syndrome.',
management: 'Counsel on agitation, tremor and sweating. Reasonable to dispense with monitoring.',
},
]}
/>console-v8/components/MedicareValue.tsx

Read-only Medicare display: masked by default, with a Show/Hide toggle so a pharmacist can reveal the full number when they need it (e.g. verifying against the physical card). Display masking only — the stored value is unchanged (it is the patient match key + needed for PBS claiming). Edit fields keep the full number.
import { MedicareValue } from 'qiri-console-v8';
<div style={{ font: '15px/1.5 var(--font-sans, system-ui)', color: 'var(--color-ink, #272F38)' }}>
<MedicareValue value="2296 78415 1 / 2" />
</div>console-v8/components/MedicineTypeahead.tsx

No JSDoc on the export yet: add the when/how guidance in the source and re-run the generator.
import { MedicineTypeahead } from 'qiri-console-v8';
<Field>
<MedicineTypeahead value="Atorva" onChange={() => {}} ariaLabel="Medicine" autoFocus placeholder="Start typing a medicine…" />
</Field>console-v8/components/Menu.tsx

Menu — a dropdown of row/context actions (the promoted `.row-menu`). Renders a card of action items, each optionally with an icon; destructive items read in danger.
When to use: per-row actions in a table (edit, resend, delete) or an overflow "…" menu. For primary page actions use Buttons. Do: put the destructive item last, separated. Don't: exceed ~6 items — reach for a panel instead.
| Prop | Type | Notes |
|---|---|---|
items | MenuItem[] |
import { Menu } from 'qiri-console-v8';
<div style={{ padding: 24, background: 'var(--color-bg,#F2F0EB)', display: 'inline-block' }}>
<Menu items={[
{ label: 'View record', icon: 'external' },
{ label: 'Edit patient', icon: 'edit' },
{ label: 'Resend SMS', icon: 'sms' },
{ label: 'Delete', icon: 'trash', danger: true },
]} />
</div>console-v8/components/MergeDiffModal.tsx

Before / after for every person-level field
import { MergeDiffModal } from 'qiri-console-v8';
<Framed><MergeDiffModal
when="11 Jul 2026"
actorName="David Hanin"
onClose={noop}
diff={{
duplicateId: 'p-2291',
duplicateName: 'Maggie Chen',
survivorName: 'Margaret Chen',
reason: 'Same patient — duplicate created at intake from an SMS booking.',
fields: [
{ label: 'Full name', before: 'Maggie Chen', after: 'Margaret Chen', changed: true },
{ label: 'Date of birth', before: '', after: '14/03/1958', changed: true },
{ label: 'Medicare', before: '2954 78122 1', after: '2954 78122 1 - 3', changed: true, mono: true },
{ label: 'Mobile', before: '0412 665 208', after: '0412 665 208', changed: false, mono: true },
{ label: 'Address', before: '12 Ourimbah Rd, Mosman NSW 2088', after: '12 Ourimbah Rd, Mosman NSW 2088', changed: false },
],
combined: { allergies: 1, medications: 4, vitals: 2, alerts: 1, dispenses: 9, interactions: 3 },
}}
/></Framed>console-v8/components/Modal.tsx

Modal — the shared dialog shell (`.modal-scrim` + `.modal`). The single shell every focused overlay should use, instead of hand-rolling the scrim/close markup (which today is duplicated across ~17 modals).
When to use: a focused task that must interrupt the current screen — sign-off, an edit form, a confirmation. For transient feedback use a toast; for a side-by-side detail use a panel. Do: give every modal a `title` and a working close. Don't: nest modals.
| Prop | Type | Notes |
|---|---|---|
open? | boolean | Controls visibility (adds the `.open` class to the scrim). |
title? | ReactNode | Heading shown in the modal header. |
onClose? | () => void | Fired by the close button and the backdrop. |
wide? | boolean | Wider variant (680px) for dense content. |
children | ReactNode | |
footer? | ReactNode | Optional footer (e.g. a Button row), pinned under the body. |
import { Modal } from 'qiri-console-v8';
<>
<style>{`.modal-scrim{position:static!important;inset:auto!important;min-height:auto!important;padding:32px!important}`}</style>
<Modal open title="Amend script" onClose={() => {}}
footer={<><Button variant="ghost">Cancel</Button><Button>Save amendment</Button></>}>
<p style={{ marginTop: 0, color: 'var(--color-ink-muted)', fontSize: 14 }}>
Amendments are append-only audit records — the original is preserved.
</p>
<TextField label="Reason for amendment" defaultValue="Dose corrected per prescriber call" />
</Modal>
</>console-v8/components/pulse-parts.tsx

The "Money in the business" panel (`pl-card`): PBS billed, stock at cost, priced lines.
import { MoneyPanel } from 'qiri-console-v8';
<div style={{ padding: 24, background: 'var(--color-bg,#F2F0EB)', maxWidth: 420 }}>
<MoneyPanel curT={{ gov: 1500000 } as any} econ={{ priced: 128, stockValue: 8420000 } as any} itemCount={150} />
</div>console-v8/components/Pager.tsx

Pager — compact previous/next pagination with a page read-out. Pairs with DataTable for long lists.
When to use: a list longer than one comfortable page. For infinite feeds use lazy loading instead. Do: disable the ends. Don't: paginate fewer than ~2 pages.
| Prop | Type | Notes |
|---|---|---|
page | number | |
pageCount | number | |
onChange? | (page: number) => void |
console-v8/components/pulse-parts.tsx

The "Product performance" card (`pl-card`): a margin-ranked table of top lines, plus thin-margin and dead-stock insight lines. Skeleton + hint until stock is priced.
import { ProductPerformance } from 'qiri-console-v8';
<div style={{ padding: 24, background: 'var(--color-bg,#F2F0EB)', maxWidth: 480 }}>
<ProductPerformance econ={econ} itemCount={150} />
</div>console-v8/components/Pulse.tsx

---------- hero band ----------
import { Pulse } from 'qiri-console-v8';
<Pulse idToken="preview" siteId="preview" items={ITEMS as any} toast={() => {}} />console-v8/components/PurchaseOrders.tsx

Purchase orders — the reorder → order → receive loop as system of record. Lives as the "Orders" tab of the Stock manager (admin-only). Create a draft from the reorder list, adjust lines, send it to the wholesaler (CSV export now; PharmX adapter is a future drop-in), then receive stock against it — receiving writes a 'received' movement to the ledger, closing the loop.
import { PurchaseOrders } from 'qiri-console-v8';
<PurchaseOrders idToken="preview" siteId="preview" items={ITEMS as any} toast={() => {}} />console-v8/components/onboarding-brand.tsx

Shared brand components for public-facing onboarding pages.
import { QiriWordmark } from 'qiri-console-v8';
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: '72px 48px', minHeight: 220,
}}>
<QiriWordmark size={200} />
</div>console-v8/components/RadioGroup.tsx

RadioGroup: one-of-many, all options visible. A `fieldset` of native radios drawn as the house 18px dot, with an optional muted hint per option. Because every option is on screen, the pharmacist can scan the whole choice before committing; which is why short clinical choices (dose form, collection method, a reason) prefer this over Select.
When to use: 2–5 mutually exclusive options worth seeing at once. For longer lists use Select; for on/off use Switch. Do: order options by likelihood or safety, not alphabetically. Don't: preselect a consequential clinical choice; make the pharmacist choose.
| Prop | Type | Notes |
|---|---|---|
label | string | Uppercase group label (rendered as the fieldset legend). |
options | RadioOption[] | |
value? | string | Controlled selected value. |
defaultValue? | string | Uncontrolled initial value. |
onChange? | (value: string) => void | |
name? | string | |
disabled? | boolean | |
className? | string |
import { RadioGroup } from 'qiri-console-v8';
<div style={wrap}>
<RadioGroup
label="Collection"
defaultValue="counter"
options={[
{ value: 'counter', label: 'At the counter', hint: 'Patient collects from staff' },
{ value: 'private', label: 'Private collection point', hint: 'No conversation needed at the desk' },
{ value: 'delivery', label: 'Home delivery', hint: 'Same-day courier where available' },
]}
/>
<RadioGroup
label="Dose form"
defaultValue="tablet"
options={[
{ value: 'tablet', label: 'Tablet 500 mg' },
{ value: 'capsule', label: 'Capsule 500 mg' },
{ value: 'liquid', label: 'Oral liquid 100 mg/5 mL', hint: 'Out of stock at this site', disabled: true },
]}
/>
</div>console-v8/components/SearchField.tsx

SearchField: the filter-a-list input: leading search glyph, the TextField chrome, and a clear affordance the moment there is anything to clear. It narrows what is already on screen (scripts, stock, patients); submitting is not its job, filtering as you type is.
When to use: filtering visible lists and tables. For choosing a medicine (a data lookup with structured results) use MedicineTypeahead. Do: debounce expensive filtering in the caller; keep the placeholder a noun ("Search stock"), not an instruction. Don't: hide the only path to an item behind search; lists stay browsable.
| Prop | Type | Notes |
|---|---|---|
label? | string | |
onValueChange? | (value: string) => void | Convenience change handler with just the string. |
import { SearchField } from 'qiri-console-v8';
<div style={wrap}>
<SearchField placeholder="Search stock" />
<SearchField placeholder="Search patients" defaultValue="Margaret Ch" />
</div>console-v8/components/Select.tsx

Select: a labelled dropdown wrapping the shared `.field-label` / `.field-input` classes, with the same hint/error contract as TextField. Wraps a NATIVE `<select>` (keyboard, screen reader and kiosk touch behaviour for free); the only chrome added is the house chevron.
When to use: choosing ONE value from a short known list (site, role, reason code). For medicines and other searchable data use MedicineTypeahead; for 2–4 visible-at-once options prefer RadioGroup so the choice is scannable. Do: keep options short and parallel ("Owner", "Pharmacist", not sentences). Don't: use a select for on/off (that is Switch) or for free text.
| Prop | Type | Notes |
|---|---|---|
label | string | Uppercase field label (required for accessibility). |
hint? | ReactNode | Helper text under the field. |
error? | ReactNode | |
children | ReactNode | `<option>` elements. |
import { Select } from 'qiri-console-v8';
<div style={wrap}>
<Select label="Site" defaultValue="newtown">
<option value="newtown">Qiri Community Pharmacy · Newtown</option>
<option value="marrickville">Qiri Pharmacy · Marrickville</option>
<option value="ashfield">Qiri Pharmacy · Ashfield</option>
</Select>
<Select label="Role" defaultValue="pharmacist" hint="Sets what this person can sign off">
<option value="owner">Owner</option>
<option value="pharmacist">Pharmacist</option>
<option value="assistant">Assistant</option>
</Select>
<Select label="Reason" defaultValue="" error="Choose a reason before refusing supply">
<option value="" disabled>Select a reason</option>
<option value="interaction">Drug interaction</option>
<option value="dose">Dose outside range</option>
<option value="id">Identity not confirmed</option>
</Select>
</div>console-v8/components/Sidebar.tsx

Help moved to the topbar icon cluster (alongside Signal intelligence).
console-v8/components/SigPreview.tsx

Live plain-English preview of pharmacist SIG shorthand under the manual-intake Directions field. As the pharmacist types ("1 m", "2 tds ac") this shows what Qiri reads it as ("Take 1 in the morning", …) so an ambiguous abbreviation is caught before the script is queued. Display aid only — it never edits what was typed; the same expansion is applied server-side during parsing regardless.
It's a thin child so the parent (NewScriptModal) can render it inside a conditional branch without breaking the rules of hooks. Resolves via the read-back-gated BFF (api.resolveSig), so it's silent — renders nothing — on a demo / not-yet-provisioned / offline console. When `confidence` < 1 the platform couldn't interpret part of the sig, so we flag it for the pharmacist to check rather than trust silently (principle 1).
console-v8/components/Skeleton.tsx

Skeleton — a shimmering placeholder for content that's loading. Prefer it to a spinner for structured content (a list, a card) so the layout doesn't jump when data lands.
When to use: the brief wait while a list or panel loads. For a whole-page or brand moment use QiriLoading. Do: mirror the shape of what's coming. Don't: leave a skeleton up for more than a couple of seconds — that's an error state.
| Prop | Type | Notes |
|---|---|---|
lines? | number | Number of stacked lines. Default 3. |
height? | number | Height of each line (px). Default 14. |
radius? | number | Corner radius (px). Default 8. |
widths? | string[] |
import { Skeleton } from 'qiri-console-v8';
<div style={{ padding: 24, background: 'var(--color-surface,#fff)', display: 'flex', flexDirection: 'column', gap: 22, maxWidth: 440 }}>
<Skeleton lines={3} />
<Skeleton lines={2} height={20} widths={['48%', '90%']} />
</div>console-v8/components/pulse-parts.tsx

The "Where it came from" donut (`pl-card`): PBS-government vs patient co-pay split, with a sweep-in animation and a key of the dollar amounts.
import { SourceDonut } from 'qiri-console-v8';
<div style={{ padding: 24, background: 'var(--color-bg,#F2F0EB)', maxWidth: 420 }}>
<SourceDonut curT={curT} mixTotal={curT.gov + curT.patient} lit />
</div>console-v8/components/pulse-parts.tsx

A single KPI stat tile (`pl-kpi`): a label with dot, a headline value (often a CountUp), and a sub-line. Optional accent/flag tone, a "new" badge, and a delta.
import { StatTile } from 'qiri-console-v8';
<div style={{ padding: 24, background: 'var(--color-bg,#F2F0EB)', display: 'grid', gridTemplateColumns: 'repeat(2,minmax(190px,1fr))', gap: 12, maxWidth: 440 }}>
<StatTile tone="accent retail" label="Gross margin" isNew value="$1,681" sub="18% · 30 days" />
<StatTile label="Avg basket" delta={0.12} value="$25.02" sub="182 sales" />
<StatTile label="GST collected" value="$3,466" sub="for your BAS" />
<StatTile tone="flag" label="Refunds" delta={0.4} deltaInv value="$220" sub="30 days" />
</div>console-v8/components/StockForecast.tsx

Reorder forecast — suggest reorder levels from measured demand (the stock ledger), then let staff review and apply them. Lives as the "Forecast" tab of the Stock manager. Suggestions only: we never silently overwrite a pharmacist's levels — applying is an explicit, selected action.
import { StockForecast } from 'qiri-console-v8';
<StockForecast idToken="preview" siteId="preview" toast={() => {}} />console-v8/components/StockSettings.tsx

Stock manager — the per-site product list the counter looks up (Phase 5c). Two tabs: "Stock list" (search / sort / paginate / inline pricing, GST + PBS) and "Add & import" (the add form + CSV import). Edits gate on view_clinical server-side. Lives in the counter platform (admin-only), so it takes auth context + a toast by prop rather than from the console provider.
import { StockSettings } from 'qiri-console-v8';
<StockSettings idToken="preview" siteId="preview" toast={() => {}} />console-v8/components/Switch.tsx

Switch: an on/off that takes effect immediately (availability, a notification channel, opens-with-no-save settings). A `role="switch"` button with the house 38×22 track and sliding knob (the same treatment the availability settings ship today), labelled inline.
When to use: instant-effect settings. If the value is saved with a form submit, use Checkbox instead; if it triggers a one-off action, use Button. Do: label the THING being switched ("SMS reminders"), not the state. Don't: use a switch for anything with clinical consequence on flip; those go through a confirm flow.
| Prop | Type | Notes |
|---|---|---|
label | ReactNode | |
checked | boolean | Controlled on/off state. |
onChange? | (next: boolean) => void | |
hint? | ReactNode | Smaller muted line under the label. |
disabled? | boolean | |
className? | string |
console-v8/components/Tabs.tsx

Tabs — the underline tab strip (`.queue-tabs`) for switching views within a surface, with an optional count per tab.
When to use: 2–6 peer views of the same data (a queue's All / In review / Held). For navigating between different areas use the Sidebar. Do: keep labels to one word where you can. Don't: use tabs for a linear step flow (that's StepProgress).
| Prop | Type | Notes |
|---|---|---|
items | TabItem[] | |
active | string | id of the active tab. |
onChange? | (id: string) => void |
console-v8/components/pulse-parts.tsx

The chunky "Takings by day" bar chart (`pl-card`), with a pill tooltip per bar and a ghosted sample state before any trading.
import { TakingsBarChart } from 'qiri-console-v8';
<div style={{ padding: 24, background: 'var(--color-bg,#F2F0EB)', maxWidth: 480 }}>
<TakingsBarChart rangeLabel="30 days" cur={cur} hasTakings barMax={barMax} peakIdx={peakIdx} ghostBars={[]} />
</div>console-v8/components/pulse-parts.tsx

The dark takings hero (`pl-hero`): the headline takings figure, its change vs the previous period, and a gradient sparkline of the recent days.
import { TakingsHero } from 'qiri-console-v8';
<div style={{ padding: 24, background: 'var(--color-bg,#F2F0EB)', maxWidth: 340 }}>
<TakingsHero rangeLabel="30 days" net={3851200} prevNet={3600000} hasTakings spark={spark} />
</div>console-v8/components/TextField.tsx

TextField — a labelled form input wrapping the shared `.field-label` / `.field-input` classes, with optional hint and error line. The console's form atom; `multiline` swaps to a textarea.
When to use: any single form value the user types. For an inline, autosaving value in a record panel use InlineEditField instead. Do: always give a `label`. Don't: rely on placeholder as the label.
| Prop | Type | Notes |
|---|---|---|
label | string | Uppercase field label (required for accessibility). |
hint? | ReactNode | Helper text under the field. |
error? | ReactNode | |
multiline? | boolean | Render a resizable textarea instead of a single-line input. |
import { TextField } from 'qiri-console-v8';
<div style={wrap}>
<TextField label="Patient name" defaultValue="Margaret Chen" />
<TextField label="Medicare number" defaultValue="2296 78415 1" hint="10 digits, from the green card" />
<TextField label="Mobile" defaultValue="0412 000" error="Enter a valid Australian mobile" />
<TextField label="Clinical note" multiline defaultValue="Counselled on INR monitoring; advised to watch for bruising." />
</div>console-v8/components/Toast.tsx

Toast — a transient confirmation or alert (`.toast-item`): a card with a tonal left rail that appears, then dismisses. The single feedback atom; render it in a top-right stack.
When to use: to confirm an action just happened ("Signed off") or flag a recoverable error, without interrupting the screen. For a decision that must be made use a Modal. Do: say what happened in the past tense. Don't: use a toast for anything the user must act on.
| Prop | Type | Notes |
|---|---|---|
tone? | ToastTone | |
title? | ReactNode | |
children? | ReactNode | |
onDismiss? | () => void |
import { Toast } from 'qiri-console-v8';
<div style={{ padding: 24, background: 'var(--color-bg,#F2F0EB)', display: 'flex', flexDirection: 'column', gap: 10, width: 360 }}>
<Toast tone="success" title="Signed off">Script approved and dispensed to the counter.</Toast>
<Toast tone="info" title="Label printing">Sent to the counter printer.</Toast>
<Toast tone="danger" title="Couldn't save">Check your connection and try again.</Toast>
<Toast tone="amber" title="Held for review">This dispense needs a pharmacist.</Toast>
</div>console-v8/components/Tooltip.tsx

Tooltip — hover/focus help on a trigger (an icon, an abbreviation). Wraps its child and shows a small dark bubble above it.
When to use: to explain an icon-only control or a clinical abbreviation without cluttering the layout. Do: keep the tip to a short phrase. Don't: hide anything essential in a tooltip — it's unavailable on touch.
| Prop | Type | Notes |
|---|---|---|
tip | ReactNode | |
children | ReactNode | |
defaultOpen? | boolean | Render the bubble open (docs/preview). |
import { Tooltip } from 'qiri-console-v8';
<div id="view-lite" style={{ padding: '64px 24px 24px 150px', background: 'var(--color-surface,#fff)', display: 'flex', gap: 40, alignItems: 'center' }}>
<Tooltip tip="Drug interaction detected" defaultOpen>
<Icon name="alert" size={18} />
</Tooltip>
<Tooltip tip="PBS — subsidised item">
<Badge tone="info">PBS</Badge>
</Tooltip>
</div>console-v8/components/Topbar.tsx

Trigger, not a field: opens the ⌘K command palette (GlobalSearch).
| Prop | Type | Notes |
|---|---|---|
onOpenSignals | () => void | |
onOpenHelp | () => void | |
onOpenDocuments | () => void | |
onOpenSearch | () => void | |
searchHint | string | |
signalUnread | number |
import { Topbar } from 'qiri-console-v8';
<div style={{ background: 'var(--color-surface, #fff)', borderBottom: '1px solid var(--color-divider, rgba(39,47,56,.1))' }}>
<Topbar />
</div>