@mrmeg/expo-ui 0.14.0 → 0.16.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/CHANGELOG.md +81 -0
- package/README.md +50 -6
- package/dist/components/Accordion.d.ts +1 -1
- package/dist/components/Accordion.js +25 -19
- package/dist/components/Badge.js +10 -7
- package/dist/components/BottomSheet.js +21 -19
- package/dist/components/Button.d.ts +2 -1
- package/dist/components/Button.js +22 -6
- package/dist/components/Card.js +5 -3
- package/dist/components/Checkbox.js +49 -19
- package/dist/components/Collapsible.d.ts +1 -1
- package/dist/components/Collapsible.js +22 -16
- package/dist/components/Drawer.js +8 -7
- package/dist/components/DropdownMenu.d.ts +1 -1
- package/dist/components/DropdownMenu.js +80 -59
- package/dist/components/EmptyState.d.ts +7 -1
- package/dist/components/EmptyState.js +26 -9
- package/dist/components/InputOTP.js +10 -4
- package/dist/components/Item.d.ts +93 -0
- package/dist/components/Item.js +125 -0
- package/dist/components/Label.js +10 -4
- package/dist/components/Notification.js +14 -24
- package/dist/components/Popover.d.ts +3 -18
- package/dist/components/Popover.js +10 -6
- package/dist/components/RadioGroup.js +49 -22
- package/dist/components/SectionHeader.d.ts +42 -0
- package/dist/components/SectionHeader.js +41 -0
- package/dist/components/Select.js +73 -41
- package/dist/components/Skeleton.js +5 -3
- package/dist/components/StatCard.d.ts +45 -0
- package/dist/components/StatCard.js +41 -0
- package/dist/components/StyledText.d.ts +20 -10
- package/dist/components/StyledText.js +85 -29
- package/dist/components/Switch.js +84 -57
- package/dist/components/Tabs.js +43 -12
- package/dist/components/TextInput.js +17 -11
- package/dist/components/Toggle.js +69 -42
- package/dist/components/ToggleGroup.js +82 -55
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +3 -0
- package/dist/constants/fonts.d.ts +44 -5
- package/dist/constants/fonts.js +42 -12
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/constants/motion.d.ts +24 -0
- package/dist/constants/motion.js +24 -0
- package/dist/constants/spacing.d.ts +7 -7
- package/dist/constants/spacing.js +7 -7
- package/dist/hooks/useResources.d.ts +4 -2
- package/dist/hooks/useResources.js +30 -8
- package/dist/hooks/useStaggeredEntrance.d.ts +1 -1
- package/dist/hooks/useStaggeredEntrance.js +2 -1
- package/dist/hooks/useTheme.d.ts +1 -1
- package/dist/hooks/useTheme.js +53 -16
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/themedStyles.d.ts +21 -0
- package/dist/lib/themedStyles.js +33 -0
- package/dist/state/themeStore.d.ts +17 -0
- package/dist/state/themeStore.js +6 -0
- package/package.json +20 -19
|
@@ -8,6 +8,7 @@ import { useTheme } from "../hooks/useTheme.js";
|
|
|
8
8
|
import { useDimensions } from "../hooks/useDimensions.js";
|
|
9
9
|
import { shouldUseNativeDriver } from "../lib/animations.js";
|
|
10
10
|
import { spacing } from "../constants/spacing.js";
|
|
11
|
+
import { durations } from "../constants/motion.js";
|
|
11
12
|
import { TextColorContext, TextClassContext } from "./StyledText.context";
|
|
12
13
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
13
14
|
import { SansSerifBoldText } from "./StyledText.js";
|
|
@@ -263,12 +264,12 @@ function DrawerOverlayContent({ swipeEnabled = true, swipeThreshold = 0.3, veloc
|
|
|
263
264
|
const animation = Animated.parallel([
|
|
264
265
|
Animated.timing(translateX, {
|
|
265
266
|
toValue: 0,
|
|
266
|
-
duration:
|
|
267
|
+
duration: durations.normal,
|
|
267
268
|
useNativeDriver: shouldUseNativeDriver,
|
|
268
269
|
}),
|
|
269
270
|
Animated.timing(backdropOpacity, {
|
|
270
271
|
toValue: 1,
|
|
271
|
-
duration:
|
|
272
|
+
duration: durations.normal,
|
|
272
273
|
useNativeDriver: shouldUseNativeDriver,
|
|
273
274
|
}),
|
|
274
275
|
]);
|
|
@@ -284,12 +285,12 @@ function DrawerOverlayContent({ swipeEnabled = true, swipeThreshold = 0.3, veloc
|
|
|
284
285
|
const animation = Animated.parallel([
|
|
285
286
|
Animated.timing(translateX, {
|
|
286
287
|
toValue: closedPosition,
|
|
287
|
-
duration:
|
|
288
|
+
duration: durations.normal,
|
|
288
289
|
useNativeDriver: shouldUseNativeDriver,
|
|
289
290
|
}),
|
|
290
291
|
Animated.timing(backdropOpacity, {
|
|
291
292
|
toValue: 0,
|
|
292
|
-
duration:
|
|
293
|
+
duration: durations.normal,
|
|
293
294
|
useNativeDriver: shouldUseNativeDriver,
|
|
294
295
|
}),
|
|
295
296
|
]);
|
|
@@ -350,7 +351,7 @@ function DrawerOverlayContent({ swipeEnabled = true, swipeThreshold = 0.3, veloc
|
|
|
350
351
|
}),
|
|
351
352
|
Animated.timing(backdropOpacity, {
|
|
352
353
|
toValue: 0,
|
|
353
|
-
duration:
|
|
354
|
+
duration: durations.normal,
|
|
354
355
|
useNativeDriver: shouldUseNativeDriver,
|
|
355
356
|
}),
|
|
356
357
|
]).start(() => {
|
|
@@ -369,7 +370,7 @@ function DrawerOverlayContent({ swipeEnabled = true, swipeThreshold = 0.3, veloc
|
|
|
369
370
|
}),
|
|
370
371
|
Animated.timing(backdropOpacity, {
|
|
371
372
|
toValue: 1,
|
|
372
|
-
duration:
|
|
373
|
+
duration: durations.fast,
|
|
373
374
|
useNativeDriver: shouldUseNativeDriver,
|
|
374
375
|
}),
|
|
375
376
|
]).start();
|
|
@@ -488,7 +489,7 @@ swipeEnabled: _swipeEnabled, swipeThreshold: _swipeThreshold, velocityThreshold:
|
|
|
488
489
|
if (previousExpanded !== null) {
|
|
489
490
|
Animated.timing(widthAnim, {
|
|
490
491
|
toValue: targetWidth,
|
|
491
|
-
duration:
|
|
492
|
+
duration: durations.normal,
|
|
492
493
|
useNativeDriver: false,
|
|
493
494
|
}).start();
|
|
494
495
|
}
|
|
@@ -46,7 +46,7 @@ declare const DropdownMenuRadioGroup: {
|
|
|
46
46
|
* props over slot props), so this only fills the gap when none is set.
|
|
47
47
|
*/
|
|
48
48
|
type DropdownMenuTriggerProps = DropdownMenuPrimitive.TriggerProps;
|
|
49
|
-
declare function DropdownMenuTrigger({ ...props }: DropdownMenuTriggerProps): React.JSX.Element;
|
|
49
|
+
declare function DropdownMenuTrigger({ disabled, ...props }: DropdownMenuTriggerProps): React.JSX.Element;
|
|
50
50
|
/**
|
|
51
51
|
* DropdownMenuSubTrigger Component
|
|
52
52
|
* Trigger for sub-menus with automatic chevron icon
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import { Platform, StyleSheet, Text, View } from "react-native";
|
|
3
|
+
import { Animated, Platform, StyleSheet, Text, View } from "react-native";
|
|
4
4
|
import { Icon } from "./Icon.js";
|
|
5
5
|
import { AnimatedView } from "./AnimatedView.js";
|
|
6
6
|
import { TextClassContext, TextSelectabilityContext } from "./StyledText.context";
|
|
7
7
|
import { useTheme } from "../hooks/useTheme.js";
|
|
8
8
|
import { spacing } from "../constants/spacing.js";
|
|
9
|
+
import { useScalePress } from "../hooks/useScalePress.js";
|
|
9
10
|
import * as DropdownMenuPrimitive from "@rn-primitives/dropdown-menu";
|
|
10
11
|
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
11
12
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
@@ -17,8 +18,13 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
|
|
17
18
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
|
18
19
|
// Platform-specific overlay
|
|
19
20
|
const FullWindowOverlay = Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment;
|
|
20
|
-
function DropdownMenuTrigger({ ...props }) {
|
|
21
|
-
|
|
21
|
+
function DropdownMenuTrigger({ disabled, ...props }) {
|
|
22
|
+
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
23
|
+
disabled: !!disabled,
|
|
24
|
+
scaleTo: 0.97,
|
|
25
|
+
haptic: false,
|
|
26
|
+
});
|
|
27
|
+
return (_jsx(Animated.View, { style: scaleStyle, children: _jsx(DropdownMenuPrimitive.Trigger, { accessibilityRole: "button", disabled: disabled, ...props, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut }) }));
|
|
22
28
|
}
|
|
23
29
|
function DropdownMenuSubTrigger({ inset = false, children, style: styleOverride, ...props }) {
|
|
24
30
|
const { theme } = useTheme();
|
|
@@ -90,68 +96,83 @@ function DropdownMenuContent({ side, align = "start", sideOffset = 4, portalHost
|
|
|
90
96
|
}
|
|
91
97
|
function DropdownMenuItem({ inset = false, variant = "default", style: styleOverride, ...props }) {
|
|
92
98
|
const { theme } = useTheme();
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
100
|
+
disabled: !!props.disabled,
|
|
101
|
+
scaleTo: 0.97,
|
|
102
|
+
haptic: false,
|
|
103
|
+
});
|
|
104
|
+
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(DropdownMenuPrimitive.Item, { ...props, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, style: {
|
|
105
|
+
...styles.item,
|
|
106
|
+
...(Platform.OS === "web" && {
|
|
107
|
+
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
108
|
+
outlineStyle: "none",
|
|
109
|
+
userSelect: "none",
|
|
110
|
+
}),
|
|
111
|
+
...(props.disabled && { opacity: 0.5 }),
|
|
112
|
+
...(inset && { paddingLeft: spacing.xl }),
|
|
113
|
+
...(styleOverride && typeof styleOverride !== "function"
|
|
114
|
+
? StyleSheet.flatten(styleOverride)
|
|
115
|
+
: {}),
|
|
116
|
+
} }) }) }) }));
|
|
106
117
|
}
|
|
107
118
|
function DropdownMenuCheckboxItem({ children, style: styleOverride, ...props }) {
|
|
108
119
|
const { theme } = useTheme();
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
120
|
+
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
121
|
+
disabled: !!props.disabled,
|
|
122
|
+
scaleTo: 0.97,
|
|
123
|
+
haptic: false,
|
|
124
|
+
});
|
|
125
|
+
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(Animated.View, { style: scaleStyle, children: _jsxs(DropdownMenuPrimitive.CheckboxItem, { ...props, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, style: {
|
|
126
|
+
...styles.indicatorItem,
|
|
127
|
+
...(Platform.OS === "web" && {
|
|
128
|
+
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
129
|
+
outlineStyle: "none",
|
|
130
|
+
userSelect: "none",
|
|
131
|
+
}),
|
|
132
|
+
...(props.disabled && { opacity: 0.5 }),
|
|
133
|
+
...(styleOverride && typeof styleOverride !== "function"
|
|
134
|
+
? StyleSheet.flatten(styleOverride)
|
|
135
|
+
: {}),
|
|
136
|
+
}, children: [_jsx(View, { style: {
|
|
137
|
+
position: "absolute",
|
|
138
|
+
left: spacing.sm,
|
|
139
|
+
height: 14,
|
|
140
|
+
width: 14,
|
|
141
|
+
alignItems: "center",
|
|
142
|
+
justifyContent: "center",
|
|
143
|
+
}, 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] }) }) }) }));
|
|
128
144
|
}
|
|
129
145
|
function DropdownMenuRadioItem({ children, style: styleOverride, ...props }) {
|
|
130
146
|
const { theme } = useTheme();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
147
|
+
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
148
|
+
disabled: !!props.disabled,
|
|
149
|
+
scaleTo: 0.97,
|
|
150
|
+
haptic: false,
|
|
151
|
+
});
|
|
152
|
+
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(Animated.View, { style: scaleStyle, children: _jsxs(DropdownMenuPrimitive.RadioItem, { ...props, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, style: {
|
|
153
|
+
...styles.indicatorItem,
|
|
154
|
+
...(Platform.OS === "web" && {
|
|
155
|
+
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
156
|
+
outlineStyle: "none",
|
|
157
|
+
userSelect: "none",
|
|
158
|
+
}),
|
|
159
|
+
...(props.disabled && { opacity: 0.5 }),
|
|
160
|
+
...(styleOverride && typeof styleOverride !== "function"
|
|
161
|
+
? StyleSheet.flatten(styleOverride)
|
|
162
|
+
: {}),
|
|
163
|
+
}, children: [_jsx(View, { style: {
|
|
164
|
+
position: "absolute",
|
|
165
|
+
left: spacing.sm,
|
|
166
|
+
height: 14,
|
|
167
|
+
width: 14,
|
|
168
|
+
alignItems: "center",
|
|
169
|
+
justifyContent: "center",
|
|
170
|
+
}, children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(View, { style: {
|
|
171
|
+
backgroundColor: theme.colors.text,
|
|
172
|
+
height: 8,
|
|
173
|
+
width: 8,
|
|
174
|
+
borderRadius: 4,
|
|
175
|
+
} }) }) }), typeof children === "function" ? null : children] }) }) }) }));
|
|
155
176
|
}
|
|
156
177
|
function DropdownMenuLabel({ inset = false, style: styleOverride, ...props }) {
|
|
157
178
|
const { theme } = useTheme();
|
|
@@ -21,6 +21,12 @@ export interface EmptyStateProps {
|
|
|
21
21
|
style?: StyleProp<ViewStyle>;
|
|
22
22
|
/** Custom children below description / above action */
|
|
23
23
|
children?: React.ReactNode;
|
|
24
|
+
/**
|
|
25
|
+
* Wraps the state in a dashed-border container with generous padding —
|
|
26
|
+
* the "empty slot" pattern. @default false (preserves existing call
|
|
27
|
+
* sites, which render without a border).
|
|
28
|
+
*/
|
|
29
|
+
bordered?: boolean;
|
|
24
30
|
}
|
|
25
31
|
/**
|
|
26
32
|
* EmptyState Component
|
|
@@ -39,4 +45,4 @@ export interface EmptyStateProps {
|
|
|
39
45
|
* />
|
|
40
46
|
* ```
|
|
41
47
|
*/
|
|
42
|
-
export declare function EmptyState({ icon, iconSize, title, description, actionLabel, onAction, actionPreset, style, children, }: EmptyStateProps): React.JSX.Element;
|
|
48
|
+
export declare function EmptyState({ icon, iconSize, title, description, actionLabel, onAction, actionPreset, style, children, bordered, }: EmptyStateProps): React.JSX.Element;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo } from "react";
|
|
3
2
|
import { View, StyleSheet } from "react-native";
|
|
4
3
|
import { SansSerifBoldText, SansSerifText } from "./StyledText.js";
|
|
5
4
|
import { Button } from "./Button.js";
|
|
6
5
|
import { Icon } from "./Icon.js";
|
|
7
6
|
import { useTheme } from "../hooks/useTheme.js";
|
|
8
7
|
import { spacing } from "../constants/spacing.js";
|
|
8
|
+
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
9
9
|
/**
|
|
10
10
|
* EmptyState Component
|
|
11
11
|
*
|
|
@@ -23,11 +23,12 @@ import { spacing } from "../constants/spacing.js";
|
|
|
23
23
|
* />
|
|
24
24
|
* ```
|
|
25
25
|
*/
|
|
26
|
-
export function EmptyState({ icon, iconSize =
|
|
26
|
+
export function EmptyState({ icon, iconSize = spacing.iconMd, title, description, actionLabel, onAction, actionPreset = "default", style, children, bordered = false, }) {
|
|
27
27
|
const { theme } = useTheme();
|
|
28
|
-
const styles =
|
|
29
|
-
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, { selectable: false, style: styles.title, children: title }), !!description && (_jsx(SansSerifText, { selectable: false, style: styles.description, children: description })), children, !!actionLabel && onAction && (_jsx(Button, { preset: actionPreset, onPress: onAction, text: actionLabel, style: styles.action }))] }));
|
|
28
|
+
const styles = themedStyles(theme);
|
|
29
|
+
return (_jsxs(View, { style: [styles.container, bordered && styles.bordered, style], children: [!!icon && (_jsx(View, { style: styles.iconWrapper, children: _jsx(Icon, { name: icon, size: iconSize, color: theme.colors.mutedForeground }) })), _jsx(SansSerifBoldText, { selectable: false, size: "lg", style: styles.title, children: title }), !!description && (_jsx(SansSerifText, { selectable: false, size: "base", style: styles.description, children: description })), children, !!actionLabel && onAction && (_jsx(Button, { preset: actionPreset, onPress: onAction, text: actionLabel, style: styles.action }))] }));
|
|
30
30
|
}
|
|
31
|
+
const MEDIA_SIZE = 56;
|
|
31
32
|
const createStyles = (theme) => StyleSheet.create({
|
|
32
33
|
container: {
|
|
33
34
|
alignItems: "center",
|
|
@@ -35,19 +36,34 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
35
36
|
paddingVertical: spacing.xxl,
|
|
36
37
|
paddingHorizontal: spacing.lg,
|
|
37
38
|
},
|
|
39
|
+
bordered: {
|
|
40
|
+
borderWidth: 1,
|
|
41
|
+
borderStyle: "dashed",
|
|
42
|
+
borderColor: theme.colors.border,
|
|
43
|
+
borderRadius: spacing.radiusLg,
|
|
44
|
+
padding: spacing.xl,
|
|
45
|
+
},
|
|
38
46
|
iconWrapper: {
|
|
47
|
+
width: MEDIA_SIZE,
|
|
48
|
+
height: MEDIA_SIZE,
|
|
49
|
+
// radiusMd here — the tokens/radius-rebase spec (which would bump this
|
|
50
|
+
// to radiusLg) hasn't landed on this branch.
|
|
51
|
+
borderRadius: spacing.radiusMd,
|
|
52
|
+
backgroundColor: theme.colors.muted,
|
|
53
|
+
alignItems: "center",
|
|
54
|
+
justifyContent: "center",
|
|
39
55
|
marginBottom: spacing.md,
|
|
40
56
|
},
|
|
41
57
|
title: {
|
|
42
|
-
fontSize
|
|
43
|
-
|
|
58
|
+
// fontSize/lineHeight/letterSpacing now come from StyledText's "lg"
|
|
59
|
+
// token (18/27/-0.09) via the size prop below, so this picks up the
|
|
60
|
+
// shared letter-spacing scale instead of a hardcoded 0.
|
|
44
61
|
color: theme.colors.foreground,
|
|
45
62
|
textAlign: "center",
|
|
46
|
-
letterSpacing: 0,
|
|
47
63
|
},
|
|
48
64
|
description: {
|
|
49
|
-
fontSize
|
|
50
|
-
|
|
65
|
+
// fontSize/lineHeight/letterSpacing now come from StyledText's "base"
|
|
66
|
+
// token (14/21/0) via the size prop below.
|
|
51
67
|
color: theme.colors.mutedForeground,
|
|
52
68
|
textAlign: "center",
|
|
53
69
|
marginTop: spacing.sm,
|
|
@@ -58,3 +74,4 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
58
74
|
alignSelf: "center",
|
|
59
75
|
},
|
|
60
76
|
});
|
|
77
|
+
const themedStyles = createThemedStyles(createStyles);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useRef, useState, useCallback } from "react";
|
|
3
3
|
import { View, TextInput as RNTextInput, Pressable, StyleSheet, Platform, } from "react-native";
|
|
4
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
5
|
import { spacing } from "../constants/spacing.js";
|
|
6
6
|
import { fontFamilies } from "../constants/fonts.js";
|
|
7
7
|
import { StyledText } from "./StyledText.js";
|
|
8
|
+
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
8
9
|
const CELL_WIDTH = 36;
|
|
9
10
|
const CELL_HEIGHT = 40;
|
|
10
11
|
const CELL_FONT_SIZE = 20;
|
|
@@ -30,7 +31,7 @@ function InputOTP({ length = 6, value = "", onChangeText, onComplete, error = fa
|
|
|
30
31
|
const { theme } = useTheme();
|
|
31
32
|
const inputRef = useRef(null);
|
|
32
33
|
const [focused, setFocused] = useState(false);
|
|
33
|
-
const styles =
|
|
34
|
+
const styles = themedStyles(theme);
|
|
34
35
|
const hasError = error || !!errorText;
|
|
35
36
|
// Active cell index: next empty cell, or last cell when full
|
|
36
37
|
const activeIndex = Math.min(value.length, length - 1);
|
|
@@ -99,8 +100,12 @@ function OTPCell({ index, total, char, isActive, hasError, disabled, theme, onPr
|
|
|
99
100
|
},
|
|
100
101
|
], children: _jsx(StyledText, { selectable: false, style: {
|
|
101
102
|
fontSize: CELL_FONT_SIZE,
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
// Semibold weight: the family carries it on native (a real static
|
|
104
|
+
// Inter_600SemiBold file); web shares one "Inter" family across
|
|
105
|
+
// weights, so it needs the numeric fontWeight to pick the right
|
|
106
|
+
// @font-face variant (same rule as StyledText's WEB_FONT_WEIGHTS).
|
|
107
|
+
...(Platform.OS === "web" && { fontWeight: CELL_FONT_WEIGHT }),
|
|
108
|
+
fontFamily: fontFamilies.sansSerif.semibold,
|
|
104
109
|
color: theme.colors.text,
|
|
105
110
|
textAlign: "center",
|
|
106
111
|
lineHeight: CELL_FONT_SIZE * 1.2,
|
|
@@ -128,4 +133,5 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
128
133
|
marginTop: spacing.xs,
|
|
129
134
|
},
|
|
130
135
|
});
|
|
136
|
+
const themedStyles = createThemedStyles(createStyles);
|
|
131
137
|
export { InputOTP };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
import { type TextProps } from "./StyledText";
|
|
4
|
+
import { type IconName, type ThemeColorName } from "./Icon";
|
|
5
|
+
export interface ItemProps {
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
/** Makes the row pressable with scale feedback (mirrors Card's pattern). */
|
|
8
|
+
onPress?: () => void;
|
|
9
|
+
/** Disables press handling when `onPress` is set. */
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/** Renders a hairline divider below the row, inset past the media slot. */
|
|
12
|
+
separator?: boolean;
|
|
13
|
+
/** Custom style override for the row. */
|
|
14
|
+
style?: StyleProp<ViewStyle>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Item
|
|
18
|
+
*
|
|
19
|
+
* Grouped-list row container: `spacing.md` padding and gap, a comfortable
|
|
20
|
+
* 44pt min height, and an optional pressable scale interaction. Compose
|
|
21
|
+
* with `ItemMedia`, `ItemContent` (+ `ItemTitle`/`ItemDescription`), and
|
|
22
|
+
* `ItemActions`.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <Item onPress={() => {}} separator>
|
|
27
|
+
* <ItemMedia icon="bell" />
|
|
28
|
+
* <ItemContent>
|
|
29
|
+
* <ItemTitle>Notifications</ItemTitle>
|
|
30
|
+
* <ItemDescription>Push, email, and SMS alerts</ItemDescription>
|
|
31
|
+
* </ItemContent>
|
|
32
|
+
* <ItemActions>
|
|
33
|
+
* <Icon name="chevron-right" size={18} color="mutedForeground" />
|
|
34
|
+
* </ItemActions>
|
|
35
|
+
* </Item>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function Item({ children, onPress, disabled, separator, style }: ItemProps): React.JSX.Element;
|
|
39
|
+
export interface ItemMediaProps {
|
|
40
|
+
/** Square size of the media slot in pixels. @default 40 */
|
|
41
|
+
size?: number;
|
|
42
|
+
/** Icon to center in the slot. Ignored if `children` is provided. */
|
|
43
|
+
icon?: IconName;
|
|
44
|
+
/** Icon color — theme color name or literal. @default "mutedForeground" */
|
|
45
|
+
iconColor?: string | ThemeColorName;
|
|
46
|
+
/** Icon size in pixels. @default 20 */
|
|
47
|
+
iconSize?: number;
|
|
48
|
+
/** Arbitrary content (e.g. an avatar image) rendered instead of the icon. */
|
|
49
|
+
children?: React.ReactNode;
|
|
50
|
+
/** Custom style override. */
|
|
51
|
+
style?: StyleProp<ViewStyle>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* ItemMedia
|
|
55
|
+
*
|
|
56
|
+
* Fixed-size leading slot for an `Item` row — an icon centered in a rounded
|
|
57
|
+
* square by default, or arbitrary content (e.g. an avatar) via `children`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function ItemMedia({ size, icon, iconColor, iconSize, children, style }: ItemMediaProps): React.JSX.Element;
|
|
60
|
+
export interface ItemContentProps {
|
|
61
|
+
children?: React.ReactNode;
|
|
62
|
+
style?: StyleProp<ViewStyle>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* ItemContent
|
|
66
|
+
*
|
|
67
|
+
* Flex-1 vertical stack for an `Item` row's title/description (or any other
|
|
68
|
+
* content). Sits between `ItemMedia` and `ItemActions`.
|
|
69
|
+
*/
|
|
70
|
+
export declare function ItemContent({ children, style }: ItemContentProps): React.JSX.Element;
|
|
71
|
+
/**
|
|
72
|
+
* ItemTitle
|
|
73
|
+
*
|
|
74
|
+
* `Item` row title — label weight (medium) at body size.
|
|
75
|
+
*/
|
|
76
|
+
export declare function ItemTitle({ children, style, ...props }: TextProps): React.JSX.Element;
|
|
77
|
+
/**
|
|
78
|
+
* ItemDescription
|
|
79
|
+
*
|
|
80
|
+
* `Item` row secondary line — caption size in the muted `textDim` color.
|
|
81
|
+
*/
|
|
82
|
+
export declare function ItemDescription({ children, style, ...props }: TextProps): React.JSX.Element;
|
|
83
|
+
export interface ItemActionsProps {
|
|
84
|
+
children?: React.ReactNode;
|
|
85
|
+
style?: StyleProp<ViewStyle>;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* ItemActions
|
|
89
|
+
*
|
|
90
|
+
* Trailing row for an `Item` — centers and gaps its children (a chevron
|
|
91
|
+
* icon, a `Switch`, value text, etc.).
|
|
92
|
+
*/
|
|
93
|
+
export declare function ItemActions({ children, style }: ItemActionsProps): React.JSX.Element;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { View, Pressable, StyleSheet, Platform, Animated, } from "react-native";
|
|
3
|
+
import { StyledText, CaptionText } from "./StyledText.js";
|
|
4
|
+
import { Icon } from "./Icon.js";
|
|
5
|
+
import { useTheme } from "../hooks/useTheme.js";
|
|
6
|
+
import { useScalePress } from "../hooks/useScalePress.js";
|
|
7
|
+
import { spacing } from "../constants/spacing.js";
|
|
8
|
+
// Default ItemMedia footprint (see ItemMedia's `size` prop) — used to inset
|
|
9
|
+
// the optional separator past the media slot without needing a context.
|
|
10
|
+
const DEFAULT_MEDIA_SIZE = 40;
|
|
11
|
+
const SEPARATOR_INSET = spacing.md + DEFAULT_MEDIA_SIZE + spacing.md;
|
|
12
|
+
/**
|
|
13
|
+
* Item
|
|
14
|
+
*
|
|
15
|
+
* Grouped-list row container: `spacing.md` padding and gap, a comfortable
|
|
16
|
+
* 44pt min height, and an optional pressable scale interaction. Compose
|
|
17
|
+
* with `ItemMedia`, `ItemContent` (+ `ItemTitle`/`ItemDescription`), and
|
|
18
|
+
* `ItemActions`.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <Item onPress={() => {}} separator>
|
|
23
|
+
* <ItemMedia icon="bell" />
|
|
24
|
+
* <ItemContent>
|
|
25
|
+
* <ItemTitle>Notifications</ItemTitle>
|
|
26
|
+
* <ItemDescription>Push, email, and SMS alerts</ItemDescription>
|
|
27
|
+
* </ItemContent>
|
|
28
|
+
* <ItemActions>
|
|
29
|
+
* <Icon name="chevron-right" size={18} color="mutedForeground" />
|
|
30
|
+
* </ItemActions>
|
|
31
|
+
* </Item>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function Item({ children, onPress, disabled, separator = false, style }) {
|
|
35
|
+
const { theme } = useTheme();
|
|
36
|
+
const { animatedStyle, pressHandlers } = useScalePress({
|
|
37
|
+
disabled: !onPress || !!disabled,
|
|
38
|
+
scaleTo: 0.98,
|
|
39
|
+
haptic: false,
|
|
40
|
+
});
|
|
41
|
+
const row = (_jsx(View, { style: [styles.row, style], children: children }));
|
|
42
|
+
const content = separator ? (_jsxs(View, { children: [row, _jsx(View, { style: [styles.separator, { backgroundColor: theme.colors.border }] })] })) : (row);
|
|
43
|
+
if (onPress) {
|
|
44
|
+
return (_jsx(Pressable, { onPress: onPress, disabled: disabled, accessibilityRole: "button", accessibilityState: { disabled: !!disabled }, ...pressHandlers, style: Platform.OS === "web" ? { cursor: "pointer" } : undefined, children: _jsx(Animated.View, { style: animatedStyle, children: content }) }));
|
|
45
|
+
}
|
|
46
|
+
return content;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* ItemMedia
|
|
50
|
+
*
|
|
51
|
+
* Fixed-size leading slot for an `Item` row — an icon centered in a rounded
|
|
52
|
+
* square by default, or arbitrary content (e.g. an avatar) via `children`.
|
|
53
|
+
*/
|
|
54
|
+
export function ItemMedia({ size = DEFAULT_MEDIA_SIZE, icon, iconColor = "mutedForeground", iconSize = 20, children, style }) {
|
|
55
|
+
const { theme } = useTheme();
|
|
56
|
+
return (_jsx(View, { style: [
|
|
57
|
+
{
|
|
58
|
+
width: size,
|
|
59
|
+
height: size,
|
|
60
|
+
borderRadius: spacing.radiusMd,
|
|
61
|
+
backgroundColor: theme.colors.muted,
|
|
62
|
+
alignItems: "center",
|
|
63
|
+
justifyContent: "center",
|
|
64
|
+
},
|
|
65
|
+
style,
|
|
66
|
+
], children: children ?? (!!icon && _jsx(Icon, { name: icon, size: iconSize, color: iconColor, decorative: true })) }));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* ItemContent
|
|
70
|
+
*
|
|
71
|
+
* Flex-1 vertical stack for an `Item` row's title/description (or any other
|
|
72
|
+
* content). Sits between `ItemMedia` and `ItemActions`.
|
|
73
|
+
*/
|
|
74
|
+
export function ItemContent({ children, style }) {
|
|
75
|
+
return _jsx(View, { style: [styles.content, style], children: children });
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* ItemTitle
|
|
79
|
+
*
|
|
80
|
+
* `Item` row title — label weight (medium) at body size.
|
|
81
|
+
*/
|
|
82
|
+
export function ItemTitle({ children, style, ...props }) {
|
|
83
|
+
return (_jsx(StyledText, { size: "body", fontWeight: "medium", ...props, style: style, children: children }));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* ItemDescription
|
|
87
|
+
*
|
|
88
|
+
* `Item` row secondary line — caption size in the muted `textDim` color.
|
|
89
|
+
*/
|
|
90
|
+
export function ItemDescription({ children, style, ...props }) {
|
|
91
|
+
const { theme } = useTheme();
|
|
92
|
+
return (_jsx(CaptionText, { ...props, style: [{ color: theme.colors.textDim }, style], children: children }));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* ItemActions
|
|
96
|
+
*
|
|
97
|
+
* Trailing row for an `Item` — centers and gaps its children (a chevron
|
|
98
|
+
* icon, a `Switch`, value text, etc.).
|
|
99
|
+
*/
|
|
100
|
+
export function ItemActions({ children, style }) {
|
|
101
|
+
return _jsx(View, { style: [styles.actions, style], children: children });
|
|
102
|
+
}
|
|
103
|
+
const styles = StyleSheet.create({
|
|
104
|
+
row: {
|
|
105
|
+
flexDirection: "row",
|
|
106
|
+
alignItems: "center",
|
|
107
|
+
minHeight: spacing.touchTarget,
|
|
108
|
+
padding: spacing.md,
|
|
109
|
+
gap: spacing.md,
|
|
110
|
+
},
|
|
111
|
+
content: {
|
|
112
|
+
flex: 1,
|
|
113
|
+
justifyContent: "center",
|
|
114
|
+
gap: spacing.xxs,
|
|
115
|
+
},
|
|
116
|
+
actions: {
|
|
117
|
+
flexDirection: "row",
|
|
118
|
+
alignItems: "center",
|
|
119
|
+
gap: spacing.sm,
|
|
120
|
+
},
|
|
121
|
+
separator: {
|
|
122
|
+
height: StyleSheet.hairlineWidth,
|
|
123
|
+
marginLeft: SEPARATOR_INSET,
|
|
124
|
+
},
|
|
125
|
+
});
|
package/dist/components/Label.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo } from "react";
|
|
3
2
|
import { StyleSheet, Platform } from "react-native";
|
|
4
3
|
import * as LabelPrimitive from "@rn-primitives/label";
|
|
5
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
6
5
|
import { spacing } from "../constants/spacing.js";
|
|
7
6
|
import { fontFamilies } from "../constants/fonts.js";
|
|
8
7
|
import { StyledText } from "./StyledText.js";
|
|
8
|
+
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
9
9
|
const SIZE_CONFIGS = {
|
|
10
10
|
sm: { fontSize: 12 },
|
|
11
11
|
md: { fontSize: 14 },
|
|
@@ -37,7 +37,7 @@ const SIZE_CONFIGS = {
|
|
|
37
37
|
*/
|
|
38
38
|
export function Label({ children, nativeID, required, size = "md", error, disabled, style, onPress, }) {
|
|
39
39
|
const { theme } = useTheme();
|
|
40
|
-
const styles =
|
|
40
|
+
const styles = themedStyles(theme);
|
|
41
41
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
42
42
|
const textStyle = StyleSheet.flatten([
|
|
43
43
|
styles.label,
|
|
@@ -56,8 +56,13 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
56
56
|
marginBottom: spacing.xs,
|
|
57
57
|
},
|
|
58
58
|
label: {
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// Medium weight: the family carries it on native (a real static
|
|
60
|
+
// Inter_500Medium file); web shares one "Inter" family across weights,
|
|
61
|
+
// so it needs the numeric fontWeight to pick the right @font-face
|
|
62
|
+
// variant. Setting fontWeight on native would faux-bold on top of the
|
|
63
|
+
// already-medium file (same rule as StyledText's WEB_FONT_WEIGHTS).
|
|
64
|
+
fontFamily: fontFamilies.sansSerif.medium,
|
|
65
|
+
...(Platform.OS === "web" && { fontWeight: "500" }),
|
|
61
66
|
color: theme.colors.text,
|
|
62
67
|
userSelect: "none",
|
|
63
68
|
},
|
|
@@ -73,3 +78,4 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
73
78
|
opacity: 0.6,
|
|
74
79
|
},
|
|
75
80
|
});
|
|
81
|
+
const themedStyles = createThemedStyles(createStyles);
|