@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
package/CHANGELOG.md CHANGED
@@ -3,6 +3,29 @@
3
3
  All notable changes to `@mrmeg/expo-ui` are documented here. This project
4
4
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## [0.15.0]
7
+
8
+ ### Added
9
+
10
+ - **New composition primitives: `SectionHeader`, `StatCard`, and `Item`.**
11
+ `EmptyState` was also upgraded with richer layout options as part of the
12
+ same pass.
13
+ - **Typography system.** The package now loads Inter with real weights and a
14
+ letter-spacing scale via `useResources`, replacing synthetic bolding.
15
+ - Motion tokens, dual-layer shadow tokens, and softer radii in the design
16
+ tokens.
17
+
18
+ ### Changed
19
+
20
+ - Interaction sweep across components: press feedback, focus rings, and
21
+ shadow adoption.
22
+ - Updated all `@rn-primitives/*` dependencies from `~1.4.0` to `~1.5.2`,
23
+ picking up upstream's native accessibility overhaul: menus, popovers, and
24
+ dialogs are now usable with VoiceOver/TalkBack (focus moves into opened
25
+ content, escape gestures dismiss), toggles and switches announce the
26
+ correct state, and `nativeID` plumbing that broke Reanimated exiting
27
+ animations was removed.
28
+
6
29
  ## [0.14.0]
7
30
 
8
31
  ### Changed
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @mrmeg/expo-ui
2
2
 
3
- Reusable Expo and React Native UI primitives shared by the template and consumer apps. The package does not ship font files; web consumers load Lato from Google Fonts and native consumers use platform sans-serif fallbacks.
3
+ Reusable Expo and React Native UI primitives shared by the template and consumer apps. Typography is Inter: native consumers load four static Inter weights via `@expo-google-fonts/inter`, web consumers load Inter from Google Fonts.
4
4
 
5
5
  This package is public for installability, reuse across MrMeg projects, and
6
6
  discoverability. It is a personal reusable Expo / React Native design-system
