mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
174 lines
4.5 KiB
TypeScript
174 lines
4.5 KiB
TypeScript
import { useState } from 'react'
|
|
import {
|
|
View,
|
|
Text,
|
|
TextInput,
|
|
Pressable,
|
|
StyleSheet,
|
|
Platform,
|
|
type KeyboardTypeOptions
|
|
} from 'react-native'
|
|
import { colors, spacing, radii, typography } from '../theme/mobile-theme'
|
|
import { BottomDrawer } from './BottomDrawer'
|
|
|
|
type Props = {
|
|
visible: boolean
|
|
title: string
|
|
message?: string
|
|
defaultValue?: string
|
|
placeholder?: string
|
|
submitLabel?: string
|
|
selectTextOnFocus?: boolean
|
|
allowEmpty?: boolean
|
|
keyboardType?: KeyboardTypeOptions
|
|
onSubmit: (value: string) => void
|
|
onCancel: () => void
|
|
}
|
|
|
|
export function TextInputModal({
|
|
visible,
|
|
title,
|
|
message,
|
|
defaultValue = '',
|
|
placeholder,
|
|
submitLabel = 'Save',
|
|
selectTextOnFocus = false,
|
|
allowEmpty = false,
|
|
keyboardType,
|
|
onSubmit,
|
|
onCancel
|
|
}: Props) {
|
|
const [value, setValue] = useState(defaultValue)
|
|
const [previousVisible, setPreviousVisible] = useState(visible)
|
|
const [previousDefaultValue, setPreviousDefaultValue] = useState(defaultValue)
|
|
|
|
// Why: reset before the opening commit so the drawer never paints the
|
|
// previous modal value while preserving the existing close animation state.
|
|
const shouldResetValue = visible && (!previousVisible || defaultValue !== previousDefaultValue)
|
|
if (visible !== previousVisible || shouldResetValue) {
|
|
setPreviousVisible(visible)
|
|
if (shouldResetValue) {
|
|
setPreviousDefaultValue(defaultValue)
|
|
setValue(defaultValue)
|
|
}
|
|
}
|
|
|
|
function handleSubmit() {
|
|
const trimmed = value.trim()
|
|
if (trimmed || allowEmpty) {
|
|
onSubmit(trimmed)
|
|
}
|
|
}
|
|
|
|
const canSubmit = allowEmpty || value.trim().length > 0
|
|
|
|
return (
|
|
<BottomDrawer visible={visible} onClose={onCancel}>
|
|
<View style={styles.header}>
|
|
<Text style={styles.title}>{title}</Text>
|
|
{message ? <Text style={styles.message}>{message}</Text> : null}
|
|
</View>
|
|
|
|
<TextInput
|
|
style={styles.input}
|
|
value={value}
|
|
onChangeText={setValue}
|
|
placeholder={placeholder}
|
|
placeholderTextColor={colors.textMuted}
|
|
autoFocus
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
selectTextOnFocus={selectTextOnFocus}
|
|
keyboardType={keyboardType}
|
|
returnKeyType="done"
|
|
onSubmitEditing={handleSubmit}
|
|
selectionColor={colors.accentBlue}
|
|
/>
|
|
|
|
<View style={styles.actions}>
|
|
<Pressable
|
|
style={({ pressed }) => [styles.cancelButton, pressed && styles.buttonPressed]}
|
|
onPress={onCancel}
|
|
>
|
|
<Text style={styles.cancelText}>Cancel</Text>
|
|
</Pressable>
|
|
<Pressable
|
|
style={({ pressed }) => [
|
|
styles.submitButton,
|
|
pressed && styles.buttonPressed,
|
|
!canSubmit && styles.submitButtonDisabled
|
|
]}
|
|
disabled={!canSubmit}
|
|
onPress={handleSubmit}
|
|
>
|
|
<Text style={styles.submitText}>{submitLabel}</Text>
|
|
</Pressable>
|
|
</View>
|
|
</BottomDrawer>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
header: {
|
|
paddingHorizontal: spacing.xs,
|
|
paddingBottom: spacing.sm
|
|
},
|
|
title: {
|
|
fontSize: 15,
|
|
fontWeight: '600',
|
|
color: colors.textPrimary
|
|
},
|
|
message: {
|
|
fontSize: 13,
|
|
color: colors.textMuted,
|
|
marginTop: 2
|
|
},
|
|
// Why: matches NewWorktreeModal's input — bgRaised on the modal
|
|
// background reads as a tappable surface (brighter than the wrapper)
|
|
// rather than a recessed pit (darker than the wrapper, which is what
|
|
// bgBase looked like inside a bgPanel group).
|
|
input: {
|
|
backgroundColor: colors.bgRaised,
|
|
color: colors.textPrimary,
|
|
borderRadius: radii.input,
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: Platform.OS === 'ios' ? spacing.sm + 2 : spacing.sm,
|
|
fontSize: typography.bodySize,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderSubtle
|
|
},
|
|
actions: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'flex-end',
|
|
gap: spacing.sm,
|
|
marginTop: spacing.md
|
|
},
|
|
cancelButton: {
|
|
paddingHorizontal: spacing.lg,
|
|
paddingVertical: spacing.sm,
|
|
borderRadius: radii.button
|
|
},
|
|
submitButton: {
|
|
backgroundColor: colors.textPrimary,
|
|
paddingHorizontal: spacing.lg,
|
|
paddingVertical: spacing.sm,
|
|
borderRadius: radii.button
|
|
},
|
|
buttonPressed: {
|
|
opacity: 0.7
|
|
},
|
|
submitButtonDisabled: {
|
|
opacity: 0.4
|
|
},
|
|
cancelText: {
|
|
color: colors.textSecondary,
|
|
fontSize: typography.bodySize,
|
|
fontWeight: '500'
|
|
},
|
|
submitText: {
|
|
color: colors.bgBase,
|
|
fontSize: typography.bodySize,
|
|
fontWeight: '600'
|
|
}
|
|
})
|