Mobile Audit — Patient Conversation Interface (375px)¶
Date: 2026-05-10 Scope: ConversationApp + MessageThread/Bubble + RichCard + MSO wizard + chat input + sidebar Auditor: /impeccable audit (Opus 4.7, 1M context) Coverage: Comprehensive across the eight scope items. No truncation.
Audit Health Score¶
| # | Dimension | Score | Key finding |
|---|---|---|---|
| 1 | Accessibility | 3/4 | Most touch targets >=44px and ARIA roles correct. aria-live="polite" on thread is good. Two issues: hidden-via-CSS streaming bubble in MessageThread (h-0 + opacity-0) is exposed/aria-hidden inconsistently across paths, and RichCard <label> rows in MatchResultsCard are missing the underlying form control association cue (visible checkbox is decorative — actual input absent). |
| 2 | Performance | 3/4 | Solid: WS streaming has rAF drain, MessageBubble is memoised with surgical comparator, in-place merge avoids whole-array re-renders. Lingering issue: streaming-cursor::after blinks via opacity, but the CurawayAvatar animate-[curaway-pulse_1.5s_ease-in-out_infinite] runs continuously on every assistant turn while streaming and prefers-reduced-motion is not honoured for that one keyframe. |
| 3 | Theming | 3/4 | Brand tokens used consistently (bg-chat-brand, text-chat-coral, hsl(var(--chat-brand))). Only one hardcoded brand hex slipped: RazorpayPaymentStep.tsx:133 ships color: '#008B8B' to the Razorpay SDK config (acceptable since the SDK requires a literal, but a getComputedStyle lookup would fix it). |
| 4 | Responsive | 2/4 | This is the weakest axis. Conversation thread max-width is 920px and the message-bubble inner content uses px-2 sm:px-3 md:px-6 — but the input bar uses max-w-3xl mx-auto (768px) with no centered flag in active state, breaking visual alignment with the thread at 375px. The MSO wizard's day-picker scrolls horizontally without scroll-shadow affordances at 375px (~5 cards = ~340px content fits, but the 7-card row exceeds viewport). The MSOSchedulingCard time-picker uses grid-cols-3 which produces ~108px-wide chips at 375px — fine — but the chip min-h-[44px] is set without min-w-[44px], and short labels (e.g. "9 AM") sit in a chip narrower than 44px. |
| 5 | Anti-patterns | 2/4 | Two BANNED side-stripe violations: DocumentChecklistCard.tsx lines 55, 61, 67, 73, 79, 85, 91 use border-l-2 (2px) for status, and ConfirmationCard.tsx:47 uses border-l-2 border-chat-coral/40 for on-site test list items. Per .impeccable.md and the audit rubric, side-stripes >1px are banned. The top accent stripe (border-t-[var(--accent-stripe-width)] = 3px) on ConfirmationCard is BORDERLINE — it's a top stripe, not a side stripe, but at 3px it reads as the same hero/anti-AI tell. No gradients, no glassmorphism, no sparklines, no hero metric layout, no AI cyan/purple palette. |
| Total | 13/20 | Solid but not impeccable — fix the side stripes and the responsive input/thread alignment to climb to 17. |
Anti-patterns verdict¶
Mostly pass. A designer would call this "Claude.ai-influenced" rather than "AI-generated slop". The chat shell is restrained, the brand colours are correctly muted, the typography is consistent with Montserrat. The only two tells:
- The 2px coloured left-borders in DocumentChecklistCard and ConfirmationCard read as "status decoration" — exactly the BANNED pattern. Should be 1px (or replaced with a status icon + tonal background, which is already there).
- The 3px top accent stripe (
--accent-stripe-width: 3px) on ConfirmationCard sub-cards is a coloured rule above a card surface — it's the same family of decoration the rubric warns about. It's not strictly banned (rubric explicitly bans side stripes >1px), but in the context of two cards it accumulates.
No gradient text, no glassmorphism, no sparkline decorations, no purple/cyan palette, no hero metric layout, no modal overuse. ConsentCard correctly uses the inline-in-chat paradigm via MobileModal only for the Terms drill-down.
Executive summary¶
- Total issues: P0=1, P1=4, P2=6, P3=3 (14 total)
- Top must-fix items:
- [P0] Welcome state input bar has no bottom-keyboard handling — at 375px on iOS, the on-screen keyboard pushes the suggestion chips off-screen and the centered layout's
flex items-center justify-centercollapses the chip area beneath the keyboard. - [P1] Banned side-stripe pattern:
border-l-2in DocumentChecklistCard (7 occurrences) + ConfirmationCard:47. - [P1] MSOSchedulingCard at 375px — the wizard renders inside
max-w-cardbut the innermin-w-[68px]day-picker × 7 days = 476px content width inside a horizontally-scrollable row that has no scroll-edge fade or "swipe for more" affordance. Patient cannot tell more days exist. - [P1] Sidebar/MobileBottomNav z-index + safe-area: bottom-nav is
z-30and usespb-[env(safe-area-inset-bottom)], but the InputBar's container only addspb-2on mobile (no safe-area on the input itself). On a notch-less iPhone SE this is fine; on a Pro Max with the home indicator, the input rests at 56px above the bottom edge but the gesture indicator overlaps the input's coral send button on landscape rotation. - [P1] Welcome-state error banner close-button has
min-w-[44px] min-h-[44px](good), but the banner itself wraps insidemax-w-2xl text-centerand at 375px the close button sits flush against the trailing edge with no inner padding — the touch target overlaps the page edge. - Recommended next commands (in priority order):
/adaptfor responsive fixes,/polishfor the side-stripe and accent-stripe replacements,/clarifyfor one piece of error microcopy.
Detailed findings¶
[P0] Welcome state — keyboard occlusion at 375px¶
- Location:
apps/patient-app/src/pages/ConversationApp.tsx:815-844 - Category: Responsive
- Impact: When a new patient lands on the welcome state at 375px and taps the input, the iOS keyboard (~290px tall) covers the suggestion chips and pushes the Curaway logo + greeting off-screen. The outer
flex items-center justify-centermeans the layout collapses upward; themin-h-[100svh]parent does not account for100vh - keyboard. Patient cannot see the chips while typing. - Evidence:
- Recommendation: Anchor the InputBar+chips block to the bottom on mobile with
md:items-center justify-end md:justify-center, or use100dvhwithinteractive-widget=resizes-contentviewport meta. Verify with iOS Safari's visualViewport API. - Suggested command:
/adapt
[P1] Banned side-stripe pattern — DocumentChecklistCard rows¶
- Location:
apps/patient-app/src/components/chat/cards/DocumentChecklistCard.tsx:55,61,67,73,79,85,91 - Category: Anti-pattern
- Impact: Rubric explicitly bans side-stripe borders >1px ("BANNED"). The
border-l-2(2px) coloured left rule on every checklist row is the textbook anti-AI tell. Status is already conveyed viaiconBg/iconColorand the row's tonalbg-status-info-bg/30— the 2px stripe is redundant decoration. - Evidence:
- Recommendation: Drop to
border-l border-l-chat-border-strong(1px) OR remove the side rule entirely and rely on the existingiconBgcircle + tonal row background. - Suggested command:
/polish
[P1] Banned side-stripe pattern — ConfirmationCard on-site tests list¶
- Location:
apps/patient-app/src/components/chat/cards/ConfirmationCard.tsx:47 - Category: Anti-pattern
- Impact: Same as above —
border-l-2 border-chat-coral/40 pl-3is a 2px coloured side stripe. - Evidence:
- Recommendation: Replace with
border-l border-chat-coral/40 pl-3(1px) or use a leading bullet/icon. - Suggested command:
/polish
[P1] MSO day-picker — overflow without affordance at 375px¶
- Location:
apps/patient-app/src/components/chat/cards/MSOSchedulingCard.tsx:233-264 - Category: Responsive
- Impact: 7 day-cards × 68px min-width + gaps = ~510px of horizontal scroll content inside a card whose available width at 375px (chat thread inset + card inset) is roughly 320px. Without a scroll shadow, fade-edge, or "Today / +6 days" bookend label, the patient does not know more days exist beyond what's visible.
- Evidence:
- Recommendation: Add
mask-image: linear-gradient(to right, black 90%, transparent)on the right edge OR a faint chevron-right indicator whenscrollLeft + clientWidth < scrollWidth. Also wirearia-hidden="false"on a "scroll for more days" SR-only span. - Suggested command:
/adapt
[P1] InputBar safe-area — bottom inset only on desktop branch¶
- Location:
apps/patient-app/src/pages/ConversationApp.tsx:878 - Category: Responsive
- Impact: The active-chat InputBar wrapper applies safe-area inset only on the desktop branch (
md:pb-[max(8px,env(safe-area-inset-bottom,0px))]) — mobile getspb-2. TheMobileBottomNavcorrectly appliespb-[env(safe-area-inset-bottom)], so on phones with a home indicator the nav clears the gesture area, but the visual rhythm between input bar and nav is uneven (8px on top of nav's safe-area = 8 + 34 = 42px gap on iPhone 15 Pro vs 8px gap on iPhone SE2). - Evidence:
- Recommendation: Use
pb-[max(8px,env(safe-area-inset-bottom,0px))]unconditionally; the bottom-nav already handles its own safe-area. - Suggested command:
/adapt
[P1] Welcome-state file-error close button overlaps page edge¶
- Location:
apps/patient-app/src/pages/ConversationApp.tsx:823-828 - Category: Accessibility
- Impact: The error banner is
mb-2 px-3 py-2 ... flex items-center gap-2and the close button hasmin-w-[44px] min-h-[44px]. At 375px, the parentmax-w-2xlcollapses to viewport width; the close button's 44px touch area extends to the right edge with zero margin. Adjacent edge gestures (back-swipe) compete with the tap. - Evidence: see ConversationApp.tsx:823-828
- Recommendation: Add
pr-2on the banner OR move the close button before the error message and reservemr-2. - Suggested command:
/polish
[P2] MessageThread streaming bubble — aria-hidden race¶
- Location:
apps/patient-app/src/components/chat/MessageThread.tsx:182-227 - Category: Accessibility
- Impact: Two adjacent
transition-opacity motion-fastdivs are toggled together withopacity-0 pointer-events-none h-0 overflow-hiddenand conditionally render their child. Thearia-hiddenflag is set from the same boolean that gates rendering, so SR users see the typing indicator appear/disappear correctly — but during the cross-fade, BOTH containers can briefly be opacity-0 (one transitioning out, the other not yet rendered). aria-live announcements may double-fire on slow devices. - Evidence: see lines 182-227
- Recommendation: Wrap the indicator + batch-panel in a single container with a discriminated union, or move the
motion-fadetoken out and rely on the inner element'saria-busy. - Suggested command:
/animate
[P2] MatchResultsCard — checkbox is visual-only div, not real input¶
- Location:
apps/patient-app/src/components/chat/cards/MatchResultsCard.tsx:71-80 - Category: Accessibility
- Impact: The "checkbox" is a
<div>styled like a checkbox inside a<label>that has no associated<input>. Screen readers announce the row as a label without a control; keyboard users cannot tab to the checkbox itself. TheonClickon the parent<label>carries the toggle, butEnter/Spaceon a focused row does nothing. - Evidence:
- Recommendation: Add a real
<input type="checkbox" className="sr-only peer" checked={isSelected} onChange={...} />and usepeer-focus-visible:ring-2on the visual div. - Suggested command:
/polish
[P2] CurawayAvatar pulse animation ignores prefers-reduced-motion¶
- Location:
apps/patient-app/src/components/chat/MessageBubble.tsx:124 - Category: Accessibility / Performance
- Impact: The
animate-[curaway-pulse_1.5s_ease-in-out_infinite]keyframe runs continuously throughout the streaming session. The global@media (prefers-reduced-motion: reduce)block in index.css covers the named animation classes (drawer-panel,progress-fill, etc.) but does NOT include this arbitrary-value Tailwind class. - Evidence:
- Recommendation: Either add
motion-safe:prefix (Tailwind ships this), or extend the@media (prefers-reduced-motion: reduce)block in index.css to setanimation: none !importanton a[data-streaming="true"]selector and toggle that attr instead. - Suggested command:
/animate
[P2] InputBar — textarea grows up to 160px without internal max-h respecting keyboard¶
- Location:
apps/patient-app/src/components/chat/InputBar.tsx:52-65 - Category: Responsive
- Impact: The textarea grows to
Math.min(scrollHeight, 160)px. At 375px with the iOS keyboard up (~290px viewport), a 160px-tall textarea + ~52px action row + nav = the entire visible area. The patient cannot see what they wrote above the cursor while typing a long message. - Evidence:
- Recommendation: Use
max(80px, min(scrollHeight, 30vh))on mobile, or querywindow.visualViewport.heightand clamp dynamically. - Suggested command:
/adapt
[P2] InputBar — disabled={sending} blocks typing during slow LLM calls¶
- Location:
apps/patient-app/src/components/chat/InputBar.tsx:64 - Category: Responsive / UX
- Impact: Setting
disabled={sending}on the textarea while the assistant is replying means a patient cannot start typing their next question while an answer is streaming. Claude.ai keeps the input enabled and queues the next turn; this UX is less responsive. - Evidence: see line 64
- Recommendation: Disable only the send button while in-flight; let the textarea remain editable so the patient can compose.
- Suggested command:
/clarify(or design discussion via/critique)
[P2] MSO time-picker — short-label chips can dip below 44px tap width¶
- Location:
apps/patient-app/src/components/chat/cards/MSOSchedulingCard.tsx:268-292 - Category: Accessibility
- Impact:
grid-cols-3 sm:grid-cols-4 gap-2withmin-h-[44px]+px-2 py-2produces ~108px-wide chips at 375px (good), but the chip lacksmin-w-[44px]— there are theoretical layouts where a single-row 4-column grid could squeeze below WCAG 2.5.5 target size. - Evidence: line 282 — only
min-h-[44px], nomin-w-[44px]. - Recommendation: Add
min-w-[44px]. - Suggested command:
/polish
[P3] Welcome suggestion chips — hardcoded English flagged in console¶
- Location:
apps/patient-app/src/pages/ConversationApp.tsx:41-53 - Category: Theming / i18n
- Impact: TODO already self-flagged. Not blocking for the audit but a known polish item.
- Recommendation: When react-i18next lands, pipe through
t(). - Suggested command:
/clarify
[P3] FileAttachmentCard — decoration-dotted underline on filename¶
- Location:
apps/patient-app/src/components/chat/FileAttachmentCard.tsx:71 - Category: Theming
- Impact: Dotted underline on the clickable filename is a small typographic tell — uncommon outside of dictionary apps. Not anti-pattern enough to flag higher.
- Recommendation: Switch to
hover:underline underline-offset-2(solid on hover, no underline at rest). - Suggested command:
/polish
[P3] DocumentChecklistCard ○ glyph for unchecked status¶
- Location:
apps/patient-app/src/components/chat/cards/DocumentChecklistCard.tsx:122 - Category: Theming
- Impact: A unicode circle is used where a Lucide
Circleicon would match the rest of the card's iconography. Already noted in code comment that emojis were removed for cross-platform consistency; this last unicode glyph slipped through. - Recommendation: Replace with
<Circle className="w-3 h-3" />from lucide-react. - Suggested command:
/polish
Patterns / systemic issues¶
- Side-stripe overuse for status: DocumentChecklistCard + ConfirmationCard both reach for
border-l-2to differentiate row state. The status is already legible via icon + tonal background; the stripes are decorative. - Mobile-first responsive gaps: Several places (
pb-2vsmd:pb-[env(...)],max-w-3xlinput vsmax-w-[920px]thread, day-picker without scroll affordance) suggest the layout was designed desktop-first then walked down to 375px without an end-to-end mobile pass. - Animation tokens partially honoured:
motion-fast/motion-slowtokens exist and are used, but arbitrary-valueanimate-[...]calls bypass theprefers-reduced-motionblock in index.css. - Touch-target hygiene is generally great:
w-11 h-11(44px) is consistently used on icon buttons in InputBar, MobileBottomNav, MobileModal close, MessageThread NewMessagePill — only the matchresults checkbox-as-div misses it.
Positive findings¶
- CurawayAvatar + brand tokens: Consistent use of
chat-brand,chat-coral,deep-oceansemantic tokens. Almost no hardcoded hex. - MessageBubble memoisation: The custom comparator is surgical and correct — load-bearing fields explicitly listed, callbacks assumed stable. This is the right way to memo a chat bubble.
- In-place server-message merge: The 559-575 merge logic preserving the placeholder ID is excellent — prevents the whole-thread re-render that historically caused the "screen flicker" SD reported.
- MSO wizard step model: The 5-step wizard with explicit
WizardStepunion, back-buttons on every step, persistent cancel link, slot-conflict 409 → bounce-to-time, payment provider routing — this is well-shaped UX. Good error handling, good fallbacks (BE charge block missing → "you'll be invoiced"). - Doctor identity strip: Repeating the doctor photo + name on the confirmation card is exactly the right reassurance pattern for medical UX.
- MobileModal: Radix-backed, properly portals, correct safe-area top + bottom handling, full-screen on mobile (
inset-0) collapsing to centered dialog onmd. aria-live="polite"on the message thread + correct semantic roles on day-picker (role="tablist"/tab) and time-picker (role="radiogroup"/radio) — top-tier accessibility setup.- WCAG-conscious touch targets: 44px is the consistent baseline. NewMessagePill, InputBar buttons, MobileBottomNav, MSO day/time chips all clear it.
Recommended commands (priority order)¶
- [P0/P1] /adapt — Mobile responsive pass: keyboard occlusion on welcome state, day-picker scroll affordance, InputBar safe-area unification, textarea max-height vs visualViewport, MSO chip
min-w-[44px]. This is the single biggest score lift (Responsive 2 → 4). - [P1] /polish — Replace
border-l-2(DocumentChecklistCard ×7, ConfirmationCard ×1), tighten the welcome error banner padding, fix the matchresults checkbox to a real input, swap the○for a LucideCircle. This kills the anti-pattern flags (Anti-patterns 2 → 4). - [P2] /animate — Add
motion-safe:to the avatar pulse, single-container the typing/batch fade, audit any other arbitrary-valueanimate-[...]againstprefers-reduced-motion. - [P2] /clarify — Decide and apply InputBar enabled-while-streaming behaviour; tighten error banner microcopy if needed.
- [P3] /critique — Optional: take the cleaned-up wizard through a UX critique to validate the doctor → time → review → pay → confirm flow at 375px against persona "anxious 60-year-old in UAE".