Skip to content

Redesign the mobile app iOS-first with native chrome and Liquid Glass - #2457

Merged
SawyerHood merged 6 commits into
mainfrom
sawyer/mobile-ios-native-redesign
Aug 26, 2026
Merged

Redesign the mobile app iOS-first with native chrome and Liquid Glass#2457
SawyerHood merged 6 commits into
mainfrom
sawyer/mobile-ios-native-redesign

Conversation

@SawyerHood

@SawyerHood SawyerHood commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

What was wrong

The Expo app was a web-parity port: Inter/Fira Code, Hugeicons, an opaque header painted in theme.css colors, bottom sheets for every menu, bordered web cards. It never read as a native iOS app, and on iOS 26 it ignored the system chrome entirely (no large titles, no native bar items or search, no Liquid Glass). Along the way three real bugs surfaced that made the native chrome impossible to adopt: large titles never collapsed (Fabric view flattening turned the screen root into a childless leaf, so UIKit's scroll-view lookup dead-ended), the navigation bar was forced to the light trait (no react-navigation theme was ever provided, so every adaptive material rendered its light flavor over a dark app), and long thread titles ran under the right-hand bar buttons.

What changed

apps/mobile only; no server, daemon, CLI, or wire changes.

Foundation

  • System fonts (SF Pro / Menlo; sans-serif / monospace on Android) and the Apple text ramp in the generated theme. The Google-font packages and the splash font gate are gone.
  • A mobile-only override layer in scripts/generate-native-theme.ts retunes the default palette to iOS values (label, separators, grouped surfaces, systemBlue tint). Tokens stay strings; custom palettes still tint through --canvas/--ink.
  • SF Symbols through the existing Icon on iOS (src/ui/Icon.ios.tsx + sf-symbol-map.ts); Hugeicons stay on Android and for brand marks.
  • @expo/ui 57.0.11 (native segmented control, icon-button menus) and expo-glass-effect as direct dependencies (both pods were already linked via expo-router).
  • Primitives: GroupedSection/GroupedRow/IconBadge, iOS buttons and fields, untinted system switch, UIKit-look sheets, confirmDestructive (Alert), promptName (Alert.prompt), ScreenTitle, GlassSurface, NativeMenu.

Navigation chrome (src/screens/shell/)

  • RootNavigator provides a react-navigation theme derived from the app theme (mode + tokens), so the UINavigationBar trait follows the in-app light/dark mode.
  • iOS 26: inline-title bars are backed by Liquid Glass — native-stack's headerBackground renders HeaderGlass, an oversized, clipped GlassSurface sized from the header-height context (an absolute-fill child of the stack's background view lays out at zero height). Large-title screens keep a native regular blur because the stack debounces their bar height while the title collapses; hidden/opaque bars drop the background. Older iOS keeps the chrome material; Android keeps an opaque bar.
  • Screen renders the ScrollView as the screen root (list branch: collapsable={false}) so UIKit and react-native-screens find the content scroll view and large titles collapse.
  • Stack.Toolbar menus for the workspace switcher, display options, thread actions and settings overflow; Stack.SearchBar (stacked placement) on Home / Plugins / Skills; modal presentation for New project and Connect with their own sheet host.

Screens

  • Home: 17pt rows under contentInsetAdjustmentBehavior="automatic", inset hairlines, SF status glyphs, Mail-style swipe actions, in-place search results.
  • Thread: tinted continuous-corner user bubble, native bar buttons, iOS-style interaction banners and queue, an iMessage-style composer on a floating GlassSurface whose measured height feeds the list inset, scroll indicators and the jump-to-latest pill; prompt chips are glass capsules. The title is capped to the room between the bar items (UIKit centers a custom title view at the laid-out size and never shrinks it).
  • Settings, machines, plugins, skills, projects, connect as inset-grouped screens with icon badges and grouped forms; panel / files / terminal restyled (pill tab bar, iOS search field, keyboard-accessory key bar).

Accessibility finding, verified on device: expo-router Link.Menu/Link.Preview and @expo/ui MenuView remove the wrapped RN subtree from the iOS accessibility tree (VoiceOver and UI automation cannot find the rows). Text-bearing rows and pills therefore use Pressable + the restyled sheets; native menus remain only on labeled icon-only buttons and header items. src/ui/README.md documents the rule.