@@ -394,21 +394,21 @@ export default function RootLayout() {
394
394
 
395
395
  ## Fonts
396
396
 
397
- This package does not ship Lato `.ttf` files or other font binaries.
397
+ Typography is Inter across every platform, in four static weights (`Inter_400Regular`, `Inter_500Medium`, `Inter_600SemiBold`, `Inter_700Bold`) from `@expo-google-fonts/inter` — a package dependency, so consumers get it automatically.
398
398
 
399
- On web, `useResources()` injects the Google Fonts Lato stylesheet after hydration if the app has not already added it. For better first paint in Expo Router web apps, add the links in app-owned `app/+html.tsx`:
399
+ On native, `useResources()` loads the four static weights so `StyledText`'s `light`–`bold` range resolves to real font files instead of a faked OS bold. On web, `useResources()` injects a single Google Fonts Inter stylesheet (covering all four weights) after hydration if the app has not already added it; weight differentiation on web comes from a numeric `fontWeight` alongside the shared `"Inter"` CSS family. For better first paint in Expo Router web apps, add the links in app-owned `app/+html.tsx`:
400
400
 
401
401
  ```tsx
402
402
  <link rel="preconnect" href="https://fonts.googleapis.com" />
403
403
  <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
404
404
  <link
405
- id="mrmeg-expo-ui-lato"
405
+ id="mrmeg-expo-ui-inter"
406
406
  rel="stylesheet"
407
- href="https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap"
407
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
408
408
  />
409
409
  ```
410
410
 
411
- On native, the package uses platform sans-serif fallbacks. `useResources()` still loads `Feather.font` from the package-managed `@expo/vector-icons` dependency for icon rendering.
411
+ `useResources()` also loads `Feather.font` from the package-managed `@expo/vector-icons` dependency for icon rendering.
412
412
 
413
413
  ## Package Checks
414
414
 
@@ -43,7 +43,7 @@ declare function AccordionItem({ children, value, style: styleOverride, ...props
43
43
  * Clickable header that expands/collapses the content
44
44
  * Includes animated chevron icon
45
45
  */
46
- declare function AccordionTrigger({ children, style: styleOverride, ...props }: AccordionPrimitive.TriggerProps & {
46
+ declare function AccordionTrigger({ children, style: styleOverride, disabled, ...props }: AccordionPrimitive.TriggerProps & {
47
47
  children?: React.ReactNode;
48
48
  } & React.RefAttributes<AccordionPrimitive.TriggerRef>): import("react").JSX.Element;
49
49
  /**
@@ -5,6 +5,7 @@ import { Icon } from "./Icon.js";
5
5
  import { TextClassContext, TextSelectabilityContext } from "./StyledText.context";
6
6
  import { useTheme } from "../hooks/useTheme.js";
7
7
  import { useReducedMotion } from "../hooks/useReduceMotion.js";
8
+ import { useScalePress } from "../hooks/useScalePress.js";
8
9
  import { spacing } from "../constants/spacing.js";
9
10
  import * as AccordionPrimitive from "@rn-primitives/accordion";
10
11
  function normalizeSingleValue(value) {
@@ -96,11 +97,16 @@ const Trigger = Platform.OS === "web" ? View : Pressable;
96
97
  * Clickable header that expands/collapses the content
97
98
  * Includes animated chevron icon
98
99
  */
99
- function AccordionTrigger({ children, style: styleOverride, ...props }) {
100
+ function AccordionTrigger({ children, style: styleOverride, disabled, ...props }) {
100
101
  const { theme } = useTheme();
101
102
  const reduceMotion = useReducedMotion();
102
103
  const { isExpanded } = AccordionPrimitive.useItemContext();
103
104
  const rotation = useRef(new Animated.Value(isExpanded ? 1 : 0)).current;
105
+ const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
106
+ disabled: !!disabled,
107
+ scaleTo: 0.97,
108
+ haptic: false,
109
+ });
104
110
  useEffect(() => {
105
111
  const target = isExpanded ? 1 : 0;
106
112
  Animated.timing(rotation, {
@@ -119,24 +125,24 @@ function AccordionTrigger({ children, style: styleOverride, ...props }) {
119
125
  },
120
126
  ],
121
127
  };
122
- return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(AccordionPrimitive.Header, { children: _jsx(AccordionPrimitive.Trigger, { ...props, asChild: true, children: _jsxs(Trigger, { style: [
123
- {
124
- flexDirection: "row",
125
- alignItems: "center",
126
- justifyContent: "space-between",
127
- gap: spacing.md,
128
- borderRadius: spacing.radiusMd,
129
- paddingVertical: spacing.md,
130
- ...(Platform.OS === "web" && {
131
- cursor: "pointer",
132
- userSelect: "none",
133
- }),
134
- },
135
- // Spread array styles from primitives to prevent nested arrays on web
136
- ...(styleOverride && typeof styleOverride !== "function"
137
- ? (Array.isArray(styleOverride) ? styleOverride : [styleOverride])
138
- : []),
139
- ], children: [_jsx(_Fragment, { children: children }), _jsx(Animated.View, { style: chevronStyle, children: _jsx(Icon, { name: "chevron-down", size: 16, color: theme.colors.textDim, decorative: true }) })] }) }) }) }) }));
128
+ return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(AccordionPrimitive.Header, { children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(AccordionPrimitive.Trigger, { ...props, disabled: disabled, asChild: true, children: _jsxs(Trigger, { onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, style: [
129
+ {
130
+ flexDirection: "row",
131
+ alignItems: "center",
132
+ justifyContent: "space-between",
133
+ gap: spacing.md,
134
+ borderRadius: spacing.radiusMd,
135
+ paddingVertical: spacing.md,
136
+ ...(Platform.OS === "web" && {
137
+ cursor: "pointer",
138
+ userSelect: "none",
139
+ }),
140
+ },
141
+ // Spread array styles from primitives to prevent nested arrays on web
142
+ ...(styleOverride && typeof styleOverride !== "function"
143
+ ? (Array.isArray(styleOverride) ? styleOverride : [styleOverride])
144
+ : []),
145
+ ], children: [_jsx(_Fragment, { children: children }), _jsx(Animated.View, { style: chevronStyle, children: _jsx(Icon, { name: "chevron-down", size: 16, color: theme.colors.textDim, decorative: true }) })] }) }) }) }) }) }));
140
146
  }
