feat(mobile): one bottom sheet for page comments, not a floating card
What
Replaces the anchored comment popover on apps/mobile page comments with a single bottom sheet, adds a native module for its quick replies, and makes the all-comments screen share the same composer.
| Before | After | |
|---|---|---|
| Surface | floating card placed by popoverPlacement() | one formSheet route |
| Quick replies | icon row inside the card | UIKit buttons with SF Symbols, under the composer |
| Presets | a second sheet pushed from the card | a native UIMenu anchored to the ellipsis |
| Keyboard | keyboardWillShow listeners + a 250ms withTiming settle | UIKit |
| Dismissal | an ✕ plus a pointer-down message from inside the WebView | drag, tap-outside, swipe |
Why
The card had to be placed, and placement kept losing. It started at an assumed ESTIMATED_HEIGHT and moved after onLayout, so the first frame was wrong. It modelled the keyboard by hand. And on a tall block both branches of the flip covered the block being commented on: below didn't fit, above sat on top of it.
A sheet has no placement. It's bottom-anchored at a height that fits its contents, and on iOS 26 a partial detent is Liquid Glass that floats clear of the edges, so the page stays visible behind it.
The native module
apps/mobile/AGENTS.md says a sheet body is RN + uniwind and never @expo/ui SwiftUI content. But an anchored menu is presented by UIKit outside the React Native view tree — JS can't draw one without it clipping to the sheet and hand-imitating the material, the animation and the VoiceOver semantics.
The house answer is a module of our own: alert-prompt wraps UIAlertController, composer already uses UIMenu. So this adds @superset/symbol-button — a UIButton whose content is an SF Symbol, which given items carries a UIMenu with showsMenuAsPrimaryAction. enabled maps to isEnabled, so a disabled quick reply stops taking taps rather than only dimming.
The branch's first cut used @expo/ui and was replaced (486ee6c4d) to honour the rule.
Its event is onTap, not onPress: React Native registers topPress as a direct event, and a view manager declaring the same name as bubbling makes registration throw.
Also in here
- The page header is now glass with no title (
pageScreenOptions→glassHeaderOptions). Four toolbar buttons plus a title read as compressed, and pages carry their own title in the content. The WebView'scontentInsetexists because of that:getBoundingClientRect()inside the document doesn't know about a native content inset, so the pin/highlight overlay starts at the same offset or every pin sits a header's height too high. popoverPlacement(shared with desktop) gains a bottom clamp so a flipped card can't run off the container.- A bun patch drops
expo-observe's podspec from Swift 6 to 5 — under Xcode 26.3 its mutable global state is a fatal diagnostic and blocked every local iOS build. Behaviour-neutral (no Swift-6-only syntax in the package), with a guard test in the house style perpatches/README.md. formatRelativeTimefell back toformat(0), whichnumeric: "always"renders as "in 0s" — a freshly posted comment read as the future for its first second.- Three new i18n ids (quick-reply labels with a
context), translated in all 17 locales. - The design plan is committed under
plans/.
Testing
packages/sharedandpackages/i18nsuites pass;tsc --noEmit, biome andcheck:i18nclean- Driven end to end on the simulator with Maestro against the production API: page → comment mode → block → typed post, thumbs-up, menu preset → pins → all comments → reply. Both native events reach JS and create threads.
Known gaps
- A selection low on the page can sit behind the sheet. Scrolling it into view needs a smooth
scroll-into-viewon the frame protocol; the existingrestore-scrolljumps instantly and locks scrolling for ~1s. - The sheet has no ✕ and no grabber, so swipe-down is the only exit — the one gesture VoiceOver and Switch Control users can't perform.
- On a dark page, Liquid Glass shows page text through the sheet's bottom safe-area band. That band is outside the React view, so it can't be tinted from RN without a visible seam.
- Quick replies dim once a draft exists, with no cue that clearing the field brings them back.