Skip to content
104 linesCodeBlameRaw
1import type { LucideIcon } from "lucide-react-native";
2import { type ReactNode, useEffect, useState } from "react";
3import { KeyboardAvoidingView, Modal, Platform, Pressable, StyleSheet, TextInput, View } from "react-native";
4import { useSafeAreaInsets } from "react-native-safe-area-context";
5
6import { Button, Text } from "@/components/ui";
7import { radius, space, useColors } from "@/theme";
8
9/** A panel from the bottom over a dimmed screen, closed by tapping outside it. */
10export function Sheet({ visible, onClose, children }: { visible: boolean; onClose: () => void; children: ReactNode }) {
11 const colors = useColors();
12 const insets = useSafeAreaInsets();
13 return (
14 <Modal visible={visible} transparent animationType="slide" onRequestClose={onClose} statusBarTranslucent>
15 <Pressable style={styles.backdrop} onPress={onClose} accessibilityLabel="Close" />
16 <KeyboardAvoidingView behavior={Platform.OS === "ios" ? "padding" : undefined}>
17 <View style={[styles.sheet, { backgroundColor: colors.surface, borderColor: colors.line, paddingBottom: insets.bottom + space.md }]}>
18 <View style={[styles.grabber, { backgroundColor: colors.lineStrong }]} />
19 {children}
20 </View>
21 </KeyboardAvoidingView>
22 </Modal>
23 );
24}
25
26/** A row in a sheet: an icon and what it does. */
27export function SheetAction({ icon: Icon, label, danger, onPress }: { icon: LucideIcon; label: string; danger?: boolean; onPress: () => void }) {
28 const colors = useColors();
29 return (
30 <Pressable onPress={onPress} style={({ pressed }) => [styles.action, { backgroundColor: pressed ? colors.raised : "transparent" }]}>
31 <Icon size={20} color={danger ? colors.danger : colors.fg} />
32 <Text style={{ color: danger ? colors.danger : colors.fg }}>{label}</Text>
33 </Pressable>
34 );
35}
36
37/** Asks for one value in a sheet: a card's amount, a new message text. */
38export function PromptSheet({
39 visible,
40 title,
41 label,
42 placeholder,
43 initial = "",
44 numeric,
45 multiline,
46 confirm,
47 onCancel,
48 onDone,
49}: {
50 visible: boolean;
51 title: string;
52 label?: string;
53 placeholder?: string;
54 initial?: string;
55 numeric?: boolean;
56 multiline?: boolean;
57 confirm: string;
58 onCancel: () => void;
59 onDone: (value: string) => void;
60}) {
61 const colors = useColors();
62 const [value, setValue] = useState(initial);
63 useEffect(() => {
64 if (visible) setValue(initial);
65 }, [visible, initial]);
66 return (
67 <Sheet visible={visible} onClose={onCancel}>
68 <View style={{ paddingHorizontal: space.lg, gap: space.md }}>
69 <Text size="heading">{title}</Text>
70 {label ? (
71 <Text size="small" tone="muted">
72 {label}
73 </Text>
74 ) : null}
75 <TextInput
76 autoFocus
77 value={value}
78 onChangeText={setValue}
79 placeholder={placeholder}
80 placeholderTextColor={colors.faint}
81 keyboardType={numeric ? "decimal-pad" : "default"}
82 multiline={multiline}
83 style={[styles.input, { color: colors.fg, backgroundColor: colors.bg, borderColor: colors.lineStrong, minHeight: multiline ? 96 : 46 }]}
84 />
85 <View style={{ flexDirection: "row", gap: space.sm }}>
86 <View style={{ flex: 1 }}>
87 <Button title="Cancel" variant="secondary" onPress={onCancel} />
88 </View>
89 <View style={{ flex: 1 }}>
90 <Button title={confirm} disabled={!value.trim()} onPress={() => onDone(value.trim())} />
91 </View>
92 </View>
93 </View>
94 </Sheet>
95 );
96}
97
98const styles = StyleSheet.create({
99 backdrop: { flex: 1, backgroundColor: "rgba(0,0,0,0.45)" },
100 sheet: { borderTopLeftRadius: radius.lg, borderTopRightRadius: radius.lg, borderWidth: StyleSheet.hairlineWidth, paddingTop: space.sm, gap: space.sm },
101 grabber: { alignSelf: "center", width: 36, height: 4, borderRadius: 2, marginBottom: space.xs },
102 action: { flexDirection: "row", alignItems: "center", gap: space.md, paddingHorizontal: space.lg, paddingVertical: space.md },
103 input: { borderWidth: 1, borderRadius: radius.md, paddingHorizontal: space.md, paddingVertical: space.sm, fontSize: 15, textAlignVertical: "top" },
104});