Files
orca/mobile/src/components/PickerListDrawer.tsx
c9bd61376f feat(mobile): combine PR sidebar and checks parity (#5641)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-06-17 22:52:12 -07:00

152 lines
3.7 KiB
TypeScript

import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'
import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native'
import { Check } from 'lucide-react-native'
import { colors, spacing, typography } from '../theme/mobile-theme'
import { BottomDrawer, BOTTOM_DRAWER_HIDE_DURATION_MS } from './BottomDrawer'
type Props<T extends { id: string; label: string }> = {
visible: boolean
title: string
items: T[]
selectedId: string
onSelect: (item: T) => void
onClose: () => void
renderIcon?: (item: T) => ReactNode
}
export function PickerListDrawer<T extends { id: string; label: string }>({
visible,
title,
items,
selectedId,
onSelect,
onClose,
renderIcon
}: Props<T>) {
const [closing, setClosing] = useState(false)
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const drawerVisible = visible && !closing
useEffect(() => {
if (visible) {
setClosing(false)
}
return () => {
if (closeTimerRef.current) {
clearTimeout(closeTimerRef.current)
closeTimerRef.current = null
}
}
}, [visible])
const finishClose = useCallback(() => {
setClosing(false)
onClose()
}, [onClose])
const closeThenSelect = useCallback(
(item: T) => {
if (closeTimerRef.current) {
clearTimeout(closeTimerRef.current)
}
setClosing(true)
closeTimerRef.current = setTimeout(() => {
closeTimerRef.current = null
onClose()
onSelect(item)
}, BOTTOM_DRAWER_HIDE_DURATION_MS)
},
[onClose, onSelect]
)
return (
<BottomDrawer
visible={drawerVisible}
onClose={finishClose}
dragContentToDismiss={false}
contentScrollable={false}
>
<View style={styles.header}>
<Text style={styles.title}>{title}</Text>
</View>
<FlatList
data={items}
keyExtractor={(item) => item.id}
style={styles.group}
contentContainerStyle={items.length === 0 ? styles.emptyContent : undefined}
keyboardShouldPersistTaps="handled"
nestedScrollEnabled
ItemSeparatorComponent={PickerSeparator}
renderItem={({ item }) => {
const selected = item.id === selectedId
return (
<Pressable
style={({ pressed }) => [styles.item, pressed && styles.itemPressed]}
onPress={() => closeThenSelect(item)}
>
{renderIcon?.(item)}
<Text
style={[styles.itemText, selected && styles.itemTextSelected]}
numberOfLines={1}
>
{item.label}
</Text>
{selected && <Check size={14} color={colors.textPrimary} />}
</Pressable>
)
}}
/>
</BottomDrawer>
)
}
function PickerSeparator() {
return <View style={styles.separator} />
}
const styles = StyleSheet.create({
header: {
paddingHorizontal: spacing.xs,
paddingBottom: spacing.sm
},
title: {
fontSize: 13,
fontWeight: '500',
color: colors.textMuted
},
group: {
backgroundColor: colors.bgPanel,
borderRadius: 12,
overflow: 'hidden',
maxHeight: 420,
flexGrow: 0
},
emptyContent: {
minHeight: spacing.xl
},
separator: {
height: StyleSheet.hairlineWidth,
backgroundColor: colors.borderSubtle,
marginHorizontal: spacing.md
},
item: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
paddingVertical: spacing.md,
paddingHorizontal: spacing.md + 2
},
itemPressed: {
backgroundColor: colors.bgRaised
},
itemText: {
flex: 1,
fontSize: typography.bodySize,
color: colors.textPrimary
},
itemTextSelected: {
fontWeight: '600'
}
})