Internal & investor material. Sign in with your @qiri.ai Google account to continue.
Every published patient-app component (React Native, rendered via react-native-web), documented from its own source. Warm, calm, built for a hand on a phone. Open the live library to build with them.
25 components, alphabetical. Guidance and props are read from the source at generation time; the drift gate checks the count.
patient-app/src/components/qiri-bloom.tsx

The state bloom as a full-screen ambient background: one soft wash centred on the whole surface (including behind the header), so the screen reads as a single calm field with no clipped edge. Drop it in as the FIRST child of the screen's root, before the SafeAreaView, and keep the header + content transparent so it shows through. Filling the layer makes StateBloom's glow centre on the true middle.
The wash eases in on mount rather than popping, so as screens change (paired with a fade transition on the moment screens) the ambient field reads as one continuous, settling wash instead of snapping in and out. Honours Reduce Motion.
| Prop | Type | Notes |
|---|---|---|
color | string | Solid brand/semantic hex the bloom washes out from (opacity is applied by the gradient stops, so pass the full-strength token colour). |
size? | number | the colour dissolves into the canvas with no visible edge. |
style? | StyleProp<ViewStyle> | |
children? | ReactNode |
import { AmbientBloom } from 'patient-app';
<View style={{ width: 300, height: 420, backgroundColor: '#F2F0EB', overflow: 'hidden', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
<AmbientBloom color={colors.success} size={520} />
<ThemedText type="subtitle">You’re all set</ThemedText>
<ThemedText type="small">3 scripts ready at Newtown</ThemedText>
</View>patient-app/src/components/brand.tsx

The standard app header — centered wordmark, alerts bell (right), optional Back (left). Full-bleed with fixed padding so its placement is identical on every screen, independent of that screen's own content padding. Render it as the first child of the (unpadded) safe area, with content in a padded wrapper below.
import { AppHeader } from 'patient-app';
<View style={{ backgroundColor: '#F2F0EB' }}>
<AppHeader />
<View style={{ padding: 24 }}>
<ThemedText type="small" themeColor="textSecondary">Screen content sits below the header.</ThemedText>
</View>
</View>patient-app/src/components/detail-scaffold.tsx

Shared layout for pushed detail screens: the standard app header (back + wordmark + bell), title, content. Pass `background` to drop a full-screen ambient layer (e.g. <AmbientBloom />) behind the header + content, matching the tab screens' empty-state wash.
import { DetailScaffold } from 'patient-app';
<DetailScaffold title="Your prescription" subtitle="Metformin 500mg · 90 tablets">
<ThemedView style={{ padding: 18, borderRadius: 18, gap: 6 }}>
<ThemedText type="smallBold">Ready to collect</ThemedText>
<ThemedText type="small">Newtown Pharmacy · open until 9pm</ThemedText>
</ThemedView>
</DetailScaffold>patient-app/src/components/hold-to-confirm.tsx

Press-and-hold destructive action. Nothing happens on a tap — the action only fires after the button is held for HOLD_MS while a danger tint fills the track, so cancelling a consult or discarding a script can't happen by accident. A JS timer decides completion (the fill is purely visual feedback, so it stays on under Reduce Motion as progress indication).
| Prop | Type | Notes |
|---|---|---|
label | string | say the gesture in the label, e.g. "Hold to cancel appointment" |
onConfirm | () => void | |
quiet? | boolean | actions like removing a script. Default is the danger-outline pill. |
import { HoldToConfirm } from 'patient-app';
<View style={{ padding: 24, backgroundColor: '#F2F0EB' }}>
<HoldToConfirm label="Hold to cancel appointment" onConfirm={() => {}} />
</View>patient-app/src/components/qiri-aura.tsx

The Qiri aura mark, alive. A slow ambient "breath" makes the brand feel present and calm; when `celebrate` is set it performs a one-time arrival — the glow blooms up on a soft breath of light, three rings ripple outward in sequence, and one success haptic lands. Delight, not confetti: everything is slow, low-contrast and warm, tuned for patients collecting sensitive medication.
Honours the OS "Reduce Motion" setting: the orb renders fully static, with no loop, bloom or haptic.
| Prop | Type | Notes |
|---|---|---|
size? | number | |
celebrate? | boolean | Play the one-time "arrival" — the orb blooms to life with a soft halo and a single success haptic. Use it for the signature moment (medication ready). When false the orb simply lives (a slow ambient breath). |
onCelebrated? | () => void | Fired once when the arrival begins (persist it so the moment plays only once per event, then settles into the calm breathing state on later visits). |
import { LivingAuraOrb } from 'patient-app';
<View style={{ padding: 24, alignItems: 'center', backgroundColor: '#F2F0EB' }}>
<LivingAuraOrb size={220} />
</View>patient-app/src/components/onboarding-scaffold.tsx

Same header + full-width progress layout as select-pharmacy, so the bar sits in one consistent place across every onboarding step.
| Prop | Type | Notes |
|---|---|---|
step? | number | 1-based, of TOTAL_STEPS; omit for standalone screens |
title | string | |
subtitle? | string | |
children | ReactNode | |
primary | Action | |
secondary? | Action | |
standalone? | boolean | Force-routed single-step screens (e.g. the complete-profile backfill) hide the Back control + step progress so a patient can't be stranded or misled by a step count they never chose to start. |
import { OnboardingScaffold } from 'patient-app';
<OnboardingScaffold
step={3}
title="Verify your identity"
subtitle="We use this to match you to your prescriptions."
primary={{ label: 'Continue', onPress: () => {} }}
secondary={{ label: 'Back', onPress: () => {} }}>
<View style={{ gap: 8 }}>
<ThemedText type="default">Enter your Medicare details to link your scripts.</ThemedText>
</View>
</OnboardingScaffold>patient-app/src/components/payment-logos.tsx

PaymentMarks: the accepted-payments strip. All four acceptance marks in one row (card marks at the card ratio, wallet wordmarks sized by height), for "we accept" rows, payment sheets and method pickers. Pass `brands` for a subset, in the order given. Don't: recolour, restyle or crop the marks; the brands' artwork ships as provided, per their acceptance-mark guidelines.
import { PaymentMarks } from 'patient-app';
<View style={{ padding: 20, gap: 18, backgroundColor: '#F2F0EB' }}>
<View style={{ gap: 8 }}>
<ThemedText type="small" themeColor="textSecondary">All methods</ThemedText>
<PaymentMarks />
</View>
<View style={{ gap: 8 }}>
<ThemedText type="small" themeColor="textSecondary">Cards only</ThemedText>
<PaymentMarks brands={['visa', 'mastercard']} />
</View>
</View>patient-app/src/components/pharmacy-sheet.tsx

Pharmacy contact sheet — one reusable surface for a pharmacy's phone, address and hours, with call + directions actions. Opened from anywhere a pharmacy is shown (collect card, home hero, send list). Kiosk-safe: the same record drives a no-call variant later. Pass `pharmacy = null` to keep it hidden.
import { PharmacySheet } from 'patient-app';
<View style={{ height: 680, backgroundColor: '#F2F0EB' }}>
<PharmacySheet
pharmacy={{
id: 'p1',
name: 'Qiri Community Pharmacy',
suburb: 'Newtown NSW',
logoUrl: 'https://qiri.ai/images/favicon-48.png',
phone: '(02) 9557 1234',
address: '188 King St, Newtown NSW 2042',
hours: 'Mon–Fri 8am–8pm · Sat 9am–5pm · Sun 10am–4pm',
}}
onClose={() => {}}
/>
</View>patient-app/src/components/brand.tsx

Large aura logo (mark with a soft radial glow) for the landing screen.
import { QiriAuraLogo } from 'patient-app';
<View style={{ padding: 24, alignItems: 'center', backgroundColor: '#F2F0EB' }}>
<QiriAuraLogo size={200} />
</View>patient-app/src/components/qiri-button.tsx

Primary = charcoal CTA fill (Qiri --color-cta). Secondary = outlined. Ghost = text-only link action (e.g. "Skip for now"). Compact = small inline action button that hugs its label.
import { QiriButton } from 'patient-app';
<View style={{ padding: 20, gap: 12, backgroundColor: '#F2F0EB' }}>
<QiriButton title="Collect my scripts" variant="primary" chevron />
<QiriButton title="Book a pharmacist consult" variant="secondary" />
<QiriButton title="Skip for now" variant="ghost" />
</View>patient-app/src/components/qiri-checkbox.tsx

Active-consent checkbox. Never pre-ticked (ADHA TC_MC_MA_001).
| Prop | Type | Notes |
|---|---|---|
checked | boolean | |
onToggle | () => void | |
children | ReactNode |
import { QiriCheckbox } from 'patient-app';
<View style={{ padding: 20, gap: 16, backgroundColor: '#F2F0EB' }}>
<QiriCheckbox checked onToggle={() => {}}>
<ThemedText type="default">I agree to the Qiri privacy terms</ThemedText>
</QiriCheckbox>
<QiriCheckbox checked={false} onToggle={() => {}}>
<ThemedText type="default">Send me collection reminders</ThemedText>
</QiriCheckbox>
</View>patient-app/src/components/qiri-field.tsx

A validation hint shown below the input (and a danger border). Keep it warm + plain; callers should only pass it once the field is worth correcting (e.g. non-empty and invalid), never as a pre-emptive scold.
import { QiriField } from 'patient-app';
<View style={{ padding: 20, gap: 16, backgroundColor: '#F2F0EB' }}>
<QiriField label="Medicare number" value="2953 74125 1" placeholder="0000 00000 0" />
<QiriField label="Mobile number" value="0412 345 678" placeholder="04__ ___ ___" />
</View>patient-app/src/components/qiri-loading.tsx

Brand loading state: the living aura breathing in place of a bare spinner. Qiri "being present" while it works is part of the signature motion language — use this wherever a wait is long enough to show anything at all.
import { QiriLoading } from 'patient-app';
<View style={{ padding: 32, alignItems: 'center', backgroundColor: '#F2F0EB' }}>
<QiriLoading message="Preparing your order…" />
</View>patient-app/src/components/brand.tsx

The Qiri aura mark (blue orb + white glyph). Decorative — hidden from AT via the wrapping View (SVG props don't map cleanly on web).
import { QiriMark } from 'patient-app';
<View style={{ padding: 28, flexDirection: 'row', alignItems: 'center', gap: 20, backgroundColor: '#F2F0EB' }}>
<QiriMark size={64} />
<QiriMark size={44} />
<QiriMark size={28} />
</View>patient-app/src/components/qiri-notice.tsx

Inline status banner: a soft semantic tint with a glyph, for transient operation errors and gentle in-flow notices. Colour never carries the state alone (the glyph does); the -bg fill does the emphasis, no hard outline.
| Prop | Type | Notes |
|---|---|---|
kind? | Kind | |
children | ReactNode | the message (string or rich ThemedText content) |
style? | StyleProp<ViewStyle> |
import { QiriNotice } from 'patient-app';
<View style={{ padding: 20, gap: 12, backgroundColor: '#F2F0EB' }}>
<Row kind="danger">This script needs a pharmacist review before collection.</Row>
<Row kind="warm">Bring your Medicare card to link your first collection.</Row>
<Row kind="info">Your prescription is on its way to the pharmacy.</Row>
<Row kind="success">Payment received — your order is being prepared.</Row>
</View>patient-app/src/components/qiri-pin-input.tsx

PIN entry as one box per digit. A single hidden TextInput holds the real value (so the number pad, paste, and backspace all behave normally) and the boxes are a pure display layer: filled boxes show a dot (PINs stay masked), the next box carries the accent focus ring. Tapping anywhere focuses the hidden input.
| Prop | Type | Notes |
|---|---|---|
label? | string | |
value | string | |
onChangeText | (v: string) => void | |
length? | number | |
onFilled? | (v: string) => void | Fires once, when the final digit lands (auto-advance / auto-submit). |
error? | boolean | Danger borders (e.g. wrong PIN, mismatch). |
hint? | string | Helper or error line under the boxes, wrapped to the row's width so it aligns with the first box's left edge and the last box's right edge. |
autoFocus? | boolean |
import { QiriPinInput } from 'patient-app';
<View style={{ padding: 24, backgroundColor: '#F2F0EB' }}>
<QiriPinInput label="Enter your Qiri PIN" value="1234" onChangeText={() => {}} length={6} hint="6-digit code from your welcome email" />
</View>patient-app/src/components/qiri-sheet.tsx

The one bottom-anchored sheet surface for the app: a scrim that fades in and a card that rises into place on a gentle spring, with a matching exit. Mount it permanently and drive it with `visible` so Reanimated can play the exit before the card unmounts.
Tap-outside dismisses via `onClose`; the card swallows its own taps.
import { QiriSheet } from 'patient-app';
<View style={{ height: 680, backgroundColor: '#F2F0EB' }}>
<QiriSheet visible onClose={() => {}}>
<ThemedText type="subtitle">Ready to collect</ThemedText>
<ThemedText type="default">
Metformin 500mg and 2 other scripts are packed and waiting at Qiri Pharmacy, Newtown.
</ThemedText>
<QiriButton title="Get directions" variant="secondary" />
<QiriButton title="I’m on my way" variant="primary" chevron />
</QiriSheet>
</View>patient-app/src/components/qiri-tab-bar.tsx

Floating glass bottom nav: Cabinet + Sessions, a raised center Add action (the primary task, reachable from anywhere), then Settings. Uses Liquid Glass (expo-glass-effect) where available; a frosted surface elsewhere.
| Prop | Type | Notes |
|---|---|---|
backgroundColor | scheme === 'dark' ? 'rgba(36,40,46,0.94)' : 'rgba(234,226… | Dark: a deep frosted surface (matches the elevated card), not the light warm frost — so the floating bar reads as its own surface on a dark canvas too. |
borderColor | c.warmBorder, |
import { QiriTabBar } from 'patient-app';
<View style={{ height: 150, backgroundColor: '#F2F0EB', position: 'relative' }}>
<QiriTabBar state={{ index: 1, routes }} navigation={{ navigate: () => {} }} />
</View>patient-app/src/components/qiri-toggle.tsx

Brand toggle: accent track when on, neutral track when off, white thumb.
import { QiriToggle } from 'patient-app';
<View style={{ padding: 20, gap: 20, backgroundColor: '#F2F0EB' }}>
<Row label="Refill reminders" value={true} />
<Row label="Share summary with pharmacist" value={false} />
<Row label="Face ID sign-in" value={true} disabled />
</View>patient-app/src/components/radial-glow.tsx

A soft radial glow built from stacked concentric circles — deliberately NOT react-native-svg. Its RadialGradient fill renders unreliably on Android (the brand aura/bloom silently vanished there), so the glow is drawn from plain Views: backgroundColor + borderRadius + opacity, the one primitive that renders identically on every platform. Wide-and-faint to small-and-dense circles composite into a smooth wash, brightest at the centre.
Because it is pure RN layout, what renders on iOS renders the same on Android.
import { RadialGlow } from 'patient-app';
<View style={{ padding: 24, alignItems: 'center', backgroundColor: '#F2F0EB' }}>
<View style={{ width: 200, height: 200, alignItems: 'center', justifyContent: 'center' }}>
<RadialGlow color={colors.accent} size={200} peak={0.45} />
</View>
</View>patient-app/src/components/qiri-bloom.tsx

The state bloom — a soft radial wash of colour that carries the screen's current emotional state (green = ready, warm = with your pharmacist, blue = action for you). Content sits centred inside it, unboxed: the bloom dissolves into the cream canvas rather than living in a card. Breathes on the same slow ambient loop as the living aura; honours Reduce Motion by rendering static.
| Prop | Type | Notes |
|---|---|---|
color | string | Solid brand/semantic hex the bloom washes out from (opacity is applied by the gradient stops, so pass the full-strength token colour). |
size? | number | the colour dissolves into the canvas with no visible edge. |
style? | StyleProp<ViewStyle> | |
children? | ReactNode |
import { StateBloom } from 'patient-app';
<View style={{ height: 380, backgroundColor: '#F2F0EB', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
<StateBloom color={colors.success} size={380}>
<ThemedText type="subtitle">You’re all set</ThemedText>
<ThemedText type="small">3 scripts ready at Newtown</ThemedText>
</StateBloom>
</View>patient-app/src/components/step-progress.tsx

Slim segmented onboarding progress: `total` segments, the first `step` filled. Taupe track, charcoal fill — replaces the old "Step X of N" text. Announces the position to screen readers via a progressbar role + spoken label.
import { StepProgress } from 'patient-app';
<View style={{ padding: 24, backgroundColor: '#F2F0EB' }}>
<StepProgress step={3} total={6} />
</View>patient-app/src/components/themed-text.tsx

Weight is carried by the font family (Plus Jakarta Sans set), so styles set fontFamily rather than fontWeight. Playfair Display for the hero title, JetBrains Mono for machine data. Dynamic Type: text scales with the OS "Larger Text" setting (allowFontScaling is on by default). Cap the multiplier per type so body copy scales generously while the tight-line-height Playfair display sizes stay within their layouts. Callers can override via maxFontSizeMultiplier. Values below the cap are honoured fully.
import { ThemedText } from 'patient-app';
<View style={{ padding: 20, gap: 8, backgroundColor: '#F2F0EB' }}>
<ThemedText type="title">Your scripts are ready</ThemedText>
<ThemedText type="default">Collect them anytime from the Qiri pharmacy near you.</ThemedText>
<ThemedText type="small">Bulk-billed · no appointment needed</ThemedText>
<ThemedText type="smallBold">3 items to collect</ThemedText>
</View>patient-app/src/components/themed-view.tsx

No JSDoc on the export yet: add the when/how guidance in the source and re-run the generator.
import { ThemedView } from 'patient-app';
<View style={{ padding: 20, backgroundColor: '#ECEAE4' }}>
<ThemedView style={{ padding: 20, borderRadius: 16 }}>
<ThemedText type="smallBold">Next collection</ThemedText>
<ThemedText type="default">Metformin 500mg · ready now</ThemedText>
</ThemedView>
</View>