syzygy-ui-rn 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/package.json +56 -0
  4. package/src/__tests__/components.test.tsx +368 -0
  5. package/src/__tests__/tokens.test.ts +47 -0
  6. package/src/components/badges/Badge.tsx +74 -0
  7. package/src/components/buttons/DestructiveButton.tsx +82 -0
  8. package/src/components/buttons/GhostButton.tsx +77 -0
  9. package/src/components/buttons/IconButton.tsx +61 -0
  10. package/src/components/buttons/PrimaryButton.tsx +82 -0
  11. package/src/components/buttons/SecondaryButton.tsx +84 -0
  12. package/src/components/cards/CardView.tsx +45 -0
  13. package/src/components/display/Avatar.tsx +53 -0
  14. package/src/components/display/Chip.tsx +64 -0
  15. package/src/components/display/CountBadge.tsx +58 -0
  16. package/src/components/display/DividerLine.tsx +29 -0
  17. package/src/components/display/LazyImageView.tsx +57 -0
  18. package/src/components/display/ListRow.tsx +76 -0
  19. package/src/components/display/SectionHeader.tsx +55 -0
  20. package/src/components/display/StarRatingView.tsx +70 -0
  21. package/src/components/feedback/EmptyStateView.tsx +93 -0
  22. package/src/components/feedback/ErrorStateView.tsx +75 -0
  23. package/src/components/feedback/LoadingView.tsx +57 -0
  24. package/src/components/feedback/ProgressBar.tsx +42 -0
  25. package/src/components/feedback/PullToRefresh.tsx +38 -0
  26. package/src/components/feedback/ShimmerView.tsx +47 -0
  27. package/src/components/feedback/ToastView.tsx +71 -0
  28. package/src/components/inputs/CheckboxInput.tsx +73 -0
  29. package/src/components/inputs/Dropdown.tsx +110 -0
  30. package/src/components/inputs/QuantityStepper.tsx +81 -0
  31. package/src/components/inputs/RadioButtonInput.tsx +73 -0
  32. package/src/components/inputs/SearchInput.tsx +107 -0
  33. package/src/components/inputs/SecureInput.tsx +105 -0
  34. package/src/components/inputs/SegmentedControl.tsx +70 -0
  35. package/src/components/inputs/SliderInput.tsx +131 -0
  36. package/src/components/inputs/TextInput.tsx +90 -0
  37. package/src/components/inputs/ToggleSwitch.tsx +50 -0
  38. package/src/components/layout/KeyboardAvoidingScrollView.tsx +40 -0
  39. package/src/components/navigation/AppBar.tsx +53 -0
  40. package/src/components/navigation/BackButton.tsx +63 -0
  41. package/src/components/navigation/BottomNavigationBar.tsx +65 -0
  42. package/src/components/navigation/PagerView.tsx +40 -0
  43. package/src/components/navigation/TabBar.tsx +68 -0
  44. package/src/components/navigation/TabBarItem.ts +8 -0
  45. package/src/components/overlay/BottomSheet.tsx +64 -0
  46. package/src/components/overlay/CollapsibleView.tsx +82 -0
  47. package/src/components/overlay/ModalDialog.tsx +47 -0
  48. package/src/index.ts +54 -0
  49. package/src/tokens/colors.ts +84 -0
  50. package/src/tokens/index.ts +4 -0
  51. package/src/tokens/radius.ts +8 -0
  52. package/src/tokens/spacing.ts +10 -0
  53. package/src/tokens/typography.ts +27 -0
  54. package/src/transitions/navigationTransitions.ts +66 -0