How you verified

  • pnpm exec turbo run typecheck lint test --filter=@bb/mobile: clean, 127 files / 888 tests pass. New guard tests pin the SF Symbol map, the font tables, and platform neutrality (iOS-only APIs live in *.ios.tsx or behind guards; expo-glass-effect only in GlassSurface.ios.tsx; siblings never import their own basename — the self-import case stack-overflowed every route once).
  • expo export for iOS and Android succeeds.
  • Maestro e2e flows (apps/mobile/e2e) updated for the native header items and run against the seeded harness backend (BB_MOBILE_E2E_PORT=41999 pnpm exec turbo run e2e:mobile-backend --filter=@bb/integration-tests) on an iPhone 17 Pro / iOS 26 simulator.
  • Manual QA on the simulator in light and dark (captures compared by pixel sampling: content measurably blurs through the glass bars in both modes; large titles collapse; lldb confirms the bar trait follows the app mode) and on an iPhone 15 Pro (Release dev-client) across several feedback rounds.
  • A review workflow over the branch found 31 issues (recycled-cell preview crash, modals pushing underneath, a SwiftUI host per row, GroupedRow swallowing the Switch for VoiceOver, keyboard dismiss mode, double bottom insets, …), all fixed.

Still open, tracked for follow-up: an Android emulator pass, an upstream issue for the Link.Menu/MenuView accessibility behavior, a designed dark splash icon, a dimmer search placeholder, and light-mode --subtle-foreground contrast. The branch is behind main and will be rebased before review.

No tracking issue; this started from a design request.

AGENT GENERATED

SawyerHood and others added 6 commits August 26, 2026 01:01
The Expo app was a web-parity port: Inter/Fira Code, Hugeicons, an opaque
header in theme.css colors, bottom sheets for every menu, bordered web cards.
This makes it read as a native iOS app while keeping Android on its existing
paths and custom palettes working.

Foundation
- System fonts (SF Pro / Menlo, sans-serif / monospace on Android); the
  Google-font packages and the splash font gate are gone. Apple text ramp in
  the generated theme (11 · 13 · 15 · 17 · 20 · 22 · 28 · 34).
- A mobile-only override layer in scripts/generate-native-theme.ts retunes
  the default palette to iOS values (label, separators, grouped surfaces,
  systemBlue tint, system reds/greens) and adds --surface-grouped(-cell).
  Tokens stay strings; palettes still tint through --canvas/--ink.
- SF Symbols through the existing Icon on iOS (src/ui/Icon.ios.tsx +
  sf-symbol-map.ts); Hugeicons stay on Android and for brand marks.
- @expo/ui 57.0.11 (exact; its pod was already linked via expo-router) for
  the native segmented control and icon-button menus.
- Primitives: GroupedSection/GroupedRow/IconBadge, iOS buttons, fields,
  untinted system switch, UIKit-look sheets, confirmDestructive (Alert),
  promptName (Alert.prompt), ScreenTitle.

Surfaces
- Transparent headers with large titles on list screens (iOS 26 draws its
  own bar; older iOS gets the chrome material), Stack.Toolbar menus for the
  workspace switcher, display options, thread actions and settings overflow,
  Stack.SearchBar on Home / Plugins / Skills, modal presentation for New
  project and Connect (with their own sheet host).
- Home: 17pt rows under contentInsetAdjustmentBehavior automatic, inset
  hairlines, SF status glyphs, sentence-case sections, Mail-style swipe
  actions, search results in place.
- Thread: tinted continuous-corner user bubble, native bar buttons, iOS-style
  interaction banners and queue, iMessage-style composer with SF buttons and
  capsule option pills.
- Settings, machines, plugins, skills, projects, connect as inset-grouped
  screens with icon badges and grouped forms; panel / files / terminal
  restyled (pill tab bar, iOS search field, keyboard-accessory key bar).

Verified on device: expo-router Link.Menu/Link.Preview and @expo/ui MenuView
remove the wrapped RN subtree from the iOS accessibility tree, so text-bearing
rows and pills use Pressable + the restyled sheets; native menus remain only
on labeled icon-only buttons and header items (src/ui/README.md documents the
rule). New guard tests pin the SF Symbol map, platform neutrality (iOS-only
APIs in *.ios.tsx or behind guards; siblings never import their own basename)
and the font tables. Maestro flows updated for the native header items.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QvMzqL3FUxzxaMxVNjciUc
…quid Glass

On device the list screens scrolled under a static large title and search
bar, and the composer read as a flat card.

