@mrmeg/expo-ui 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/LLM_USAGE.md +4 -0
- package/README.md +13 -9
- package/dist/components/Accordion.d.ts +1 -1
- package/dist/components/Accordion.js +25 -19
- package/dist/components/Badge.js +6 -5
- package/dist/components/BottomSheet.js +21 -19
- package/dist/components/Button.d.ts +2 -1
- package/dist/components/Button.js +14 -4
- package/dist/components/Checkbox.js +49 -19
- package/dist/components/Collapsible.d.ts +1 -1
- package/dist/components/Collapsible.js +22 -16
- package/dist/components/Drawer.js +8 -7
- package/dist/components/DropdownMenu.d.ts +1 -1
- package/dist/components/DropdownMenu.js +80 -59
- package/dist/components/EmptyState.d.ts +7 -1
- package/dist/components/EmptyState.js +23 -7
- package/dist/components/InputOTP.js +6 -2
- package/dist/components/Item.d.ts +93 -0
- package/dist/components/Item.js +125 -0
- package/dist/components/Label.js +7 -2
- package/dist/components/Notification.js +10 -22
- package/dist/components/Popover.d.ts +3 -18
- package/dist/components/Popover.js +10 -6
- package/dist/components/RadioGroup.js +49 -22
- package/dist/components/SectionHeader.d.ts +42 -0
- package/dist/components/SectionHeader.js +41 -0
- package/dist/components/Select.js +73 -41
- package/dist/components/Skeleton.js +2 -2
- package/dist/components/StatCard.d.ts +45 -0
- package/dist/components/StatCard.js +41 -0
- package/dist/components/StyledText.d.ts +20 -10
- package/dist/components/StyledText.js +64 -25
- package/dist/components/Switch.js +84 -57
- package/dist/components/Tabs.js +43 -12
- package/dist/components/TextInput.js +8 -9
- package/dist/components/Toggle.js +69 -42
- package/dist/components/ToggleGroup.js +82 -55
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +3 -0
- package/dist/constants/fonts.d.ts +3 -0
- package/dist/constants/fonts.js +35 -12
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/constants/motion.d.ts +24 -0
- package/dist/constants/motion.js +24 -0
- package/dist/constants/spacing.d.ts +7 -7
- package/dist/constants/spacing.js +7 -7
- package/dist/hooks/useResources.d.ts +4 -2
- package/dist/hooks/useResources.js +30 -8
- package/dist/hooks/useStaggeredEntrance.d.ts +1 -1
- package/dist/hooks/useStaggeredEntrance.js +2 -1
- package/dist/hooks/useTheme.d.ts +1 -1
- package/dist/hooks/useTheme.js +53 -16
- package/package.json +24 -24
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,40 @@
|
|
|
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
|
+
|
|
29
|
+
## [0.14.0]
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Aligned the declared compatibility floor with the package's `@expo/ui`
|
|
34
|
+
requirement: Expo 56+, React Native 0.85+, and their matching Expo modules.
|
|
35
|
+
- Kept Expo 57 and React Native 0.86 support in the same compatibility window.
|
|
36
|
+
- Removed the duplicate workspace dev copy of
|
|
37
|
+
`react-native-keyboard-controller`; the root consumer now supplies the peer
|
|
38
|
+
consistently for tests, builds, and native autolinking.
|
|
39
|
+
|
|
6
40
|
## [0.13.0]
|
|
7
41
|
|
|
8
42
|
### Added
|
package/LLM_USAGE.md
CHANGED
|
@@ -29,6 +29,10 @@ Use only exported package paths: root, `components`, `components/*`,
|
|
|
29
29
|
`constants`, `constants/*`, `hooks`, `hooks/*`, `state`, and `lib`. Do not
|
|
30
30
|
import from `@mrmeg/expo-ui/dist/*` or from a source checkout path.
|
|
31
31
|
|
|
32
|
+
Supported hosts are Expo 56–57, React 19.2, React Native 0.85–0.86, and React
|
|
33
|
+
Native Web 0.21. Install the peer versions recommended by the consuming app's
|
|
34
|
+
Expo SDK.
|
|
35
|
+
|
|
32
36
|
## Required App Setup
|
|
33
37
|
|
|
34
38
|
Call `useResources()` once near the Expo app root. Mount `UIProvider` once
|
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.
|
|
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
|
|
@@ -28,9 +28,9 @@ bun add @mrmeg/expo-ui
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
Consumers must also install the native and Expo peer dependencies listed in
|
|
31
|
-
`package.json`. The tested
|
|
32
|
-
Native 0.85, and React Native Web 0.21. UI animations and
|
|
33
|
-
sheet offsets use React Native Animated by default.
|
|
31
|
+
`package.json`. The tested compatibility window is Expo SDK 56–57 with React
|
|
32
|
+
19.2, React Native 0.85–0.86, and React Native Web 0.21. UI animations and
|
|
33
|
+
keyboard-aware sheet offsets use React Native Animated by default.
|
|
34
34
|
`@rn-primitives/*` packages are managed by `@mrmeg/expo-ui` because they are
|
|
35
35
|
implementation details of the exported UI components. Native bottom sheet
|
|
36
36
|
keyboard avoidance uses React Native keyboard events. i18n setup is optional;
|
|
@@ -394,21 +394,21 @@ export default function RootLayout() {
|
|
|
394
394
|
|
|
395
395
|
## Fonts
|
|
396
396
|
|
|
397
|
-
|
|
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
|
|
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-
|
|
405
|
+
id="mrmeg-expo-ui-inter"
|
|
406
406
|
rel="stylesheet"
|
|
407
|
-
href="https://fonts.googleapis.com/css2?family=
|
|
407
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
408
408
|
/>
|
|
409
409
|
```
|
|
410
410
|
|
|
411
|
-
|
|
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
|
|
|
@@ -428,6 +428,9 @@ The release command requires a clean working tree by default. Commit current
|
|
|
428
428
|
changes first, or pass `--allow-dirty` when you intentionally want to release
|
|
429
429
|
from uncommitted local changes.
|
|
430
430
|
|
|
431
|
+
Release validation includes `bun run packages:peer-check`; CI also installs and
|
|
432
|
+
exports packed consumers against Expo 56 and 57.
|
|
433
|
+
|
|
431
434
|
If npm login email is unavailable, publish through GitHub Actions trusted
|
|
432
435
|
publishing instead:
|
|
433
436
|
|
|
@@ -459,6 +462,7 @@ reruns do not bump again.
|
|
|
459
462
|
Manual package checks:
|
|
460
463
|
|
|
461
464
|
```sh
|
|
465
|
+
bun run packages:peer-check
|
|
462
466
|
bun run ui:typecheck
|
|
463
467
|
bun run ui:test
|
|
464
468
|
bun run ui:build
|
|
@@ -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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
package/dist/components/Badge.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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:
|
|
267
|
+
duration: durations.normal,
|
|
267
268
|
useNativeDriver: shouldUseNativeDriver,
|
|
268
269
|
}),
|
|
269
270
|
Animated.timing(backdropOpacity, {
|
|
270
271
|
toValue: 1,
|
|
271
|
-
duration:
|
|
272
|
+
duration: durations.normal,
|
|
272
273
|
useNativeDriver: shouldUseNativeDriver,
|
|
273
274
|
}),
|
|
274
275
|
]);
|
|
@@ -284,12 +285,12 @@ function DrawerOverlayContent({ swipeEnabled = true, swipeThreshold = 0.3, veloc
|
|
|
284
285
|
const animation = Animated.parallel([
|
|
285
286
|
Animated.timing(translateX, {
|
|
286
287
|
toValue: closedPosition,
|
|
287
|
-
duration:
|
|
288
|
+
duration: durations.normal,
|
|
288
289
|
useNativeDriver: shouldUseNativeDriver,
|
|
289
290
|
}),
|
|
290
291
|
Animated.timing(backdropOpacity, {
|
|
291
292
|
toValue: 0,
|
|
292
|
-
duration:
|
|
293
|
+
duration: durations.normal,
|
|
293
294
|
useNativeDriver: shouldUseNativeDriver,
|
|
294
295
|
}),
|
|
295
296
|
]);
|
|
@@ -350,7 +351,7 @@ function DrawerOverlayContent({ swipeEnabled = true, swipeThreshold = 0.3, veloc
|
|
|
350
351
|
}),
|
|
351
352
|
Animated.timing(backdropOpacity, {
|
|
352
353
|
toValue: 0,
|
|
353
|
-
duration:
|
|
354
|
+
duration: durations.normal,
|
|
354
355
|
useNativeDriver: shouldUseNativeDriver,
|
|
355
356
|
}),
|
|
356
357
|
]).start(() => {
|
|
@@ -369,7 +370,7 @@ function DrawerOverlayContent({ swipeEnabled = true, swipeThreshold = 0.3, veloc
|
|
|
369
370
|
}),
|
|
370
371
|
Animated.timing(backdropOpacity, {
|
|
371
372
|
toValue: 1,
|
|
372
|
-
duration:
|
|
373
|
+
duration: durations.fast,
|
|
373
374
|
useNativeDriver: shouldUseNativeDriver,
|
|
374
375
|
}),
|
|
375
376
|
]).start();
|
|
@@ -488,7 +489,7 @@ swipeEnabled: _swipeEnabled, swipeThreshold: _swipeThreshold, velocityThreshold:
|
|
|
488
489
|
if (previousExpanded !== null) {
|
|
489
490
|
Animated.timing(widthAnim, {
|
|
490
491
|
toValue: targetWidth,
|
|
491
|
-
duration:
|
|
492
|
+
duration: durations.normal,
|
|
492
493
|
useNativeDriver: false,
|
|
493
494
|
}).start();
|
|
494
495
|
}
|
|
@@ -46,7 +46,7 @@ declare const DropdownMenuRadioGroup: {
|
|
|
46
46
|
* props over slot props), so this only fills the gap when none is set.
|
|
47
47
|
*/
|
|
48
48
|
type DropdownMenuTriggerProps = DropdownMenuPrimitive.TriggerProps;
|
|
49
|
-
declare function DropdownMenuTrigger({ ...props }: DropdownMenuTriggerProps): React.JSX.Element;
|
|
49
|
+
declare function DropdownMenuTrigger({ disabled, ...props }: DropdownMenuTriggerProps): React.JSX.Element;
|
|
50
50
|
/**
|
|
51
51
|
* DropdownMenuSubTrigger Component
|
|
52
52
|
* Trigger for sub-menus with automatic chevron icon
|