@@ -0,0 +1,131 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import {
3
+ GestureResponderEvent,
4
+ LayoutChangeEvent,
5
+ PanResponder,
6
+ StyleProp,
7
+ StyleSheet,
8
+ Text,
9
+ View,
10
+ ViewStyle,
11
+ useColorScheme,
12
+ } from 'react-native';
13
+
14
+ import { getColors } from '../../tokens/colors';
15
+ import { spacing } from '../../tokens/spacing';
16
+ import { fontSizes } from '../../tokens/typography';
17
+
18
+ export interface SliderInputProps {
19
+ label: string;
20
+ value: number;
21
+ onValueChange: (value: number) => void;
22
+ minimumValue?: number;
23
+ maximumValue?: number;
24
+ style?: StyleProp<ViewStyle>;
25
+ accessibilityLabel?: string;
26
+ }
27
+
28
+ /**
29
+ * A labeled slider with a live value readout. React Native's core `Slider`
30
+ * was removed in favor of a community package, so this is a lightweight
31
+ * `PanResponder`-based implementation with no third-party dependency.
32
+ */
33
+ export const SliderInput: React.FC<SliderInputProps> = ({
34
+ label,
35
+ value,
36
+ onValueChange,
37
+ minimumValue = 0,
38
+ maximumValue = 1,
39
+ style,
40
+ accessibilityLabel,
41
+ }) => {
42
+ const scheme = useColorScheme();
43
+ const colors = getColors(scheme);
44
+ const [trackWidth, setTrackWidth] = useState(0);
45
+
46
+ const clamp = (v: number) => Math.min(maximumValue, Math.max(minimumValue, v));
47
+
48
+ const updateFromLocation = (locationX: number) => {
49
+ if (trackWidth <= 0) return;
50
+ const ratio = Math.min(1, Math.max(0, locationX / trackWidth));
51
+ const raw = minimumValue + ratio * (maximumValue - minimumValue);
52
+ onValueChange(clamp(raw));
53
+ };
54
+
55
+ const panResponder = useRef(
56
+ PanResponder.create({
57
+ onStartShouldSetPanResponder: () => true,
58
+ onMoveShouldSetPanResponder: () => true,
59
+ onPanResponderGrant: (evt: GestureResponderEvent) =>
60
+ updateFromLocation(evt.nativeEvent.locationX),
61
+ onPanResponderMove: (evt: GestureResponderEvent) =>
62
+ updateFromLocation(evt.nativeEvent.locationX),
63
+ })
64
+ ).current;
65
+
66
+ const ratio = (clamp(value) - minimumValue) / (maximumValue - minimumValue);
67
+
68
+ const onLayout = (e: LayoutChangeEvent) => setTrackWidth(e.nativeEvent.layout.width);
69
+
70
+ return (
71
+ <View style={style}>
72
+ <View style={styles.header}>
73
+ <Text style={{ color: colors.textPrimary, fontSize: fontSizes.sm }}>{label}</Text>
74
+ <Text style={{ color: colors.textSecondary, fontSize: fontSizes.sm }}>
75
+ {value.toFixed(2)}
76
+ </Text>
77
+ </View>
78
+ <View
79
+ onLayout={onLayout}
80
+ {...panResponder.panHandlers}
81
+ accessibilityRole="adjustable"
82
+ accessibilityLabel={accessibilityLabel ?? label}
83
+ accessibilityValue={{ min: minimumValue, max: maximumValue, now: value }}
84
+ style={[styles.track, { backgroundColor: colors.border }]}
85
+ >
86
+ <View
87
+ style={[
88
+ styles.fill,
89
+ { width: `${ratio * 100}%`, backgroundColor: colors.primary },
90
+ ]}
91
+ />
92
+ <View
93
+ style={[
94
+ styles.thumb,
95
+ { left: `${ratio * 100}%`, backgroundColor: colors.primary },
96
+ ]}
97
+ />
98
+ </View>
99
+ </View>
100
+ );
101
+ };
102
+
103
+ const TRACK_HEIGHT = 4;
104
+ const THUMB_SIZE = 20;
105
+
106
+ const styles = StyleSheet.create({
107
+ header: {
108
+ flexDirection: 'row',
109
+ justifyContent: 'space-between',
110
+ marginBottom: spacing.xs,
111
+ },
112
+ track: {
113
+ height: TRACK_HEIGHT,
114
+ borderRadius: TRACK_HEIGHT / 2,
115
+ justifyContent: 'center',
116
+ marginVertical: (44 - TRACK_HEIGHT) / 2,
117
+ },
118
+ fill: {
119
+ height: TRACK_HEIGHT,
120
+ borderRadius: TRACK_HEIGHT / 2,
121
+ position: 'absolute',
122
+ left: 0,
123
+ },
124
+ thumb: {
125
+ position: 'absolute',
126
+ width: THUMB_SIZE,
127
+ height: THUMB_SIZE,
128
+ borderRadius: THUMB_SIZE / 2,
129
+ marginLeft: -THUMB_SIZE / 2,
130
+ },
131
+ });
@@ -0,0 +1,90 @@
1
+ import React from 'react';
2
+ import {
3
+ StyleProp,
4
+ StyleSheet,
5
+ Text,
6
+ TextInput as RNTextInput,
7
+ TextInputProps as RNTextInputProps,
8
+ useColorScheme,
9
+ View,
10
+ ViewStyle,
11
+ } from 'react-native';
12
+
13
+ import { getColors } from '../../tokens/colors';
14
+ import { radius } from '../../tokens/radius';
15
+ import { spacing } from '../../tokens/spacing';
16
+ import { fontSizes, fontWeights } from '../../tokens/typography';
17
+
18
+ export interface TextInputProps extends Omit<RNTextInputProps, 'style'> {
19
+ label: string;
20
+ error?: string;
21
+ style?: StyleProp<ViewStyle>;
22
+ accessibilityLabel?: string;
23
+ }
24
+
25
+ export const TextInput: React.FC<TextInputProps> = ({
26
+ label,
27
+ error,
28
+ style,
29
+ accessibilityLabel,
30
+ maxLength,
31
+ value,
32
+ ...rest
33
+ }) => {
34
+ const scheme = useColorScheme();
35
+ const colors = getColors(scheme);
36
+ const hasError = Boolean(error);
37
+
38
+ return (
39
+ <View style={style}>
40
+ <Text style={[styles.label, { color: colors.textSecondary }]}>{label}</Text>
41
+ <RNTextInput
42
+ accessibilityLabel={accessibilityLabel ?? label}
43
+ placeholderTextColor={colors.textSecondary}
44
+ style={[
45
+ styles.input,
46
+ {
47
+ backgroundColor: colors.surface,
48
+ borderColor: hasError ? colors.error : colors.border,
49
+ color: colors.textPrimary,
50
+ },
51
+ ]}
52
+ maxLength={maxLength}
53
+ value={value}
54
+ {...rest}
55
+ />
56
+ {hasError ? (
57
+ <Text style={[styles.error, { color: colors.error }]}>{error}</Text>
58
+ ) : null}
59
+ {maxLength != null ? (
60
+ <Text style={[styles.counter, { color: colors.textSecondary }]}>
61
+ {`${(value ?? '').length}/${maxLength}`}
62
+ </Text>
63
+ ) : null}
64
+ </View>
65
+ );
66
+ };
67
+
68
+ const styles = StyleSheet.create({
69
+ label: {
70
+ fontSize: fontSizes.sm,
71
+ fontWeight: fontWeights.medium,
72
+ marginBottom: spacing.xs,
73
+ },
74
+ input: {
75
+ minHeight: 44,
76
+ borderWidth: 1,
77
+ borderRadius: radius.md,
78
+ paddingHorizontal: spacing.md,
79
+ fontSize: fontSizes.md,
80
+ },
81
+ error: {
82
+ fontSize: fontSizes.xs,
83
+ marginTop: spacing.xs,
84
+ },
85
+ counter: {
86
+ fontSize: fontSizes.xs,
87
+ marginTop: spacing.xs,
88
+ textAlign: 'right',
89
+ },
90
+ });
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import { StyleProp, StyleSheet, Switch, Text, useColorScheme, View, ViewStyle } from 'react-native';
3
+
4
+ import { getColors } from '../../tokens/colors';
5
+ import { spacing } from '../../tokens/spacing';
6
+
7
+ export interface ToggleSwitchProps {
8
+ label: string;
9
+ value: boolean;
10
+ onValueChange: (value: boolean) => void;
11
+ style?: StyleProp<ViewStyle>;
12
+ accessibilityLabel?: string;
13
+ }
14
+
15
+ /** A labeled on/off toggle, wrapping React Native's core `Switch`. */
16
+ export const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
17
+ label,
18
+ value,
19
+ onValueChange,
20
+ style,
21
+ accessibilityLabel,
22
+ }) => {
23
+ const scheme = useColorScheme();
24
+ const colors = getColors(scheme);
25
+
26
+ return (
27
+ <View
28
+ style={[styles.container, style]}
29
+ accessibilityLabel={accessibilityLabel ?? label}
30
+ >
31
+ <Text style={{ color: colors.textPrimary }}>{label}</Text>
32
+ <Switch
33
+ value={value}
34
+ onValueChange={onValueChange}
35
+ trackColor={{ false: colors.disabled, true: colors.primary }}
36
+ accessibilityLabel={accessibilityLabel ?? label}
37
+ />
38
+ </View>
39
+ );
40
+ };
41
+
42
+ const styles = StyleSheet.create({
43
+ container: {
44
+ minHeight: 44,
45
+ flexDirection: 'row',
46
+ alignItems: 'center',
47
+ justifyContent: 'space-between',
48
+ paddingVertical: spacing.xs,
49
+ },
50
+ });
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import {
3
+ KeyboardAvoidingView,
4
+ Platform,
5
+ ScrollView,
6
+ StyleProp,
7
+ StyleSheet,
8
+ ViewStyle,
9
+ } from 'react-native';
10
+
11
+ export interface KeyboardAvoidingScrollViewProps {
12
+ children: React.ReactNode;
13
+ style?: StyleProp<ViewStyle>;
14
+ }
15
+
16
+ /**
17
+ * A scrollable container that automatically insets its content to avoid the
18
+ * on-screen keyboard, wrapping React Native's core `KeyboardAvoidingView`.
19
+ */
20
+ export const KeyboardAvoidingScrollView: React.FC<KeyboardAvoidingScrollViewProps> = ({
21
+ children,
22
+ style,
23
+ }) => {
24
+ return (
25
+ <KeyboardAvoidingView
26
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
27
+ style={styles.flex}
28
+ >
29
+ <ScrollView style={[styles.flex, style]} keyboardShouldPersistTaps="handled">
30
+ {children}
31
+ </ScrollView>
32
+ </KeyboardAvoidingView>
33
+ );
34
+ };
35
+
36
+ const styles = StyleSheet.create({
37
+ flex: {
38
+ flex: 1,
39
+ },
40
+ });
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import { StyleProp, StyleSheet, Text, View, ViewStyle, useColorScheme } from 'react-native';
3
+
4
+ import { getColors } from '../../tokens/colors';
5
+ import { spacing } from '../../tokens/spacing';
6
+ import { fontSizes, fontWeights } from '../../tokens/typography';
7
+
8
+ export interface AppBarProps {
9
+ title: string;
10
+ leading?: React.ReactNode;
11
+ trailing?: React.ReactNode;
12
+ style?: StyleProp<ViewStyle>;
13
+ }
14
+
15
+ /** A top navigation bar with a centered title and optional leading/trailing accessories. */
16
+ export const AppBar: React.FC<AppBarProps> = ({ title, leading, trailing, style }) => {
17
+ const scheme = useColorScheme();
18
+ const colors = getColors(scheme);
19
+
20
+ return (
21
+ <View
22
+ style={[
23
+ styles.container,
24
+ { backgroundColor: colors.surface, borderBottomColor: colors.border },
25
+ style,
26
+ ]}
27
+ accessibilityRole="header"
28
+ >
29
+ <View style={styles.side}>{leading}</View>
30
+ <Text style={{ color: colors.textPrimary, fontSize: fontSizes.lg, fontWeight: fontWeights.semibold }}>
31
+ {title}
32
+ </Text>
33
+ <View style={[styles.side, styles.trailing]}>{trailing}</View>
34
+ </View>
35
+ );
36
+ };
37
+
38
+ const styles = StyleSheet.create({
39
+ container: {
40
+ minHeight: 44,
41
+ flexDirection: 'row',
42
+ alignItems: 'center',
43
+ justifyContent: 'space-between',
44
+ paddingHorizontal: spacing.sm,
45
+ borderBottomWidth: StyleSheet.hairlineWidth,
46
+ },
47
+ side: {
48
+ minWidth: 44,
49
+ },
50
+ trailing: {
51
+ alignItems: 'flex-end',
52
+ },
53
+ });
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import {
3
+ StyleProp,
4
+ StyleSheet,
5
+ Text,
6
+ TouchableOpacity,
7
+ useColorScheme,
8
+ ViewStyle,
9
+ } from 'react-native';
10
+
11
+ import { getColors } from '../../tokens/colors';
12
+ import { radius } from '../../tokens/radius';
13
+ import { spacing } from '../../tokens/spacing';
14
+ import { fontSizes, fontWeights } from '../../tokens/typography';
15
+
16
+ export interface BackButtonProps {
17
+ onPress: () => void;
18
+ label?: string;
19
+ style?: StyleProp<ViewStyle>;
20
+ accessibilityLabel?: string;
21
+ }
22
+
23
+ export const BackButton: React.FC<BackButtonProps> = ({
24
+ onPress,
25
+ label = 'Back',
26
+ style,
27
+ accessibilityLabel,
28
+ }) => {
29
+ const scheme = useColorScheme();
30
+ const colors = getColors(scheme);
31
+
32
+ return (
33
+ <TouchableOpacity
34
+ onPress={onPress}
35
+ accessibilityRole="button"
36
+ accessibilityLabel={accessibilityLabel ?? label}
37
+ style={[styles.container, style]}
38
+ >
39
+ <Text style={[styles.arrow, { color: colors.primary }]}>{'‹'}</Text>
40
+ <Text style={[styles.label, { color: colors.primary }]}>{label}</Text>
41
+ </TouchableOpacity>
42
+ );
43
+ };
44
+
45
+ const styles = StyleSheet.create({
46
+ container: {
47
+ minHeight: 44,
48
+ minWidth: 44,
49
+ flexDirection: 'row',
50
+ alignItems: 'center',
51
+ paddingHorizontal: spacing.sm,
52
+ borderRadius: radius.sm,
53
+ },
54
+ arrow: {
55
+ fontSize: fontSizes.xl,
56
+ fontWeight: fontWeights.bold,
57
+ marginRight: spacing.xs,
58
+ },
59
+ label: {
60
+ fontSize: fontSizes.md,
61
+ fontWeight: fontWeights.medium,
62
+ },
63
+ });
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ import { StyleProp, StyleSheet, TouchableOpacity, View, ViewStyle, useColorScheme } from 'react-native';
3
+
4
+ import { getColors } from '../../tokens/colors';
5
+ import { radius } from '../../tokens/radius';
6
+ import { spacing } from '../../tokens/spacing';
7
+
8
+ import { TabBarItem } from './TabBarItem';
9
+
10
+ export interface BottomNavigationBarProps<T> {
11
+ items: TabBarItem<T>[];
12
+ selection: T;
13
+ onSelectionChange: (tag: T) => void;
14
+ style?: StyleProp<ViewStyle>;
15
+ }
16
+
17
+ /**
18
+ * A floating, icon-only bottom navigation pill — a visual alternative to
19
+ * `TabBar` for screens that want a compact, inset navigation surface.
20
+ */
21
+ export function BottomNavigationBar<T>({
22
+ items,
23
+ selection,
24
+ onSelectionChange,
25
+ style,
26
+ }: BottomNavigationBarProps<T>) {
27
+ const scheme = useColorScheme();
28
+ const colors = getColors(scheme);
29
+
30
+ return (
31
+ <View style={[styles.container, { backgroundColor: colors.surface }, style]}>
32
+ {items.map((item, index) => {
33
+ const selected = item.tag === selection;
34
+ return (
35
+ <TouchableOpacity
36
+ key={index}
37
+ onPress={() => onSelectionChange(item.tag)}
38
+ accessibilityRole="button"
39
+ accessibilityState={{ selected }}
40
+ accessibilityLabel={item.label}
41
+ style={[styles.item, selected ? { backgroundColor: colors.primary } : null]}
42
+ >
43
+ {item.icon}
44
+ </TouchableOpacity>
45
+ );
46
+ })}
47
+ </View>
48
+ );
49
+ }
50
+
51
+ const styles = StyleSheet.create({
52
+ container: {
53
+ flexDirection: 'row',
54
+ alignSelf: 'flex-start',
55
+ borderRadius: radius.full,
56
+ padding: spacing.xs,
57
+ },
58
+ item: {
59
+ width: 44,
60
+ height: 44,
61
+ borderRadius: 22,
62
+ alignItems: 'center',
63
+ justifyContent: 'center',
64
+ },
65
+ });
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { NativeScrollEvent, NativeSyntheticEvent, ScrollView, StyleProp, StyleSheet, ViewStyle } from 'react-native';
3
+
4
+ export interface PagerViewProps {
5
+ onPageChange?: (page: number) => void;
6
+ children: React.ReactNode;
7
+ style?: StyleProp<ViewStyle>;
8
+ }
9
+
10
+ /**
11
+ * Swipeable, paged content — e.g. onboarding screens or an image carousel.
12
+ * Distinct from `TabBar`, which is navigation chrome; this has no chrome of
13
+ * its own, just paged content via a horizontally-paging `ScrollView`.
14
+ */
15
+ export const PagerView: React.FC<PagerViewProps> = ({ onPageChange, children, style }) => {
16
+ const handleScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
17
+ if (!onPageChange) return;
18
+ const { contentOffset, layoutMeasurement } = e.nativeEvent;
19
+ const page = Math.round(contentOffset.x / layoutMeasurement.width);
20
+ onPageChange(page);
21
+ };
22
+
23
+ return (
24
+ <ScrollView
25
+ horizontal
26
+ pagingEnabled
27
+ showsHorizontalScrollIndicator={false}
28
+ onMomentumScrollEnd={handleScroll}
29
+ style={[styles.container, style]}
30
+ >
31
+ {children}
32
+ </ScrollView>
33
+ );
34
+ };
35
+
36
+ const styles = StyleSheet.create({
37
+ container: {
38
+ flex: 1,
39
+ },
40
+ });
@@ -0,0 +1,68 @@
1
+ import React from 'react';
2
+ import { StyleProp, StyleSheet, Text, TouchableOpacity, View, ViewStyle, useColorScheme } from 'react-native';
3
+
4
+ import { getColors } from '../../tokens/colors';
5
+ import { spacing } from '../../tokens/spacing';
6
+ import { fontSizes } from '../../tokens/typography';
7
+
8
+ import { TabBarItem } from './TabBarItem';
9
+
10
+ export interface TabBarProps<T> {
11
+ items: TabBarItem<T>[];
12
+ selection: T;
13
+ onSelectionChange: (tag: T) => void;
14
+ style?: StyleProp<ViewStyle>;
15
+ }
16
+
17
+ /**
18
+ * An edge-to-edge, icon-and-label tab bar. Presentational only — this
19
+ * library has no navigation dependency, so wire `onSelectionChange` into
20
+ * your own navigator.
21
+ */
22
+ export function TabBar<T>({ items, selection, onSelectionChange, style }: TabBarProps<T>) {
23
+ const scheme = useColorScheme();
24
+ const colors = getColors(scheme);
25
+
26
+ return (
27
+ <View
28
+ style={[
29
+ styles.container,
30
+ { backgroundColor: colors.surface, borderTopColor: colors.border },
31
+ style,
32
+ ]}
33
+ >
34
+ {items.map((item, index) => {
35
+ const selected = item.tag === selection;
36
+ return (
37
+ <TouchableOpacity
38
+ key={index}
39
+ onPress={() => onSelectionChange(item.tag)}
40
+ accessibilityRole="button"
41
+ accessibilityState={{ selected }}
42
+ accessibilityLabel={item.label}
43
+ style={styles.item}
44
+ >
45
+ {item.icon}
46
+ <Text style={{ color: selected ? colors.primary : colors.textSecondary, fontSize: fontSizes.xs }}>
47
+ {item.label}
48
+ </Text>
49
+ </TouchableOpacity>
50
+ );
51
+ })}
52
+ </View>
53
+ );
54
+ }
55
+
56
+ const styles = StyleSheet.create({
57
+ container: {
58
+ flexDirection: 'row',
59
+ borderTopWidth: StyleSheet.hairlineWidth,
60
+ paddingTop: spacing.xs,
61
+ },
62
+ item: {
63
+ flex: 1,
64
+ minHeight: 48,
65
+ alignItems: 'center',
66
+ justifyContent: 'center',
67
+ },
68
+ });
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+
3
+ /** A single destination shown in `TabBar` or `BottomNavigationBar`. */
4
+ export interface TabBarItem<T> {
5
+ tag: T;
6
+ icon: React.ReactNode;
7
+ label: string;
8
+ }
@@ -0,0 +1,64 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, Dimensions, Modal, Pressable, StyleSheet, useColorScheme } from 'react-native';
3
+
4
+ import { getColors } from '../../tokens/colors';
5
+ import { radius } from '../../tokens/radius';
6
+ import { spacing } from '../../tokens/spacing';
7
+
8
+ export interface BottomSheetProps {
9
+ visible: boolean;
10
+ onDismiss: () => void;
11
+ children: React.ReactNode;
12
+ }
13
+
14
+ const SCREEN_HEIGHT = Dimensions.get('window').height;
15
+
16
+ /** A bottom-anchored sheet with a dimmed scrim, sliding up from the bottom edge. */
17
+ export const BottomSheet: React.FC<BottomSheetProps> = ({ visible, onDismiss, children }) => {
18
+ const scheme = useColorScheme();
19
+ const colors = getColors(scheme);
20
+ const translateY = useRef(new Animated.Value(SCREEN_HEIGHT)).current;
21
+
22
+ useEffect(() => {
23
+ Animated.timing(translateY, {
24
+ toValue: visible ? 0 : SCREEN_HEIGHT,
25
+ duration: 250,
26
+ useNativeDriver: true,
27
+ }).start();
28
+ }, [visible, translateY]);
29
+
30
+ return (
31
+ <Modal visible={visible} transparent animationType="fade" onRequestClose={onDismiss}>
32
+ <Pressable style={[styles.backdrop, { backgroundColor: colors.overlay }]} onPress={onDismiss}>
33
+ <Animated.View
34
+ style={[
35
+ styles.sheet,
36
+ { backgroundColor: colors.surface, transform: [{ translateY }] },
37
+ ]}
38
+ >
39
+ <Pressable style={[styles.handle, { backgroundColor: colors.border }]} />
40
+ {children}
41
+ </Animated.View>
42
+ </Pressable>
43
+ </Modal>
44
+ );
45
+ };
46
+
47
+ const styles = StyleSheet.create({
48
+ backdrop: {
49
+ flex: 1,
50
+ justifyContent: 'flex-end',
51
+ },
52
+ sheet: {
53
+ borderTopLeftRadius: radius.lg,
54
+ borderTopRightRadius: radius.lg,
55
+ padding: spacing.lg,
56
+ },
57
+ handle: {
58
+ alignSelf: 'center',
59
+ width: 36,
60
+ height: 4,
61
+ borderRadius: 2,
62
+ marginBottom: spacing.md,
63
+ },
64
+ });