Skip to content

Commit 28fb6ca

Browse files
Jinwoo-Horca-ide
andcommitted
Improve mobile UI polish
- BottomDrawer: shift drawer up when keyboard opens using Keyboard events, add ScrollView for content overflow - NewWorktreeModal: replace FlatList with plain map to avoid nested VirtualizedList error - Home: replace Server icon with Monitor, hide IP from cards (show in action sheet), add connection status subtitle, update hero copy - About: remove redundant link labels, use neutral text color for URLs Co-authored-by: Orca <help@stably.ai>
1 parent 2b929fc commit 28fb6ca

4 files changed

Lines changed: 83 additions & 44 deletions

File tree

mobile/app/about.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ export default function AboutScreen() {
4747
onPress={() => void Linking.openURL('https://onOrca.dev')}
4848
>
4949
<Globe size={16} color={colors.textSecondary} />
50-
<Text style={styles.rowLabel}>Website</Text>
5150
<Text style={styles.rowValue}>onOrca.dev</Text>
5251
</Pressable>
5352
<View style={styles.separator} />
@@ -56,7 +55,6 @@ export default function AboutScreen() {
5655
onPress={() => void Linking.openURL('https://github.com/stablyai/orca')}
5756
>
5857
<GithubIcon />
59-
<Text style={styles.rowLabel}>GitHub</Text>
6058
<Text style={styles.rowValue}>stablyai/orca</Text>
6159
</Pressable>
6260
<View style={styles.separator} />
@@ -65,7 +63,6 @@ export default function AboutScreen() {
6563
onPress={() => void Linking.openURL('https://x.com/orca_build')}
6664
>
6765
<XIcon />
68-
<Text style={styles.rowLabel}>X</Text>
6966
<Text style={styles.rowValue}>@orca_build</Text>
7067
</Pressable>
7168
</View>
@@ -135,8 +132,10 @@ const styles = StyleSheet.create({
135132
color: colors.textPrimary
136133
},
137134
rowValue: {
135+
flex: 1,
136+
textAlign: 'right',
138137
fontSize: typography.bodySize,
139-
color: colors.accentBlue
138+
color: colors.textSecondary
140139
},
141140
separator: {
142141
height: StyleSheet.hairlineWidth,

mobile/app/index.tsx

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useState, useCallback, useEffect, useMemo } from 'react'
22
import { View, Text, StyleSheet, Pressable, FlatList } from 'react-native'
33
import { SafeAreaView } from 'react-native-safe-area-context'
44
import { useRouter, useFocusEffect } from 'expo-router'
5-
import { MoreHorizontal, QrCode, Server, Settings } from 'lucide-react-native'
5+
import { Monitor, MoreHorizontal, QrCode, Settings } from 'lucide-react-native'
66
import { loadHosts, removeHost, renameHost } from '../src/transport/host-store'
77
import { connect } from '../src/transport/rpc-client'
88
import type { ConnectionState, HostProfile } from '../src/transport/types'
@@ -22,6 +22,15 @@ function endpointLabel(endpoint: string): string {
2222
}
2323
}
2424

25+
const STATUS_LABELS: Record<ConnectionState, string> = {
26+
connected: 'Connected',
27+
connecting: 'Connecting…',
28+
disconnected: 'Disconnected',
29+
reconnecting: 'Reconnecting…',
30+
handshaking: 'Connecting…',
31+
'auth-failed': 'Auth failed'
32+
}
33+
2534
export default function HomeScreen() {
2635
const router = useRouter()
2736
const [hosts, setHosts] = useState<HostProfile[]>([])
@@ -106,8 +115,8 @@ export default function HomeScreen() {
106115
contentContainerStyle={styles.list}
107116
ListHeaderComponent={
108117
<View style={styles.hero}>
109-
<Text style={styles.heroTitle}>Desktops</Text>
110-
<Text style={styles.heroSubtitle}>Continue where your agents are running.</Text>
118+
<Text style={styles.heroTitle}>Paired Desktops</Text>
119+
<Text style={styles.heroSubtitle}>Monitor workspaces and terminals remotely.</Text>
111120
</View>
112121
}
113122
ItemSeparatorComponent={() => <View style={styles.cardGap} />}
@@ -122,7 +131,7 @@ export default function HomeScreen() {
122131
delayLongPress={400}
123132
>
124133
<View style={styles.hostIcon}>
125-
<Server size={18} color={colors.textPrimary} />
134+
<Monitor size={18} color={colors.textPrimary} />
126135
</View>
127136
<View style={styles.hostMain}>
128137
<View style={styles.hostTitleRow}>
@@ -131,8 +140,8 @@ export default function HomeScreen() {
131140
{item.name}
132141
</Text>
133142
</View>
134-
<Text style={styles.hostEndpoint} numberOfLines={1}>
135-
{endpointLabel(item.endpoint)}
143+
<Text style={styles.hostStatus} numberOfLines={1}>
144+
{STATUS_LABELS[hostStates[item.id] ?? 'connecting']}
136145
</Text>
137146
</View>
138147
<Pressable
@@ -147,7 +156,7 @@ export default function HomeScreen() {
147156
ListFooterComponent={
148157
<Pressable style={styles.pairCard} onPress={() => router.push('/pair-scan')}>
149158
<View style={styles.pairIcon}>
150-
<QrCode size={18} color={colors.accentBlue} />
159+
<QrCode size={18} color={colors.textSecondary} />
151160
</View>
152161
<View style={styles.pairTextBlock}>
153162
<Text style={styles.pairTitle}>Pair another desktop</Text>
@@ -161,6 +170,7 @@ export default function HomeScreen() {
161170
<ActionSheetModal
162171
visible={actionTarget != null}
163172
title={actionTarget?.name}
173+
message={actionTarget ? endpointLabel(actionTarget.endpoint) : undefined}
164174
actions={[
165175
{
166176
label: 'Rename',
@@ -288,15 +298,13 @@ const styles = StyleSheet.create({
288298
},
289299
heroTitle: {
290300
color: colors.textPrimary,
291-
fontSize: 28,
301+
fontSize: 24,
292302
fontWeight: '800'
293303
},
294304
heroSubtitle: {
295-
color: colors.textSecondary,
305+
color: colors.textMuted,
296306
fontSize: typography.bodySize,
297-
lineHeight: 20,
298-
marginTop: spacing.xs,
299-
maxWidth: 320
307+
marginTop: spacing.xs
300308
},
301309
cardGap: {
302310
height: spacing.sm
@@ -309,9 +317,7 @@ const styles = StyleSheet.create({
309317
paddingVertical: spacing.sm,
310318
minHeight: 70,
311319
borderRadius: radii.row,
312-
backgroundColor: colors.bgPanel,
313-
borderWidth: 1,
314-
borderColor: colors.borderSubtle
320+
backgroundColor: colors.bgPanel
315321
},
316322
hostCardPressed: {
317323
backgroundColor: colors.bgRaised
@@ -349,10 +355,9 @@ const styles = StyleSheet.create({
349355
justifyContent: 'center',
350356
marginLeft: spacing.xs
351357
},
352-
hostEndpoint: {
353-
color: colors.textSecondary,
358+
hostStatus: {
359+
color: colors.textMuted,
354360
fontSize: 13,
355-
fontFamily: typography.monoFamily,
356361
marginTop: spacing.xs
357362
},
358363
pairCard: {
@@ -364,17 +369,15 @@ const styles = StyleSheet.create({
364369
paddingRight: spacing.md,
365370
paddingVertical: spacing.sm,
366371
borderRadius: radii.row,
367-
backgroundColor: colors.bgBase,
368-
borderWidth: 1,
369-
borderColor: colors.borderSubtle
372+
backgroundColor: colors.bgPanel
370373
},
371374
pairIcon: {
372375
width: 44,
373376
height: 44,
374377
borderRadius: 11,
375378
alignItems: 'center',
376379
justifyContent: 'center',
377-
backgroundColor: colors.accentBlue + '18',
380+
backgroundColor: colors.bgRaised,
378381
marginRight: spacing.md
379382
},
380383
pairTextBlock: {

mobile/src/components/BottomDrawer.tsx

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import { type ReactNode, useCallback, useEffect } from 'react'
2-
import { Modal, View, Pressable, StyleSheet, Platform, useWindowDimensions } from 'react-native'
2+
import {
3+
Modal,
4+
View,
5+
Pressable,
6+
StyleSheet,
7+
Platform,
8+
useWindowDimensions,
9+
ScrollView,
10+
Keyboard
11+
} from 'react-native'
312
import { useSafeAreaInsets } from 'react-native-safe-area-context'
413
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'
514
import Animated, {
@@ -29,6 +38,7 @@ type Props = {
2938
export function BottomDrawer({ visible, onClose, children }: Props) {
3039
const translateY = useSharedValue(0)
3140
const backdropOpacity = useSharedValue(0)
41+
const keyboardOffset = useSharedValue(0)
3242
const { height: screenHeight } = useWindowDimensions()
3343
const insets = useSafeAreaInsets()
3444

@@ -39,6 +49,31 @@ export function BottomDrawer({ visible, onClose, children }: Props) {
3949
}
4050
}, [visible])
4151

52+
// Why: KeyboardAvoidingView and useAnimatedKeyboard are both unreliable
53+
// inside Modal (iOS ignores KAV; Android needs adjustNothing for
54+
// useAnimatedKeyboard). Keyboard event listeners work on both platforms
55+
// and give us the exact height to shift the drawer by.
56+
useEffect(() => {
57+
if (!visible) return
58+
59+
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'
60+
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'
61+
62+
const onShow = Keyboard.addListener(showEvent, (e) => {
63+
const height = e.endCoordinates.height - insets.bottom
64+
keyboardOffset.value = withTiming(Math.max(height, 0), { duration: e.duration || 250 })
65+
})
66+
const onHide = Keyboard.addListener(hideEvent, (e) => {
67+
keyboardOffset.value = withTiming(0, { duration: e.duration || 250 })
68+
})
69+
70+
return () => {
71+
onShow.remove()
72+
onHide.remove()
73+
keyboardOffset.value = 0
74+
}
75+
}, [visible, insets.bottom])
76+
4277
const dismiss = useCallback(() => {
4378
onClose()
4479
}, [onClose])
@@ -66,7 +101,7 @@ export function BottomDrawer({ visible, onClose, children }: Props) {
66101
})
67102

68103
const drawerStyle = useAnimatedStyle(() => ({
69-
transform: [{ translateY: translateY.value }]
104+
transform: [{ translateY: translateY.value - keyboardOffset.value }]
70105
}))
71106

72107
const backdropStyle = useAnimatedStyle(() => ({
@@ -86,7 +121,13 @@ export function BottomDrawer({ visible, onClose, children }: Props) {
86121
style={[styles.drawer, { paddingBottom: insets.bottom + spacing.lg }, drawerStyle]}
87122
>
88123
<View style={styles.handle} />
89-
{children}
124+
<ScrollView
125+
bounces={false}
126+
keyboardShouldPersistTaps="handled"
127+
showsVerticalScrollIndicator={false}
128+
>
129+
{children}
130+
</ScrollView>
90131
<View style={styles.bottomExtension} />
91132
</Animated.View>
92133
</GestureDetector>
@@ -135,10 +176,10 @@ const styles = StyleSheet.create({
135176
},
136177
bottomExtension: {
137178
position: 'absolute',
138-
bottom: -100,
179+
bottom: -500,
139180
left: 0,
140181
right: 0,
141-
height: 100,
182+
height: 500,
142183
backgroundColor: colors.bgBase
143184
}
144185
})

mobile/src/components/NewWorktreeModal.tsx

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import {
88
StyleSheet,
99
Platform,
1010
ActivityIndicator,
11-
Image,
12-
FlatList
11+
Image
1312
} from 'react-native'
1413
import { ChevronDown, ChevronUp, Check, Terminal } from 'lucide-react-native'
1514
import Svg, { Path, G } from 'react-native-svg'
@@ -215,14 +214,11 @@ function PickerListModal<T extends { id: string; label: string }>({
215214
<Text style={styles.pickerTitle}>{title}</Text>
216215
</View>
217216
<View style={styles.pickerGroup}>
218-
<FlatList
219-
data={items}
220-
keyExtractor={(item) => item.id}
221-
style={styles.pickerList}
222-
ItemSeparatorComponent={() => <View style={styles.pickerSeparator} />}
223-
renderItem={({ item }) => {
224-
const selected = item.id === selectedId
225-
return (
217+
{items.map((item, index) => {
218+
const selected = item.id === selectedId
219+
return (
220+
<View key={item.id}>
221+
{index > 0 && <View style={styles.pickerSeparator} />}
226222
<Pressable
227223
style={({ pressed }) => [styles.pickerItem, pressed && styles.pickerItemPressed]}
228224
onPress={() => {
@@ -239,9 +235,9 @@ function PickerListModal<T extends { id: string; label: string }>({
239235
</Text>
240236
{selected && <Check size={14} color={colors.textPrimary} />}
241237
</Pressable>
242-
)
243-
}}
244-
/>
238+
</View>
239+
)
240+
})}
245241
</View>
246242
</BottomDrawer>
247243
)

0 commit comments

Comments
 (0)