@mrmeg/expo-ui 0.14.0 → 0.15.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 +23 -0
- package/README.md +6 -6
- package/dist/components/Accordion.d.ts +1 -1
- package/dist/components/Accordion.js +25 -19
- package/dist/components/Badge.js +6 -5
- package/dist/components/BottomSheet.js +21 -19
- package/dist/components/Button.d.ts +2 -1
- package/dist/components/Button.js +14 -4
- 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 +23 -7
- package/dist/components/InputOTP.js +6 -2
- package/dist/components/Item.d.ts +93 -0
- package/dist/components/Item.js +125 -0
- package/dist/components/Label.js +7 -2
- package/dist/components/Notification.js +10 -22
- 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 +2 -2
- 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 +64 -25
- package/dist/components/Switch.js +84 -57
- package/dist/components/Tabs.js +43 -12
- package/dist/components/TextInput.js +8 -9
- 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 +3 -0
- package/dist/constants/fonts.js +35 -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/package.json +20 -19
|
@@ -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;
|
|
@@ -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
28
|
const styles = useMemo(() => createStyles(theme), [theme]);
|
|
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 }))] }));
|
|
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,
|
|
@@ -99,8 +99,12 @@ function OTPCell({ index, total, char, isActive, hasError, disabled, theme, onPr
|
|
|
99
99
|
},
|
|
100
100
|
], children: _jsx(StyledText, { selectable: false, style: {
|
|
101
101
|
fontSize: CELL_FONT_SIZE,
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
// Semibold weight: the family carries it on native (a real static
|
|
103
|
+
// Inter_600SemiBold file); web shares one "Inter" family across
|
|
104
|
+
// weights, so it needs the numeric fontWeight to pick the right
|
|
105
|
+
// @font-face variant (same rule as StyledText's WEB_FONT_WEIGHTS).
|
|
106
|
+
...(Platform.OS === "web" && { fontWeight: CELL_FONT_WEIGHT }),
|
|
107
|
+
fontFamily: fontFamilies.sansSerif.semibold,
|
|
104
108
|
color: theme.colors.text,
|
|
105
109
|
textAlign: "center",
|
|
106
110
|
lineHeight: CELL_FONT_SIZE * 1.2,
|
|
@@ -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
|
@@ -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
|
},
|
|
@@ -2,17 +2,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useMemo, useCallback, use, useEffect, useEffectEvent, useRef } from "react";
|
|
3
3
|
import { Animated, Easing, StyleSheet, View, ActivityIndicator, Pressable, Platform } from "react-native";
|
|
4
4
|
import { SafeAreaInsetsContext } from "react-native-safe-area-context";
|
|
5
|
-
import { fontFamilies } from "../constants/fonts.js";
|
|
6
5
|
import { Icon } from "./Icon.js";
|
|
7
6
|
import { useTheme } from "../hooks/useTheme.js";
|
|
8
7
|
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
9
8
|
import { spacing } from "../constants/spacing.js";
|
|
9
|
+
import { durations } from "../constants/motion.js";
|
|
10
10
|
import { StyledText } from "./StyledText.js";
|
|
11
11
|
import { palette } from "../constants/colors.js";
|
|
12
12
|
import { translateText } from "../lib/i18n.js";
|
|
13
13
|
import { globalUIStore } from "../state/globalUIStore.js";
|
|
14
|
-
const timingIn = { duration:
|
|
15
|
-
const timingOut = { duration:
|
|
14
|
+
const timingIn = { duration: durations.fast, easing: Easing.out(Easing.quad), useNativeDriver: true };
|
|
15
|
+
const timingOut = { duration: durations.instant, easing: Easing.in(Easing.quad), useNativeDriver: true };
|
|
16
16
|
/**
|
|
17
17
|
* Notification
|
|
18
18
|
*
|
|
@@ -219,14 +219,14 @@ export const Notification = () => {
|
|
|
219
219
|
styles.alert,
|
|
220
220
|
isBottom && styles.alertBottom,
|
|
221
221
|
getShadowStyle("elevated"),
|
|
222
|
-
], children: [_jsx(View, { style: [styles.iconBadge, { backgroundColor: iconBgColor }], children: alert?.loading ? (_jsx(ActivityIndicator, { size: "small", color: iconColor })) : (_jsx(Icon, { name: icon, size: 18, color: iconColor })) }), _jsxs(View, { style: styles.alertContent, children: [!!title && (_jsx(StyledText, { selectable: false,
|
|
222
|
+
], children: [_jsx(View, { style: [styles.iconBadge, { backgroundColor: iconBgColor }], children: alert?.loading ? (_jsx(ActivityIndicator, { size: "small", color: iconColor })) : (_jsx(Icon, { name: icon, size: 18, color: iconColor })) }), _jsxs(View, { style: styles.alertContent, children: [!!title && (_jsx(StyledText, { selectable: false, size: "base", fontWeight: "semibold", style: { color: theme.colors.foreground }, numberOfLines: 1, children: title })), hasMessage && (_jsx(StyledText, { selectable: false, size: "sm", style: { color: theme.colors.mutedForeground }, numberOfLines: 2, children: message }))] }), action && (_jsx(Pressable, { style: ({ pressed }) => [
|
|
223
223
|
styles.actionButton,
|
|
224
224
|
{
|
|
225
225
|
borderColor: theme.colors.primary + "30",
|
|
226
226
|
backgroundColor: theme.colors.primary + "10",
|
|
227
227
|
},
|
|
228
228
|
pressed && styles.actionButtonPressed,
|
|
229
|
-
], hitSlop: spacing.xs, onPress: handleActionPress, accessibilityLabel: action.label, accessibilityRole: "button", children: _jsx(StyledText, { selectable: false,
|
|
229
|
+
], hitSlop: spacing.xs, onPress: handleActionPress, accessibilityLabel: action.label, accessibilityRole: "button", children: _jsx(StyledText, { selectable: false, size: "sm", fontWeight: "semibold", style: { color: theme.colors.primary }, numberOfLines: 1, children: action.label }) })), _jsx(Pressable, { style: styles.closeButton, hitSlop: spacing.sm, onPress: animateOut, accessibilityLabel: "Dismiss notification", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: 16, color: theme.colors.mutedForeground }) })] }) }));
|
|
230
230
|
};
|
|
231
231
|
const createStyles = (theme) => StyleSheet.create({
|
|
232
232
|
container: {
|
|
@@ -270,17 +270,11 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
270
270
|
justifyContent: "center",
|
|
271
271
|
gap: spacing.xxs,
|
|
272
272
|
},
|
|
273
|
-
alertTitle
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
},
|
|
279
|
-
alertDescription: {
|
|
280
|
-
fontFamily: fontFamilies.sansSerif.regular,
|
|
281
|
-
fontSize: 13,
|
|
282
|
-
lineHeight: 18,
|
|
283
|
-
},
|
|
273
|
+
// alertTitle/alertDescription/actionLabel font styling (family, size,
|
|
274
|
+
// weight, lineHeight) now comes entirely from StyledText's size/fontWeight
|
|
275
|
+
// props (see JSX above) — the nearest tokens to the previous hardcoded
|
|
276
|
+
// values (14/20/600, 13/18/regular, 13/18/600) are "base"+"semibold",
|
|
277
|
+
// "sm", and "sm"+"semibold" respectively.
|
|
284
278
|
actionButton: {
|
|
285
279
|
minHeight: 28,
|
|
286
280
|
maxWidth: 140,
|
|
@@ -295,12 +289,6 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
295
289
|
actionButtonPressed: {
|
|
296
290
|
opacity: 0.75,
|
|
297
291
|
},
|
|
298
|
-
actionLabel: {
|
|
299
|
-
fontFamily: fontFamilies.sansSerif.regular,
|
|
300
|
-
fontWeight: "600",
|
|
301
|
-
fontSize: 13,
|
|
302
|
-
lineHeight: 18,
|
|
303
|
-
},
|
|
304
292
|
closeButton: {
|
|
305
293
|
position: "absolute",
|
|
306
294
|
top: spacing.sm,
|
|
@@ -5,15 +5,8 @@ import { View, ViewProps } from "react-native";
|
|
|
5
5
|
* Popover Trigger Component
|
|
6
6
|
* The element that triggers the popover to open/close
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
asChild?: boolean;
|
|
11
|
-
} & {
|
|
12
|
-
onKeyDown?: (ev: React.KeyboardEvent) => void;
|
|
13
|
-
onKeyUp?: (ev: React.KeyboardEvent) => void;
|
|
14
|
-
} & React.RefAttributes<import("@rn-primitives/popover").TriggerRef>): React.JSX.Element;
|
|
15
|
-
displayName: string;
|
|
16
|
-
};
|
|
8
|
+
type PopoverTriggerProps = PopoverPrimitive.TriggerProps;
|
|
9
|
+
declare function PopoverTrigger({ disabled, ...props }: PopoverTriggerProps): React.JSX.Element;
|
|
17
10
|
interface PopoverContentProps extends PopoverPrimitive.ContentProps {
|
|
18
11
|
/**
|
|
19
12
|
* Optional portal host name for custom portal mounting
|
|
@@ -62,15 +55,7 @@ declare const Popover: {
|
|
|
62
55
|
} & React.RefAttributes<View>): React.JSX.Element;
|
|
63
56
|
displayName: string;
|
|
64
57
|
} & {
|
|
65
|
-
Trigger:
|
|
66
|
-
({ asChild, onPress: onPressProp, disabled, ref, ...props }: Omit<import("react-native").PressableProps & React.RefAttributes<View>, "ref"> & {
|
|
67
|
-
asChild?: boolean;
|
|
68
|
-
} & {
|
|
69
|
-
onKeyDown?: (ev: React.KeyboardEvent) => void;
|
|
70
|
-
onKeyUp?: (ev: React.KeyboardEvent) => void;
|
|
71
|
-
} & React.RefAttributes<import("@rn-primitives/popover").TriggerRef>): React.JSX.Element;
|
|
72
|
-
displayName: string;
|
|
73
|
-
};
|
|
58
|
+
Trigger: typeof PopoverTrigger;
|
|
74
59
|
Content: typeof PopoverContent;
|
|
75
60
|
Header: typeof PopoverHeader;
|
|
76
61
|
Body: typeof PopoverBody;
|
|
@@ -3,16 +3,20 @@ import { AnimatedView } from "./AnimatedView.js";
|
|
|
3
3
|
import { TextClassContext, TextColorContext } from "./StyledText.context";
|
|
4
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
5
|
import { spacing } from "../constants/spacing.js";
|
|
6
|
+
import { useScalePress } from "../hooks/useScalePress.js";
|
|
6
7
|
import * as PopoverPrimitive from "@rn-primitives/popover";
|
|
7
8
|
import * as React from "react";
|
|
8
|
-
import { Platform, StyleSheet, View } from "react-native";
|
|
9
|
+
import { Animated, Platform, StyleSheet, View } from "react-native";
|
|
9
10
|
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
10
11
|
import { palette } from "../constants/colors.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
function PopoverTrigger({ disabled, ...props }) {
|
|
13
|
+
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
14
|
+
disabled: !!disabled,
|
|
15
|
+
scaleTo: 0.97,
|
|
16
|
+
haptic: false,
|
|
17
|
+
});
|
|
18
|
+
return (_jsx(Animated.View, { style: scaleStyle, children: _jsx(PopoverPrimitive.Trigger, { disabled: disabled, ...props, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut }) }));
|
|
19
|
+
}
|
|
16
20
|
/**
|
|
17
21
|
* FullWindowOverlay wrapper - uses native overlay on iOS for proper z-index handling
|
|
18
22
|
* On Android/Web, uses Fragment to avoid unnecessary nesting
|