@mrmeg/expo-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.
- package/README.md +96 -0
- package/dist/components/Accordion.d.ts +54 -0
- package/dist/components/Accordion.js +149 -0
- package/dist/components/Alert.d.ts +30 -0
- package/dist/components/Alert.js +25 -0
- package/dist/components/AnimatedView.d.ts +55 -0
- package/dist/components/AnimatedView.js +39 -0
- package/dist/components/Badge.d.ts +23 -0
- package/dist/components/Badge.js +74 -0
- package/dist/components/BottomSheet.d.ts +74 -0
- package/dist/components/BottomSheet.js +513 -0
- package/dist/components/Button.d.ts +129 -0
- package/dist/components/Button.js +216 -0
- package/dist/components/Card.d.ts +42 -0
- package/dist/components/Card.js +126 -0
- package/dist/components/Checkbox.d.ts +39 -0
- package/dist/components/Checkbox.js +96 -0
- package/dist/components/Collapsible.d.ts +67 -0
- package/dist/components/Collapsible.js +38 -0
- package/dist/components/Dialog.d.ts +140 -0
- package/dist/components/Dialog.js +167 -0
- package/dist/components/DismissKeyboard.d.ts +15 -0
- package/dist/components/DismissKeyboard.js +13 -0
- package/dist/components/Drawer.d.ts +74 -0
- package/dist/components/Drawer.js +423 -0
- package/dist/components/DropdownMenu.d.ts +120 -0
- package/dist/components/DropdownMenu.js +211 -0
- package/dist/components/EmptyState.d.ts +42 -0
- package/dist/components/EmptyState.js +58 -0
- package/dist/components/ErrorBoundary.d.ts +53 -0
- package/dist/components/ErrorBoundary.js +75 -0
- package/dist/components/Icon.d.ts +46 -0
- package/dist/components/Icon.js +40 -0
- package/dist/components/InputOTP.d.ts +72 -0
- package/dist/components/InputOTP.js +155 -0
- package/dist/components/Label.d.ts +61 -0
- package/dist/components/Label.js +72 -0
- package/dist/components/MaxWidthContainer.d.ts +58 -0
- package/dist/components/MaxWidthContainer.js +64 -0
- package/dist/components/Notification.d.ts +26 -0
- package/dist/components/Notification.js +230 -0
- package/dist/components/Popover.d.ts +79 -0
- package/dist/components/Popover.js +91 -0
- package/dist/components/Progress.d.ts +28 -0
- package/dist/components/Progress.js +107 -0
- package/dist/components/RadioGroup.d.ts +65 -0
- package/dist/components/RadioGroup.js +142 -0
- package/dist/components/Select.d.ts +88 -0
- package/dist/components/Select.js +172 -0
- package/dist/components/Separator.d.ts +83 -0
- package/dist/components/Separator.js +85 -0
- package/dist/components/Skeleton.d.ts +68 -0
- package/dist/components/Skeleton.js +99 -0
- package/dist/components/Slider.d.ts +24 -0
- package/dist/components/Slider.js +162 -0
- package/dist/components/StatusBar.d.ts +1 -0
- package/dist/components/StatusBar.js +19 -0
- package/dist/components/StyledText.d.ts +161 -0
- package/dist/components/StyledText.js +193 -0
- package/dist/components/Switch.d.ts +44 -0
- package/dist/components/Switch.js +129 -0
- package/dist/components/Tabs.d.ts +31 -0
- package/dist/components/Tabs.js +127 -0
- package/dist/components/TextInput.d.ts +120 -0
- package/dist/components/TextInput.js +263 -0
- package/dist/components/Toggle.d.ts +106 -0
- package/dist/components/Toggle.js +150 -0
- package/dist/components/ToggleGroup.d.ts +80 -0
- package/dist/components/ToggleGroup.js +189 -0
- package/dist/components/Tooltip.d.ts +121 -0
- package/dist/components/Tooltip.js +132 -0
- package/dist/components/index.d.ts +35 -0
- package/dist/components/index.js +35 -0
- package/dist/constants/colors.d.ts +82 -0
- package/dist/constants/colors.js +116 -0
- package/dist/constants/fonts.d.ts +32 -0
- package/dist/constants/fonts.js +91 -0
- package/dist/constants/index.d.ts +3 -0
- package/dist/constants/index.js +3 -0
- package/dist/constants/spacing.d.ts +40 -0
- package/dist/constants/spacing.js +48 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/useDimensions.d.ts +19 -0
- package/dist/hooks/useDimensions.js +55 -0
- package/dist/hooks/useReduceMotion.d.ts +5 -0
- package/dist/hooks/useReduceMotion.js +64 -0
- package/dist/hooks/useResources.d.ts +12 -0
- package/dist/hooks/useResources.js +56 -0
- package/dist/hooks/useScalePress.d.ts +57 -0
- package/dist/hooks/useScalePress.js +55 -0
- package/dist/hooks/useStaggeredEntrance.d.ts +67 -0
- package/dist/hooks/useStaggeredEntrance.js +74 -0
- package/dist/hooks/useTheme.d.ts +88 -0
- package/dist/hooks/useTheme.js +328 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/lib/animations.d.ts +1 -0
- package/dist/lib/animations.js +3 -0
- package/dist/lib/haptics.d.ts +3 -0
- package/dist/lib/haptics.js +29 -0
- package/dist/lib/index.d.ts +3 -0
- package/dist/lib/index.js +3 -0
- package/dist/lib/sentry.d.ts +16 -0
- package/dist/lib/sentry.js +55 -0
- package/dist/state/globalUIStore.d.ts +30 -0
- package/dist/state/globalUIStore.js +8 -0
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.js +2 -0
- package/dist/state/themeStore.d.ts +6 -0
- package/dist/state/themeStore.js +38 -0
- package/package.json +92 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Platform, StyleSheet, Text, View } from "react-native";
|
|
4
|
+
import { Icon } from "./Icon";
|
|
5
|
+
import { AnimatedView } from "./AnimatedView";
|
|
6
|
+
import { TextClassContext } from "./StyledText";
|
|
7
|
+
import { useTheme } from "../hooks/useTheme";
|
|
8
|
+
import { spacing } from "../constants/spacing";
|
|
9
|
+
import * as DropdownMenuPrimitive from "@rn-primitives/dropdown-menu";
|
|
10
|
+
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
11
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
12
|
+
// Re-export primitives that don't need styling
|
|
13
|
+
const DropdownMenu = DropdownMenuPrimitive.Root;
|
|
14
|
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
15
|
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
|
16
|
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
|
17
|
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
|
18
|
+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
|
19
|
+
// Platform-specific overlay
|
|
20
|
+
const FullWindowOverlay = Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment;
|
|
21
|
+
function DropdownMenuSubTrigger({ inset = false, children, style: styleOverride, ...props }) {
|
|
22
|
+
const { theme } = useTheme();
|
|
23
|
+
const { open } = DropdownMenuPrimitive.useSubContext();
|
|
24
|
+
const iconName = Platform.OS === "web" ? "chevron-right" : open ? "chevron-up" : "chevron-down";
|
|
25
|
+
return (_jsx(TextClassContext.Provider, { value: "", children: _jsxs(DropdownMenuPrimitive.SubTrigger, { ...props, style: {
|
|
26
|
+
flexDirection: "row",
|
|
27
|
+
alignItems: "center",
|
|
28
|
+
borderRadius: spacing.radiusSm,
|
|
29
|
+
paddingHorizontal: spacing.sm,
|
|
30
|
+
paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
|
|
31
|
+
backgroundColor: open ? theme.colors.card : "transparent",
|
|
32
|
+
...(Platform.OS === "web" && {
|
|
33
|
+
cursor: "pointer",
|
|
34
|
+
outlineStyle: "none",
|
|
35
|
+
}),
|
|
36
|
+
...(inset && { paddingLeft: spacing.xl }),
|
|
37
|
+
...(styleOverride && typeof styleOverride !== "function"
|
|
38
|
+
? StyleSheet.flatten(styleOverride)
|
|
39
|
+
: {}),
|
|
40
|
+
}, children: [typeof children === "function" ? null : children, _jsx(View, { style: { marginLeft: "auto" }, children: _jsx(Icon, { name: iconName, size: 16, color: theme.colors.text }) })] }) }));
|
|
41
|
+
}
|
|
42
|
+
function DropdownMenuSubContent({ style: styleOverride, ...props }) {
|
|
43
|
+
const { theme, getShadowStyle } = useTheme();
|
|
44
|
+
const shadowStyle = StyleSheet.flatten(getShadowStyle("soft"));
|
|
45
|
+
return (_jsx(AnimatedView, { type: "fade", children: _jsx(DropdownMenuPrimitive.SubContent, { ...props, style: {
|
|
46
|
+
backgroundColor: theme.colors.background,
|
|
47
|
+
borderWidth: 1,
|
|
48
|
+
borderColor: theme.colors.border,
|
|
49
|
+
borderRadius: spacing.radiusMd,
|
|
50
|
+
padding: spacing.xs,
|
|
51
|
+
minWidth: 192,
|
|
52
|
+
overflow: "hidden",
|
|
53
|
+
...shadowStyle,
|
|
54
|
+
...(Platform.OS === "web" && {
|
|
55
|
+
zIndex: 50,
|
|
56
|
+
}),
|
|
57
|
+
...(styleOverride && typeof styleOverride !== "function"
|
|
58
|
+
? StyleSheet.flatten(styleOverride)
|
|
59
|
+
: {}),
|
|
60
|
+
} }) }));
|
|
61
|
+
}
|
|
62
|
+
function DropdownMenuContent({ side, align = "start", sideOffset = 4, portalHost, style: styleOverride, ...props }) {
|
|
63
|
+
const { theme, getShadowStyle } = useTheme();
|
|
64
|
+
const shadowStyle = StyleSheet.flatten(getShadowStyle("soft"));
|
|
65
|
+
const insets = useSafeAreaInsets();
|
|
66
|
+
return (_jsx(DropdownMenuPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(DropdownMenuPrimitive.Overlay, { style: Platform.select({
|
|
67
|
+
native: StyleSheet.absoluteFill,
|
|
68
|
+
default: undefined,
|
|
69
|
+
}), children: _jsx(AnimatedView, { type: "fade", children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(DropdownMenuPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, insets: insets, avoidCollisions: true, ...props, style: {
|
|
70
|
+
backgroundColor: theme.colors.background,
|
|
71
|
+
borderWidth: 1,
|
|
72
|
+
borderColor: theme.colors.border,
|
|
73
|
+
borderRadius: spacing.radiusSm,
|
|
74
|
+
padding: spacing.xs,
|
|
75
|
+
minWidth: 128,
|
|
76
|
+
overflow: "hidden",
|
|
77
|
+
...shadowStyle,
|
|
78
|
+
...(Platform.OS === "web" && {
|
|
79
|
+
zIndex: 50,
|
|
80
|
+
cursor: "default",
|
|
81
|
+
}),
|
|
82
|
+
...(styleOverride && typeof styleOverride !== "function"
|
|
83
|
+
? StyleSheet.flatten(styleOverride)
|
|
84
|
+
: {}),
|
|
85
|
+
} }) }) }) }) }) }));
|
|
86
|
+
}
|
|
87
|
+
function DropdownMenuItem({ inset = false, variant = "default", style: styleOverride, ...props }) {
|
|
88
|
+
const { theme } = useTheme();
|
|
89
|
+
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(DropdownMenuPrimitive.Item, { ...props, style: {
|
|
90
|
+
position: "relative",
|
|
91
|
+
flexDirection: "row",
|
|
92
|
+
alignItems: "center",
|
|
93
|
+
gap: spacing.sm,
|
|
94
|
+
borderRadius: spacing.radiusSm,
|
|
95
|
+
paddingHorizontal: spacing.sm,
|
|
96
|
+
paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
|
|
97
|
+
backgroundColor: "transparent",
|
|
98
|
+
...(Platform.OS === "web" && {
|
|
99
|
+
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
100
|
+
outlineStyle: "none",
|
|
101
|
+
}),
|
|
102
|
+
...(props.disabled && { opacity: 0.5 }),
|
|
103
|
+
...(inset && { paddingLeft: spacing.xl }),
|
|
104
|
+
...(styleOverride && typeof styleOverride !== "function"
|
|
105
|
+
? StyleSheet.flatten(styleOverride)
|
|
106
|
+
: {}),
|
|
107
|
+
} }) }));
|
|
108
|
+
}
|
|
109
|
+
function DropdownMenuCheckboxItem({ children, style: styleOverride, ...props }) {
|
|
110
|
+
const { theme } = useTheme();
|
|
111
|
+
return (_jsx(TextClassContext.Provider, { value: "", children: _jsxs(DropdownMenuPrimitive.CheckboxItem, { ...props, style: {
|
|
112
|
+
position: "relative",
|
|
113
|
+
flexDirection: "row",
|
|
114
|
+
alignItems: "center",
|
|
115
|
+
gap: spacing.sm,
|
|
116
|
+
borderRadius: spacing.radiusSm,
|
|
117
|
+
paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
|
|
118
|
+
paddingLeft: spacing.xl,
|
|
119
|
+
paddingRight: spacing.sm,
|
|
120
|
+
backgroundColor: "transparent",
|
|
121
|
+
...(Platform.OS === "web" && {
|
|
122
|
+
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
123
|
+
outlineStyle: "none",
|
|
124
|
+
}),
|
|
125
|
+
...(props.disabled && { opacity: 0.5 }),
|
|
126
|
+
...(styleOverride && typeof styleOverride !== "function"
|
|
127
|
+
? StyleSheet.flatten(styleOverride)
|
|
128
|
+
: {}),
|
|
129
|
+
}, children: [_jsx(View, { style: {
|
|
130
|
+
position: "absolute",
|
|
131
|
+
left: spacing.sm,
|
|
132
|
+
height: 14,
|
|
133
|
+
width: 14,
|
|
134
|
+
alignItems: "center",
|
|
135
|
+
justifyContent: "center",
|
|
136
|
+
}, children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(Icon, { name: "check", size: 16, color: theme.colors.text, ...(Platform.OS === "web" && { style: { pointerEvents: "none" } }) }) }) }), typeof children === "function" ? null : children] }) }));
|
|
137
|
+
}
|
|
138
|
+
function DropdownMenuRadioItem({ children, style: styleOverride, ...props }) {
|
|
139
|
+
const { theme } = useTheme();
|
|
140
|
+
return (_jsx(TextClassContext.Provider, { value: "", children: _jsxs(DropdownMenuPrimitive.RadioItem, { ...props, style: {
|
|
141
|
+
position: "relative",
|
|
142
|
+
flexDirection: "row",
|
|
143
|
+
alignItems: "center",
|
|
144
|
+
gap: spacing.sm,
|
|
145
|
+
borderRadius: spacing.radiusSm,
|
|
146
|
+
paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
|
|
147
|
+
paddingLeft: spacing.xl,
|
|
148
|
+
paddingRight: spacing.sm,
|
|
149
|
+
backgroundColor: "transparent",
|
|
150
|
+
...(Platform.OS === "web" && {
|
|
151
|
+
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
152
|
+
outlineStyle: "none",
|
|
153
|
+
}),
|
|
154
|
+
...(props.disabled && { opacity: 0.5 }),
|
|
155
|
+
...(styleOverride && typeof styleOverride !== "function"
|
|
156
|
+
? StyleSheet.flatten(styleOverride)
|
|
157
|
+
: {}),
|
|
158
|
+
}, children: [_jsx(View, { style: {
|
|
159
|
+
position: "absolute",
|
|
160
|
+
left: spacing.sm,
|
|
161
|
+
height: 14,
|
|
162
|
+
width: 14,
|
|
163
|
+
alignItems: "center",
|
|
164
|
+
justifyContent: "center",
|
|
165
|
+
}, children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(View, { style: {
|
|
166
|
+
backgroundColor: theme.colors.text,
|
|
167
|
+
height: 8,
|
|
168
|
+
width: 8,
|
|
169
|
+
borderRadius: 4,
|
|
170
|
+
} }) }) }), typeof children === "function" ? null : children] }) }));
|
|
171
|
+
}
|
|
172
|
+
function DropdownMenuLabel({ inset = false, style: styleOverride, ...props }) {
|
|
173
|
+
const { theme } = useTheme();
|
|
174
|
+
return (_jsx(DropdownMenuPrimitive.Label, { ...props, style: {
|
|
175
|
+
paddingHorizontal: spacing.sm,
|
|
176
|
+
paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
|
|
177
|
+
fontSize: 14,
|
|
178
|
+
fontWeight: "500",
|
|
179
|
+
color: theme.colors.text,
|
|
180
|
+
...(inset && { paddingLeft: spacing.xl }),
|
|
181
|
+
...(styleOverride && typeof styleOverride !== "function"
|
|
182
|
+
? StyleSheet.flatten(styleOverride)
|
|
183
|
+
: {}),
|
|
184
|
+
} }));
|
|
185
|
+
}
|
|
186
|
+
function DropdownMenuSeparator({ style: styleOverride, ...props }) {
|
|
187
|
+
const { theme } = useTheme();
|
|
188
|
+
return (_jsx(DropdownMenuPrimitive.Separator, { ...props, style: {
|
|
189
|
+
backgroundColor: theme.colors.border,
|
|
190
|
+
marginHorizontal: -spacing.xs,
|
|
191
|
+
marginVertical: spacing.xs,
|
|
192
|
+
height: 1,
|
|
193
|
+
...(styleOverride && typeof styleOverride !== "function"
|
|
194
|
+
? StyleSheet.flatten(styleOverride)
|
|
195
|
+
: {}),
|
|
196
|
+
} }));
|
|
197
|
+
}
|
|
198
|
+
function DropdownMenuShortcut({ style: styleOverride, ...props }) {
|
|
199
|
+
const { theme } = useTheme();
|
|
200
|
+
return (_jsx(Text, { ...props, style: [
|
|
201
|
+
{
|
|
202
|
+
marginLeft: "auto",
|
|
203
|
+
fontSize: 12,
|
|
204
|
+
letterSpacing: 2,
|
|
205
|
+
color: theme.colors.text,
|
|
206
|
+
opacity: 0.6,
|
|
207
|
+
},
|
|
208
|
+
styleOverride,
|
|
209
|
+
] }));
|
|
210
|
+
}
|
|
211
|
+
export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
import { type ButtonProps } from "./Button";
|
|
4
|
+
import { type IconName } from "./Icon";
|
|
5
|
+
export interface EmptyStateProps {
|
|
6
|
+
/** Icon name to display */
|
|
7
|
+
icon?: IconName;
|
|
8
|
+
/** Icon size in pixels */
|
|
9
|
+
iconSize?: number;
|
|
10
|
+
/** Title text */
|
|
11
|
+
title: string;
|
|
12
|
+
/** Description text */
|
|
13
|
+
description?: string;
|
|
14
|
+
/** CTA button label */
|
|
15
|
+
actionLabel?: string;
|
|
16
|
+
/** CTA button press handler */
|
|
17
|
+
onAction?: () => void;
|
|
18
|
+
/** Button preset variant */
|
|
19
|
+
actionPreset?: ButtonProps["preset"];
|
|
20
|
+
/** Custom style override */
|
|
21
|
+
style?: StyleProp<ViewStyle>;
|
|
22
|
+
/** Custom children below description / above action */
|
|
23
|
+
children?: React.ReactNode;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* EmptyState Component
|
|
27
|
+
*
|
|
28
|
+
* Displays a centered placeholder for empty lists, search results,
|
|
29
|
+
* or blank screens. Works well as FlatList's ListEmptyComponent.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* <EmptyState
|
|
34
|
+
* icon="inbox"
|
|
35
|
+
* title="No messages"
|
|
36
|
+
* description="You don't have any messages yet."
|
|
37
|
+
* actionLabel="Compose"
|
|
38
|
+
* onAction={() => router.push("/compose")}
|
|
39
|
+
* />
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare function EmptyState({ icon, iconSize, title, description, actionLabel, onAction, actionPreset, style, children, }: EmptyStateProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { View, StyleSheet } from "react-native";
|
|
3
|
+
import { SansSerifBoldText, SansSerifText } from "./StyledText";
|
|
4
|
+
import { Button } from "./Button";
|
|
5
|
+
import { Icon } from "./Icon";
|
|
6
|
+
import { useTheme } from "../hooks/useTheme";
|
|
7
|
+
import { spacing } from "../constants/spacing";
|
|
8
|
+
/**
|
|
9
|
+
* EmptyState Component
|
|
10
|
+
*
|
|
11
|
+
* Displays a centered placeholder for empty lists, search results,
|
|
12
|
+
* or blank screens. Works well as FlatList's ListEmptyComponent.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <EmptyState
|
|
17
|
+
* icon="inbox"
|
|
18
|
+
* title="No messages"
|
|
19
|
+
* description="You don't have any messages yet."
|
|
20
|
+
* actionLabel="Compose"
|
|
21
|
+
* onAction={() => router.push("/compose")}
|
|
22
|
+
* />
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function EmptyState({ icon, iconSize = 48, title, description, actionLabel, onAction, actionPreset = "default", style, children, }) {
|
|
26
|
+
const { theme } = useTheme();
|
|
27
|
+
const styles = createStyles(theme);
|
|
28
|
+
return (_jsxs(View, { style: [styles.container, style], children: [!!icon && (_jsx(View, { style: styles.iconWrapper, children: _jsx(Icon, { name: icon, size: iconSize, color: theme.colors.mutedForeground }) })), _jsx(SansSerifBoldText, { style: styles.title, children: title }), !!description && (_jsx(SansSerifText, { style: styles.description, children: description })), children, !!actionLabel && onAction && (_jsx(Button, { preset: actionPreset, onPress: onAction, style: styles.action, children: actionLabel }))] }));
|
|
29
|
+
}
|
|
30
|
+
const createStyles = (theme) => StyleSheet.create({
|
|
31
|
+
container: {
|
|
32
|
+
alignItems: "center",
|
|
33
|
+
justifyContent: "center",
|
|
34
|
+
paddingVertical: spacing.xxl,
|
|
35
|
+
paddingHorizontal: spacing.lg,
|
|
36
|
+
},
|
|
37
|
+
iconWrapper: {
|
|
38
|
+
marginBottom: spacing.md,
|
|
39
|
+
},
|
|
40
|
+
title: {
|
|
41
|
+
fontSize: 18,
|
|
42
|
+
lineHeight: 24,
|
|
43
|
+
color: theme.colors.foreground,
|
|
44
|
+
textAlign: "center",
|
|
45
|
+
letterSpacing: -0.3,
|
|
46
|
+
},
|
|
47
|
+
description: {
|
|
48
|
+
fontSize: 14,
|
|
49
|
+
lineHeight: 20,
|
|
50
|
+
color: theme.colors.mutedForeground,
|
|
51
|
+
textAlign: "center",
|
|
52
|
+
marginTop: spacing.sm,
|
|
53
|
+
maxWidth: 280,
|
|
54
|
+
},
|
|
55
|
+
action: {
|
|
56
|
+
marginTop: spacing.lg,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React, { Component, ErrorInfo, ReactNode } from "react";
|
|
2
|
+
interface ErrorBoundaryProps {
|
|
3
|
+
/**
|
|
4
|
+
* When to catch errors
|
|
5
|
+
* - "always": Catch in all environments
|
|
6
|
+
* - "dev": Only catch in development
|
|
7
|
+
* - "prod": Only catch in production
|
|
8
|
+
* - "never": Never catch (errors bubble up)
|
|
9
|
+
*/
|
|
10
|
+
catchErrors: "always" | "dev" | "prod" | "never";
|
|
11
|
+
/**
|
|
12
|
+
* Child components to render
|
|
13
|
+
*/
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* Custom error fallback component
|
|
17
|
+
*/
|
|
18
|
+
FallbackComponent?: React.ComponentType<{
|
|
19
|
+
error: Error;
|
|
20
|
+
errorInfo: ErrorInfo | null;
|
|
21
|
+
resetError: () => void;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
interface ErrorBoundaryState {
|
|
25
|
+
error: Error | null;
|
|
26
|
+
errorInfo: ErrorInfo | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* ErrorBoundary catches JavaScript errors anywhere in the child component tree,
|
|
30
|
+
* logs those errors, and displays a fallback UI.
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* ```tsx
|
|
34
|
+
* <ErrorBoundary catchErrors="always" FallbackComponent={ErrorScreen}>
|
|
35
|
+
* <App />
|
|
36
|
+
* </ErrorBoundary>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
40
|
+
state: ErrorBoundaryState;
|
|
41
|
+
/**
|
|
42
|
+
* Determine if errors should be caught based on config and environment
|
|
43
|
+
*/
|
|
44
|
+
private shouldCatchErrors;
|
|
45
|
+
/**
|
|
46
|
+
* Reset the error state to try rendering again
|
|
47
|
+
*/
|
|
48
|
+
resetError: () => void;
|
|
49
|
+
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState>;
|
|
50
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
|
|
51
|
+
render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element | null | undefined;
|
|
52
|
+
}
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Component } from "react";
|
|
3
|
+
import { captureException } from "../lib/sentry";
|
|
4
|
+
/**
|
|
5
|
+
* ErrorBoundary catches JavaScript errors anywhere in the child component tree,
|
|
6
|
+
* logs those errors, and displays a fallback UI.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```tsx
|
|
10
|
+
* <ErrorBoundary catchErrors="always" FallbackComponent={ErrorScreen}>
|
|
11
|
+
* <App />
|
|
12
|
+
* </ErrorBoundary>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export class ErrorBoundary extends Component {
|
|
16
|
+
state = {
|
|
17
|
+
error: null,
|
|
18
|
+
errorInfo: null,
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Determine if errors should be caught based on config and environment
|
|
22
|
+
*/
|
|
23
|
+
shouldCatchErrors() {
|
|
24
|
+
const { catchErrors } = this.props;
|
|
25
|
+
if (catchErrors === "always")
|
|
26
|
+
return true;
|
|
27
|
+
if (catchErrors === "never")
|
|
28
|
+
return false;
|
|
29
|
+
if (catchErrors === "dev")
|
|
30
|
+
return __DEV__;
|
|
31
|
+
if (catchErrors === "prod")
|
|
32
|
+
return !__DEV__;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Reset the error state to try rendering again
|
|
37
|
+
*/
|
|
38
|
+
resetError = () => {
|
|
39
|
+
this.setState({ error: null, errorInfo: null });
|
|
40
|
+
};
|
|
41
|
+
static getDerivedStateFromError(error) {
|
|
42
|
+
return { error };
|
|
43
|
+
}
|
|
44
|
+
componentDidCatch(error, errorInfo) {
|
|
45
|
+
// Update state with error info
|
|
46
|
+
this.setState({ errorInfo });
|
|
47
|
+
// Log error to console in development
|
|
48
|
+
if (__DEV__) {
|
|
49
|
+
console.error("ErrorBoundary caught an error:", error);
|
|
50
|
+
console.error("Error info:", errorInfo);
|
|
51
|
+
}
|
|
52
|
+
// Report to Sentry (no-op if DSN not configured)
|
|
53
|
+
captureException(error, {
|
|
54
|
+
contexts: {
|
|
55
|
+
react: {
|
|
56
|
+
componentStack: errorInfo?.componentStack ?? undefined,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
render() {
|
|
62
|
+
const { children, FallbackComponent } = this.props;
|
|
63
|
+
const { error, errorInfo } = this.state;
|
|
64
|
+
// If no error or we shouldn't catch, render children normally
|
|
65
|
+
if (!error || !this.shouldCatchErrors()) {
|
|
66
|
+
return children;
|
|
67
|
+
}
|
|
68
|
+
// If custom fallback component provided, use it
|
|
69
|
+
if (FallbackComponent) {
|
|
70
|
+
return (_jsx(FallbackComponent, { error: error, errorInfo: errorInfo, resetError: this.resetError }));
|
|
71
|
+
}
|
|
72
|
+
// Default: just return null (you should provide a FallbackComponent)
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
import Feather from "@expo/vector-icons/Feather";
|
|
4
|
+
/**
|
|
5
|
+
* Theme color names that can be used as shortcuts
|
|
6
|
+
* Only includes colors that actually exist in the theme
|
|
7
|
+
*/
|
|
8
|
+
export type ThemeColorName = "primary" | "primaryForeground" | "secondary" | "muted" | "destructive" | "success" | "warning" | "text" | "textDim";
|
|
9
|
+
export type IconName = React.ComponentProps<typeof Feather>["name"];
|
|
10
|
+
type IconBaseProps = {
|
|
11
|
+
/** Size of the icon in pixels */
|
|
12
|
+
size?: number;
|
|
13
|
+
/** Icon color - can be a hex color or a theme color name. Defaults to theme's text color */
|
|
14
|
+
color?: string | ThemeColorName;
|
|
15
|
+
/** Additional styles for positioning, transforms, etc. */
|
|
16
|
+
style?: StyleProp<ViewStyle>;
|
|
17
|
+
/** When true, hides the icon from the accessibility tree. @default false */
|
|
18
|
+
decorative?: boolean;
|
|
19
|
+
};
|
|
20
|
+
type FeatherIconProps = IconBaseProps & {
|
|
21
|
+
/** The icon name to render (Feather icons) */
|
|
22
|
+
name: IconName;
|
|
23
|
+
component?: never;
|
|
24
|
+
};
|
|
25
|
+
type CustomIconProps = IconBaseProps & {
|
|
26
|
+
name?: never;
|
|
27
|
+
/** Custom component to render instead of Feather. Receives size and color as props. */
|
|
28
|
+
component: React.ComponentType<{
|
|
29
|
+
size: number;
|
|
30
|
+
color: string;
|
|
31
|
+
}>;
|
|
32
|
+
};
|
|
33
|
+
export type IconProps = FeatherIconProps | CustomIconProps;
|
|
34
|
+
/**
|
|
35
|
+
* Universal Icon Component
|
|
36
|
+
* Wraps @expo/vector-icons Feather with theme integration and style support
|
|
37
|
+
*
|
|
38
|
+
* Usage:
|
|
39
|
+
* ```tsx
|
|
40
|
+
* <Icon name="check" color="primary" size={16} />
|
|
41
|
+
* <Icon name="calendar" color="#FF0000" size={24} />
|
|
42
|
+
* <Icon name="terminal" style={{ marginRight: 8 }} />
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function Icon(props: IconProps): import("react/jsx-runtime").JSX.Element;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { useTheme } from "../hooks/useTheme";
|
|
4
|
+
import Feather from "@expo/vector-icons/Feather";
|
|
5
|
+
const THEME_COLOR_KEYS = [
|
|
6
|
+
"primary", "primaryForeground", "secondary", "muted",
|
|
7
|
+
"destructive", "success", "warning", "text", "textDim",
|
|
8
|
+
];
|
|
9
|
+
function resolveIconColor(color, themeColors) {
|
|
10
|
+
if (!color)
|
|
11
|
+
return themeColors.text;
|
|
12
|
+
if (THEME_COLOR_KEYS.includes(color)) {
|
|
13
|
+
return themeColors[color];
|
|
14
|
+
}
|
|
15
|
+
return color;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Universal Icon Component
|
|
19
|
+
* Wraps @expo/vector-icons Feather with theme integration and style support
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <Icon name="check" color="primary" size={16} />
|
|
24
|
+
* <Icon name="calendar" color="#FF0000" size={24} />
|
|
25
|
+
* <Icon name="terminal" style={{ marginRight: 8 }} />
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function Icon(props) {
|
|
29
|
+
const { size = 24, color, style, decorative = false } = props;
|
|
30
|
+
const { theme } = useTheme();
|
|
31
|
+
const iconColor = resolveIconColor(color, theme.colors);
|
|
32
|
+
const CustomComponent = "component" in props ? props.component : undefined;
|
|
33
|
+
// Wrap in View with pointerEvents="none" to prevent icons from
|
|
34
|
+
// intercepting touches when used inside TouchableOpacity on iOS
|
|
35
|
+
return (_jsx(View, { pointerEvents: "none", style: style, accessible: !decorative, ...(decorative && {
|
|
36
|
+
importantForAccessibility: "no",
|
|
37
|
+
accessibilityElementsHidden: true,
|
|
38
|
+
"aria-hidden": true,
|
|
39
|
+
}), children: CustomComponent ? (_jsx(CustomComponent, { size: size, color: iconColor })) : (_jsx(Feather, { name: props.name, size: size, color: iconColor })) }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { StyleProp, ViewStyle } from "react-native";
|
|
2
|
+
export interface InputOTPProps {
|
|
3
|
+
/**
|
|
4
|
+
* Number of OTP cells
|
|
5
|
+
* @default 6
|
|
6
|
+
*/
|
|
7
|
+
length?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Current value
|
|
10
|
+
* @default ""
|
|
11
|
+
*/
|
|
12
|
+
value?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Called when the value changes
|
|
15
|
+
*/
|
|
16
|
+
onChangeText?: (value: string) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Called when all cells are filled
|
|
19
|
+
*/
|
|
20
|
+
onComplete?: (value: string) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the input is in an error state
|
|
23
|
+
* @default false
|
|
24
|
+
*/
|
|
25
|
+
error?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Error message displayed below the cells
|
|
28
|
+
*/
|
|
29
|
+
errorText?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Whether the input is disabled
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Whether to auto-focus the input on mount
|
|
37
|
+
* @default false
|
|
38
|
+
*/
|
|
39
|
+
autoFocus?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Whether to mask the input with bullet characters
|
|
42
|
+
* @default false
|
|
43
|
+
*/
|
|
44
|
+
secureTextEntry?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Input mode for the keyboard
|
|
47
|
+
* @default "numeric"
|
|
48
|
+
*/
|
|
49
|
+
inputMode?: "numeric" | "text";
|
|
50
|
+
/**
|
|
51
|
+
* Custom style override for the container
|
|
52
|
+
*/
|
|
53
|
+
style?: StyleProp<ViewStyle>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* OTP/verification code input with individual character cells.
|
|
57
|
+
*
|
|
58
|
+
* A single hidden TextInput captures keyboard input. Visible cells are
|
|
59
|
+
* Pressable views that focus the hidden input on tap.
|
|
60
|
+
*
|
|
61
|
+
* Usage:
|
|
62
|
+
* ```tsx
|
|
63
|
+
* <InputOTP
|
|
64
|
+
* length={6}
|
|
65
|
+
* value={code}
|
|
66
|
+
* onChangeText={setCode}
|
|
67
|
+
* onComplete={(code) => verify(code)}
|
|
68
|
+
* />
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare function InputOTP({ length, value, onChangeText, onComplete, error, errorText, disabled, autoFocus, secureTextEntry, inputMode, style: styleOverride, }: InputOTPProps): import("react/jsx-runtime").JSX.Element;
|
|
72
|
+
export { InputOTP };
|