@cyberkaidev/ui 0.1.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 (152) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +721 -0
  3. package/lib/module/ThemeProvider.js +84 -0
  4. package/lib/module/ThemeProvider.js.map +1 -0
  5. package/lib/module/components/AppBar.js +185 -0
  6. package/lib/module/components/AppBar.js.map +1 -0
  7. package/lib/module/components/BounceEffect.js +50 -0
  8. package/lib/module/components/BounceEffect.js.map +1 -0
  9. package/lib/module/components/Button.js +48 -0
  10. package/lib/module/components/Button.js.map +1 -0
  11. package/lib/module/components/Chips.js +94 -0
  12. package/lib/module/components/Chips.js.map +1 -0
  13. package/lib/module/components/IconButton.js +71 -0
  14. package/lib/module/components/IconButton.js.map +1 -0
  15. package/lib/module/components/InforList.js +92 -0
  16. package/lib/module/components/InforList.js.map +1 -0
  17. package/lib/module/components/LoadingApp.js +58 -0
  18. package/lib/module/components/LoadingApp.js.map +1 -0
  19. package/lib/module/components/Overlays.js +20 -0
  20. package/lib/module/components/Overlays.js.map +1 -0
  21. package/lib/module/components/RippleEffect.js +178 -0
  22. package/lib/module/components/RippleEffect.js.map +1 -0
  23. package/lib/module/components/Scaffold.js +172 -0
  24. package/lib/module/components/Scaffold.js.map +1 -0
  25. package/lib/module/components/SearchBar.js +161 -0
  26. package/lib/module/components/SearchBar.js.map +1 -0
  27. package/lib/module/components/SegmentedButton.js +151 -0
  28. package/lib/module/components/SegmentedButton.js.map +1 -0
  29. package/lib/module/components/ShapedButton.js +58 -0
  30. package/lib/module/components/ShapedButton.js.map +1 -0
  31. package/lib/module/components/SlideModal.js +196 -0
  32. package/lib/module/components/SlideModal.js.map +1 -0
  33. package/lib/module/components/TabBar.js +107 -0
  34. package/lib/module/components/TabBar.js.map +1 -0
  35. package/lib/module/components/Text.js +47 -0
  36. package/lib/module/components/Text.js.map +1 -0
  37. package/lib/module/components/TextButton.js +53 -0
  38. package/lib/module/components/TextButton.js.map +1 -0
  39. package/lib/module/components/TextInput.js +83 -0
  40. package/lib/module/components/TextInput.js.map +1 -0
  41. package/lib/module/components/Toast.js +101 -0
  42. package/lib/module/components/Toast.js.map +1 -0
  43. package/lib/module/hooks/useAppBarScroll.js +20 -0
  44. package/lib/module/hooks/useAppBarScroll.js.map +1 -0
  45. package/lib/module/hooks/usePressAndHoldRepeat.js +71 -0
  46. package/lib/module/hooks/usePressAndHoldRepeat.js.map +1 -0
  47. package/lib/module/icons/Icon.js +85 -0
  48. package/lib/module/icons/Icon.js.map +1 -0
  49. package/lib/module/index.js +42 -0
  50. package/lib/module/index.js.map +1 -0
  51. package/lib/module/package.json +1 -0
  52. package/lib/module/responsive.js +28 -0
  53. package/lib/module/responsive.js.map +1 -0
  54. package/lib/module/stores/useLoadingApp.js +14 -0
  55. package/lib/module/stores/useLoadingApp.js.map +1 -0
  56. package/lib/module/stores/useToast.js +24 -0
  57. package/lib/module/stores/useToast.js.map +1 -0
  58. package/lib/module/theme.js +86 -0
  59. package/lib/module/theme.js.map +1 -0
  60. package/lib/module/types.js +2 -0
  61. package/lib/module/types.js.map +1 -0
  62. package/lib/typescript/jest.setup.d.ts +2 -0
  63. package/lib/typescript/jest.setup.d.ts.map +1 -0
  64. package/lib/typescript/package.json +1 -0
  65. package/lib/typescript/src/ThemeProvider.d.ts +39 -0
  66. package/lib/typescript/src/ThemeProvider.d.ts.map +1 -0
  67. package/lib/typescript/src/components/AppBar.d.ts +33 -0
  68. package/lib/typescript/src/components/AppBar.d.ts.map +1 -0
  69. package/lib/typescript/src/components/BounceEffect.d.ts +12 -0
  70. package/lib/typescript/src/components/BounceEffect.d.ts.map +1 -0
  71. package/lib/typescript/src/components/Button.d.ts +24 -0
  72. package/lib/typescript/src/components/Button.d.ts.map +1 -0
  73. package/lib/typescript/src/components/Chips.d.ts +23 -0
  74. package/lib/typescript/src/components/Chips.d.ts.map +1 -0
  75. package/lib/typescript/src/components/IconButton.d.ts +14 -0
  76. package/lib/typescript/src/components/IconButton.d.ts.map +1 -0
  77. package/lib/typescript/src/components/InforList.d.ts +28 -0
  78. package/lib/typescript/src/components/InforList.d.ts.map +1 -0
  79. package/lib/typescript/src/components/LoadingApp.d.ts +8 -0
  80. package/lib/typescript/src/components/LoadingApp.d.ts.map +1 -0
  81. package/lib/typescript/src/components/Overlays.d.ts +12 -0
  82. package/lib/typescript/src/components/Overlays.d.ts.map +1 -0
  83. package/lib/typescript/src/components/RippleEffect.d.ts +18 -0
  84. package/lib/typescript/src/components/RippleEffect.d.ts.map +1 -0
  85. package/lib/typescript/src/components/Scaffold.d.ts +62 -0
  86. package/lib/typescript/src/components/Scaffold.d.ts.map +1 -0
  87. package/lib/typescript/src/components/SearchBar.d.ts +20 -0
  88. package/lib/typescript/src/components/SearchBar.d.ts.map +1 -0
  89. package/lib/typescript/src/components/SegmentedButton.d.ts +37 -0
  90. package/lib/typescript/src/components/SegmentedButton.d.ts.map +1 -0
  91. package/lib/typescript/src/components/ShapedButton.d.ts +18 -0
  92. package/lib/typescript/src/components/ShapedButton.d.ts.map +1 -0
  93. package/lib/typescript/src/components/SlideModal.d.ts +15 -0
  94. package/lib/typescript/src/components/SlideModal.d.ts.map +1 -0
  95. package/lib/typescript/src/components/TabBar.d.ts +20 -0
  96. package/lib/typescript/src/components/TabBar.d.ts.map +1 -0
  97. package/lib/typescript/src/components/Text.d.ts +26 -0
  98. package/lib/typescript/src/components/Text.d.ts.map +1 -0
  99. package/lib/typescript/src/components/TextButton.d.ts +15 -0
  100. package/lib/typescript/src/components/TextButton.d.ts.map +1 -0
  101. package/lib/typescript/src/components/TextInput.d.ts +22 -0
  102. package/lib/typescript/src/components/TextInput.d.ts.map +1 -0
  103. package/lib/typescript/src/components/Toast.d.ts +4 -0
  104. package/lib/typescript/src/components/Toast.d.ts.map +1 -0
  105. package/lib/typescript/src/hooks/useAppBarScroll.d.ts +10 -0
  106. package/lib/typescript/src/hooks/useAppBarScroll.d.ts.map +1 -0
  107. package/lib/typescript/src/hooks/usePressAndHoldRepeat.d.ts +21 -0
  108. package/lib/typescript/src/hooks/usePressAndHoldRepeat.d.ts.map +1 -0
  109. package/lib/typescript/src/icons/Icon.d.ts +64 -0
  110. package/lib/typescript/src/icons/Icon.d.ts.map +1 -0
  111. package/lib/typescript/src/index.d.ts +30 -0
  112. package/lib/typescript/src/index.d.ts.map +1 -0
  113. package/lib/typescript/src/responsive.d.ts +15 -0
  114. package/lib/typescript/src/responsive.d.ts.map +1 -0
  115. package/lib/typescript/src/stores/useLoadingApp.d.ts +14 -0
  116. package/lib/typescript/src/stores/useLoadingApp.d.ts.map +1 -0
  117. package/lib/typescript/src/stores/useToast.d.ts +25 -0
  118. package/lib/typescript/src/stores/useToast.d.ts.map +1 -0
  119. package/lib/typescript/src/theme.d.ts +51 -0
  120. package/lib/typescript/src/theme.d.ts.map +1 -0
  121. package/lib/typescript/src/types.d.ts +26 -0
  122. package/lib/typescript/src/types.d.ts.map +1 -0
  123. package/package.json +189 -0
  124. package/src/ThemeProvider.tsx +108 -0
  125. package/src/components/AppBar.tsx +226 -0
  126. package/src/components/BounceEffect.tsx +53 -0
  127. package/src/components/Button.tsx +70 -0
  128. package/src/components/Chips.tsx +115 -0
  129. package/src/components/IconButton.tsx +84 -0
  130. package/src/components/InforList.tsx +110 -0
  131. package/src/components/LoadingApp.tsx +55 -0
  132. package/src/components/Overlays.tsx +21 -0
  133. package/src/components/RippleEffect.tsx +244 -0
  134. package/src/components/Scaffold.tsx +219 -0
  135. package/src/components/SearchBar.tsx +166 -0
  136. package/src/components/SegmentedButton.tsx +206 -0
  137. package/src/components/ShapedButton.tsx +74 -0
  138. package/src/components/SlideModal.tsx +232 -0
  139. package/src/components/TabBar.tsx +122 -0
  140. package/src/components/Text.tsx +70 -0
  141. package/src/components/TextButton.tsx +57 -0
  142. package/src/components/TextInput.tsx +108 -0
  143. package/src/components/Toast.tsx +95 -0
  144. package/src/hooks/useAppBarScroll.ts +19 -0
  145. package/src/hooks/usePressAndHoldRepeat.ts +85 -0
  146. package/src/icons/Icon.tsx +51 -0
  147. package/src/index.tsx +64 -0
  148. package/src/responsive.ts +32 -0
  149. package/src/stores/useLoadingApp.ts +19 -0
  150. package/src/stores/useToast.ts +36 -0
  151. package/src/theme.ts +125 -0
  152. package/src/types.ts +27 -0
