mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(mobile): one button style for the session header and tab strip; size tabs to titles (#29222)
* fix(mobile): put the session tabs in the top bar on a landscape phone A sideways phone is about 390pt tall and the session header spent two rows on it. On a landscape phone the tab strip now shares the top bar with the back button, title and panel icons. Portrait and tablet layouts are unchanged. * fix(mobile): give session tabs side borders and a filled active tab In the landscape top bar the tabs sat between the title and the panel icons with nothing marking where one tab ended. Each tab now has a 1pt border on its sides, running the full height of the bar, and the active tab gets the raised fill, as the desktop tab strip draws them. The same strip is used in portrait, so both orientations match. * fix(mobile): one button style for the session header and tab strip; size tabs to titles The header had three button sizes and the new-tab and quick-commands buttons were bare 16pt glyphs in a 40x36 area with no extra touch room, so "+" was hard to hit. Every header and tab-strip button now uses the same 36pt rounded button with an 18pt icon and a 44pt touch area. Tabs size to their titles (44 to 128pt) instead of a fixed 128pt, the strip row is 44pt, terminal tabs get a terminal icon, and tabs expose the tab role and selected state to screen readers. * fix(mobile): reset tab strip measurements on view attachment * style(mobile): format tab strip ref dependencies * fix(mobile): reserve independent header button touch targets * fix(mobile): type header button test against its component * fix(mobile): keep two header rows in landscape windows too narrow for top-bar tabs A short, narrow window (a ~390x350 web window) is wider than tall, so it counted as a landscape phone and moved the tabs into a top bar that has no width left for them. Top-bar tabs now also need at least 568pt of width, the narrowest landscape phone. * fix(mobile): give the landscape top-bar tab strip a fixed left edge so scrolled tabs never read as part of the title * chore: recompute mergeability after retarget to main
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { createElement, type ReactNode } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { Pressable } from 'react-native'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createFakeBridgePortPair } from '../mobile-web-shell/bridge/bridge-port-pair-test-harness'
|
||||
import { RpcClientProvider } from '../transport/client-context.web'
|
||||
@@ -124,7 +125,9 @@ describe('host sidebar reveal in the session header', () => {
|
||||
}
|
||||
|
||||
function buttons(label: string) {
|
||||
return tree().root.findAllByProps({ accessibilityLabel: label })
|
||||
return tree().root.findAll(
|
||||
(node) => node.type === Pressable && node.props.accessibilityLabel === label
|
||||
)
|
||||
}
|
||||
|
||||
it('offers no reveal in a page beside the native sidebar, which it does not draw', async () => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ChevronLeft, Folder, GitBranch, MoreHorizontal, PanelLeftOpen } from 'l
|
||||
import { MobileSessionHeaderIconButton } from './MobileSessionHeaderIconButton'
|
||||
import { MobileSessionTabStrip } from './MobileSessionTabStrip'
|
||||
import { StatusDot } from '../components/StatusDot'
|
||||
import { colors } from '../theme/mobile-theme'
|
||||
import { styles } from './mobile-session-styles'
|
||||
import { useHostSidebarReveal } from '../layout/host-sidebar-reveal'
|
||||
import { useResponsiveLayout } from '../layout/responsive-layout'
|
||||
@@ -38,25 +37,17 @@ export function MobileSessionHeader({ controller }: { controller: MobileSessionC
|
||||
<SafeAreaView style={styles.sessionChrome} edges={['top']}>
|
||||
<View style={styles.sessionTopBar}>
|
||||
{revealSidebar ? (
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.backButton, pressed && styles.backButtonPressed]}
|
||||
onPress={revealSidebar}
|
||||
hitSlop={8}
|
||||
accessibilityRole="button"
|
||||
<MobileSessionHeaderIconButton
|
||||
accessibilityLabel="Show sidebar"
|
||||
>
|
||||
<PanelLeftOpen size={18} color={colors.textSecondary} />
|
||||
</Pressable>
|
||||
icon={PanelLeftOpen}
|
||||
onPress={revealSidebar}
|
||||
/>
|
||||
) : null}
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.backButton, pressed && styles.backButtonPressed]}
|
||||
onPress={requestLeaveSession}
|
||||
hitSlop={8}
|
||||
accessibilityRole="button"
|
||||
<MobileSessionHeaderIconButton
|
||||
accessibilityLabel="Back to worktrees"
|
||||
>
|
||||
<ChevronLeft size={22} color={colors.textSecondary} strokeWidth={2.2} />
|
||||
</Pressable>
|
||||
icon={ChevronLeft}
|
||||
onPress={requestLeaveSession}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={[styles.sessionTitleBlock, tabsInTopBar && styles.sessionTitleBlockBesideTabs]}
|
||||
|
||||
@@ -11,13 +11,16 @@ type HeaderIconProps = {
|
||||
|
||||
type MobileSessionHeaderIconButtonProps = {
|
||||
active?: boolean
|
||||
disabled?: boolean
|
||||
accessibilityLabel: string
|
||||
icon: ComponentType<HeaderIconProps>
|
||||
onPress: () => void
|
||||
}
|
||||
|
||||
// Why: every header and tab-strip button shares this one look and a 44pt touch area (36pt + 4pt slop each side).
|
||||
export function MobileSessionHeaderIconButton({
|
||||
active = false,
|
||||
disabled = false,
|
||||
accessibilityLabel,
|
||||
icon: Icon,
|
||||
onPress
|
||||
@@ -25,12 +28,15 @@ export function MobileSessionHeaderIconButton({
|
||||
return (
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
styles.filesButton,
|
||||
pressed && styles.filesButtonPressed,
|
||||
active && styles.filesButtonActive
|
||||
styles.headerButton,
|
||||
pressed && styles.headerButtonPressed,
|
||||
active && styles.headerButtonActive,
|
||||
disabled && styles.headerButtonDisabled
|
||||
]}
|
||||
disabled={disabled}
|
||||
onPress={onPress}
|
||||
hitSlop={8}
|
||||
hitSlop={4}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
>
|
||||
<Icon size={18} color={colors.textSecondary} strokeWidth={2.1} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from 'react'
|
||||
import { View, Text, ScrollView, Pressable } from 'react-native'
|
||||
import { File, FileText, Globe, Plus } from 'lucide-react-native'
|
||||
import { File, FileText, Globe, Plus, SquareTerminal } from 'lucide-react-native'
|
||||
import { triggerMediumImpact } from '../platform/haptics'
|
||||
import { MobileAgentIcon } from '../components/MobileAgentIcon'
|
||||
import {
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
resolveMobileTerminalTabAgentId
|
||||
} from './mobile-terminal-tab-agent'
|
||||
import { colors } from '../theme/mobile-theme'
|
||||
import { MobileSessionHeaderIconButton } from './MobileSessionHeaderIconButton'
|
||||
import { QuickCommandsTabButton } from './QuickCommandsTabButton'
|
||||
import { styles } from './mobile-session-styles'
|
||||
import { useKeyboardPersistingTaps } from '../platform/keyboard-persisting-taps'
|
||||
@@ -100,6 +101,9 @@ export function MobileSessionTabStrip({
|
||||
}
|
||||
}}
|
||||
onPress={() => switchSessionTab(t)}
|
||||
accessibilityRole="tab"
|
||||
accessibilityLabel={getMobileSessionTabTitle(t)}
|
||||
accessibilityState={{ selected: t.id === activeSessionTabId }}
|
||||
onLongPress={() => {
|
||||
triggerMediumImpact()
|
||||
openSessionTabActionSheetAfterKeyboardDismiss(t)
|
||||
@@ -120,7 +124,11 @@ export function MobileSessionTabStrip({
|
||||
{t.type === 'terminal' &&
|
||||
(() => {
|
||||
const agentId = resolveMobileTerminalTabAgentId(t)
|
||||
return agentId ? <MobileAgentIcon agentId={agentId} size={13} /> : null
|
||||
return agentId ? (
|
||||
<MobileAgentIcon agentId={agentId} size={13} />
|
||||
) : (
|
||||
<SquareTerminal size={13} color={colors.textSecondary} strokeWidth={2.1} />
|
||||
)
|
||||
})()}
|
||||
<Text
|
||||
style={[styles.tabText, t.id === activeSessionTabId && styles.tabTextActive]}
|
||||
@@ -133,22 +141,15 @@ export function MobileSessionTabStrip({
|
||||
))}
|
||||
</ScrollView>
|
||||
{/* Why: pinned outside the scroll strip so the new-agent button stays reachable however far the tabs scroll. */}
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
styles.newTerminalButton,
|
||||
pressed && styles.newTerminalButtonPressed,
|
||||
(creating || creatingBrowser || creatingMarkdown || connState !== 'connected') &&
|
||||
styles.newTerminalButtonDisabled
|
||||
]}
|
||||
<MobileSessionHeaderIconButton
|
||||
disabled={creating || creatingBrowser || creatingMarkdown || connState !== 'connected'}
|
||||
onPress={() => {
|
||||
setCreateError('')
|
||||
setShowCreateTabDrawer(true)
|
||||
}}
|
||||
accessibilityLabel="New tab"
|
||||
>
|
||||
<Plus size={16} color={colors.textSecondary} strokeWidth={2.2} />
|
||||
</Pressable>
|
||||
icon={Plus}
|
||||
/>
|
||||
{/* Why: stable placement matters, while old hosts must stay gated because they strip agentPrompt. */}
|
||||
<QuickCommandsTabButton
|
||||
disabled={creating || creatingBrowser || creatingMarkdown || connState !== 'connected'}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Pressable, View } from 'react-native'
|
||||
import { View } from 'react-native'
|
||||
import { SquareChevronRight } from 'lucide-react-native'
|
||||
|
||||
import { colors } from '../theme/mobile-theme'
|
||||
import { MobileSessionHeaderIconButton } from './MobileSessionHeaderIconButton'
|
||||
import { styles } from './mobile-session-styles'
|
||||
|
||||
type Props = {
|
||||
@@ -13,18 +13,12 @@ export function QuickCommandsTabButton({ disabled, onPress }: Props) {
|
||||
return (
|
||||
<>
|
||||
<View style={styles.tabActionDivider} />
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
styles.newTerminalButton,
|
||||
pressed && styles.newTerminalButtonPressed,
|
||||
disabled && styles.newTerminalButtonDisabled
|
||||
]}
|
||||
<MobileSessionHeaderIconButton
|
||||
disabled={disabled}
|
||||
onPress={onPress}
|
||||
accessibilityLabel="Quick commands"
|
||||
>
|
||||
<SquareChevronRight size={16} color={colors.textSecondary} strokeWidth={2.2} />
|
||||
</Pressable>
|
||||
icon={SquareChevronRight}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,32 +32,25 @@ export const mobileSessionFrameStyles = StyleSheet.create({
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: spacing.xs
|
||||
},
|
||||
backButton: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: spacing.xs
|
||||
},
|
||||
backButtonPressed: {
|
||||
backgroundColor: colors.bgRaised
|
||||
},
|
||||
filesButton: {
|
||||
// Each 36pt visual reserves 4pt per side for its 44pt touch area.
|
||||
headerButton: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: radii.button,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: spacing.xs
|
||||
marginHorizontal: spacing.xs
|
||||
},
|
||||
filesButtonPressed: {
|
||||
headerButtonPressed: {
|
||||
backgroundColor: colors.bgRaised
|
||||
},
|
||||
// Selected state for the active docked-panel icon on wide layouts (R2).
|
||||
filesButtonActive: {
|
||||
headerButtonActive: {
|
||||
backgroundColor: colors.bgRaised
|
||||
},
|
||||
headerButtonDisabled: {
|
||||
opacity: 0.45
|
||||
},
|
||||
sessionTitleBlock: {
|
||||
flex: 1,
|
||||
minWidth: 0
|
||||
@@ -106,7 +99,7 @@ export const mobileSessionFrameStyles = StyleSheet.create({
|
||||
},
|
||||
tabScroll: {
|
||||
flex: 1,
|
||||
maxHeight: 36
|
||||
maxHeight: 44
|
||||
},
|
||||
tabScrollInTopBar: {
|
||||
alignSelf: 'stretch',
|
||||
@@ -119,10 +112,11 @@ export const mobileSessionFrameStyles = StyleSheet.create({
|
||||
tabContentInTopBar: {
|
||||
paddingLeft: 0
|
||||
},
|
||||
// Sized to the title (capped), so short titles don't waste width.
|
||||
tab: {
|
||||
width: 128,
|
||||
minWidth: 44,
|
||||
maxWidth: 128,
|
||||
minHeight: 36,
|
||||
minHeight: 44,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: spacing.sm,
|
||||
@@ -157,20 +151,9 @@ export const mobileSessionFrameStyles = StyleSheet.create({
|
||||
tabTextActive: {
|
||||
color: colors.textPrimary
|
||||
},
|
||||
newTerminalButton: {
|
||||
width: 40,
|
||||
height: 36,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
newTerminalButtonPressed: {
|
||||
backgroundColor: colors.bgRaised
|
||||
},
|
||||
newTerminalButtonDisabled: {
|
||||
opacity: 0.45
|
||||
},
|
||||
// Divider between the + new-terminal button and the Quick Commands launcher,
|
||||
// matching the tab strip's borderSubtle separators.
|
||||
tabActionDivider: {
|
||||
|
||||
Reference in New Issue
Block a user