Skip to content
Source
Sign inSign up

ComponentCatalogScreen.tsx

320 lines12.0 KB

1import { NativeAppBarExample } from "./NativeAppBarExample";
2import { Toast } from "../../components/ui/Toast";
3import { Progress } from "../../components/ui/Progress";
4import { useNavigation } from "@react-navigation/native";
5import { useRef, useState } from "react";
6import { View, type TextInputInstance } from "react-native";
7import { useSafeAreaInsets } from "react-native-safe-area-context";
8​
9import { AppText, AppTextInput } from "../../components/ui/AppText";
10import { Sheet } from "../../components/ui/Sheet";
11import { Chip } from "../../components/ui/Chip";
12import { Menu } from "../../components/ui/Menu";
13import { ListActionRow } from "../../components/ui/ListActionRow";
14import { ListChoiceRow } from "../../components/ui/ListChoiceRow";
15import { ListRow } from "../../components/ui/ListRow";
16import { ListSection } from "../../components/ui/ListSection";
17import { ListToggleRow } from "../../components/ui/ListToggleRow";
18import { Button } from "../../components/ui/Button";
19import { IconButton } from "../../components/ui/IconButton";
20import { FloatingActionButton } from "../../components/ui/FloatingActionButton";
21import { SearchField } from "../../components/ui/SearchField";
22import { Notice } from "../../components/ui/Notice";
23import { NumberField } from "../../components/ui/NumberField";
24import { AppBar } from "../../components/ui/AppBar";
25import { ScreenScrollView } from "../../components/ui/ScreenScrollView";
26import { SegmentedControl } from "../../components/ui/SegmentedControl";
27import { SliderRow } from "../../components/ui/SliderRow";
28import * as Dialog from "../../components/ui/Dialog";
29import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
30import { MOBILE_THEME_IDS } from "../../lib/mobileTheme";
31​
32const CHOICES = [
33 { value: "overview", label: "Overview" },
34 { value: "activity", label: "Activity" },
35];
36​
37export function ComponentCatalogScreen() {
38 const insets = useSafeAreaInsets();
39 const navigation = useNavigation();
40 const appearance = useAppearancePreferences();
41 const [page, setPage] = useState("controls");
42 const [tab, setTab] = useState("overview");
43 const [enabled, setEnabled] = useState(true);
44 const [value, setValue] = useState(3);
45 const [text, setText] = useState("");
46 const [search, setSearch] = useState("");
47 const [sheet, setSheet] = useState(false);
48 const [notice, setNotice] = useState(true);
49 const searchRef = useRef<TextInputInstance>(null);
50 const announceAction = () => setNotice(true);
51 const confirm = () => {
52 Dialog.confirm({
53 title: "Discard changes?",
54 message: "This example only changes local catalog state.",
55 confirmText: "Discard",
56 destructive: true,
57 onConfirm: () => setText(""),
58 });
59 };
60 return (
61 <View className="flex-1 bg-screen">
62 <AppBar title="UI components" onBack={() => navigation.goBack()} />
63 <ScreenScrollView
64 contentInsetAdjustmentBehavior="automatic"
65 contentContainerStyle={{ padding: 16, gap: 20, paddingBottom: 40 }}
66 keyboardShouldPersistTaps="handled"
67 >
68 <View className="gap-3">
69 <AppText className="text-sm text-foreground-muted">Theme preview</AppText>
70 <View className="flex-row flex-wrap gap-2">
71 {MOBILE_THEME_IDS.filter(
72 (id) => id !== "material-you" || appearance.systemColorsAvailable,
73 ).map((id) => (
74 <Chip
75 key={id}
76 label={id}
77 selected={appearance.themeId === id}
78 onPress={() => appearance.setThemeIdForBothAppearances(id)}
79 />
80 ))}
81 </View>
82 <SegmentedControl
83 options={[
84 { value: "light", label: "Light" },
85 { value: "dark", label: "Dark" },
86 ]}
87 selected={appearance.themeAppearance}
88 onSelect={appearance.setThemeMode}
89 size="compact"
90 />
91 </View>
92 <SegmentedControl
93 options={[
94 { value: "controls", label: "Controls" },
95 { value: "forms", label: "Forms" },
96 { value: "feedback", label: "Feedback" },
97 ]}
98 selected={page}
99 onSelect={setPage}
100 size="compact"
101 />
102 {page === "controls" ? (
103 <>
104 <View className="gap-3">
105 <AppText className="text-lg font-t3-semibold">Lists and selection</AppText>
106 <ListSection>
107 <View className="p-4">
108 <SegmentedControl
109 variant="card"
110 options={CHOICES}
111 selected={tab}
112 onSelect={setTab}
113 size="compact"
114 role="tab"
115 />
116 </View>
117 <ListRow icon="folder" label="Projects" detail="4" onPress={() => setSheet(true)} />
118 <ListToggleRow
119 icon="bell.badge"
120 label="Notifications"
121 description="Updates from your active threads"
122 value={enabled}
123 onValueChange={setEnabled}
124 />
125 <ListChoiceRow label="Selected option" selected onPress={announceAction} />
126 <ListActionRow
127 icon="trash"
128 label="Discard changes"
129 tone="danger"
130 onPress={confirm}
131 />
132 </ListSection>
133 <SegmentedControl
134 options={CHOICES}
135 selected={tab}
136 onSelect={setTab}
137 size="compact"
138 role="tab"
139 />
140 <View className="flex-row flex-wrap gap-2">
141 <Chip
142 label="Selected filter"
143 selected={enabled}
144 onPress={() => setEnabled(!enabled)}
145 />
146 <Chip label="Label" size="small" />
147 <Chip label="Disabled" disabled onPress={announceAction} />
148 </View>
149 </View>
150 <View className="gap-3">
151 <NativeAppBarExample />
152 <AppText className="text-lg font-t3-semibold">Buttons</AppText>
153 <View className="flex-row flex-wrap gap-2">
154 <Button label="Primary" tone="primary" onPress={announceAction} />
155 <Button label="Secondary" onPress={announceAction} />
156 <Button label="Contrast" tone="contrast" onPress={announceAction} />
157 </View>
158 <View className="flex-row flex-wrap gap-2">
159 <Button label="Disabled" disabled onPress={announceAction} />
160 <Button label="Loading" loading onPress={announceAction} />
161 <IconButton
162 accessibilityLabel="Open example sheet"
163 icon="plus"
164 variant="tonal"
165 onPress={() => setSheet(true)}
166 />
167 </View>
168 </View>
169 </>
170 ) : null}
171 {page === "forms" ? (
172 <View className="gap-3">
173 <AppText className="text-lg font-t3-semibold">Forms</AppText>
174 <AppTextInput
175 accessibilityLabel="Example name"
176 placeholder="Name"
177 value={text}
178 onChangeText={setText}
179 />
180 <View className="flex-row">
181 <SearchField
182 inputRef={searchRef}
183 accessibilityLabel="Search examples"
184 clearAccessibilityLabel="Clear examples search"
185 placeholder="Search examples"
186 value={search}
187 onChangeText={setSearch}
188 autoFocus={false}
189 />
190 </View>
191 <View className="flex-row items-center justify-between">
192 <AppText>Count</AppText>
193 <NumberField
194 label="Example count"
195 min={1}
196 max={10}
197 value={value}
198 onValueChange={setValue}
199 />
200 </View>
201 <ListSection>
202 <SliderRow
203 icon="textformat.size"
204 label="Example slider"
205 valueLabel={String(value)}
206 min={1}
207 max={10}
208 step={1}
209 value={value}
210 onChange={setValue}
211 />
212 </ListSection>
213 </View>
214 ) : null}
215 {page === "feedback" ? (
216 <View className="gap-3">
217 <AppText className="text-lg font-t3-semibold">Feedback and presentation</AppText>
218 <Notice message="This action could not finish. Try again." tone="danger" />
219 <Button label="Show notice" onPress={announceAction} />
220 <View className="flex-row flex-wrap gap-2">
221 <Button label="Sheet" onPress={() => setSheet(true)} />
222 <Button label="Dialog" onPress={confirm} />
223 <Button
224 label="Alert"
225 onPress={() =>
226 Dialog.alert("Example alert", "Choose an action.", [
227 {
228 text: "More info",
229 onPress: () =>
230 Dialog.alert("More information", "This second alert stays open."),
231 },
232 { text: "Cancel", style: "cancel" },
233 { text: "Continue", onPress: announceAction },
234 ])
235 }
236 />
237 <Button
238 label="Prompt"
239 onPress={() =>
240 Dialog.prompt({
241 title: "Rename example",
242 initialValue: text,
243 confirmText: "Rename",
244 onConfirm: setText,
245 })
246 }
247 />
248 <Menu
249 title="Example menu"
250 actions={[
251 { id: "copy", title: "Copy", image: "doc.on.doc" },
252 { id: "discard", title: "Discard", attributes: { destructive: true } },
253 ]}
254 onPressAction={({ nativeEvent }) =>
255 nativeEvent.event === "discard" ? confirm() : announceAction()
256 }
257 >
258 <Chip label="Menu" trailingIcon="chevron.down" />
259 </Menu>
260 </View>
261 <Progress value={0.6} label="Example upload" />
262 <Progress label="Connecting" />
263 <View className="items-start">
264 <FloatingActionButton
265 label="New thread"
266 icon="plus"
267 variant="extended"
268 tone="primary"
269 onPress={() => setSheet(true)}
270 />
271 </View>
272 </View>
273 ) : null}
274 </ScreenScrollView>
275 {page === "feedback" && notice ? (
276 <View
277 collapsable={false}
278 className="absolute inset-x-3 z-[100]"
279 style={{ bottom: insets.bottom + 12 }}
280 pointerEvents="box-none"
281 >
282 <Toast
283 message="Changes are ready to review."
284 tone="success"
285 dismissMode="swipe"
286 action={{
287 label: "Undo",
288 onPress: () => {
289 setNotice(false);
290 setText("Restored example");
291 },
292 }}
293 onDismiss={() => setNotice(false)}
294 />
295 </View>
296 ) : null}
297 {sheet ? (
298 <Sheet title="Example sheet" onClose={() => setSheet(false)}>
299 <ScreenScrollView
300 contentInsetAdjustmentBehavior="automatic"
301 contentContainerClassName="p-4"
302 >
303 <ListSection>
304 <ListChoiceRow
305 label="Overview"
306 selected={tab === "overview"}
307 onPress={() => setTab("overview")}
308 />
309 <ListChoiceRow
310 label="Activity"
311 selected={tab === "activity"}
312 onPress={() => setTab("activity")}
313 />
314 </ListSection>
315 </ScreenScrollView>
316 </Sheet>
317 ) : null}
318 </View>
319 );