Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/mobile/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ The shell is set by [ADR-0211](../../docs/adr/0211-the-mobile-app-has-two-tabs-w
**Chat**

- Header: the system navigation header of the root native stack (expo-router `Stack`), so back, the swipe back and the iOS 26 header transitions are the platform's own. It has no bar fill, title text, subtitle or state dot. Leading, from the left: Back, then right beside it the title chip (the Bot's avatar and name, leading-aligned, never centred), which opens the Bot profile. Trailing: one round button for the Bot's computer. Back is the native back button; a plain back control replaces it only when the chat is the first screen of the stack, and returns to Chats.
- iOS: the header is transparent (`headerTransparent`) with no `headerStyle.backgroundColor`, so UIKit draws the bar; a clear colour would remove its material. The chip is a native bar item of type `custom` (`unstable_headerLeftItems`), and `headerTitle` is empty. The computer button, and the plain back control when the chat is the first screen, are native bar buttons with SF Symbols (`macwindow`, `chevron.left`), so UIKit tints them. The chip's text uses the system `label` colour, not `fg`, because the bar's glass picks its own light or dark look from what is under it and from Reduce Transparency; a fixed `fg` went white on a light grey item in dark mode with Reduce Transparency on. A zero-width `spacing` item separates the chip from Back so each gets its own glass shape: a round Back and a separate capsule. iOS 26 draws that Liquid Glass behind the bar items itself, so the items carry no `glassEffect` or fill of their own (that would draw a second, skinny capsule). The chip is capped so the computer button always fits. Before iOS 26 there is no Liquid Glass, and UIKit's own bar material (`headerBlurEffect`) drew a frosted band with a hard bottom edge across the top of the chat. So before iOS 26 the bar has no material at all: it stays transparent, the top edge is the same drawn fade as Android's (see below), and Back, the chip and the computer button are the three opaque islands Android uses (`headerIslands`: `headerGlassFill`'s frosted `raised` fill with a `raised` hairline), as `custom` bar items. Back is the round `HeaderBackButton` in place of UIKit's back button, which sat 8pt from the screen edge; a custom item gets the bar's standard leading margin, as the computer button gets the trailing one.
- iOS: the header is transparent (`headerTransparent`) with no `headerStyle.backgroundColor`, so UIKit draws the bar; a clear colour would remove its material. The chip is a native bar item of type `custom` (`unstable_headerLeftItems`), and `headerTitle` is empty. The computer button, and the plain back control when the chat is the first screen, are native bar buttons with SF Symbols (`macwindow`, `chevron.left`), so UIKit tints them. The chip's text uses the system `label` colour, not `fg`, because the bar's glass picks its own light or dark look from what is under it and from Reduce Transparency; a fixed `fg` went white on a light grey item in dark mode with Reduce Transparency on. A zero-width `spacing` item separates the chip from Back so each gets its own glass shape: a round Back and a separate capsule. iOS 26 draws that Liquid Glass behind the bar items itself, so the items carry no `glassEffect` or fill of their own (that would draw a second, skinny capsule). The chip is capped so the computer button always fits. Before iOS 26 there is no Liquid Glass, and UIKit's own bar material (`headerBlurEffect`) drew a frosted band with a hard bottom edge across the top of the chat. So before iOS 26 the bar has no material at all: it stays transparent, the top edge is the same drawn fade as Android's (see below), and Back, the chip and the computer button are the three islands Android uses (`headerIslands`, `headerGlassFill`), as `custom` bar items, but with the fully opaque `raised` fill and `raised` hairline: there is no blur behind them, so Android's translucent frosted fill let message text show through the title and compete with it on the device. The chip's text there uses `fg`, not the system `label` colour: our own fill follows the app's appearance, and `label` follows the system's, which drew a black name on the dark chip when the app was dark and the system light. Back is the round `HeaderBackButton` in place of UIKit's back button, which sat 8pt from the screen edge; a custom item gets the bar's standard leading margin, as the computer button gets the trailing one.
- Android: the header floats over the messages like iOS's (`headerTransparent`, no bar fill, no shadow), with the same three shapes as Grok's chat header: a round Back, the title chip at the start (`headerTitleAlign: 'left'`) as its own capsule island, and a round computer button, each its own shape `space.x3` apart (the title wrapper's horizontal margin), never touching. Each shape uses the opaque-iOS fallback of the header buttons (`headerGlassFill`: frosted `raised` fill with a `raised` hairline), so the chip reads as an island, not a label in a bar. Back is always this round button (it replaces the native navigation icon; system and predictive back still work). The list pads its own top by the header height, and messages scroll under the header through the drawn top fade (see below). Android has no system scroll edge effect: Material 3 only tints the top app bar on scroll, and the View fading edge (`fadingEdgeLength`) fades content linearly at the scroll view's own bounds, so it cannot keep what passes under the floating chrome hidden. Back's icon is `BackIcon`: a chevron on iOS and the Material `arrow_back` on Android, as in drawer and sheet headers.
- The chip holds, in one row, the Bot's avatar (28pt, round, no state ring) and its name in `body` strong; a group shows its mark and name. It is `theme.touch` tall with padding concentric with its corners. The Bot's state is read out in its accessibility label only. Pressing shows the `raised` pressed fill. Icons in the header are `fg`, never the Bot's accent.
- iOS: messages run under the transparent header and the status bar, and UIKit's scroll edge effect (left on `automatic`) softens them there. The list uses `contentInsetAdjustmentBehavior="automatic"`, so UIKit, not a manual padding, keeps the header's height free above the oldest message at rest; the edge effect needs that automatic inset. The edge effect and React Native Screens both find the content scroll view through the first-subview chain of the screen, so the message list is the first thing in that chain: a default or colour chat background is the frame's own `backgroundColor`, not a view in front of the list. An image background still needs a view behind the list, and with one the soft edge does not reach the list. UIKit also adds the home-indicator safe area to the bottom inset while the list reaches the screen bottom (keyboard closed); the list cancels it with a negative `contentInset.bottom`, because the composer's height already includes that padding. Without this, scrolling to the very end left 34pt more space below the newest message than the resting position. On iOS 26 there is no painted top gradient. A transparent header dimmed the whole message list on iOS 26 while the list was inverted (a scaleY flip); the list now runs top to bottom, which this header depends on.
Expand Down
8 changes: 6 additions & 2 deletions apps/mobile/src/ui/chat/header-chrome.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useId, type ReactNode } from 'react';
import { Pressable, View } from 'react-native';
import { Platform, Pressable, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';

Expand Down Expand Up @@ -69,7 +69,11 @@ export function headerGlassFill(theme: DesignTheme, glass: boolean, pressed: boo
return {
borderWidth: edge.hairline,
borderColor: theme.layer.raised.edge,
backgroundColor: pressed ? theme.layer.raised.pressed : frosted(theme.layer.raised.fill),
backgroundColor: pressed
? theme.layer.raised.pressed
: Platform.OS === 'ios'
? theme.layer.raised.fill
: frosted(theme.layer.raised.fill),
};
}

Expand Down
5 changes: 2 additions & 3 deletions apps/mobile/src/ui/chat/native-header.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useHeaderHeight } from 'expo-router/react-navigation';
import { Platform, PlatformColor, type TextStyle } from 'react-native';

import { floatingHeader } from './header-kind';
import { floatingHeader, glassHeader } from './header-kind';

export { floatingHeader, glassHeader, headerIslands } from './header-kind';

Expand All @@ -15,5 +15,4 @@ export function useHeaderPadding(): number {
return floatingHeader ? height : 0;
}

export const barLabelStyle: TextStyle =
Platform.OS === 'ios' ? { color: PlatformColor('label') } : {};
export const barLabelStyle: TextStyle = glassHeader ? { color: PlatformColor('label') } : {};
Loading