Qiri · Confidential

Platform documentation

Internal & investor material. Sign in with your @qiri.ai Google account to continue.

Qiri
Qiri · Design system · Reference

Patient app components

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.

AmbientBloom

patient-app/src/components/qiri-bloom.tsx

AmbientBloom preview

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.

Props (4)
PropTypeNotes
colorstringSolid brand/semantic hex the bloom washes out from (opacity is applied by the gradient stops, so pass the full-strength token colour).
size?numberthe colour dissolves into the canvas with no visible edge.
style?StyleProp<ViewStyle>
children?ReactNode
Example: ReadyScreen + 1 more cell in the library preview
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>

AppHeader

patient-app/src/components/brand.tsx

AppHeader preview

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.

Example: TopLevel + 1 more cell in the library preview
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>

DetailScaffold

patient-app/src/components/detail-scaffold.tsx

DetailScaffold preview

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.

Example: Default
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>

HoldToConfirm

patient-app/src/components/hold-to-confirm.tsx

HoldToConfirm preview

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).

Props (3)
PropTypeNotes
labelstringsay the gesture in the label, e.g. "Hold to cancel appointment"
onConfirm() => void
quiet?booleanactions like removing a script. Default is the danger-outline pill.
Example: Cancel + 1 more cell in the library preview
import { HoldToConfirm } from 'patient-app';

<View style={{ padding: 24, backgroundColor: '#F2F0EB' }}>
  <HoldToConfirm label="Hold to cancel appointment" onConfirm={() => {}} />
</View>

LivingAuraOrb

patient-app/src/components/qiri-aura.tsx

LivingAuraOrb preview

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.

Props (3)
PropTypeNotes
size?number
celebrate?booleanPlay 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?() => voidFired 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).
Example: Resting
import { LivingAuraOrb } from 'patient-app';

<View style={{ padding: 24, alignItems: 'center', backgroundColor: '#F2F0EB' }}>
  <LivingAuraOrb size={220} />
</View>

OnboardingScaffold

patient-app/src/components/onboarding-scaffold.tsx

OnboardingScaffold preview

Same header + full-width progress layout as select-pharmacy, so the bar sits in one consistent place across every onboarding step.

Props (7)
PropTypeNotes
step?number1-based, of TOTAL_STEPS; omit for standalone screens
titlestring
subtitle?string
childrenReactNode
primaryAction
secondary?Action
standalone?booleanForce-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.
Example: Step
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>

PaymentMarks

patient-app/src/components/payment-logos.tsx

PaymentMarks preview

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.

Example: Strip
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>

PharmacySheet

patient-app/src/components/pharmacy-sheet.tsx

PharmacySheet preview

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.

Example: ContactSheet
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>

QiriButton

patient-app/src/components/qiri-button.tsx

QiriButton preview

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.

Example: Variants + 1 more cell in the library preview
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>

QiriCheckbox

patient-app/src/components/qiri-checkbox.tsx

QiriCheckbox preview

Active-consent checkbox. Never pre-ticked (ADHA TC_MC_MA_001).

Props (3)
PropTypeNotes
checkedboolean
onToggle() => void
childrenReactNode
Example: States
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>

QiriField

patient-app/src/components/qiri-field.tsx

QiriField preview

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.

Example: Filled + 1 more cell in the library preview
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>

QiriFooter

patient-app/src/components/brand.tsx

QiriFooter preview

Footer for pages without the tab bar: Privacy · Terms links + copyright. On a health app that handles prescriptions, these links are both a felt-trust cue and a Play Store expectation at sign-up / log-in.

Example: Default
import { QiriFooter } from 'patient-app';

<View style={{ backgroundColor: '#F2F0EB', paddingHorizontal: 24, paddingTop: 40 }}>
  <QiriFooter />
</View>

QiriLoading

patient-app/src/components/qiri-loading.tsx

QiriLoading preview

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.

Example: Preparing + 1 more cell in the library preview
import { QiriLoading } from 'patient-app';

<View style={{ padding: 32, alignItems: 'center', backgroundColor: '#F2F0EB' }}>
  <QiriLoading message="Preparing your order…" />
</View>

QiriMark

patient-app/src/components/brand.tsx

QiriMark preview

The Qiri aura mark (blue orb + white glyph). Decorative — hidden from AT via the wrapping View (SVG props don't map cleanly on web).

Example: Sizes
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>

QiriNotice

patient-app/src/components/qiri-notice.tsx

QiriNotice preview

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.

Props (3)
PropTypeNotes
kind?Kind
childrenReactNodethe message (string or rich ThemedText content)
style?StyleProp<ViewStyle>
Example: Tones
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>

QiriPinInput

patient-app/src/components/qiri-pin-input.tsx

QiriPinInput preview

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.

Props (8)
PropTypeNotes
label?string
valuestring
onChangeText(v: string) => void
length?number
onFilled?(v: string) => voidFires once, when the final digit lands (auto-advance / auto-submit).
error?booleanDanger borders (e.g. wrong PIN, mismatch).
hint?stringHelper 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
Example: Filled + 1 more cell in the library preview
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>

QiriSheet

patient-app/src/components/qiri-sheet.tsx

QiriSheet preview

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.

Example: CollectSheet
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>

QiriTabBar

patient-app/src/components/qiri-tab-bar.tsx

QiriTabBar preview

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.

Props (2)
PropTypeNotes
backgroundColorscheme === '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.
borderColorc.warmBorder,
Example: Bar
import { QiriTabBar } from 'patient-app';

<View style={{ height: 150, backgroundColor: '#F2F0EB', position: 'relative' }}>
  <QiriTabBar state={{ index: 1, routes }} navigation={{ navigate: () => {} }} />
</View>

QiriToggle

patient-app/src/components/qiri-toggle.tsx

QiriToggle preview

Brand toggle: accent track when on, neutral track when off, white thumb.

Example: States
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>

RadialGlow

patient-app/src/components/radial-glow.tsx

RadialGlow preview

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.

Example: Accent + 2 more cells in the library preview
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>

StateBloom

patient-app/src/components/qiri-bloom.tsx

StateBloom preview

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.

Props (4)
PropTypeNotes
colorstringSolid brand/semantic hex the bloom washes out from (opacity is applied by the gradient stops, so pass the full-strength token colour).
size?numberthe colour dissolves into the canvas with no visible edge.
style?StyleProp<ViewStyle>
children?ReactNode
Example: Ready + 2 more cells in the library preview
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>

StepProgress

patient-app/src/components/step-progress.tsx

StepProgress preview

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.

Example: Midway + 1 more cell in the library preview
import { StepProgress } from 'patient-app';

<View style={{ padding: 24, backgroundColor: '#F2F0EB' }}>
  <StepProgress step={3} total={6} />
</View>

ThemedText

patient-app/src/components/themed-text.tsx

ThemedText preview

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.

Example: Scale
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>

ThemedView

patient-app/src/components/themed-view.tsx

ThemedView preview

No JSDoc on the export yet: add the when/how guidance in the source and re-run the generator.

Example: Surface
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>
Qiri platform documentation · v1.2 · 2026-07-23. Brand tokens from design-system/qiri-tokens.css. Source lives in design-site/src/; edit there, then run node build.mjs.
© 2026 Qiri.ai – All rights reserved
scroll to zoom · drag to pan · Esc to close