141
147
  /**
142
148
  * Accordion Content Component
@@ -30,9 +30,13 @@ function Badge({ children, text, variant = "default", style: styleOverride }) {
30
30
  ];
31
31
  const normalizedChildren = React.Children.toArray(badgeContent);
32
32
  const hasOnlyTextChildren = normalizedChildren.every((child) => typeof child === "string" || typeof child === "number");
33
- const content = hasOnlyTextChildren ? (_jsx(StyledText, { selectable: false, style: textStyle, children: normalizedChildren.join("") })) : (React.Children.map(badgeContent, (child) => {
33
+ // size="sm" + fontWeight="medium" match this badge's fontSize/lineHeight/weight
34
+ // (12/18/500) via the shared StyledText scale — including its "sm" letter
35
+ // spacing and correct native weight-file resolution — instead of a hand-rolled
36
+ // fontSize/fontWeight pair.
37
+ const content = hasOnlyTextChildren ? (_jsx(StyledText, { selectable: false, size: "sm", fontWeight: "medium", style: textStyle, children: normalizedChildren.join("") })) : (React.Children.map(badgeContent, (child) => {
34
38
  if (typeof child === "string" || typeof child === "number") {
35
- return _jsx(StyledText, { selectable: false, style: textStyle, children: child });
39
+ return _jsx(StyledText, { selectable: false, size: "sm", fontWeight: "medium", style: textStyle, children: child });
36
40
  }
37
41
  return child;
38
42
  }));
@@ -67,9 +71,6 @@ const createStyles = (theme) => StyleSheet.create({
67
71
  backgroundColor: theme.colors.destructive,
68
72
  },
69
73
  text: {
70
- fontSize: 12,
71
- fontWeight: "500",
72
- lineHeight: 18,
73
74
  userSelect: "none",
74
75
  },
75
76
  });
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { createContext, use, useCallback, useEffect, useMemo, useReducer, useRef, useSyncExternalStore } from "react";
3
- import { View, Pressable, StyleSheet, ScrollView, Platform, useWindowDimensions, } from "react-native";
3
+ import { View, Pressable, StyleSheet, ScrollView, Platform, Animated, useWindowDimensions, } from "react-native";
4
4
  import { BottomSheet as NativeBottomSheet } from "@expo/ui/community/bottom-sheet";
5
5
  import { KeyboardController } from "react-native-keyboard-controller";
6
6
  import { useSafeAreaInsets, initialWindowMetrics } from "react-native-safe-area-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 { TextColorContext, TextClassContext } from "./StyledText.context";
10
11
  import { Icon } from "./Icon.js";
11
12
  import { dismissKeyboardFocusedInput, hasKeyboardFocusedInput, subscribeKeyboardFocus, } from "./keyboardFocusRegistry.js";
@@ -140,24 +141,25 @@ function useShowClose() {
140
141
  function SheetCloseButton({ style }) {
141
142
  const { onOpenChange } = useBottomSheetContext();
142
143
  const { theme } = useTheme();
143
- return (_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Close", onPress: () => onOpenChange(false), hitSlop: spacing.sm, style: ({ pressed }) => [
144
- {
145
- width: spacing.xl,
146
- height: spacing.xl,
147
- borderRadius: spacing.xl / 2,
148
- alignItems: "center",
149
- justifyContent: "center",
150
- // Higher-contrast than `muted`: a solid `secondary` fill with a
151
- // bordered edge so the control reads clearly against the card, and a
152
- // full-strength `foreground` glyph instead of the faint muted one.
153
- backgroundColor: theme.colors.secondary,
154
- borderWidth: 1,
155
- borderColor: theme.colors.border,
156
- opacity: pressed ? 0.7 : 1,
157
- },
158
- Platform.OS === "web" && { cursor: "pointer" },
159
- style,
160
- ], children: _jsx(Icon, { name: "x", size: 22, color: "text" }) }));
144
+ const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
145
+ scaleTo: 0.96,
146
+ });
147
+ return (_jsx(Animated.View, { style: [style, scaleStyle], children: _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Close", onPress: () => onOpenChange(false), onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, hitSlop: spacing.sm, style: [
148
+ {
149
+ width: spacing.xl,
150
+ height: spacing.xl,
151
+ borderRadius: spacing.xl / 2,
152
+ alignItems: "center",
153
+ justifyContent: "center",
154
+ // Higher-contrast than `muted`: a solid `secondary` fill with a
155
+ // bordered edge so the control reads clearly against the card, and a
156
+ // full-strength `foreground` glyph instead of the faint muted one.
157
+ backgroundColor: theme.colors.secondary,
158
+ borderWidth: 1,
159
+ borderColor: theme.colors.border,
160
+ },
161
+ Platform.OS === "web" && { cursor: "pointer" },
162
+ ], children: _jsx(Icon, { name: "x", size: 22, color: "text" }) }) }));
161
163
  }
162
164
  // ============================================================================
163
165
  // Keyboard dismiss overlay — tap-away dismissal inside the sheet
@@ -90,7 +90,8 @@ export interface ButtonProps extends PressableProps {
90
90
  */