Large titles never collapsed because Fabric's view flattening turned the
Screen root View (background + testID) into a childless sibling at
subviews[0]; UIKit and react-native-screens look for the content scroll view
along the first-subview chain, so the lookup dead-ended on every screen
(verified with an lldb recursiveDescription dump). The scroll branch now
renders the ScrollView as the screen root and the list branch keeps its
root with collapsable={false}. With the scroll view adopted, iOS 26 draws
its own glass scroll-edge effect under the compact bar, so the header stays
transparent with no blur material (the Liquid Glass guidance: no custom bar
backgrounds); older iOS keeps the chrome material.

The composer is a GlassSurface (expo-glass-effect GlassView on iOS 26+, the
previous card elsewhere) floating over the list and the timeline: the home
dock and the thread prompt area become transparent overlays whose measured
height feeds the list's bottom inset, scroll indicators and the
jump-to-latest pill; floating banners and the chip stack sit on a raised
opaque panel so forms stay readable over the timeline. expo-glass-effect is
now a direct dependency (its pod was already linked through expo-router);
the platform-neutrality test keeps its import inside the .ios.tsx sibling.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QvMzqL3FUxzxaMxVNjciUc
…meline

iOS 26's default soft scroll-edge effect is a gradient fade that still lets
rows read through the bar. List screens now ask for the hard edge style (the
Liquid Glass guidance for dense lists): an opaque bar region with a hairline
while the large title and bar items stay glass. The thread timeline gets an
opaque inline header with a hairline instead, so nothing scrolls under the
title and status line.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QvMzqL3FUxzxaMxVNjciUc
The bar is now a frosted material instead of opaque or transparent: a chrome
material on the compact bar, with the large-title area transparent at rest.
The adaptive material resolves to its light flavor inside the bar on iOS 26
regardless of the app's scheme, so the dark/light flavor is picked from the
theme mode (which also honors an in-app override); the system scroll-edge
effect is hidden so the two do not stack. The thread timeline uses the same
frosted bar instead of an opaque one.

The prompt chips above the composer no longer sit on a solid panel: each chip
is a Liquid Glass capsule floating over the timeline (foreground-toned detail
text for legibility); only the pending-interaction form and the queued list
keep a raised opaque panel.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QvMzqL3FUxzxaMxVNjciUc
The thread list and timeline bars rendered as a flat slab: with no
react-navigation ThemeProvider the native stack assumed a light theme and
set the UINavigationBar's overrideUserInterfaceStyle to light, so every
adaptive material blurred in its light flavor over the dark app (hence the
pale block behind large titles) and the explicit *Dark flavors had to be
hand-picked. Provide the navigation theme from the app theme (mode +
tokens) so the bar trait follows the in-app mode, then use the adaptive
ultra-thin material: content now measurably blurs through the bar in both
modes instead of sitting under a near-opaque chrome fill.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QvMzqL3FUxzxaMxVNjciUc
The frosted bar still read as a flat tinted band on the phone: a UIKit
blur material over mostly white or black content has nothing to show
through, so it looks like a solid panel next to the composer's glass.
Inline-title bars now get the composer's material: the native stack
renders `headerBackground` as a view sized to the bar behind a transparent
UINavigationBar, and `HeaderGlass` fills it with an oversized, clipped
`GlassSurface` so only the bottom edge of the glass shows. The stack's
absolute-fill background view lays out at zero height, so the clip takes
the bar height from the header-height context explicitly. Large-title
bars keep a native blur (the classic `regular` frost) because the stack
debounces their height while the title collapses; hidden and opaque bars
drop the background.

The thread title view was capped at 240pt: UIKit centers a custom title
view at the size we lay out, so long titles ran under the right-hand
glass group. Cap it to the room between the bar items instead; UIKit
shifts a narrower view off-center to fit.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QvMzqL3FUxzxaMxVNjciUc
@SawyerHood
SawyerHood force-pushed the sawyer/mobile-ios-native-redesign branch from 7323f5d to 8be68d4 Compare August 26, 2026 01:01
@SawyerHood
SawyerHood marked this pull request as ready for review August 26, 2026 01:03
@SawyerHood
SawyerHood merged commit 31d66d9 into main Aug 26, 2026
15 checks passed
@SawyerHood
SawyerHood deleted the sawyer/mobile-ios-native-redesign branch August 26, 2026 02:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant