@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +50 -6
  3. package/dist/components/Accordion.d.ts +1 -1
  4. package/dist/components/Accordion.js +25 -19
  5. package/dist/components/Badge.js +10 -7
  6. package/dist/components/BottomSheet.js +21 -19
  7. package/dist/components/Button.d.ts +2 -1
  8. package/dist/components/Button.js +22 -6
  9. package/dist/components/Card.js +5 -3
  10. package/dist/components/Checkbox.js +49 -19
  11. package/dist/components/Collapsible.d.ts +1 -1
  12. package/dist/components/Collapsible.js +22 -16
  13. package/dist/components/Drawer.js +8 -7
  14. package/dist/components/DropdownMenu.d.ts +1 -1
  15. package/dist/components/DropdownMenu.js +80 -59
  16. package/dist/components/EmptyState.d.ts +7 -1
  17. package/dist/components/EmptyState.js +26 -9
  18. package/dist/components/InputOTP.js +10 -4
  19. package/dist/components/Item.d.ts +93 -0
  20. package/dist/components/Item.js +125 -0
  21. package/dist/components/Label.js +10 -4
  22. package/dist/components/Notification.js +14 -24
  23. package/dist/components/Popover.d.ts +3 -18
  24. package/dist/components/Popover.js +10 -6
  25. package/dist/components/RadioGroup.js +49 -22
  26. package/dist/components/SectionHeader.d.ts +42 -0
  27. package/dist/components/SectionHeader.js +41 -0
  28. package/dist/components/Select.js +73 -41
  29. package/dist/components/Skeleton.js +5 -3
  30. package/dist/components/StatCard.d.ts +45 -0
  31. package/dist/components/StatCard.js +41 -0
  32. package/dist/components/StyledText.d.ts +20 -10
  33. package/dist/components/StyledText.js +85 -29
  34. package/dist/components/Switch.js +84 -57
  35. package/dist/components/Tabs.js +43 -12
  36. package/dist/components/TextInput.js +17 -11
  37. package/dist/components/Toggle.js +69 -42
  38. package/dist/components/ToggleGroup.js +82 -55
  39. package/dist/components/index.d.ts +3 -0
  40. package/dist/components/index.js +3 -0
  41. package/dist/constants/fonts.d.ts +44 -5
  42. package/dist/constants/fonts.js +42 -12
  43. package/dist/constants/index.d.ts +1 -0
  44. package/dist/constants/index.js +1 -0
  45. package/dist/constants/motion.d.ts +24 -0
  46. package/dist/constants/motion.js +24 -0
  47. package/dist/constants/spacing.d.ts +7 -7
  48. package/dist/constants/spacing.js +7 -7
  49. package/dist/hooks/useResources.d.ts +4 -2
  50. package/dist/hooks/useResources.js +30 -8
  51. package/dist/hooks/useStaggeredEntrance.d.ts +1 -1
  52. package/dist/hooks/useStaggeredEntrance.js +2 -1
  53. package/dist/hooks/useTheme.d.ts +1 -1
  54. package/dist/hooks/useTheme.js +53 -16
  55. package/dist/lib/index.d.ts +1 -0
  56. package/dist/lib/index.js +1 -0
  57. package/dist/lib/themedStyles.d.ts +21 -0
  58. package/dist/lib/themedStyles.js +33 -0
  59. package/dist/state/themeStore.d.ts +17 -0
  60. package/dist/state/themeStore.js +6 -0
  61. 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: 200,
267
+ duration: durations.normal,
267
268
  useNativeDriver: shouldUseNativeDriver,
268
269
  }),
269
270
  Animated.timing(backdropOpacity, {
270
271
  toValue: 1,
271
- duration: 200,
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: 200,
288
+ duration: durations.normal,
288
289
  useNativeDriver: shouldUseNativeDriver,
289
290
  }),
290
291
  Animated.timing(backdropOpacity, {
291
292
  toValue: 0,
292
- duration: 200,
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: 200,
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: 150,
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: 180,
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
- 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;
@@ -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 = 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
- 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 }))] }));
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: 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,
@@ -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 { useMemo, useRef, useState, useCallback } from "react";
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 = useMemo(() => createStyles(theme), [theme]);
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
- fontWeight: CELL_FONT_WEIGHT,
103
- fontFamily: fontFamilies.sansSerif.regular,
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
+ });
@@ -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 = useMemo(() => createStyles(theme), [theme]);
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
- 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
  },
@@ -73,3 +78,4 @@ const createStyles = (theme) => StyleSheet.create({
73
78
  opacity: 0.6,
74
79
  },
75
80
  });
81
+ const themedStyles = createThemedStyles(createStyles);