Files
paseo/packages/app/src/components/left-sidebar.tsx
T

891 lines
27 KiB
TypeScript

import {
memo,
useCallback,
useMemo,
useState,
useEffect,
useRef,
useSyncExternalStore,
type Dispatch,
type RefObject,
type SetStateAction,
} from 'react'
import { View, Pressable, Text, Platform } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import Animated, {
useAnimatedStyle,
interpolate,
Extrapolation,
runOnJS,
useSharedValue,
} from 'react-native-reanimated'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
import { MessagesSquare, Plus, Settings } from 'lucide-react-native'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { Shortcut } from '@/components/ui/shortcut'
import { router, usePathname } from 'expo-router'
import { usePanelStore } from '@/stores/panel-store'
import { SidebarWorkspaceList } from './sidebar-workspace-list'
import { SidebarAgentListSkeleton } from './sidebar-agent-list-skeleton'
import { useSidebarShortcutModel } from '@/hooks/use-sidebar-shortcut-model'
import { useSidebarWorkspacesList, type SidebarProjectEntry } from '@/hooks/use-sidebar-workspaces-list'
import { useSidebarAnimation } from '@/contexts/sidebar-animation-context'
import { useDesktopDragHandlers, useTrafficLightPadding } from '@/utils/desktop-window'
import { Combobox } from '@/components/ui/combobox'
import { getHostRuntimeStore, useHosts } from '@/runtime/host-runtime'
import { formatConnectionStatus } from '@/utils/daemons'
import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE } from '@/constants/layout'
import {
buildHostAgentsRoute,
buildHostSettingsRoute,
mapPathnameToServer,
parseServerIdFromPathname,
} from '@/utils/host-routes'
import { useOpenProjectPicker } from '@/hooks/use-open-project-picker'
const DESKTOP_SIDEBAR_WIDTH = 320
type SidebarShortcutModel = ReturnType<typeof useSidebarShortcutModel>
type SidebarTheme = ReturnType<typeof useUnistyles>['theme']
interface LeftSidebarProps {
selectedAgentId?: string
}
interface HostOption {
id: string
label: string
description: string
}
interface SidebarSharedProps {
theme: SidebarTheme
activeServerId: string | null
activeHostLabel: string
activeHostStatusColor: string
hostOptions: HostOption[]
hostTriggerRef: RefObject<View | null>
isHostPickerOpen: boolean
setIsHostPickerOpen: Dispatch<SetStateAction<boolean>>
projects: SidebarProjectEntry[]
isInitialLoad: boolean
isRevalidating: boolean
isManualRefresh: boolean
collapsedProjectKeys: SidebarShortcutModel['collapsedProjectKeys']
shortcutIndexByWorkspaceKey: SidebarShortcutModel['shortcutIndexByWorkspaceKey']
toggleProjectCollapsed: SidebarShortcutModel['toggleProjectCollapsed']
setProjectCollapsed: SidebarShortcutModel['setProjectCollapsed']
handleRefresh: () => void
handleHostSelect: (nextServerId: string) => void
handleOpenProject: () => void
handleSettings: () => void
}
interface MobileSidebarProps extends SidebarSharedProps {
insetsTop: number
insetsBottom: number
isOpen: boolean
closeToAgent: () => void
handleViewMoreNavigate: () => void
}
interface DesktopSidebarProps extends SidebarSharedProps {
isOpen: boolean
handleViewMore: () => void
}
export const LeftSidebar = memo(function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarProps) {
void _selectedAgentId
const { theme } = useUnistyles()
const insets = useSafeAreaInsets()
const isMobile = UnistylesRuntime.breakpoint === 'xs' || UnistylesRuntime.breakpoint === 'sm'
const mobileView = usePanelStore((state) => state.mobileView)
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen)
const closeToAgent = usePanelStore((state) => state.closeToAgent)
const pathname = usePathname()
const daemons = useHosts()
const runtime = getHostRuntimeStore()
const runtimeConnectionStatusSignature = useSyncExternalStore(
(onStoreChange) => runtime.subscribeAll(onStoreChange),
() =>
daemons
.map(
(daemon) =>
`${daemon.serverId}:${
runtime.getSnapshot(daemon.serverId)?.connectionStatus ?? 'connecting'
}`
)
.join('|'),
() =>
daemons
.map(
(daemon) =>
`${daemon.serverId}:${
runtime.getSnapshot(daemon.serverId)?.connectionStatus ?? 'connecting'
}`
)
.join('|')
)
const activeServerIdFromPath = useMemo(() => parseServerIdFromPathname(pathname), [pathname])
const activeDaemon = useMemo(() => {
if (daemons.length === 0) {
return null
}
if (activeServerIdFromPath) {
const routeMatch = daemons.find((entry) => entry.serverId === activeServerIdFromPath)
if (routeMatch) {
return routeMatch
}
}
return daemons[0] ?? null
}, [activeServerIdFromPath, daemons])
const activeServerId = activeDaemon?.serverId ?? null
const activeHostLabel = useMemo(() => {
if (!activeDaemon) return 'No host'
const trimmed = activeDaemon.label?.trim()
return trimmed && trimmed.length > 0 ? trimmed : activeDaemon.serverId
}, [activeDaemon])
const activeHostStatus = activeServerId
? (runtime.getSnapshot(activeServerId)?.connectionStatus ?? 'connecting')
: 'idle'
const activeHostStatusColor =
activeHostStatus === 'online'
? theme.colors.palette.green[400]
: activeHostStatus === 'connecting'
? theme.colors.palette.amber[500]
: theme.colors.palette.red[500]
const hostOptions = useMemo(
() =>
daemons.map((daemon) => ({
id: daemon.serverId,
label: daemon.label?.trim() || daemon.serverId,
description: formatConnectionStatus(
runtime.getSnapshot(daemon.serverId)?.connectionStatus ?? 'connecting'
),
})),
[daemons, runtime, runtimeConnectionStatusSignature]
)
const hostTriggerRef = useRef<View | null>(null)
const [isHostPickerOpen, setIsHostPickerOpen] = useState(false)
const isOpen = isMobile ? mobileView === 'agent-list' : desktopAgentListOpen
const { projects, isInitialLoad, isRevalidating, refreshAll } = useSidebarWorkspacesList({
serverId: activeServerId,
enabled: isOpen,
})
const { collapsedProjectKeys, shortcutIndexByWorkspaceKey, toggleProjectCollapsed, setProjectCollapsed } =
useSidebarShortcutModel(projects)
const [isManualRefresh, setIsManualRefresh] = useState(false)
const handleRefresh = useCallback(() => {
setIsManualRefresh(true)
refreshAll()
}, [refreshAll])
useEffect(() => {
if (!isRevalidating && isManualRefresh) {
setIsManualRefresh(false)
}
}, [isRevalidating, isManualRefresh])
const openProjectPicker = useOpenProjectPicker(activeServerId)
const handleOpenProjectMobile = useCallback(() => {
closeToAgent()
void openProjectPicker()
}, [closeToAgent, openProjectPicker])
const handleOpenProjectDesktop = useCallback(() => {
void openProjectPicker()
}, [openProjectPicker])
const handleSettingsMobile = useCallback(() => {
if (!activeServerId) {
return
}
closeToAgent()
router.push(buildHostSettingsRoute(activeServerId) as any)
}, [activeServerId, closeToAgent])
const handleSettingsDesktop = useCallback(() => {
if (!activeServerId) {
return
}
router.push(buildHostSettingsRoute(activeServerId) as any)
}, [activeServerId])
const handleViewMoreNavigate = useCallback(() => {
if (!activeServerId) {
return
}
router.push(buildHostAgentsRoute(activeServerId) as any)
}, [activeServerId])
const handleHostSelect = useCallback(
(nextServerId: string) => {
if (!nextServerId) {
return
}
const nextPath = mapPathnameToServer(pathname, nextServerId)
setIsHostPickerOpen(false)
router.push(nextPath as any)
},
[pathname]
)
const sharedProps = {
theme,
activeServerId,
activeHostLabel,
activeHostStatusColor,
hostOptions,
hostTriggerRef,
isHostPickerOpen,
setIsHostPickerOpen,
projects,
isInitialLoad,
isRevalidating,
isManualRefresh,
collapsedProjectKeys,
shortcutIndexByWorkspaceKey,
toggleProjectCollapsed,
setProjectCollapsed,
handleRefresh,
handleHostSelect,
}
if (isMobile) {
return (
<MobileSidebar
{...sharedProps}
insetsTop={insets.top}
insetsBottom={insets.bottom}
isOpen={isOpen}
closeToAgent={closeToAgent}
handleOpenProject={handleOpenProjectMobile}
handleSettings={handleSettingsMobile}
handleViewMoreNavigate={handleViewMoreNavigate}
/>
)
}
return (
<DesktopSidebar
{...sharedProps}
isOpen={isOpen}
handleOpenProject={handleOpenProjectDesktop}
handleSettings={handleSettingsDesktop}
handleViewMore={handleViewMoreNavigate}
/>
)
})
function MobileSidebar({
theme,
activeServerId,
activeHostLabel,
activeHostStatusColor,
hostOptions,
hostTriggerRef,
isHostPickerOpen,
setIsHostPickerOpen,
projects,
isInitialLoad,
isRevalidating,
isManualRefresh,
collapsedProjectKeys,
shortcutIndexByWorkspaceKey,
toggleProjectCollapsed,
setProjectCollapsed,
handleRefresh,
handleHostSelect,
handleOpenProject,
handleSettings,
insetsTop,
insetsBottom,
isOpen,
closeToAgent,
handleViewMoreNavigate,
}: MobileSidebarProps) {
const {
translateX,
backdropOpacity,
windowWidth,
animateToOpen,
animateToClose,
isGesturing,
closeGestureRef,
} = useSidebarAnimation()
const closeTouchStartX = useSharedValue(0)
const closeTouchStartY = useSharedValue(0)
const handleClose = useCallback(() => {
closeToAgent()
}, [closeToAgent])
const handleViewMore = useCallback(() => {
if (!activeServerId) {
return
}
translateX.value = -windowWidth
backdropOpacity.value = 0
closeToAgent()
handleViewMoreNavigate()
}, [
activeServerId,
backdropOpacity,
closeToAgent,
handleViewMoreNavigate,
translateX,
windowWidth,
])
const closeGesture = useMemo(
() =>
Gesture.Pan()
.withRef(closeGestureRef)
.enabled(isOpen)
.manualActivation(true)
.onTouchesDown((event) => {
const touch = event.changedTouches[0]
if (!touch) {
return
}
closeTouchStartX.value = touch.absoluteX
closeTouchStartY.value = touch.absoluteY
})
.onTouchesMove((event, stateManager) => {
const touch = event.changedTouches[0]
if (!touch || event.numberOfTouches !== 1) {
stateManager.fail()
return
}
const deltaX = touch.absoluteX - closeTouchStartX.value
const deltaY = touch.absoluteY - closeTouchStartY.value
const absDeltaX = Math.abs(deltaX)
const absDeltaY = Math.abs(deltaY)
if (deltaX >= 10) {
stateManager.fail()
return
}
if (absDeltaY > 10 && absDeltaY > absDeltaX) {
stateManager.fail()
return
}
if (deltaX <= -15 && absDeltaX > absDeltaY) {
stateManager.activate()
}
})
.onStart(() => {
isGesturing.value = true
})
.onUpdate((event) => {
const newTranslateX = Math.min(0, Math.max(-windowWidth, event.translationX))
translateX.value = newTranslateX
backdropOpacity.value = interpolate(
newTranslateX,
[-windowWidth, 0],
[0, 1],
Extrapolation.CLAMP
)
})
.onEnd((event) => {
isGesturing.value = false
const shouldClose = event.translationX < -windowWidth / 3 || event.velocityX < -500
if (shouldClose) {
animateToClose()
runOnJS(handleClose)()
} else {
animateToOpen()
}
})
.onFinalize(() => {
isGesturing.value = false
}),
[
isOpen,
closeGestureRef,
closeTouchStartX,
closeTouchStartY,
isGesturing,
windowWidth,
translateX,
backdropOpacity,
animateToClose,
animateToOpen,
handleClose,
]
)
const mobileSidebarInsetStyle = useMemo(
() => ({ width: windowWidth, paddingTop: insetsTop, paddingBottom: insetsBottom }),
[windowWidth, insetsTop, insetsBottom]
)
const hostStatusDotStyle = useMemo(
() => [styles.hostStatusDot, { backgroundColor: activeHostStatusColor }],
[activeHostStatusColor]
)
const sidebarAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}))
const backdropAnimatedStyle = useAnimatedStyle(() => ({
opacity: backdropOpacity.value,
pointerEvents: backdropOpacity.value > 0.01 ? 'auto' : 'none',
}))
const overlayPointerEvents = Platform.OS === 'web' ? (isOpen ? 'auto' : 'none') : 'box-none'
return (
<View style={StyleSheet.absoluteFillObject} pointerEvents={overlayPointerEvents}>
<Animated.View style={[styles.backdrop, backdropAnimatedStyle]}>
<Pressable style={styles.backdropPressable} onPress={handleClose} />
</Animated.View>
<GestureDetector gesture={closeGesture} touchAction="pan-y">
<Animated.View
style={[styles.mobileSidebar, mobileSidebarInsetStyle, sidebarAnimatedStyle]}
pointerEvents="auto"
>
<View style={styles.sidebarContent} pointerEvents="auto">
<View style={styles.sidebarHeader}>
<View style={styles.sidebarHeaderRow}>
<Pressable
style={styles.newAgentButton}
testID="sidebar-sessions"
onPress={handleViewMore}
>
{({ hovered }) => (
<>
<MessagesSquare
size={theme.iconSize.md}
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
/>
<Text
style={[styles.newAgentButtonText, hovered && styles.newAgentButtonTextHovered]}
>
Sessions
</Text>
</>
)}
</Pressable>
</View>
</View>
{isInitialLoad ? (
<SidebarAgentListSkeleton />
) : (
<SidebarWorkspaceList
serverId={activeServerId}
collapsedProjectKeys={collapsedProjectKeys}
onToggleProjectCollapsed={toggleProjectCollapsed}
onSetProjectCollapsed={setProjectCollapsed}
shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey}
projects={projects}
isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh}
onWorkspacePress={closeToAgent}
parentGestureRef={closeGestureRef}
/>
)}
<View style={styles.sidebarFooter}>
<View style={styles.footerHostSlot}>
<Pressable
ref={hostTriggerRef}
style={({ hovered = false }) => [
styles.hostTrigger,
hovered && styles.hostTriggerHovered,
]}
onPress={() => setIsHostPickerOpen(true)}
disabled={hostOptions.length === 0}
>
<View style={hostStatusDotStyle} />
<Text style={styles.hostTriggerText} numberOfLines={1}>
{activeHostLabel}
</Text>
</Pressable>
</View>
<View style={styles.footerIconRow}>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<Pressable
style={styles.footerIconButton}
testID="sidebar-add-project"
nativeID="sidebar-add-project"
collapsable={false}
accessible
accessibilityLabel="Add project"
accessibilityRole="button"
onPress={handleOpenProject}
>
{({ hovered }) => (
<Plus
size={theme.iconSize.lg}
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
/>
)}
</Pressable>
</TooltipTrigger>
<TooltipContent side="top" align="center" offset={8}>
<View style={styles.tooltipRow}>
<Text style={styles.tooltipText}>Add project</Text>
<Shortcut keys={['⌘', '⇧', 'O']} />
</View>
</TooltipContent>
</Tooltip>
<Pressable
style={styles.footerIconButton}
testID="sidebar-settings"
nativeID="sidebar-settings"
collapsable={false}
accessible
accessibilityLabel="Settings"
accessibilityRole="button"
onPress={handleSettings}
>
{({ hovered }) => (
<Settings
size={theme.iconSize.lg}
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
/>
)}
</Pressable>
</View>
<Combobox
options={hostOptions}
value={activeServerId ?? ''}
onSelect={handleHostSelect}
searchable={false}
title="Switch host"
searchPlaceholder="Search hosts..."
open={isHostPickerOpen}
onOpenChange={setIsHostPickerOpen}
anchorRef={hostTriggerRef}
/>
</View>
</View>
</Animated.View>
</GestureDetector>
</View>
)
}
function DesktopSidebar({
theme,
activeServerId,
activeHostLabel,
activeHostStatusColor,
hostOptions,
hostTriggerRef,
isHostPickerOpen,
setIsHostPickerOpen,
projects,
isInitialLoad,
isRevalidating,
isManualRefresh,
collapsedProjectKeys,
shortcutIndexByWorkspaceKey,
toggleProjectCollapsed,
setProjectCollapsed,
handleRefresh,
handleHostSelect,
handleOpenProject,
handleSettings,
isOpen,
handleViewMore,
}: DesktopSidebarProps) {
const dragHandlers = useDesktopDragHandlers()
const trafficLightPadding = useTrafficLightPadding()
const hostStatusDotStyle = useMemo(
() => [styles.hostStatusDot, { backgroundColor: activeHostStatusColor }],
[activeHostStatusColor]
)
if (!isOpen) {
return null
}
return (
<View style={[styles.desktopSidebar, { width: DESKTOP_SIDEBAR_WIDTH }]}>
{trafficLightPadding.top > 0 ? (
<View style={{ height: trafficLightPadding.top }} {...dragHandlers} />
) : null}
<View style={styles.sidebarHeader} {...dragHandlers}>
<View style={styles.sidebarHeaderRow}>
<Pressable
style={styles.newAgentButton}
testID="sidebar-sessions"
onPress={handleViewMore}
>
{({ hovered }) => (
<>
<MessagesSquare
size={theme.iconSize.md}
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
/>
<Text
style={[styles.newAgentButtonText, hovered && styles.newAgentButtonTextHovered]}
>
Sessions
</Text>
</>
)}
</Pressable>
</View>
</View>
{isInitialLoad ? (
<SidebarAgentListSkeleton />
) : (
<SidebarWorkspaceList
serverId={activeServerId}
collapsedProjectKeys={collapsedProjectKeys}
onToggleProjectCollapsed={toggleProjectCollapsed}
onSetProjectCollapsed={setProjectCollapsed}
shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey}
projects={projects}
isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh}
/>
)}
<View style={styles.sidebarFooter}>
<View style={styles.footerHostSlot}>
<Pressable
ref={hostTriggerRef}
style={({ hovered = false }) => [
styles.hostTrigger,
hovered && styles.hostTriggerHovered,
]}
onPress={() => setIsHostPickerOpen(true)}
disabled={hostOptions.length === 0}
>
<View style={hostStatusDotStyle} />
<Text style={styles.hostTriggerText} numberOfLines={1}>
{activeHostLabel}
</Text>
</Pressable>
</View>
<View style={styles.footerIconRow}>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<Pressable
style={styles.footerIconButton}
testID="sidebar-add-project"
nativeID="sidebar-add-project"
collapsable={false}
accessible
accessibilityLabel="Add project"
accessibilityRole="button"
onPress={handleOpenProject}
>
{({ hovered }) => (
<Plus
size={theme.iconSize.lg}
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
/>
)}
</Pressable>
</TooltipTrigger>
<TooltipContent side="top" align="center" offset={8}>
<View style={styles.tooltipRow}>
<Text style={styles.tooltipText}>Add project</Text>
<Shortcut keys={['⌘', '⇧', 'O']} />
</View>
</TooltipContent>
</Tooltip>
<Pressable
style={styles.footerIconButton}
testID="sidebar-settings"
nativeID="sidebar-settings"
collapsable={false}
accessible
accessibilityLabel="Settings"
accessibilityRole="button"
onPress={handleSettings}
>
{({ hovered }) => (
<Settings
size={theme.iconSize.lg}
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
/>
)}
</Pressable>
</View>
<Combobox
options={hostOptions}
value={activeServerId ?? ''}
onSelect={handleHostSelect}
searchable={false}
title="Switch host"
searchPlaceholder="Search hosts..."
open={isHostPickerOpen}
onOpenChange={setIsHostPickerOpen}
anchorRef={hostTriggerRef}
/>
</View>
</View>
)
}
const styles = StyleSheet.create((theme) => ({
backdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
backdropPressable: {
flex: 1,
},
mobileSidebar: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
backgroundColor: theme.colors.surfaceSidebar,
overflow: 'hidden',
},
sidebarContent: {
flex: 1,
minHeight: 0,
overflow: 'hidden',
},
desktopSidebar: {
borderRightWidth: 1,
borderRightColor: theme.colors.border,
backgroundColor: theme.colors.surfaceSidebar,
},
sidebarHeader: {
height: {
xs: HEADER_INNER_HEIGHT_MOBILE,
md: HEADER_INNER_HEIGHT,
},
paddingHorizontal: theme.spacing[2],
justifyContent: 'center',
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
userSelect: 'none',
},
sidebarHeaderRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: theme.spacing[2],
},
newAgentButton: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing[2],
paddingVertical: theme.spacing[1],
paddingRight: theme.spacing[1],
paddingLeft: theme.spacing[3],
flexShrink: 0,
},
newAgentButtonHovered: {},
newAgentButtonText: {
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.normal,
color: theme.colors.foregroundMuted,
},
newAgentButtonTextHovered: {
color: theme.colors.foreground,
},
hostTrigger: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
gap: theme.spacing[2],
minWidth: 0,
paddingVertical: theme.spacing[1],
paddingHorizontal: theme.spacing[2],
borderRadius: theme.borderRadius.lg,
},
hostTriggerHovered: {
backgroundColor: theme.colors.surface1,
},
hostStatusDot: {
width: 8,
height: 8,
borderRadius: theme.borderRadius.full,
},
hostTriggerText: {
fontSize: theme.fontSize.sm,
color: theme.colors.foregroundMuted,
flexShrink: 1,
minWidth: 0,
},
sidebarFooter: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[3],
borderTopWidth: 1,
borderTopColor: theme.colors.border,
},
footerHostSlot: {
flexGrow: 0,
flexShrink: 1,
minWidth: 0,
marginRight: theme.spacing[2],
},
footerIconRow: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing[2],
flexShrink: 0,
},
footerIconButton: {
width: 28,
height: 28,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: theme.spacing[1],
paddingHorizontal: theme.spacing[1],
},
hostPickerList: {
gap: theme.spacing[2],
},
hostPickerOption: {
paddingVertical: theme.spacing[3],
paddingHorizontal: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
backgroundColor: theme.colors.surface2,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
},
hostPickerOptionText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
},
hostPickerCancel: {
paddingVertical: theme.spacing[3],
paddingHorizontal: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
backgroundColor: theme.colors.surface0,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
alignItems: 'center',
},
hostPickerCancelText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
},
tooltipRow: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing[2],
},
tooltipText: {
fontSize: theme.fontSize.sm,
color: theme.colors.popoverForeground,
},
}))