91
91
  disabledStyle?: StyleProp<ViewStyle>;
92
92
  /**
93
- * Whether to show shadow
93
+ * Whether to show shadow.
94
+ * @default true for the `default` preset, `false` for every other preset.
94
95
  */
95
96
  withShadow?: boolean;
96
97
  /**
@@ -70,11 +70,15 @@ const getNativeHitSlop = (sizeConfig) => Math.ceil(Math.max(0, spacing.touchTarg
70
70
  * ```
71
71
  */
72
72
  function ButtonRoot(props) {
73
- const { tx, text, txOptions, style: styleOverride, pressedStyle: pressedStyleOverride, textStyle: textStyleOverride, pressedTextStyle: pressedTextStyleOverride, disabledTextStyle: disabledTextStyleOverride, children, RightAccessory, LeftAccessory, disabled, disabledStyle: disabledStyleOverride, withShadow = false, preset = "default", size = "md", loading = false, fullWidth = false, onFocus, onBlur, onPressIn, onPressOut, ...rest } = props;
73
+ const { tx, text, txOptions, style: styleOverride, pressedStyle: pressedStyleOverride, textStyle: textStyleOverride, pressedTextStyle: pressedTextStyleOverride, disabledTextStyle: disabledTextStyleOverride, children, RightAccessory, LeftAccessory, disabled, disabledStyle: disabledStyleOverride, withShadow: withShadowProp, preset = "default", size = "md", loading = false, fullWidth = false, onFocus, onBlur, onPressIn, onPressOut, ...rest } = props;
74
74
  const { theme, getContrastingColor, getFocusRingStyle, getShadowStyle } = useTheme();
75
75
  const styles = useMemo(() => createStyles(theme, size), [theme, size]);
76
- const shadowStyle = getShadowStyle("base");
76
+ const shadowStyle = getShadowStyle("subtle");
77
77
  const sizeConfig = SIZE_CONFIGS[size];
78
+ // Filled (`default`) buttons float by default; ghost/outline/link stay flat
79
+ // (transparent background reads oddly with a shadow), and destructive/secondary
80
+ // keep the old opt-in behavior. Callers can still override via `withShadow`.
81
+ const withShadow = withShadowProp ?? preset === "default";
78
82
  const focusRingStyle = getFocusRingStyle();
79
83
  // Pre-compute background color for contrast calculation
80
84
  // Always flatten to handle both array styles (from Slot) and RegisteredStyle
@@ -278,9 +282,15 @@ const createStyles = (theme, size) => {
278
282
  width: "100%",
279
283
  },
280
284
  text: {
281
- fontFamily: fontFamilies.sansSerif.regular,
285
+ // Button labels render at medium weight. On native, the family itself
286
+ // carries the weight (a real static Inter_500Medium file) — pairing it
287
+ // with a numeric fontWeight would faux-bold on top of that file. Web
288
+ // shares one "Inter" family across weights, so it needs the numeric
289
+ // fontWeight to pick the right @font-face variant. (Same rule as
290
+ // StyledText's getFontFamilyWeight / WEB_FONT_WEIGHTS.)
291
+ fontFamily: fontFamilies.sansSerif.medium,
282
292
  fontSize: sizeConfig.fontSize,
283
- fontWeight: "500",
293
+ ...(Platform.OS === "web" && { fontWeight: "500" }),
284
294
  textAlign: "center",
285
295
  lineHeight: sizeConfig.fontSize * 1.4,
286
296
  flexShrink: 0,
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef } from "react";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import { View, StyleSheet, Pressable, Platform, Animated } from "react-native";
4
4
  import { Icon } from "./Icon.js";
5
5
  import { StyledText } from "./StyledText.js";
@@ -7,6 +7,7 @@ import { useTheme } from "../hooks/useTheme.js";
7
7
  import { spacing } from "../constants/spacing.js";
8
8
  import { hapticLight } from "../lib/haptics.js";
9
9
  import { useReducedMotion } from "../hooks/useReduceMotion.js";
10
+ import { useScalePress } from "../hooks/useScalePress.js";
10
11
  import * as CheckboxPrimitive from "@rn-primitives/checkbox";
11
12
  const DEFAULT_HIT_SLOP = 8;
12
13
  const SIZE_CONFIGS = {
@@ -15,9 +16,34 @@ const SIZE_CONFIGS = {
15
16
  lg: { size: 24, iconSize: 20, strokeWidth: 3.5 },
16
17
  };
17
18
  function Checkbox({ size = "md", label, indeterminate = false, error = false, style: styleOverride, labelStyle, required = false, checked, onCheckedChange, disabled, ...props }) {
18
- const { theme, getContrastingColor } = useTheme();
19
+ const { theme, getContrastingColor, getFocusRingStyle } = useTheme();
19
20
  const reduceMotion = useReducedMotion();
20
21
  const sizeConfig = SIZE_CONFIGS[size];
22
+ const focusRingStyle = getFocusRingStyle();
23
+ const [focused, setFocused] = useState(false);
24
+ const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
25
+ disabled: !!disabled,
26
+ scaleTo: 0.92,
27
+ haptic: false,
28
+ });
29
+ const showFocusRing = (event) => {
30
+ let ringVisible = true;
31
+ if (Platform.OS === "web") {
32
+ const target = event?.nativeEvent?.target;
33
+ if (target && typeof target.matches === "function") {
34
+ try {
35
+ ringVisible = target.matches(":focus-visible");
36
+ }
37
+ catch {
38
+ ringVisible = true;
39
+ }
40
+ }
41
+ }
42
+ setFocused(ringVisible);
43
+ };
44
+ const hideFocusRing = () => {
45
+ setFocused(false);
46
+ };
21
47
  // Simple fast opacity for the checkmark icon
22
48
  const checkOpacity = useRef(new Animated.Value(checked || indeterminate ? 1 : 0)).current;
23
49
  const isVisuallyChecked = !!checked || indeterminate;
@@ -45,22 +71,23 @@ function Checkbox({ size = "md", label, indeterminate = false, error = false, st
45
71
  : getContrastingColor(theme.colors.background, theme.colors.text, theme.colors.textDim);
46
72
  // Flatten style override for web compatibility
47
73
  const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
48
- const checkboxElement = (_jsx(CheckboxPrimitive.Root, { ...props, checked: checked, onCheckedChange: wrappedOnCheckedChange, disabled: disabled, style: {
49
- ...styles.box,
50
- borderColor,
51
- backgroundColor: isVisuallyChecked ? theme.colors.primary : theme.colors.background,
52
- width: sizeConfig.size,
53
- height: sizeConfig.size,
54
- opacity: disabled ? 0.5 : 1,
55
- ...(Platform.OS === "web" && { cursor: disabled ? "not-allowed" : "pointer" }),
56
- ...(flattenedStyle || {}),
57
- }, hitSlop: DEFAULT_HIT_SLOP, accessibilityRole: "checkbox", accessibilityState: {
58
- checked: indeterminate ? "mixed" : checked,
59
- disabled: !!disabled,
60
- }, accessibilityLabel: label, children: _jsx(CheckboxPrimitive.Indicator, { style: {
61
- justifyContent: "center",
62
- alignItems: "center",
63
- }, children: _jsx(Animated.View, { style: { opacity: checkOpacity }, children: indeterminate ? (_jsx(Icon, { name: "minus", size: sizeConfig.iconSize, color: theme.colors.primaryForeground })) : (_jsx(Icon, { name: "check", size: sizeConfig.iconSize, color: theme.colors.primaryForeground })) }) }) }));
74
+ const checkboxElement = (_jsx(Animated.View, { style: scaleStyle, children: _jsx(CheckboxPrimitive.Root, { ...props, checked: checked, onCheckedChange: wrappedOnCheckedChange, disabled: disabled, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
75
+ ...styles.box,
76
+ borderColor,
77
+ backgroundColor: isVisuallyChecked ? theme.colors.primary : theme.colors.background,
78
+ width: sizeConfig.size,
79
+ height: sizeConfig.size,
80
+ opacity: disabled ? 0.5 : 1,
81
+ ...(Platform.OS === "web" && { cursor: disabled ? "not-allowed" : "pointer" }),
82
+ ...(focused && !disabled ? focusRingStyle : null),
83
+ ...(flattenedStyle || {}),
84
+ }, hitSlop: DEFAULT_HIT_SLOP, accessibilityRole: "checkbox", accessibilityState: {
85
+ checked: indeterminate ? "mixed" : checked,
86
+ disabled: !!disabled,
87
+ }, accessibilityLabel: label, children: _jsx(CheckboxPrimitive.Indicator, { style: {
88
+ justifyContent: "center",
89
+ alignItems: "center",
90
+ }, children: _jsx(Animated.View, { style: { opacity: checkOpacity }, children: indeterminate ? (_jsx(Icon, { name: "minus", size: sizeConfig.iconSize, color: theme.colors.primaryForeground })) : (_jsx(Icon, { name: "check", size: sizeConfig.iconSize, color: theme.colors.primaryForeground })) }) }) }) }));
64
91
  // If no label, return just the checkbox
65
92
  if (!label) {
66
93
  return checkboxElement;
@@ -78,7 +105,10 @@ function Checkbox({ size = "md", label, indeterminate = false, error = false, st
78
105
  }
79
106
  const styles = StyleSheet.create({
80
107
  box: {
81
- borderRadius: spacing.radiusSm,
108
+ // radiusXs (not radiusSm) — at the checkbox's 16-24px sizes, radiusSm
109
+ // post-rebase (8px) reads as over-rounded; radiusXs keeps the same
110
+ // proportion the box had before the radius rebase.
111
+ borderRadius: spacing.radiusXs,
82
112
  borderWidth: 1,
83
113
  justifyContent: "center",
84
114
  alignItems: "center",
@@ -41,7 +41,7 @@ declare function Collapsible({ children, ...props }: CollapsibleProps): React.JS
41
41
  * ```
42
42
  */
43
43
  type CollapsibleTriggerProps = CollapsiblePrimitive.TriggerProps;
44
- declare function CollapsibleTrigger({ style: styleOverride, ...props }: CollapsibleTriggerProps): React.JSX.Element;
44
+ declare function CollapsibleTrigger({ style: styleOverride, disabled, ...props }: CollapsibleTriggerProps): React.JSX.Element;
45
45
  /**
46
46
  * CollapsibleContent Component
47
47
  * The content that expands and collapses
@@ -1,28 +1,34 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Platform, StyleSheet, View } from "react-native";
2
+ import { Animated, Platform, StyleSheet, View } from "react-native";
3
3
  import { TextClassContext, TextSelectabilityContext } from "./StyledText.context";
4
4
  import { spacing } from "../constants/spacing.js";
5
5
  import { useTheme } from "../hooks/useTheme.js";
6
+ import { useScalePress } from "../hooks/useScalePress.js";
6
7
  import * as CollapsiblePrimitive from "@rn-primitives/collapsible";
7
8
  function Collapsible({ children, ...props }) {
8
9
  return (_jsx(CollapsiblePrimitive.Root, { ...props, asChild: Platform.OS !== "web", children: _jsx(View, { children: children }) }));
9
10
  }
10
- function CollapsibleTrigger({ style: styleOverride, ...props }) {
11
+ function CollapsibleTrigger({ style: styleOverride, disabled, ...props }) {
11
12
  const { theme } = useTheme();
12
- return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(CollapsiblePrimitive.Trigger, { ...props, style: {
13
- flexDirection: "row",
14
- alignItems: "center",
15
- justifyContent: "space-between",
16
- paddingVertical: spacing.sm,
17
- ...(Platform.OS === "web" && {
18
- cursor: "pointer",
19
- outlineStyle: "none",
20
- userSelect: "none",
21
- }),
22
- ...(styleOverride && typeof styleOverride !== "function"
23
- ? StyleSheet.flatten(styleOverride)
24
- : {}),
25
- } }) }) }));
13
+ const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
14
+ disabled: !!disabled,
15
+ scaleTo: 0.97,
16
+ haptic: false,
17
+ });
18
+ return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(CollapsiblePrimitive.Trigger, { disabled: disabled, ...props, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, style: {
19
+ flexDirection: "row",
20
+ alignItems: "center",
21
+ justifyContent: "space-between",
22
+ paddingVertical: spacing.sm,
23
+ ...(Platform.OS === "web" && {
24
+ cursor: "pointer",
25
+ outlineStyle: "none",
26
+ userSelect: "none",
27
+ }),
28
+ ...(styleOverride && typeof styleOverride !== "function"
29
+ ? StyleSheet.flatten(styleOverride)
30
+ : {}),
31
+ } }) }) }) }));
26
32
  }
27
33
  function CollapsibleContent({ forceMount, style: styleOverride, children, ...props }) {
28
34
  return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(CollapsiblePrimitive.Content, { ...props, forceMount: forceMount, children: _jsx(View, { style: {
@@ -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