@mrmeg/expo-ui 0.27.1 → 0.29.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 +666 -0
- package/LLM_USAGE.md +241 -37
- package/README.md +591 -136
- package/bin/expo-ui-icons.mjs +509 -0
- package/dist/components/Accordion.js +12 -6
- package/dist/components/AnimatedView.d.ts +1 -1
- package/dist/components/AnimatedView.js +5 -2
- package/dist/components/Avatar.d.ts +1 -1
- package/dist/components/Avatar.js +2 -2
- package/dist/components/Badge.js +5 -1
- package/dist/components/BottomSheet.d.ts +29 -1
- package/dist/components/BottomSheet.js +107 -30
- package/dist/components/Button.d.ts +20 -4
- package/dist/components/Button.js +60 -43
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +24 -4
- package/dist/components/Carousel.js +2 -2
- package/dist/components/Checkbox.js +51 -44
- package/dist/components/Collapsible.js +7 -3
- package/dist/components/Dialog.js +87 -16
- package/dist/components/Drawer.d.ts +1 -1
- package/dist/components/Drawer.js +65 -55
- package/dist/components/DropdownMenu.js +11 -7
- package/dist/components/EmptyState.d.ts +2 -2
- package/dist/components/EmptyState.js +5 -2
- package/dist/components/Hydrated.d.ts +31 -0
- package/dist/components/Hydrated.js +30 -0
- package/dist/components/Icon.d.ts +71 -28
- package/dist/components/Icon.js +70 -28
- package/dist/components/IconGlyph.d.ts +15 -0
- package/dist/components/IconGlyph.js +32 -0
- package/dist/components/IconGlyph.native.d.ts +10 -0
- package/dist/components/IconGlyph.native.js +30 -0
- package/dist/components/InputOTP.js +5 -2
- package/dist/components/Item.d.ts +103 -4
- package/dist/components/Item.js +168 -15
- package/dist/components/KeyboardAvoidingView.d.ts +1 -1
- package/dist/components/KeyboardAvoidingView.js +4 -1
- package/dist/components/Label.js +3 -2
- package/dist/components/MaxWidthContainer.js +1 -1
- package/dist/components/Notification.js +45 -27
- package/dist/components/Popover.d.ts +18 -5
- package/dist/components/Popover.js +72 -8
- package/dist/components/Progress.js +8 -6
- package/dist/components/RadioGroup.js +52 -49
- package/dist/components/Screen.d.ts +61 -0
- package/dist/components/Screen.js +62 -0
- package/dist/components/SectionHeader.d.ts +1 -1
- package/dist/components/SegmentedControl.d.ts +12 -4
- package/dist/components/SegmentedControl.js +94 -4
- package/dist/components/Select.js +21 -26
- package/dist/components/Skeleton.js +24 -10
- package/dist/components/Slider.d.ts +11 -3
- package/dist/components/Slider.js +73 -6
- package/dist/components/StatCard.d.ts +4 -2
- package/dist/components/StatCard.js +3 -1
- package/dist/components/StyledText.d.ts +7 -1
- package/dist/components/StyledText.js +5 -3
- package/dist/components/Switch.d.ts +1 -1
- package/dist/components/Switch.js +20 -30
- package/dist/components/Tabs.d.ts +2 -2
- package/dist/components/Tabs.js +13 -24
- package/dist/components/TextInput.d.ts +1 -1
- package/dist/components/TextInput.js +71 -26
- package/dist/components/ThemeSelector.d.ts +52 -0
- package/dist/components/ThemeSelector.js +90 -0
- package/dist/components/Toggle.d.ts +5 -49
- package/dist/components/Toggle.js +53 -29
- package/dist/components/ToggleGroup.d.ts +4 -8
- package/dist/components/ToggleGroup.js +52 -34
- package/dist/components/Tooltip.d.ts +1 -1
- package/dist/components/Tooltip.js +9 -3
- package/dist/components/UIProvider.d.ts +22 -2
- package/dist/components/UIProvider.js +15 -3
- package/dist/components/bottomSheetDismiss.d.ts +19 -0
- package/dist/components/bottomSheetDismiss.js +44 -0
- package/dist/components/iconBuiltins.generated.d.ts +143 -0
- package/dist/components/iconBuiltins.generated.js +25 -0
- package/dist/components/iconTypes.d.ts +37 -0
- package/dist/components/iconTypes.js +1 -0
- package/dist/components/index.d.ts +47 -44
- package/dist/components/index.js +3 -0
- package/dist/components/keyboardDismiss.js +12 -4
- package/dist/components/nativeTextField.android.js +13 -9
- package/dist/constants/colors.d.ts +40 -5
- package/dist/constants/colors.js +26 -8
- package/dist/constants/fonts.d.ts +41 -2
- package/dist/constants/fonts.js +46 -7
- package/dist/constants/index.d.ts +5 -4
- package/dist/constants/index.js +1 -0
- package/dist/constants/interaction.d.ts +18 -0
- package/dist/constants/interaction.js +18 -0
- package/dist/constants/spacing.d.ts +2 -1
- package/dist/constants/spacing.js +2 -1
- package/dist/design-system.json +429 -132
- package/dist/hooks/index.d.ts +12 -7
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/useDimensions.d.ts +13 -8
- package/dist/hooks/useDimensions.js +123 -48
- package/dist/hooks/useFocusVisible.d.ts +30 -0
- package/dist/hooks/useFocusVisible.js +45 -0
- package/dist/hooks/useFontStyle.d.ts +2 -2
- package/dist/hooks/useFontStyle.js +3 -2
- package/dist/hooks/useHydrated.d.ts +13 -0
- package/dist/hooks/useHydrated.js +19 -0
- package/dist/hooks/useNotificationOffset.d.ts +20 -0
- package/dist/hooks/useNotificationOffset.js +34 -0
- package/dist/hooks/useResources.d.ts +21 -20
- package/dist/hooks/useResources.js +72 -68
- package/dist/hooks/useScalePress.d.ts +8 -5
- package/dist/hooks/useScalePress.js +16 -9
- package/dist/hooks/useShape.d.ts +24 -0
- package/dist/hooks/useShape.js +24 -0
- package/dist/hooks/useStaggeredEntrance.js +9 -7
- package/dist/hooks/useTheme.d.ts +3 -3
- package/dist/hooks/useTheme.js +177 -66
- package/dist/hooks/useWindowInsets.d.ts +13 -0
- package/dist/hooks/useWindowInsets.js +23 -0
- package/dist/icons/LICENSE.lucide +43 -0
- package/dist/icons/lucide.json +2144 -0
- package/dist/index.d.ts +5 -5
- package/dist/lib/composeRefs.d.ts +8 -0
- package/dist/lib/composeRefs.js +20 -0
- package/dist/lib/haptics.d.ts +12 -0
- package/dist/lib/haptics.js +21 -0
- package/dist/lib/index.d.ts +6 -5
- package/dist/lib/index.js +1 -0
- package/dist/lib/interFonts.d.ts +15 -0
- package/dist/lib/interFonts.js +1 -0
- package/dist/lib/interFonts.native.d.ts +3 -0
- package/dist/lib/interFonts.native.js +20 -0
- package/dist/lib/popoverPlacement.d.ts +36 -0
- package/dist/lib/popoverPlacement.js +29 -0
- package/dist/lib/themedStyles.d.ts +1 -1
- package/dist/lib/useAnimatedValue.d.ts +15 -0
- package/dist/lib/useAnimatedValue.js +19 -0
- package/dist/lib/webHitTarget.d.ts +40 -0
- package/dist/lib/webHitTarget.js +37 -0
- package/dist/state/feedbackStore.d.ts +24 -0
- package/dist/state/feedbackStore.js +16 -0
- package/dist/state/globalUIStore.d.ts +27 -0
- package/dist/state/globalUIStore.js +23 -0
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +2 -0
- package/dist/state/initialScheme.d.ts +27 -0
- package/dist/state/initialScheme.js +29 -0
- package/dist/state/notify.d.ts +1 -1
- package/dist/state/themeColorScope.d.ts +1 -1
- package/dist/state/themeStore.d.ts +69 -16
- package/dist/state/themeStore.js +65 -38
- package/llms-full.md +59 -10
- package/llms.txt +10 -1
- package/package.json +79 -4
- package/dist/components/iconRegistry.generated.d.ts +0 -148
- package/dist/components/iconRegistry.generated.js +0 -296
|
@@ -3,9 +3,12 @@ import * as React from "react";
|
|
|
3
3
|
import { Animated, Platform, StyleSheet, View } from "react-native";
|
|
4
4
|
import { Icon } from "./Icon.js";
|
|
5
5
|
import { AnimatedView } from "./AnimatedView.js";
|
|
6
|
-
import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context";
|
|
6
|
+
import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context.js";
|
|
7
7
|
import { useTheme } from "../hooks/useTheme.js";
|
|
8
|
+
import { shapeRadius, useShape } from "../hooks/useShape.js";
|
|
9
|
+
import { useFocusVisible } from "../hooks/useFocusVisible.js";
|
|
8
10
|
import { spacing } from "../constants/spacing.js";
|
|
11
|
+
import { interaction } from "../constants/interaction.js";
|
|
9
12
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
10
13
|
import * as SelectPrimitive from "@rn-primitives/select";
|
|
11
14
|
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
@@ -36,34 +39,18 @@ const SIZE_CONFIGS = {
|
|
|
36
39
|
};
|
|
37
40
|
function SelectTrigger({ size = "md", error = false, children, style: styleOverride, disabled, ...props }) {
|
|
38
41
|
const { theme, getFocusRingStyle } = useTheme();
|
|
42
|
+
const inputShape = useShape("input");
|
|
39
43
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
40
44
|
const focusRingStyle = getFocusRingStyle();
|
|
41
|
-
const [focused, setFocused] = React.useState(false);
|
|
42
45
|
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
43
46
|
disabled: !!disabled,
|
|
44
47
|
scaleTo: 0.97,
|
|
45
48
|
haptic: false,
|
|
46
49
|
});
|
|
47
|
-
const showFocusRing = (
|
|
48
|
-
let ringVisible = true;
|
|
49
|
-
if (Platform.OS === "web") {
|
|
50
|
-
const target = event?.nativeEvent?.target;
|
|
51
|
-
if (target && typeof target.matches === "function") {
|
|
52
|
-
try {
|
|
53
|
-
ringVisible = target.matches(":focus-visible");
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
ringVisible = true;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
setFocused(ringVisible);
|
|
61
|
-
};
|
|
62
|
-
const hideFocusRing = () => {
|
|
63
|
-
setFocused(false);
|
|
64
|
-
};
|
|
50
|
+
const { focused, onFocus: showFocusRing, onBlur: hideFocusRing } = useFocusVisible();
|
|
65
51
|
return (_jsx(Animated.View, { style: scaleStyle, children: _jsx(SelectPrimitive.Trigger, { disabled: disabled, ...props, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
|
|
66
52
|
...styles.trigger,
|
|
53
|
+
...shapeRadius(inputShape),
|
|
67
54
|
height: sizeConfig.height,
|
|
68
55
|
paddingHorizontal: sizeConfig.paddingHorizontal,
|
|
69
56
|
borderColor: error ? theme.colors.destructive : theme.colors.border,
|
|
@@ -73,7 +60,7 @@ function SelectTrigger({ size = "md", error = false, children, style: styleOverr
|
|
|
73
60
|
outlineStyle: "none",
|
|
74
61
|
userSelect: "none",
|
|
75
62
|
}),
|
|
76
|
-
...(disabled && { opacity:
|
|
63
|
+
...(disabled && { opacity: interaction.disabledOpacity }),
|
|
77
64
|
...(focused && !disabled ? focusRingStyle : null),
|
|
78
65
|
...(styleOverride && typeof styleOverride !== "function"
|
|
79
66
|
? StyleSheet.flatten(styleOverride)
|
|
@@ -100,7 +87,7 @@ function SelectContent({ side, align = "start", sideOffset = 4, portalHost, styl
|
|
|
100
87
|
return (_jsx(SelectPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(SelectPrimitive.Overlay, { style: Platform.select({
|
|
101
88
|
native: StyleSheet.absoluteFill,
|
|
102
89
|
default: undefined,
|
|
103
|
-
}), children: _jsx(AnimatedView, { type: "fade", style: StyleSheet.absoluteFill,
|
|
90
|
+
}), children: _jsx(AnimatedView, { type: "fade", style: [StyleSheet.absoluteFill, styles.passThrough], children: _jsx(TextColorContext.Provider, { value: theme.colors.popoverForeground, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(SelectPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, insets: insets, avoidCollisions: true, ...props, style: {
|
|
104
91
|
backgroundColor: theme.colors.popover,
|
|
105
92
|
borderWidth: 1,
|
|
106
93
|
borderColor: theme.colors.border,
|
|
@@ -128,17 +115,22 @@ function SelectItem({ children, style: styleOverride, ...props }) {
|
|
|
128
115
|
const hasCustomChildren = React.isValidElement(children) || Array.isArray(children);
|
|
129
116
|
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
130
117
|
disabled: !!props.disabled,
|
|
131
|
-
scaleTo:
|
|
118
|
+
scaleTo: interaction.pressedScale,
|
|
132
119
|
haptic: false,
|
|
133
120
|
});
|
|
134
|
-
|
|
121
|
+
// On web the primitive's item is a Radix `div` that forwards every prop to
|
|
122
|
+
// the DOM, so RN press handlers would land there as unknown attributes
|
|
123
|
+
// ("Unknown event handler property onPressIn"). Native renders a Pressable
|
|
124
|
+
// and keeps the press scale.
|
|
125
|
+
const itemPressHandlers = Platform.OS === "web" ? undefined : pressHandlers;
|
|
126
|
+
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(Animated.View, { style: scaleStyle, children: _jsxs(SelectPrimitive.Item, { ...props, ...itemPressHandlers, style: {
|
|
135
127
|
...styles.item,
|
|
136
128
|
...(Platform.OS === "web" && {
|
|
137
129
|
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
138
130
|
outlineStyle: "none",
|
|
139
131
|
userSelect: "none",
|
|
140
132
|
}),
|
|
141
|
-
...(props.disabled && { opacity:
|
|
133
|
+
...(props.disabled && { opacity: interaction.disabledOpacity }),
|
|
142
134
|
...(styleOverride && typeof styleOverride !== "function"
|
|
143
135
|
? StyleSheet.flatten(styleOverride)
|
|
144
136
|
: {}),
|
|
@@ -188,7 +180,10 @@ function SelectSeparator({ style: styleOverride, ...props }) {
|
|
|
188
180
|
: {}),
|
|
189
181
|
} }));
|
|
190
182
|
}
|
|
191
|
-
const styles = StyleSheet.create({
|
|
183
|
+
const styles = /*#__PURE__*/ StyleSheet.create({
|
|
184
|
+
// Overlay layer: covers the screen without swallowing touches (style, not the
|
|
185
|
+
// deprecated `pointerEvents` prop).
|
|
186
|
+
passThrough: { pointerEvents: "box-none" },
|
|
192
187
|
trigger: {
|
|
193
188
|
flexDirection: "row",
|
|
194
189
|
justifyContent: "space-between",
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect
|
|
2
|
+
import { useEffect } from "react";
|
|
3
3
|
import { View, StyleSheet, Animated } from "react-native";
|
|
4
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
|
+
import { shapeRadius, useShape } from "../hooks/useShape.js";
|
|
5
6
|
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
6
7
|
import { spacing } from "../constants/spacing.js";
|
|
7
8
|
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
9
|
+
import { useAnimatedValue } from "../lib/useAnimatedValue.js";
|
|
10
|
+
import { shouldUseNativeDriver } from "../lib/animations.js";
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Base Skeleton
|
|
13
|
+
// ============================================================================
|
|
14
|
+
/** Low point of the pulse; high enough that the bar never disappears. */
|
|
15
|
+
const PULSE_MIN = 0.55;
|
|
16
|
+
/** Opacity held when the OS asks for reduced motion (no pulse at all). */
|
|
17
|
+
const STATIC_OPACITY = 0.8;
|
|
8
18
|
/**
|
|
9
19
|
* Skeleton Component
|
|
10
20
|
*
|
|
@@ -20,23 +30,23 @@ import { createThemedStyles } from "../lib/themedStyles.js";
|
|
|
20
30
|
export function Skeleton({ width = "100%", height = 20, borderRadius = spacing.radiusMd, circle = false, style, }) {
|
|
21
31
|
const { theme } = useTheme();
|
|
22
32
|
const reduceMotion = useReducedMotion();
|
|
23
|
-
const opacity =
|
|
33
|
+
const opacity = useAnimatedValue(reduceMotion ? STATIC_OPACITY : PULSE_MIN);
|
|
24
34
|
useEffect(() => {
|
|
25
35
|
if (reduceMotion) {
|
|
26
|
-
opacity.setValue(
|
|
36
|
+
opacity.setValue(STATIC_OPACITY);
|
|
27
37
|
return;
|
|
28
38
|
}
|
|
29
|
-
opacity.setValue(
|
|
39
|
+
opacity.setValue(PULSE_MIN);
|
|
30
40
|
const animation = Animated.loop(Animated.sequence([
|
|
31
41
|
Animated.timing(opacity, {
|
|
32
42
|
toValue: 1,
|
|
33
43
|
duration: 800,
|
|
34
|
-
useNativeDriver:
|
|
44
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
35
45
|
}),
|
|
36
46
|
Animated.timing(opacity, {
|
|
37
|
-
toValue:
|
|
47
|
+
toValue: PULSE_MIN,
|
|
38
48
|
duration: 800,
|
|
39
|
-
useNativeDriver:
|
|
49
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
40
50
|
}),
|
|
41
51
|
]));
|
|
42
52
|
animation.start();
|
|
@@ -48,7 +58,9 @@ export function Skeleton({ width = "100%", height = 20, borderRadius = spacing.r
|
|
|
48
58
|
width: circle ? resolvedSize : width,
|
|
49
59
|
height: circle ? resolvedSize : height,
|
|
50
60
|
borderRadius: circle ? (resolvedSize / 2) : borderRadius,
|
|
51
|
-
|
|
61
|
+
// `borderStrong`, not `muted`: on a white card `muted` (#F4F4F5) is
|
|
62
|
+
// a 1.05:1 step and the pulse's low point made it vanish outright.
|
|
63
|
+
backgroundColor: theme.colors.borderStrong,
|
|
52
64
|
},
|
|
53
65
|
{ opacity },
|
|
54
66
|
style,
|
|
@@ -74,7 +86,9 @@ export function SkeletonAvatar({ size = 40, style }) {
|
|
|
74
86
|
export function SkeletonCard({ showImage = true, imageHeight = 140, showAvatar = true, textLines = 3, style, }) {
|
|
75
87
|
const { theme, getShadowStyle } = useTheme();
|
|
76
88
|
const styles = themedStyles(theme);
|
|
77
|
-
|
|
89
|
+
// Host-app `setShape({ card })`: the placeholder keeps the Card's corners.
|
|
90
|
+
const cardRadius = shapeRadius(useShape("card"));
|
|
91
|
+
return (_jsxs(View, { style: [styles.card, cardRadius, getShadowStyle("subtle"), style], children: [showImage && (_jsx(Skeleton, { width: "100%", height: imageHeight, borderRadius: 0 })), _jsxs(View, { style: styles.body, children: [showAvatar && (_jsxs(View, { style: styles.avatarRow, children: [_jsx(SkeletonAvatar, { size: 36 }), _jsxs(View, { style: styles.avatarText, children: [_jsx(Skeleton, { width: "50%", height: 14 }), _jsx(Skeleton, { width: "30%", height: 12 })] })] })), _jsx(SkeletonText, { lines: textLines })] })] }));
|
|
78
92
|
}
|
|
79
93
|
const createCardStyles = (theme) => StyleSheet.create({
|
|
80
94
|
card: {
|
|
@@ -98,4 +112,4 @@ const createCardStyles = (theme) => StyleSheet.create({
|
|
|
98
112
|
gap: spacing.xs,
|
|
99
113
|
},
|
|
100
114
|
});
|
|
101
|
-
const themedStyles = createThemedStyles(createCardStyles);
|
|
115
|
+
const themedStyles = /*#__PURE__*/ createThemedStyles(createCardStyles);
|
|
@@ -6,15 +6,23 @@ import { StyleProp, ViewStyle } from "react-native";
|
|
|
6
6
|
*
|
|
7
7
|
* - iOS: SwiftUI `Slider`
|
|
8
8
|
* - Android: Material 3 `Slider`
|
|
9
|
-
* - Web: native `<input type="range"
|
|
9
|
+
* - Web: native `<input type="range">`, drawn by the kit's own
|
|
10
|
+
* stylesheet (see `SLIDER_WEB_CSS`)
|
|
10
11
|
*
|
|
11
12
|
* The public `SliderProps` surface (value / onValueChange / min / max / step /
|
|
12
13
|
* disabled / showValue / size / style) is preserved, and the active track is
|
|
13
14
|
* themed with the design system's accent color on every platform. Thumb and
|
|
14
|
-
* inactive-track tints additionally apply on Android (iOS
|
|
15
|
-
* thumb). Haptic feedback fires on each step change, matching the prior
|
|
15
|
+
* inactive-track tints additionally apply on Android and web (iOS draws the
|
|
16
|
+
* system thumb). Haptic feedback fires on each step change, matching the prior
|
|
16
17
|
* hand-rolled slider.
|
|
17
18
|
*
|
|
19
|
+
* Web: `@expo/ui` only maps `minimumTrackTintColor` to CSS `accent-color`, and
|
|
20
|
+
* Chromium then paints the unfilled track dark in light mode. The kit sets
|
|
21
|
+
* `appearance: none` on the input and draws the track, fill and thumb from
|
|
22
|
+
* CSS variables it puts on the element, through one hoisted `<style>` (React
|
|
23
|
+
* dedupes it by `href`). The stylesheet is scoped to `[data-expo-ui-slider]`,
|
|
24
|
+
* so other range inputs on the page keep their look.
|
|
25
|
+
*
|
|
18
26
|
* Platform-owned behaviors (props accepted for ergonomics, but the platform
|
|
19
27
|
* decides):
|
|
20
28
|
* - `size` is accepted for call-site compatibility but has no effect — the
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { palette } from "../constants/colors.js";
|
|
3
|
+
import { interaction } from "../constants/interaction.js";
|
|
3
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
4
5
|
import { hapticLight } from "../lib/haptics.js";
|
|
5
6
|
import { stateSurfaceProps } from "../lib/stateSurface.js";
|
|
6
|
-
import { useCallback, useRef } from "react";
|
|
7
|
-
import { View } from "react-native";
|
|
7
|
+
import React, { useCallback, useRef } from "react";
|
|
8
|
+
import { Platform, View } from "react-native";
|
|
8
9
|
import { Slider as NativeSlider } from "@expo/ui/community/slider";
|
|
9
10
|
import { StyledText } from "./StyledText.js";
|
|
10
11
|
function Slider({ value = 0, onValueChange, min = 0, max = 100, step = 1,
|
|
11
12
|
// Accepted for call-site compatibility; the platform owns track/thumb sizing.
|
|
12
13
|
size: _size = "md", disabled = false, showValue = false, style: styleOverride, }) {
|
|
13
14
|
const { theme, withAlpha } = useTheme();
|
|
14
|
-
|
|
15
|
+
// Light: `border`, not `muted` — on a card `muted` is a 1.05:1 step and the
|
|
16
|
+
// unfilled track disappeared. Dark unchanged.
|
|
17
|
+
const inactiveTrackColor = theme.dark ? withAlpha(palette.white, 0.1) : theme.colors.border;
|
|
15
18
|
const activeTrackColor = disabled
|
|
16
19
|
? theme.dark
|
|
17
20
|
? withAlpha(palette.white, 0.28)
|
|
@@ -32,15 +35,79 @@ size: _size = "md", disabled = false, showValue = false, style: styleOverride, }
|
|
|
32
35
|
}
|
|
33
36
|
onValueChange?.(next);
|
|
34
37
|
}, [onValueChange]);
|
|
35
|
-
return (_jsxs(View, { ...stateSurfaceProps(), style: [{ opacity: disabled ?
|
|
38
|
+
return (_jsxs(View, { ...stateSurfaceProps(), ...webSliderScope, style: [{ opacity: disabled ? interaction.disabledOpacity : 1, alignSelf: "stretch" }, styleOverride], children: [Platform.OS === "web" && sliderWebStylesheet, showValue && (_jsx(StyledText, { selectable: false, style: {
|
|
36
39
|
fontSize: 12,
|
|
37
40
|
color: theme.colors.textDim,
|
|
38
41
|
marginBottom: 4,
|
|
39
42
|
userSelect: "none",
|
|
40
|
-
}, children: value })), _jsx(NativeSlider, { value: value, minimumValue: min, maximumValue: max, step: step, disabled: disabled, onValueChange: handleValueChange, style:
|
|
43
|
+
}, children: value })), _jsx(NativeSlider, { value: value, minimumValue: min, maximumValue: max, step: step, disabled: disabled, onValueChange: handleValueChange, style: [
|
|
44
|
+
{ width: "100%" },
|
|
45
|
+
Platform.OS === "web" &&
|
|
46
|
+
webSliderVariables({
|
|
47
|
+
fill: fillRatio(value, min, max),
|
|
48
|
+
track: inactiveTrackColor,
|
|
49
|
+
active: activeTrackColor,
|
|
50
|
+
thumb: thumbTintColor,
|
|
51
|
+
ring: theme.colors.ring,
|
|
52
|
+
ringBackground: theme.colors.background,
|
|
53
|
+
}),
|
|
54
|
+
],
|
|
41
55
|
// Active track — honored on iOS, Android, and web (`accentColor`).
|
|
42
56
|
minimumTrackTintColor: activeTrackColor,
|
|
43
|
-
// Inactive track + thumb — honored on Android
|
|
57
|
+
// Inactive track + thumb — honored on Android (and web through the
|
|
58
|
+
// CSS variables above); system-drawn on iOS.
|
|
44
59
|
maximumTrackTintColor: inactiveTrackColor, thumbTintColor: thumbTintColor })] }));
|
|
45
60
|
}
|
|
61
|
+
/** Filled share of the track, 0–1, for the web fill gradient. */
|
|
62
|
+
function fillRatio(value, min, max) {
|
|
63
|
+
if (!(max > min))
|
|
64
|
+
return 0;
|
|
65
|
+
return Math.min(1, Math.max(0, (value - min) / (max - min)));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* CSS custom properties the web stylesheet reads. They ride on the input's
|
|
69
|
+
* inline `style` (React DOM writes `--*` keys as custom properties), so each
|
|
70
|
+
* slider carries its own theme values and the stylesheet stays static.
|
|
71
|
+
*/
|
|
72
|
+
function webSliderVariables(v) {
|
|
73
|
+
return {
|
|
74
|
+
"--expo-ui-slider-fill": `${Number((v.fill * 100).toFixed(2))}%`,
|
|
75
|
+
"--expo-ui-slider-track": v.track,
|
|
76
|
+
"--expo-ui-slider-active": v.active,
|
|
77
|
+
"--expo-ui-slider-thumb": v.thumb,
|
|
78
|
+
"--expo-ui-slider-ring": v.ring,
|
|
79
|
+
"--expo-ui-slider-ring-bg": v.ringBackground,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** `data-expo-ui-slider` on the wrapper (react-native-web's `dataSet`), scoping the stylesheet. */
|
|
83
|
+
const webSliderScope = Platform.OS === "web" ? { dataSet: { expoUiSlider: "" } } : {};
|
|
84
|
+
const TRACK = "[data-expo-ui-slider] input[type=\"range\"]";
|
|
85
|
+
const THUMB_SHADOW = "0 1px 3px rgba(0,0,0,.25)";
|
|
86
|
+
const RING_SHADOW = "0 0 0 2px var(--expo-ui-slider-ring-bg),0 0 0 4px var(--expo-ui-slider-ring)";
|
|
87
|
+
const SLIDER_WEB_CSS = [
|
|
88
|
+
`${TRACK}{-webkit-appearance:none;appearance:none;width:100%;height:24px;margin:0;padding:0;background:transparent;cursor:pointer;outline:none;--expo-ui-slider-fill:0%}`,
|
|
89
|
+
`${TRACK}:disabled{cursor:not-allowed}`,
|
|
90
|
+
`${TRACK}::-webkit-slider-runnable-track{height:4px;border-radius:2px;background:linear-gradient(var(--expo-ui-slider-active),var(--expo-ui-slider-active)) 0 0/var(--expo-ui-slider-fill) 100% no-repeat var(--expo-ui-slider-track)}`,
|
|
91
|
+
`${TRACK}::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:20px;height:20px;margin-top:-8px;border:0;border-radius:50%;background:var(--expo-ui-slider-thumb);box-shadow:${THUMB_SHADOW}}`,
|
|
92
|
+
`${TRACK}:focus-visible::-webkit-slider-thumb{box-shadow:${RING_SHADOW}}`,
|
|
93
|
+
`${TRACK}::-moz-range-track{height:4px;border-radius:2px;background:var(--expo-ui-slider-track)}`,
|
|
94
|
+
`${TRACK}::-moz-range-progress{height:4px;border-radius:2px;background:var(--expo-ui-slider-active)}`,
|
|
95
|
+
`${TRACK}::-moz-range-thumb{width:20px;height:20px;border:0;border-radius:50%;background:var(--expo-ui-slider-thumb);box-shadow:${THUMB_SHADOW}}`,
|
|
96
|
+
`${TRACK}:focus-visible::-moz-range-thumb{box-shadow:${RING_SHADOW}}`,
|
|
97
|
+
].join("");
|
|
98
|
+
/**
|
|
99
|
+
* One hoistable stylesheet (React 19 `<style href precedence>`): rendered by
|
|
100
|
+
* every web Slider, mounted in `<head>` once, and part of the server markup
|
|
101
|
+
* so the first paint is already themed. The CSS goes through
|
|
102
|
+
* `dangerouslySetInnerHTML` rather than a text child so the element stays
|
|
103
|
+
* representable outside the DOM renderer (the RN test renderer rejects text
|
|
104
|
+
* outside `<Text>`); the string is a package constant, never user input.
|
|
105
|
+
*/
|
|
106
|
+
const sliderWebStylesheet = Platform.OS === "web"
|
|
107
|
+
? React.createElement("style", {
|
|
108
|
+
href: "expo-ui-slider",
|
|
109
|
+
precedence: "default",
|
|
110
|
+
dangerouslySetInnerHTML: { __html: SLIDER_WEB_CSS },
|
|
111
|
+
})
|
|
112
|
+
: null;
|
|
46
113
|
export { Slider };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { StyleProp, ViewStyle } from "react-native";
|
|
3
|
-
import { type IconName } from "./Icon";
|
|
3
|
+
import { type IconName } from "./Icon.js";
|
|
4
4
|
export type StatChangeDirection = "up" | "down" | "neutral";
|
|
5
5
|
export interface StatCardChange {
|
|
6
6
|
/** Signed change text, e.g. "+12.5%" or "-3 orders". */
|
|
@@ -30,7 +30,9 @@ export interface StatCardProps {
|
|
|
30
30
|
* Dashboard stat idiom: a tiny uppercase tracked label, a large
|
|
31
31
|
* tabular-nums value (with an optional muted unit suffix), and an optional
|
|
32
32
|
* signed change line (success/destructive/muted by direction). Builds on
|
|
33
|
-
* `Card` for the container, default shadow, and press feedback
|
|
33
|
+
* `Card` for the container, default shadow, and press feedback, so it follows
|
|
34
|
+
* Card's rule: a tile in a collection (a scrolling metrics rail) or one
|
|
35
|
+
* tappable metric, not a box around every number on a summary screen.
|
|
34
36
|
*
|
|
35
37
|
* @example
|
|
36
38
|
* ```tsx
|
|
@@ -16,7 +16,9 @@ const DIRECTION_ICON = {
|
|
|
16
16
|
* Dashboard stat idiom: a tiny uppercase tracked label, a large
|
|
17
17
|
* tabular-nums value (with an optional muted unit suffix), and an optional
|
|
18
18
|
* signed change line (success/destructive/muted by direction). Builds on
|
|
19
|
-
* `Card` for the container, default shadow, and press feedback
|
|
19
|
+
* `Card` for the container, default shadow, and press feedback, so it follows
|
|
20
|
+
* Card's rule: a tile in a collection (a scrolling metrics rail) or one
|
|
21
|
+
* tappable metric, not a box around every number on a summary screen.
|
|
20
22
|
*
|
|
21
23
|
* @example
|
|
22
24
|
* ```tsx
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ComponentRef, type Ref } from "react";
|
|
2
2
|
import { Text as RNText, TextProps as RNTextProps } from "react-native";
|
|
3
|
-
import { type FontVariant } from "../constants/fonts";
|
|
3
|
+
import { type FontVariant } from "../constants/fonts.js";
|
|
4
4
|
/**
|
|
5
5
|
* Font size variants for the type scale (Inter sans-serif / Georgia serif)
|
|
6
6
|
*/
|
|
@@ -27,6 +27,12 @@ export type TextProps = RNTextProps & {
|
|
|
27
27
|
* Font weight - light, regular, medium, semibold, bold
|
|
28
28
|
*/
|
|
29
29
|
fontWeight?: FontWeight;
|
|
30
|
+
/**
|
|
31
|
+
* Italic. A real italic face when the family has one (an app's `setFonts`
|
|
32
|
+
* `italic` map, or the Newsreader serif preset); otherwise `fontStyle:
|
|
33
|
+
* "italic"` and the platform synthesizes the slant.
|
|
34
|
+
*/
|
|
35
|
+
italic?: boolean;
|
|
30
36
|
/**
|
|
31
37
|
* Font size variant
|
|
32
38
|
*/
|
|
@@ -5,7 +5,7 @@ import { useTheme } from "../hooks/useTheme.js";
|
|
|
5
5
|
import { resolveFontStyle } from "../constants/fonts.js";
|
|
6
6
|
import { useThemeStore } from "../state/themeStore.js";
|
|
7
7
|
import { translateText } from "../lib/i18n.js";
|
|
8
|
-
import { TextColorContext, TextSelectabilityContext, TextStyleContext } from "./StyledText.context";
|
|
8
|
+
import { TextColorContext, TextSelectabilityContext, TextStyleContext } from "./StyledText.context.js";
|
|
9
9
|
const FONT_SIZES = {
|
|
10
10
|
xs: 11,
|
|
11
11
|
sm: 12,
|
|
@@ -76,11 +76,12 @@ const SEMANTIC_CONFIGS = {
|
|
|
76
76
|
* - numberOfLines and ellipsizeMode support from RN TextProps
|
|
77
77
|
*/
|
|
78
78
|
export function StyledText(props) {
|
|
79
|
-
const { tx, text, txOptions, style, variant = "sansSerif", fontWeight, size, semantic, align, selectable, children, ref, ...otherProps } = props;
|
|
79
|
+
const { tx, text, txOptions, style, variant = "sansSerif", fontWeight, italic, size, semantic, align, selectable, children, ref, ...otherProps } = props;
|
|
80
80
|
const { theme } = useTheme();
|
|
81
81
|
// Subscribed, not read via getState(), so a `setFonts` call after mount
|
|
82
82
|
// re-renders text instead of leaving stale families on screen.
|
|
83
83
|
const fontOverrides = useThemeStore((s) => s.fontOverrides);
|
|
84
|
+
const serifPreset = useThemeStore((s) => s.serifPreset);
|
|
84
85
|
// Check if there's a color override from parent context (e.g., Button)
|
|
85
86
|
const contextColor = use(TextColorContext);
|
|
86
87
|
const contextTextStyle = use(TextStyleContext);
|
|
@@ -99,7 +100,7 @@ export function StyledText(props) {
|
|
|
99
100
|
// faces via `setFonts`; resolution — including the per-group merge and the
|
|
100
101
|
// numeric-vs-family weight strategy — lives in `resolveFontStyle`
|
|
101
102
|
// (constants/fonts.ts), shared with the control components' `useFontStyle`.
|
|
102
|
-
const { fontFamily, fontWeight: resolvedFontWeight } = resolveFontStyle(fontOverrides, variant, finalFontWeight);
|
|
103
|
+
const { fontFamily, fontWeight: resolvedFontWeight, fontStyle } = resolveFontStyle(fontOverrides, variant, finalFontWeight, { italic, serifPreset });
|
|
103
104
|
// Get fontSize and lineHeight from size variant
|
|
104
105
|
const fontSize = FONT_SIZES[finalSize];
|
|
105
106
|
const lineHeight = LINE_HEIGHTS[finalSize];
|
|
@@ -124,6 +125,7 @@ export function StyledText(props) {
|
|
|
124
125
|
fontFamily,
|
|
125
126
|
fontSize,
|
|
126
127
|
...(resolvedFontWeight !== undefined && { fontWeight: resolvedFontWeight }),
|
|
128
|
+
...(fontStyle !== undefined && { fontStyle }),
|
|
127
129
|
...(resolvedLineHeight !== undefined && { lineHeight: resolvedLineHeight }),
|
|
128
130
|
userSelect: resolvedSelectable ? "auto" : "none",
|
|
129
131
|
...(letterSpacing !== undefined && { letterSpacing }),
|
|
@@ -41,5 +41,5 @@ interface SwitchProps extends Omit<SwitchPrimitives.RootProps, "style"> {
|
|
|
41
41
|
*/
|
|
42
42
|
style?: StyleProp<ViewStyle>;
|
|
43
43
|
}
|
|
44
|
-
declare function Switch({ variant, labelOn, labelOff, size, thumbSize, loading, style: styleOverride, ...props }: SwitchProps): React.JSX.Element;
|
|
44
|
+
declare function Switch({ variant, labelOn, labelOff, size, thumbSize, loading, style: styleOverride, onCheckedChange, ...props }: SwitchProps): React.JSX.Element;
|
|
45
45
|
export { Switch };
|
|
@@ -2,61 +2,51 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { palette } from "../constants/colors.js";
|
|
3
3
|
import { spacing } from "../constants/spacing.js";
|
|
4
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
|
-
import {
|
|
5
|
+
import { useFocusVisible } from "../hooks/useFocusVisible.js";
|
|
6
|
+
import { hapticSelection } from "../lib/haptics.js";
|
|
7
|
+
import { interaction } from "../constants/interaction.js";
|
|
6
8
|
import { stateSurfaceProps } from "../lib/stateSurface.js";
|
|
9
|
+
import { useAnimatedValue } from "../lib/useAnimatedValue.js";
|
|
7
10
|
import * as SwitchPrimitives from "@rn-primitives/switch";
|
|
8
|
-
import { useCallback, useEffect, useRef
|
|
11
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
9
12
|
import { ActivityIndicator, Platform, StyleSheet, View, Animated } from "react-native";
|
|
10
13
|
import { StyledText } from "./StyledText.js";
|
|
11
14
|
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
12
15
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
16
|
+
import { shouldUseNativeDriver } from "../lib/animations.js";
|
|
17
|
+
import { useItemControlLabel } from "./Item.js";
|
|
13
18
|
const DEFAULT_HIT_SLOP = 8;
|
|
14
|
-
function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, height: 24 }, thumbSize = 20, loading = false, style: styleOverride, ...props }) {
|
|
19
|
+
function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, height: 24 }, thumbSize = 20, loading = false, style: styleOverride, onCheckedChange, ...props }) {
|
|
15
20
|
const { theme, getContrastingColor, getShadowStyle, getFocusRingStyle, withAlpha } = useTheme();
|
|
16
21
|
const reduceMotion = useReducedMotion();
|
|
17
22
|
const hasMounted = useRef(false);
|
|
18
|
-
const [focused, setFocused] = useState(false);
|
|
19
23
|
const focusRingStyle = getFocusRingStyle();
|
|
20
24
|
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
21
25
|
disabled: !!props.disabled,
|
|
22
|
-
scaleTo:
|
|
26
|
+
scaleTo: interaction.controlPressedScale,
|
|
23
27
|
haptic: false,
|
|
24
28
|
});
|
|
25
|
-
const showFocusRing = (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (target && typeof target.matches === "function") {
|
|
30
|
-
try {
|
|
31
|
-
ringVisible = target.matches(":focus-visible");
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
ringVisible = true;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
setFocused(ringVisible);
|
|
39
|
-
};
|
|
40
|
-
const hideFocusRing = () => {
|
|
41
|
-
setFocused(false);
|
|
42
|
-
};
|
|
29
|
+
const { focused, onFocus: showFocusRing, onBlur: hideFocusRing } = useFocusVisible();
|
|
30
|
+
// Inside an Item row with no label of its own, the switch is named by the
|
|
31
|
+
// row title (aria-labelledby on web, accessibilityLabel on native).
|
|
32
|
+
const rowLabel = useItemControlLabel(props);
|
|
43
33
|
// Fire haptic on user-initiated toggles (skip initial mount)
|
|
44
34
|
const wrappedOnCheckedChange = useCallback((checked) => {
|
|
45
35
|
if (hasMounted.current)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}, [
|
|
36
|
+
hapticSelection();
|
|
37
|
+
onCheckedChange?.(checked);
|
|
38
|
+
}, [onCheckedChange]);
|
|
49
39
|
useEffect(() => {
|
|
50
40
|
hasMounted.current = true;
|
|
51
41
|
}, []);
|
|
52
42
|
// Single animated value drives everything: 0 = off, 1 = on
|
|
53
|
-
const progress =
|
|
43
|
+
const progress = useAnimatedValue(props.checked ? 1 : 0);
|
|
54
44
|
useEffect(() => {
|
|
55
45
|
const target = props.checked ? 1 : 0;
|
|
56
46
|
Animated.timing(progress, {
|
|
57
47
|
toValue: target,
|
|
58
48
|
duration: reduceMotion ? 0 : 120,
|
|
59
|
-
useNativeDriver:
|
|
49
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
60
50
|
}).start();
|
|
61
51
|
}, [props.checked, reduceMotion, progress]);
|
|
62
52
|
// Thumb slides from left to right with equal inset on every side.
|
|
@@ -93,7 +83,7 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
|
|
|
93
83
|
const labelFontSize = size.height / 3;
|
|
94
84
|
// Flatten style override for web compatibility
|
|
95
85
|
const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
|
|
96
|
-
return (_jsx(Animated.View, { style: scaleStyle, children: _jsxs(SwitchPrimitives.Root, { ...props, onCheckedChange: wrappedOnCheckedChange, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
|
|
86
|
+
return (_jsx(Animated.View, { style: scaleStyle, children: _jsxs(SwitchPrimitives.Root, { ...rowLabel, ...props, onCheckedChange: wrappedOnCheckedChange, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
|
|
97
87
|
position: "relative",
|
|
98
88
|
width: size.width,
|
|
99
89
|
height: size.height,
|
|
@@ -148,7 +138,7 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
|
|
|
148
138
|
userSelect: "none",
|
|
149
139
|
}, children: labelOff }) }))] }) }));
|
|
150
140
|
}
|
|
151
|
-
const styles = StyleSheet.create({
|
|
141
|
+
const styles = /*#__PURE__*/ StyleSheet.create({
|
|
152
142
|
label: {
|
|
153
143
|
position: "absolute",
|
|
154
144
|
top: 0,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { type StyleProp, type ViewStyle } from "react-native";
|
|
3
3
|
import * as TabsPrimitive from "@rn-primitives/tabs";
|
|
4
|
-
import { StyledText } from "./StyledText";
|
|
5
|
-
import { type IconName } from "./Icon";
|
|
4
|
+
import { StyledText } from "./StyledText.js";
|
|
5
|
+
import { type IconName } from "./Icon.js";
|
|
6
6
|
type TabsVariant = "underline" | "pill";
|
|
7
7
|
type TabsSize = "sm" | "md";
|
|
8
8
|
export interface TabsProps extends TabsPrimitive.RootProps {
|
package/dist/components/Tabs.js
CHANGED
|
@@ -3,12 +3,15 @@ import * as React from "react";
|
|
|
3
3
|
import { Animated, Platform, StyleSheet, View } from "react-native";
|
|
4
4
|
import * as TabsPrimitive from "@rn-primitives/tabs";
|
|
5
5
|
import { StyledText } from "./StyledText.js";
|
|
6
|
-
import { TextClassContext, TextColorContext } from "./StyledText.context";
|
|
6
|
+
import { TextClassContext, TextColorContext } from "./StyledText.context.js";
|
|
7
7
|
import { Icon } from "./Icon.js";
|
|
8
8
|
import { useTheme } from "../hooks/useTheme.js";
|
|
9
|
+
import { useFocusVisible } from "../hooks/useFocusVisible.js";
|
|
9
10
|
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
10
11
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
12
|
+
import { useAnimatedValue } from "../lib/useAnimatedValue.js";
|
|
11
13
|
import { spacing } from "../constants/spacing.js";
|
|
14
|
+
import { shouldUseNativeDriver } from "../lib/animations.js";
|
|
12
15
|
// ============================================================================
|
|
13
16
|
// Size configs
|
|
14
17
|
// ============================================================================
|
|
@@ -65,12 +68,12 @@ function TabsTriggerInner({ icon, style, children, value, ...props }) {
|
|
|
65
68
|
// Determine selected state by comparing trigger value with root value
|
|
66
69
|
const rootContext = TabsPrimitive.useRootContext();
|
|
67
70
|
const isSelected = rootContext.value === value;
|
|
68
|
-
const activeOpacity =
|
|
71
|
+
const activeOpacity = useAnimatedValue(isSelected ? 1 : 0);
|
|
69
72
|
React.useEffect(() => {
|
|
70
73
|
Animated.timing(activeOpacity, {
|
|
71
74
|
toValue: isSelected ? 1 : 0,
|
|
72
75
|
duration: reduceMotion ? 0 : 200,
|
|
73
|
-
useNativeDriver:
|
|
76
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
74
77
|
}).start();
|
|
75
78
|
}, [isSelected, reduceMotion, activeOpacity]);
|
|
76
79
|
const textColor = isDisabled
|
|
@@ -79,32 +82,18 @@ function TabsTriggerInner({ icon, style, children, value, ...props }) {
|
|
|
79
82
|
? theme.colors.foreground
|
|
80
83
|
: theme.colors.mutedForeground;
|
|
81
84
|
const focusRingStyle = getFocusRingStyle();
|
|
82
|
-
const [focused, setFocused] = React.useState(false);
|
|
83
85
|
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
84
86
|
disabled: isDisabled,
|
|
85
87
|
scaleTo: 0.97,
|
|
86
88
|
haptic: false,
|
|
87
89
|
});
|
|
88
|
-
const showFocusRing = (
|
|
89
|
-
let ringVisible = true;
|
|
90
|
-
if (Platform.OS === "web") {
|
|
91
|
-
const target = event?.nativeEvent?.target;
|
|
92
|
-
if (target && typeof target.matches === "function") {
|
|
93
|
-
try {
|
|
94
|
-
ringVisible = target.matches(":focus-visible");
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
ringVisible = true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
setFocused(ringVisible);
|
|
102
|
-
};
|
|
103
|
-
const hideFocusRing = () => {
|
|
104
|
-
setFocused(false);
|
|
105
|
-
};
|
|
90
|
+
const { focused, onFocus: showFocusRing, onBlur: hideFocusRing } = useFocusVisible();
|
|
106
91
|
const triggerBaseStyle = {
|
|
107
|
-
flex: 1
|
|
92
|
+
// The wrapper below shares the list's width (`flex: 1` on the row axis);
|
|
93
|
+
// the trigger only fills it. `flex: 1` here would act on the wrapper's
|
|
94
|
+
// column axis, where a 0% basis in an indefinite-height parent collapses
|
|
95
|
+
// the trigger to its text (16px tall on web, under the 36px it declares).
|
|
96
|
+
alignSelf: "stretch",
|
|
108
97
|
// Allow the trigger to shrink below its content's intrinsic width so long
|
|
109
98
|
// labels are constrained to the trigger's flex share (and can ellipsize)
|
|
110
99
|
// instead of overflowing and clipping at the screen edge.
|
|
@@ -165,7 +154,7 @@ function TabsContent({ style, children, ...props }) {
|
|
|
165
154
|
// ============================================================================
|
|
166
155
|
// Styles
|
|
167
156
|
// ============================================================================
|
|
168
|
-
const triggerContentStyles = StyleSheet.create({
|
|
157
|
+
const triggerContentStyles = /*#__PURE__*/ StyleSheet.create({
|
|
169
158
|
container: {
|
|
170
159
|
flexDirection: "row",
|
|
171
160
|
alignItems: "center",
|
|
@@ -120,7 +120,7 @@ interface TextInputCustomProps extends TextInputProps {
|
|
|
120
120
|
* // With custom elements
|
|
121
121
|
* <TextInput
|
|
122
122
|
* label="Search"
|
|
123
|
-
* leftElement={<Icon
|
|
123
|
+
* leftElement={<Icon name="search" size={20} />}
|
|
124
124
|
* />
|
|
125
125
|
* ```
|
|
126
126
|
*/
|