@@ -0,0 +1,226 @@
1
+ import { BlurView } from 'expo-blur';
2
+ import Constants from 'expo-constants';
3
+ import { Platform, StyleSheet, View } from 'react-native';
4
+ import type { ColorValue } from 'react-native';
5
+ import Animated, {
6
+ Extrapolation,
7
+ interpolate,
8
+ useAnimatedStyle,
9
+ useDerivedValue,
10
+ } from 'react-native-reanimated';
11
+ import type { SharedValue } from 'react-native-reanimated';
12
+
13
+ import { Icon } from '../icons/Icon';
14
+ import type { IconNameType } from '../icons/Icon';
15
+ import { useTheme, useThemedStyles } from '../ThemeProvider';
16
+ import { pageSpacing } from '../theme';
17
+ import type { ThemeType } from '../theme';
18
+ import { heightDP } from '../responsive';
19
+ import { Button } from './Button';
20
+ import { Text } from './Text';
21
+
22
+ const LARGE_TITLE_HEIGHT = heightDP(6);
23
+ const COLLAPSE_DEAD_ZONE = heightDP(0.8);
24
+ const CONTENT_HEIGHT = heightDP(7);
25
+ const IS_IOS = Platform.OS === 'ios';
26
+
27
+ /**
28
+ * Height of the fixed app bar, including the status bar. Uses the default
29
+ * `pageSpacing.top` — a themed override is not reflected here.
30
+ */
31
+ export const APP_BAR_HEIGHT =
32
+ Constants.statusBarHeight + CONTENT_HEIGHT + pageSpacing.top;
33
+
34
+ /** Height of the app bar when the large title is expanded. */
35
+ export const APP_BAR_LARGE_HEIGHT = APP_BAR_HEIGHT + LARGE_TITLE_HEIGHT;
36
+
37
+ export type AppBarTrailingType = {
38
+ label?: string;
39
+ icon?: IconNameType;
40
+ tintColor?: ColorValue;
41
+ hidden?: boolean;
42
+ onPress: () => void;
43
+ };
44
+
45
+ type Props = {
46
+ title: string;
47
+ /** Shows a back control; provide `onBack` to handle it. */
48
+ enabledBack?: boolean;
49
+ onBack?: () => void;
50
+ /** Render a large, collapsing title below the bar. */
51
+ large?: boolean;
52
+ /** Optional action on the trailing side. */
53
+ trailing?: AppBarTrailingType;
54
+ /** Scroll offset that drives the large-title collapse (see `useAppBarScroll`). */
55
+ scrollY?: SharedValue<number>;
56
+ };
57
+
58
+ /** Fixed top app bar with an optional collapsing large title. */
59
+ export function AppBar({
60
+ title,
61
+ enabledBack,
62
+ onBack,
63
+ large,
64
+ trailing,
65
+ scrollY,
66
+ }: Props) {
67
+ const { colors, borderRadius } = useTheme();
68
+ const styles = useThemedStyles(makeStyles);
69
+
70
+ const collapseProgress = useDerivedValue(() => {
71
+ if (!scrollY) return 0;
72
+ return interpolate(
73
+ scrollY.value,
74
+ [COLLAPSE_DEAD_ZONE, COLLAPSE_DEAD_ZONE + LARGE_TITLE_HEIGHT],
75
+ [0, 1],
76
+ Extrapolation.CLAMP
77
+ );
78
+ });
79
+
80
+ const titleAnimatedStyle = useAnimatedStyle(() => ({
81
+ opacity: large ? collapseProgress.value : 1,
82
+ }));
83
+
84
+ const largeTitleAnimatedStyle = useAnimatedStyle(() => ({
85
+ opacity: 1 - collapseProgress.value,
86
+ height: LARGE_TITLE_HEIGHT * (1 - collapseProgress.value),
87
+ }));
88
+
89
+ return (
90
+ <View
91
+ style={[
92
+ styles.container,
93
+ {
94
+ backgroundColor: colors.surface_90,
95
+ borderBottomColor: colors.border,
96
+ },
97
+ ]}
98
+ >
99
+ {IS_IOS && <BlurView style={styles.blur} tint="dark" intensity={20} />}
100
+ <View style={styles.content}>
101
+ <View style={[styles.side, styles.sideLeft]}>
102
+ {enabledBack && (
103
+ <View style={styles.radiusOverflow}>
104
+ <Button
105
+ testID="app-bar-back-button"
106
+ hitSlop={{ top: 20, bottom: 20, left: 25, right: 15 }}
107
+ onPress={onBack}
108
+ style={styles.iconButton}
109
+ >
110
+ <Icon name="arrow_back" color={colors.text} />
111
+ </Button>
112
+ </View>
113
+ )}
114
+ </View>
115
+
116
+ <Animated.View style={[styles.center, titleAnimatedStyle]}>
117
+ <Text weight="medium" numberOfLines={1}>
118
+ {title}
119
+ </Text>
120
+ </Animated.View>
121
+
122
+ <View style={[styles.side, styles.sideRight]}>
123
+ {trailing && !trailing.hidden && (
124
+ <View
125
+ style={[
126
+ styles.radiusOverflow,
127
+ {
128
+ borderRadius: trailing.label
129
+ ? borderRadius.radius_20
130
+ : borderRadius.radius_100,
131
+ },
132
+ ]}
133
+ >
134
+ <Button
135
+ testID="app-bar-trailing-button"
136
+ hitSlop={{ top: 20, bottom: 20, left: 25, right: 15 }}
137
+ onPress={trailing.onPress}
138
+ style={styles.iconButton}
139
+ >
140
+ {trailing.icon && (
141
+ <Icon
142
+ name={trailing.icon}
143
+ color={(trailing.tintColor as string) ?? colors.text}
144
+ />
145
+ )}
146
+ {trailing.label && (
147
+ <Text weight="medium" color={trailing.tintColor}>
148
+ {trailing.label}
149
+ </Text>
150
+ )}
151
+ </Button>
152
+ </View>
153
+ )}
154
+ </View>
155
+ </View>
156
+
157
+ {large && (
158
+ <Animated.View
159
+ style={[styles.largeTitleContainer, largeTitleAnimatedStyle]}
160
+ >
161
+ <Text weight="bold" size="xl" numberOfLines={1}>
162
+ {title}
163
+ </Text>
164
+ </Animated.View>
165
+ )}
166
+ </View>
167
+ );
168
+ }
169
+
170
+ function makeStyles(t: ThemeType) {
171
+ return StyleSheet.create({
172
+ container: {
173
+ position: 'absolute',
174
+ top: 0,
175
+ left: 0,
176
+ right: 0,
177
+ zIndex: 10,
178
+ width: '100%',
179
+ borderStyle: 'solid',
180
+ borderBottomWidth: 1,
181
+ },
182
+ content: {
183
+ width: '100%',
184
+ height: CONTENT_HEIGHT,
185
+ flexDirection: 'row',
186
+ alignItems: 'center',
187
+ marginTop: Constants.statusBarHeight,
188
+ paddingHorizontal: t.spaces.horizontal.xs,
189
+ paddingTop: COLLAPSE_DEAD_ZONE,
190
+ },
191
+ side: {
192
+ flex: 1,
193
+ },
194
+ sideLeft: {
195
+ alignItems: 'flex-start',
196
+ },
197
+ sideRight: {
198
+ alignItems: 'flex-end',
199
+ },
200
+ center: {
201
+ flex: 2,
202
+ alignItems: 'center',
203
+ },
204
+ largeTitleContainer: {
205
+ width: '100%',
206
+ overflow: 'hidden',
207
+ justifyContent: 'flex-end',
208
+ paddingHorizontal: t.spaces.horizontal.l,
209
+ paddingBottom: t.spaces.vertical.s,
210
+ },
211
+ iconButton: {
212
+ padding: t.spaces.horizontal.xs,
213
+ },
214
+ radiusOverflow: {
215
+ borderRadius: t.borderRadius.radius_100,
216
+ overflow: 'hidden',
217
+ },
218
+ blur: {
219
+ position: 'absolute',
220
+ top: 0,
221
+ left: 0,
222
+ bottom: 0,
223
+ right: 0,
224
+ },
225
+ });
226
+ }
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import { Animated, Pressable } from 'react-native';
3
+ import type { PressableProps } from 'react-native';
4
+
5
+ type Props = {
6
+ /** Fired after the release animation settles. */
7
+ onActionTrigger: () => void;
8
+ children: React.ReactNode;
9
+ style?: PressableProps['style'];
10
+ };
11
+
12
+ /** Wraps a child and plays a scale "bounce" on press, firing on release. */
13
+ export function BounceEffect({ onActionTrigger, children, style }: Props) {
14
+ const scaleValue = React.useRef(new Animated.Value(1)).current;
15
+
16
+ const animateIn = () => {
17
+ Animated.spring(scaleValue, {
18
+ toValue: 0.9,
19
+ useNativeDriver: true,
20
+ }).start();
21
+ };
22
+
23
+ const animateOut = () => {
24
+ Animated.spring(scaleValue, {
25
+ toValue: 1,
26
+ useNativeDriver: true,
27
+ }).start();
28
+ };
29
+
30
+ const handlePress = () => {
31
+ Animated.spring(scaleValue, {
32
+ toValue: 1,
33
+ friction: 4,
34
+ tension: 40,
35
+ useNativeDriver: true,
36
+ }).start(() => {
37
+ onActionTrigger();
38
+ });
39
+ };
40
+
41
+ return (
42
+ <Pressable
43
+ style={style}
44
+ onPressIn={animateIn}
45
+ onPressOut={animateOut}
46
+ onPress={handlePress}
47
+ >
48
+ <Animated.View style={{ transform: [{ scale: scaleValue }] }}>
49
+ {children}
50
+ </Animated.View>
51
+ </Pressable>
52
+ );
53
+ }
@@ -0,0 +1,70 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Platform, TouchableOpacity } from 'react-native';
3
+ import type { Insets, StyleProp, ViewStyle } from 'react-native';
4
+
5
+ import { RippleEffect } from './RippleEffect';
6
+
7
+ type Props = {
8
+ children: ReactNode;
9
+ onPress?: () => void;
10
+ onPressIn?: () => void;
11
+ onPressOut?: () => void;
12
+ disabled?: boolean;
13
+ style?: StyleProp<ViewStyle>;
14
+ testID?: string;
15
+ hitSlop?: Insets;
16
+ /** Android only. */
17
+ rippleColor?: string;
18
+ /** Android only. */
19
+ rippleContainerBorderRadius?: number;
20
+ };
21
+
22
+ /**
23
+ * Base pressable. Android gets a touch ripple; every other platform gets
24
+ * opacity feedback. Kept in one file (not `.ios`/`.android`) so consumers'
25
+ * type resolution works without `moduleSuffixes`.
26
+ */
27
+ export function Button({
28
+ children,
29
+ onPress,
30
+ onPressIn,
31
+ onPressOut,
32
+ disabled,
33
+ style,
34
+ testID,
35
+ hitSlop,
36
+ rippleColor,
37
+ rippleContainerBorderRadius,
38
+ }: Props) {
39
+ if (Platform.OS === 'android') {
40
+ return (
41
+ <RippleEffect
42
+ testID={testID}
43
+ style={style}
44
+ disabled={disabled}
45
+ onPress={onPress}
46
+ onPressIn={onPressIn}
47
+ onPressOut={onPressOut}
48
+ hitSlop={hitSlop}
49
+ rippleColor={rippleColor}
50
+ rippleContainerBorderRadius={rippleContainerBorderRadius}
51
+ >
52
+ {children}
53
+ </RippleEffect>
54
+ );
55
+ }
56
+
57
+ return (
58
+ <TouchableOpacity
59
+ testID={testID}
60
+ style={style}
61
+ disabled={disabled}
62
+ onPress={onPress}
63
+ onPressIn={onPressIn}
64
+ onPressOut={onPressOut}
65
+ hitSlop={hitSlop}
66
+ >
67
+ {children}
68
+ </TouchableOpacity>
69
+ );
70
+ }
@@ -0,0 +1,115 @@
1
+ import type { ReactNode } from 'react';
2
+ import { ScrollView, StyleSheet, View } from 'react-native';
3
+
4
+ import { useTheme, useThemedStyles } from '../ThemeProvider';
5
+ import type { ThemeType } from '../theme';
6
+ import { Text } from './Text';
7
+ import { Button } from './Button';
8
+
9
+ type ChipScheme = 'primary' | 'surface' | 'on_surface';
10
+
11
+ type ChipItem<Item> = {
12
+ key: Item;
13
+ label: string;
14
+ disabled?: boolean;
15
+ /** Optional node rendered before the label (e.g. an icon). */
16
+ prefix?: ReactNode;
17
+ /** Visual scheme. Default `'surface'`. */
18
+ scheme?: ChipScheme;
19
+ };
20
+
21
+ type Props<Item> = {
22
+ data: ChipItem<Item>[];
23
+ /** Called with the item `key`. Omit to render the chips non-interactive. */
24
+ onPress?: (key: Item) => void;
25
+ marginT?: number;
26
+ marginB?: number;
27
+ testID?: string;
28
+ };
29
+
30
+ /** Horizontally scrolling row of selectable chips. */
31
+ export function Chips<Item>({
32
+ data,
33
+ marginB = 0,
34
+ marginT = 0,
35
+ onPress,
36
+ testID,
37
+ }: Props<Item>) {
38
+ const { colors } = useTheme();
39
+ const styles = useThemedStyles(makeStyles);
40
+ const isInteractive = typeof onPress === 'function';
41
+
42
+ const schemeButton: Record<
43
+ ChipScheme,
44
+ { borderColor: string; backgroundColor: string }
45
+ > = {
46
+ surface: { borderColor: colors.border, backgroundColor: colors.surface },
47
+ on_surface: {
48
+ borderColor: colors.border,
49
+ backgroundColor: colors.on_surface,
50
+ },
51
+ primary: { borderColor: colors.primary, backgroundColor: colors.surface },
52
+ };
53
+
54
+ const schemeTextColor: Record<ChipScheme, string> = {
55
+ surface: colors.text,
56
+ on_surface: colors.text,
57
+ primary: colors.primary,
58
+ };
59
+
60
+ return (
61
+ <View
62
+ testID={testID}
63
+ style={[styles.container, { marginTop: marginT, marginBottom: marginB }]}
64
+ >
65
+ <ScrollView
66
+ horizontal
67
+ showsHorizontalScrollIndicator={false}
68
+ contentContainerStyle={styles.content}
69
+ >
70
+ {data.map(({ key, label, disabled, prefix, scheme = 'surface' }) => (
71
+ <Button
72
+ testID={`chip-${String(key)}`}
73
+ key={String(key)}
74
+ disabled={!isInteractive || disabled}
75
+ style={[
76
+ styles.button,
77
+ schemeButton[scheme],
78
+ disabled ? styles.dimmed : null,
79
+ ]}
80
+ onPress={() => onPress?.(key)}
81
+ >
82
+ {prefix}
83
+ <Text color={schemeTextColor[scheme]}>{label}</Text>
84
+ </Button>
85
+ ))}
86
+ </ScrollView>
87
+ </View>
88
+ );
89
+ }
90
+
91
+ function makeStyles(t: ThemeType) {
92
+ return StyleSheet.create({
93
+ container: {
94
+ width: '100%',
95
+ },
96
+ content: {
97
+ gap: t.spaces.horizontal.l,
98
+ paddingHorizontal: t.pageSpacing.horizontal,
99
+ },
100
+ button: {
101
+ paddingHorizontal: t.spaces.horizontal.m,
102
+ paddingVertical: t.spaces.vertical.xs,
103
+ borderRadius: t.borderRadius.radius_20,
104
+ borderWidth: 1,
105
+ borderStyle: 'solid',
106
+ flexDirection: 'row',
107
+ alignItems: 'center',
108
+ gap: t.spaces.horizontal.s,
109
+ overflow: 'hidden',
110
+ },
111
+ dimmed: {
112
+ opacity: 0.5,
113
+ },
114
+ });
115
+ }
@@ -0,0 +1,84 @@
1
+ import { StyleSheet, View } from 'react-native';
2
+
3
+ import { Icon } from '../icons/Icon';
4
+ import type { IconNameType } from '../icons/Icon';
5
+ import { useTheme, useThemedStyles } from '../ThemeProvider';
6
+ import type { ThemeType } from '../theme';
7
+ import { widthDP } from '../responsive';
8
+ import { Button } from './Button';
9
+
10
+ const BUTTON_SIZE = widthDP(10);
11
+
12
+ type Props = {
13
+ /** Icon name (see the `names` map in `Icon`). */
14
+ icon: IconNameType;
15
+ /** Highlights the icon and shows a dot badge. */
16
+ active?: boolean;
17
+ disabled?: boolean;
18
+ onPress: () => void;
19
+ testID?: string;
20
+ };
21
+
22
+ /** Circular icon-only button with an optional "active" indicator. */
23
+ export function IconButton({
24
+ icon,
25
+ active = false,
26
+ disabled = false,
27
+ onPress,
28
+ testID,
29
+ }: Props) {
30
+ const { colors } = useTheme();
31
+ const styles = useThemedStyles(makeStyles);
32
+
33
+ return (
34
+ <View style={styles.container}>
35
+ <Button
36
+ testID={testID ?? 'icon-button'}
37
+ style={[
38
+ styles.button,
39
+ { backgroundColor: colors.on_surface, borderColor: colors.border },
40
+ ]}
41
+ disabled={disabled}
42
+ onPress={onPress}
43
+ >
44
+ <Icon name={icon} color={active ? colors.primary : colors.text} />
45
+ {active && (
46
+ <View
47
+ style={[
48
+ styles.badge,
49
+ { backgroundColor: colors.primary, borderColor: colors.surface },
50
+ ]}
51
+ />
52
+ )}
53
+ </Button>
54
+ </View>
55
+ );
56
+ }
57
+
58
+ function makeStyles(t: ThemeType) {
59
+ return StyleSheet.create({
60
+ container: {
61
+ padding: 2,
62
+ borderRadius: t.borderRadius.radius_100,
63
+ overflow: 'hidden',
64
+ },
65
+ button: {
66
+ width: BUTTON_SIZE,
67
+ height: BUTTON_SIZE,
68
+ justifyContent: 'center',
69
+ alignItems: 'center',
70
+ borderWidth: 1,
71
+ borderRadius: t.borderRadius.radius_100,
72
+ overflow: 'hidden',
73
+ },
74
+ badge: {
75
+ position: 'absolute',
76
+ top: widthDP(1.5),
77
+ right: widthDP(1.5),
78
+ width: widthDP(2),
79
+ height: widthDP(2),
80
+ borderRadius: t.borderRadius.radius_100,
81
+ borderWidth: 1,
82
+ },
83
+ });
84
+ }
@@ -0,0 +1,110 @@
1
+ import React from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+
4
+ import { useTheme, useThemedStyles } from '../ThemeProvider';
5
+ import type { ThemeType } from '../theme';
6
+ import { heightDP } from '../responsive';
7
+ import { Button } from './Button';
8
+ import { Text } from './Text';
9
+
10
+ type ContainerProps = {
11
+ children: React.ReactNode;
12
+ marginT?: number;
13
+ marginB?: number;
14
+ };
15
+
16
+ type ContentProps = {
17
+ children: React.ReactNode;
18
+ /** Makes the row pressable. */
19
+ onPress?: () => void;
20
+ };
21
+
22
+ type TitleProps = {
23
+ children: React.ReactNode;
24
+ color?: string;
25
+ };
26
+
27
+ /** A label inside an `InforList.Content` row. */
28
+ function Title({ children, color }: TitleProps) {
29
+ return (
30
+ <Text weight="medium" color={color}>
31
+ {children}
32
+ </Text>
33
+ );
34
+ }
35
+
36
+ /** A single row; pressable when `onPress` is provided. */
37
+ function Content({ children, onPress }: ContentProps) {
38
+ const styles = useThemedStyles(makeStyles);
39
+ const isInteractive = typeof onPress === 'function';
40
+
41
+ return (
42
+ <Button
43
+ disabled={!isInteractive}
44
+ onPress={() => onPress?.()}
45
+ style={styles.content}
46
+ >
47
+ {children}
48
+ </Button>
49
+ );
50
+ }
51
+
52
+ /** Grouped list with dividers between rows. */
53
+ function Container({ children, marginB, marginT }: ContainerProps) {
54
+ const { colors } = useTheme();
55
+ const styles = useThemedStyles(makeStyles);
56
+ const items = React.Children.toArray(children);
57
+
58
+ return (
59
+ <View
60
+ style={[
61
+ styles.container,
62
+ {
63
+ marginTop: marginT,
64
+ marginBottom: marginB,
65
+ backgroundColor: colors.surface,
66
+ borderColor: colors.border,
67
+ },
68
+ ]}
69
+ >
70
+ {items.map((child, index) => (
71
+ <React.Fragment key={index}>
72
+ {child}
73
+ {index < items.length - 1 && (
74
+ <View style={[styles.line, { backgroundColor: colors.border }]} />
75
+ )}
76
+ </React.Fragment>
77
+ ))}
78
+ </View>
79
+ );
80
+ }
81
+
82
+ export const InforList = {
83
+ Title,
84
+ Content,
85
+ Container,
86
+ };
87
+
88
+ function makeStyles(t: ThemeType) {
89
+ return StyleSheet.create({
90
+ container: {
91
+ width: '100%',
92
+ alignItems: 'stretch',
93
+ borderRadius: t.borderRadius.radius_20,
94
+ overflow: 'hidden',
95
+ borderWidth: 1,
96
+ },
97
+ content: {
98
+ flexDirection: 'row',
99
+ justifyContent: 'space-between',
100
+ alignItems: 'center',
101
+ height: heightDP(6),
102
+ paddingHorizontal: t.spaces.horizontal.l,
103
+ },
104
+ line: {
105
+ flex: 1,
106
+ height: 1,
107
+ marginLeft: t.spaces.horizontal.l,
108
+ },
109
+ });
110
+ }
@@ -0,0 +1,55 @@
1
+ import { ActivityIndicator, Modal, StyleSheet, View } from 'react-native';
2
+
3
+ import { useTheme } from '../ThemeProvider';
4
+ import { useLoadingApp } from '../stores/useLoadingApp';
5
+ import { Text } from './Text';
6
+
7
+ type Props = {
8
+ /** Optional caption under the spinner. */
9
+ label?: string;
10
+ };
11
+
12
+ /** Blocking full-screen loading overlay. Drive it with `useLoadingApp().setLoadingVisible(...)`. */
13
+ export function LoadingApp({ label }: Props) {
14
+ const { colors, spaces } = useTheme();
15
+ const { visible } = useLoadingApp();
16
+
17
+ return (
18
+ <Modal
19
+ visible={visible}
20
+ transparent
21
+ statusBarTranslucent
22
+ animationType="fade"
23
+ >
24
+ <View
25
+ style={[styles.background, { backgroundColor: colors.background_60 }]}
26
+ >
27
+ <View style={styles.content}>
28
+ <ActivityIndicator color={colors.text} size="small" />
29
+ {label ? (
30
+ <Text marginL={spaces.horizontal.s} weight="medium">
31
+ {label}
32
+ </Text>
33
+ ) : null}
34
+ </View>
35
+ </View>
36
+ </Modal>
37
+ );
38
+ }
39
+
40
+ const styles = StyleSheet.create({
41
+ background: {
42
+ position: 'absolute',
43
+ top: 0,
44
+ left: 0,
45
+ right: 0,
46
+ bottom: 0,
47
+ justifyContent: 'center',
48
+ alignItems: 'center',
49
+ },
50
+ content: {
51
+ flexDirection: 'row',
52
+ justifyContent: 'center',
53
+ alignItems: 'center',
54
+ },
55
+ });