@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +6 -6
  3. package/dist/components/Accordion.d.ts +1 -1
  4. package/dist/components/Accordion.js +25 -19
  5. package/dist/components/Badge.js +6 -5
  6. package/dist/components/BottomSheet.js +21 -19
  7. package/dist/components/Button.d.ts +2 -1
  8. package/dist/components/Button.js +14 -4
  9. package/dist/components/Checkbox.js +49 -19
  10. package/dist/components/Collapsible.d.ts +1 -1
  11. package/dist/components/Collapsible.js +22 -16
  12. package/dist/components/Drawer.js +8 -7
  13. package/dist/components/DropdownMenu.d.ts +1 -1
  14. package/dist/components/DropdownMenu.js +80 -59
  15. package/dist/components/EmptyState.d.ts +7 -1
  16. package/dist/components/EmptyState.js +23 -7
  17. package/dist/components/InputOTP.js +6 -2
  18. package/dist/components/Item.d.ts +93 -0
  19. package/dist/components/Item.js +125 -0
  20. package/dist/components/Label.js +7 -2
  21. package/dist/components/Notification.js +10 -22
  22. package/dist/components/Popover.d.ts +3 -18
  23. package/dist/components/Popover.js +10 -6
  24. package/dist/components/RadioGroup.js +49 -22
  25. package/dist/components/SectionHeader.d.ts +42 -0
  26. package/dist/components/SectionHeader.js +41 -0
  27. package/dist/components/Select.js +73 -41
  28. package/dist/components/Skeleton.js +2 -2
  29. package/dist/components/StatCard.d.ts +45 -0
  30. package/dist/components/StatCard.js +41 -0
  31. package/dist/components/StyledText.d.ts +20 -10
  32. package/dist/components/StyledText.js +64 -25
  33. package/dist/components/Switch.js +84 -57
  34. package/dist/components/Tabs.js +43 -12
  35. package/dist/components/TextInput.js +8 -9
  36. package/dist/components/Toggle.js +69 -42
  37. package/dist/components/ToggleGroup.js +82 -55
  38. package/dist/components/index.d.ts +3 -0
  39. package/dist/components/index.js +3 -0
  40. package/dist/constants/fonts.d.ts +3 -0
  41. package/dist/constants/fonts.js +35 -12
  42. package/dist/constants/index.d.ts +1 -0
  43. package/dist/constants/index.js +1 -0
  44. package/dist/constants/motion.d.ts +24 -0
  45. package/dist/constants/motion.js +24 -0
  46. package/dist/constants/spacing.d.ts +7 -7
  47. package/dist/constants/spacing.js +7 -7
  48. package/dist/hooks/useResources.d.ts +4 -2
  49. package/dist/hooks/useResources.js +30 -8
  50. package/dist/hooks/useStaggeredEntrance.d.ts +1 -1
  51. package/dist/hooks/useStaggeredEntrance.js +2 -1
  52. package/dist/hooks/useTheme.d.ts +1 -1
  53. package/dist/hooks/useTheme.js +53 -16
  54. 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
- return _jsx(DropdownMenuPrimitive.Trigger, { accessibilityRole: "button", ...props });
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
- return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(DropdownMenuPrimitive.Item, { ...props, style: {
94
- ...styles.item,
95
- ...(Platform.OS === "web" && {
96
- cursor: props.disabled ? "not-allowed" : "pointer",
97
- outlineStyle: "none",
98
- userSelect: "none",
99
- }),
100
- ...(props.disabled && { opacity: 0.5 }),
101
- ...(inset && { paddingLeft: spacing.xl }),
102
- ...(styleOverride && typeof styleOverride !== "function"
103
- ? StyleSheet.flatten(styleOverride)
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
- return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsxs(DropdownMenuPrimitive.CheckboxItem, { ...props, style: {
110
- ...styles.indicatorItem,
111
- ...(Platform.OS === "web" && {
112
- cursor: props.disabled ? "not-allowed" : "pointer",
113
- outlineStyle: "none",
114
- userSelect: "none",
115
- }),
116
- ...(props.disabled && { opacity: 0.5 }),
117
- ...(styleOverride && typeof styleOverride !== "function"
118
- ? StyleSheet.flatten(styleOverride)
119
- : {}),
120
- }, children: [_jsx(View, { style: {
121
- position: "absolute",
122
- left: spacing.sm,
123
- height: 14,
124
- width: 14,
125
- alignItems: "center",
126
- justifyContent: "center",
127
- }, 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] }) }) }));
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
- return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsxs(DropdownMenuPrimitive.RadioItem, { ...props, style: {
132
- ...styles.indicatorItem,
133
- ...(Platform.OS === "web" && {
134
- cursor: props.disabled ? "not-allowed" : "pointer",
135
- outlineStyle: "none",
136
- userSelect: "none",
137
- }),
138
- ...(props.disabled && { opacity: 0.5 }),
139
- ...(styleOverride && typeof styleOverride !== "function"
140
- ? StyleSheet.flatten(styleOverride)
141
- : {}),
142
- }, children: [_jsx(View, { style: {
143
- position: "absolute",
144
- left: spacing.sm,
145
- height: 14,
146
- width: 14,
147
- alignItems: "center",
148
- justifyContent: "center",
149
- }, children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(View, { style: {
150
- backgroundColor: theme.colors.text,
151
- height: 8,
152
- width: 8,
153
- borderRadius: 4,
154
- } }) }) }), typeof children === "function" ? null : children] }) }) }));
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 = 48, title, description, actionLabel, onAction, actionPreset = "default", style, children, }) {
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: 18,
43
- lineHeight: 24,
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: 14,
50
- lineHeight: 20,
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
- fontWeight: CELL_FONT_WEIGHT,
103
- fontFamily: fontFamilies.sansSerif.regular,
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
+ });
@@ -56,8 +56,13 @@ const createStyles = (theme) => StyleSheet.create({
56
56
  marginBottom: spacing.xs,
57
57
  },
58
58
  label: {
59
- fontFamily: fontFamilies.sansSerif.regular,
60
- fontWeight: "500",
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: 150, easing: Easing.out(Easing.quad), useNativeDriver: true };
15
- const timingOut = { duration: 100, easing: Easing.in(Easing.quad), useNativeDriver: true };
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, style: [styles.alertTitle, { color: theme.colors.foreground }], numberOfLines: 1, children: title })), hasMessage && (_jsx(StyledText, { selectable: false, style: [styles.alertDescription, { color: theme.colors.mutedForeground }], numberOfLines: 2, children: message }))] }), action && (_jsx(Pressable, { style: ({ pressed }) => [
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, style: [styles.actionLabel, { 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 }) })] }) }));
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
- fontFamily: fontFamilies.sansSerif.regular,
275
- fontWeight: "600",
276
- fontSize: 14,
277
- lineHeight: 20,
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
- declare const PopoverTrigger: {
9
- ({ asChild, onPress: onPressProp, disabled, ref, ...props }: Omit<import("react-native").PressableProps & React.RefAttributes<View>, "ref"> & {
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
- * Popover Trigger Component
13
- * The element that triggers the popover to open/close
14
- */
15
- const PopoverTrigger = PopoverPrimitive.Trigger;
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