@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
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { AnimatedView } from "./AnimatedView.js";
|
|
3
|
-
import { TextClassContext, TextColorContext } from "./StyledText.context";
|
|
3
|
+
import { TextClassContext, TextColorContext } from "./StyledText.context.js";
|
|
4
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
5
|
import { spacing } from "../constants/spacing.js";
|
|
6
6
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
7
|
+
import { resolvePopoverPlacement } from "../lib/popoverPlacement.js";
|
|
7
8
|
import * as PopoverPrimitive from "@rn-primitives/popover";
|
|
8
9
|
import * as React from "react";
|
|
9
|
-
import { Animated, Platform, StyleSheet, View } from "react-native";
|
|
10
|
+
import { Animated, Dimensions, Platform, ScrollView, StyleSheet, View, } from "react-native";
|
|
11
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
10
12
|
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
11
13
|
import { palette } from "../constants/colors.js";
|
|
14
|
+
// The overlay layer covers the screen but must not swallow touches around the
|
|
15
|
+
// content; a style, not the deprecated `pointerEvents` prop (web warns and
|
|
16
|
+
// lazily loads its warning module mid-render).
|
|
17
|
+
const styles = /*#__PURE__*/ StyleSheet.create({
|
|
18
|
+
passThrough: { pointerEvents: "box-none" },
|
|
19
|
+
});
|
|
12
20
|
function PopoverTrigger({ disabled, ...props }) {
|
|
13
21
|
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
14
22
|
disabled: !!disabled,
|
|
@@ -22,15 +30,69 @@ function PopoverTrigger({ disabled, ...props }) {
|
|
|
22
30
|
* On Android/Web, uses Fragment to avoid unnecessary nesting
|
|
23
31
|
*/
|
|
24
32
|
const FullWindowOverlay = Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment;
|
|
33
|
+
/**
|
|
34
|
+
* Radix measures the viewport room on web and exposes it on the content
|
|
35
|
+
* element; the primitive's web Content applies no size cap of its own.
|
|
36
|
+
*/
|
|
37
|
+
const WEB_PLACEMENT_STYLE = {
|
|
38
|
+
maxWidth: "var(--radix-popover-content-available-width)",
|
|
39
|
+
maxHeight: "var(--radix-popover-content-available-height)",
|
|
40
|
+
};
|
|
41
|
+
const claimNoTouch = () => false;
|
|
25
42
|
/**
|
|
26
43
|
* Popover Content Component
|
|
27
|
-
* The content that appears in the popover overlay
|
|
28
|
-
*
|
|
44
|
+
* The content that appears in the popover overlay. `side` is a preference: on
|
|
45
|
+
* native the popover opens on the other side when the content does not fit
|
|
46
|
+
* and there is more room there, caps its height to the room, and scrolls.
|
|
47
|
+
* A `style` merges over the themed surface; `insets` default to the safe area.
|
|
29
48
|
*/
|
|
30
|
-
function PopoverContent({
|
|
31
|
-
const { theme,
|
|
49
|
+
function PopoverContent({ portalHost, insets, ...props }) {
|
|
50
|
+
const { theme, getContrastingColor } = useTheme();
|
|
51
|
+
const safeAreaInsets = useSafeAreaInsets();
|
|
32
52
|
// Calculate text color for popover content based on background
|
|
33
53
|
const textColor = getContrastingColor(theme.colors.popover, palette.white, palette.black);
|
|
54
|
+
const card = (_jsx(AnimatedView, { type: "fade", enterDuration: 200, style: [StyleSheet.absoluteFill, styles.passThrough], collapsable: false, children: _jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(PopoverCard, { insets: insets ?? safeAreaInsets, ...props }) }) }) }));
|
|
55
|
+
return (_jsx(PopoverPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: Platform.OS === "web" ? (_jsx(PopoverPrimitive.Overlay, { children: card })) : (_jsxs(_Fragment, { children: [_jsx(PopoverPrimitive.Overlay, { style: StyleSheet.absoluteFill }), card] })) }) }));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The positioned card. Rendered inside the portal, so it mounts on open and
|
|
59
|
+
* its measurements start fresh every time.
|
|
60
|
+
*/
|
|
61
|
+
function PopoverCard({ side, align = "center", sideOffset = 4, insets, avoidCollisions = true, scrollable = true, style, children, testID, ...props }) {
|
|
62
|
+
const { theme, getShadowStyle } = useTheme();
|
|
63
|
+
const { triggerPosition, contentLayout } = PopoverPrimitive.useRootContext();
|
|
64
|
+
const [scrollFrameHeight, setScrollFrameHeight] = React.useState();
|
|
65
|
+
const [scrollContentHeight, setScrollContentHeight] = React.useState();
|
|
66
|
+
const isWeb = Platform.OS === "web";
|
|
67
|
+
const wrapsInScroll = scrollable && !props.asChild;
|
|
68
|
+
// The card's own chrome (padding, border) plus the uncapped scroll content.
|
|
69
|
+
const naturalHeight = wrapsInScroll &&
|
|
70
|
+
contentLayout &&
|
|
71
|
+
scrollFrameHeight !== undefined &&
|
|
72
|
+
scrollContentHeight !== undefined
|
|
73
|
+
? contentLayout.height - scrollFrameHeight + scrollContentHeight
|
|
74
|
+
: undefined;
|
|
75
|
+
let placementSide = side;
|
|
76
|
+
let placementStyle = WEB_PLACEMENT_STYLE;
|
|
77
|
+
if (!isWeb) {
|
|
78
|
+
const placement = resolvePopoverPlacement({
|
|
79
|
+
side,
|
|
80
|
+
sideOffset,
|
|
81
|
+
trigger: triggerPosition ?? null,
|
|
82
|
+
screenHeight: Dimensions.get("screen").height,
|
|
83
|
+
insets: insets ?? {},
|
|
84
|
+
naturalHeight,
|
|
85
|
+
avoidCollisions,
|
|
86
|
+
});
|
|
87
|
+
placementSide = placement.side;
|
|
88
|
+
// Hide the first measured frame while it may still flip. The key is
|
|
89
|
+
// omitted otherwise, so the primitive's hidden-until-measured style holds.
|
|
90
|
+
const mayStillFlip = wrapsInScroll && avoidCollisions && !!triggerPosition && naturalHeight === undefined;
|
|
91
|
+
placementStyle = {
|
|
92
|
+
...(placement.maxHeight !== undefined ? { maxHeight: placement.maxHeight } : null),
|
|
93
|
+
...(mayStillFlip ? { opacity: 0 } : null),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
34
96
|
const contentStyle = StyleSheet.flatten([
|
|
35
97
|
{
|
|
36
98
|
backgroundColor: theme.colors.popover,
|
|
@@ -40,8 +102,10 @@ function PopoverContent({ side, align = "center", sideOffset = 4, portalHost, ..
|
|
|
40
102
|
padding: spacing.xs,
|
|
41
103
|
...getShadowStyle("soft"),
|
|
42
104
|
},
|
|
105
|
+
placementStyle,
|
|
106
|
+
style,
|
|
43
107
|
]) ?? undefined;
|
|
44
|
-
return (_jsx(PopoverPrimitive.
|
|
108
|
+
return (_jsx(PopoverPrimitive.Content, { side: placementSide, align: align, sideOffset: sideOffset, insets: insets, avoidCollisions: avoidCollisions, testID: testID, ...(isWeb ? null : { onStartShouldSetResponder: claimNoTouch }), ...props, style: contentStyle, children: wrapsInScroll ? (_jsx(ScrollView, { testID: testID ? `${testID}-scroll` : undefined, alwaysBounceVertical: false, keyboardShouldPersistTaps: "handled", onLayout: (event) => setScrollFrameHeight(event.nativeEvent.layout.height), onContentSizeChange: (_width, height) => setScrollContentHeight(height), children: children })) : (children) }));
|
|
45
109
|
}
|
|
46
110
|
function PopoverHeader({ children, style, ...props }) {
|
|
47
111
|
const { theme } = useTheme();
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect
|
|
2
|
+
import { useEffect } from "react";
|
|
3
3
|
import { Animated, StyleSheet, View } from "react-native";
|
|
4
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
5
|
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
6
|
+
import { useAnimatedValue } from "../lib/useAnimatedValue.js";
|
|
7
|
+
import { shouldUseNativeDriver } from "../lib/animations.js";
|
|
6
8
|
// ============================================================================
|
|
7
9
|
// Constants
|
|
8
10
|
// ============================================================================
|
|
@@ -55,12 +57,12 @@ function DeterminateFill({ value, fillColor, height, borderRadius, reduceMotion,
|
|
|
55
57
|
// Animate scaleX (GPU compositor) instead of width (JS-thread layout each
|
|
56
58
|
// frame). transformOrigin "left" grows the fill from the left edge, so a
|
|
57
59
|
// full-width bar scaled by clamped/100 needs no container measurement.
|
|
58
|
-
const scaleX =
|
|
60
|
+
const scaleX = useAnimatedValue(0);
|
|
59
61
|
useEffect(() => {
|
|
60
62
|
Animated.timing(scaleX, {
|
|
61
63
|
toValue: clamped / 100,
|
|
62
64
|
duration: reduceMotion ? 0 : 300,
|
|
63
|
-
useNativeDriver:
|
|
65
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
64
66
|
}).start();
|
|
65
67
|
}, [clamped, reduceMotion, scaleX]);
|
|
66
68
|
return (_jsx(Animated.View, { style: [
|
|
@@ -75,7 +77,7 @@ function DeterminateFill({ value, fillColor, height, borderRadius, reduceMotion,
|
|
|
75
77
|
] }));
|
|
76
78
|
}
|
|
77
79
|
function IndeterminateFill({ fillColor, height, borderRadius, reduceMotion, }) {
|
|
78
|
-
const opacity =
|
|
80
|
+
const opacity = useAnimatedValue(reduceMotion ? 0.7 : 0.4);
|
|
79
81
|
useEffect(() => {
|
|
80
82
|
if (reduceMotion) {
|
|
81
83
|
opacity.setValue(0.7);
|
|
@@ -86,12 +88,12 @@ function IndeterminateFill({ fillColor, height, borderRadius, reduceMotion, }) {
|
|
|
86
88
|
Animated.timing(opacity, {
|
|
87
89
|
toValue: 1,
|
|
88
90
|
duration: 800,
|
|
89
|
-
useNativeDriver:
|
|
91
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
90
92
|
}),
|
|
91
93
|
Animated.timing(opacity, {
|
|
92
94
|
toValue: 0.4,
|
|
93
95
|
duration: 800,
|
|
94
|
-
useNativeDriver:
|
|
96
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
95
97
|
}),
|
|
96
98
|
]));
|
|
97
99
|
animation.start();
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { createContext, use, useEffect
|
|
2
|
+
import React, { createContext, use, useEffect } from "react";
|
|
3
3
|
import { View, StyleSheet, Pressable, Platform, Animated } from "react-native";
|
|
4
4
|
import { StyledText } from "./StyledText.js";
|
|
5
5
|
import { useTheme } from "../hooks/useTheme.js";
|
|
6
|
+
import { useFocusVisible } from "../hooks/useFocusVisible.js";
|
|
6
7
|
import { spacing } from "../constants/spacing.js";
|
|
8
|
+
import { interaction } from "../constants/interaction.js";
|
|
7
9
|
import { hapticLight } from "../lib/haptics.js";
|
|
10
|
+
import { useAnimatedValue } from "../lib/useAnimatedValue.js";
|
|
8
11
|
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
9
12
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
13
|
+
import { shouldUseNativeDriver } from "../lib/animations.js";
|
|
10
14
|
import * as RadioGroupPrimitive from "@rn-primitives/radio-group";
|
|
11
15
|
const DEFAULT_HIT_SLOP = 8;
|
|
12
16
|
const SIZE_CONFIGS = {
|
|
@@ -60,38 +64,20 @@ function RadioGroupItem({ label, required = false, style: styleOverride, labelSt
|
|
|
60
64
|
const { size, error, value: groupValue, onValueChange } = useRadioGroupContext();
|
|
61
65
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
62
66
|
const focusRingStyle = getFocusRingStyle();
|
|
63
|
-
const [focused, setFocused] = useState(false);
|
|
64
67
|
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
65
68
|
disabled: !!disabled,
|
|
66
69
|
scaleTo: 0.92,
|
|
67
70
|
haptic: false,
|
|
68
71
|
});
|
|
69
|
-
const showFocusRing = (
|
|
70
|
-
let ringVisible = true;
|
|
71
|
-
if (Platform.OS === "web") {
|
|
72
|
-
const target = event?.nativeEvent?.target;
|
|
73
|
-
if (target && typeof target.matches === "function") {
|
|
74
|
-
try {
|
|
75
|
-
ringVisible = target.matches(":focus-visible");
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
ringVisible = true;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
setFocused(ringVisible);
|
|
83
|
-
};
|
|
84
|
-
const hideFocusRing = () => {
|
|
85
|
-
setFocused(false);
|
|
86
|
-
};
|
|
72
|
+
const { focused, onFocus: showFocusRing, onBlur: hideFocusRing } = useFocusVisible();
|
|
87
73
|
const isChecked = groupValue === itemValue;
|
|
88
74
|
// Animated dot scale — follows Checkbox opacity pattern
|
|
89
|
-
const dotScale =
|
|
75
|
+
const dotScale = useAnimatedValue(isChecked ? 1 : 0);
|
|
90
76
|
useEffect(() => {
|
|
91
77
|
Animated.timing(dotScale, {
|
|
92
78
|
toValue: isChecked ? 1 : 0,
|
|
93
79
|
duration: reduceMotion ? 0 : 60,
|
|
94
|
-
useNativeDriver:
|
|
80
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
95
81
|
}).start();
|
|
96
82
|
}, [isChecked, reduceMotion, dotScale]);
|
|
97
83
|
// Border color follows Checkbox pattern
|
|
@@ -101,48 +87,65 @@ function RadioGroupItem({ label, required = false, style: styleOverride, labelSt
|
|
|
101
87
|
? theme.colors.primary
|
|
102
88
|
: getContrastingColor(theme.colors.background, theme.colors.text, theme.colors.textDim);
|
|
103
89
|
const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
90
|
+
// The primitive Item is the ONE control; the drawn circle is a child so the
|
|
91
|
+
// control can be bigger than it looks. `hitSlop` is inert on web, where a
|
|
92
|
+
// 16/20px circle sits under the 24px WCAG 2.5.8 minimum: the control grows
|
|
93
|
+
// to `spacing.minTarget` and takes the extra back in negative margins.
|
|
94
|
+
const hitSize = Platform.OS === "web" ? Math.max(sizeConfig.outer, spacing.minTarget) : sizeConfig.outer;
|
|
95
|
+
const hitOverflow = (hitSize - sizeConfig.outer) / 2;
|
|
96
|
+
// The label text names the control on every platform; no `useId` link (ids
|
|
97
|
+
// do not survive Expo Router's streamed hydration — see hooks/useHydrated).
|
|
98
|
+
const labelLink = label !== undefined ? { accessibilityLabel: label } : undefined;
|
|
99
|
+
const radioElement = (_jsx(Animated.View, { style: scaleStyle, children: _jsx(RadioGroupPrimitive.Item, { ...labelLink, ...props, value: itemValue, disabled: disabled, onPress: handleRadioPress, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
|
|
100
|
+
...styles.control,
|
|
101
|
+
minWidth: hitSize,
|
|
102
|
+
minHeight: hitSize,
|
|
103
|
+
...(hitOverflow > 0 && { margin: -hitOverflow }),
|
|
113
104
|
...(Platform.OS === "web" && { cursor: disabled ? "not-allowed" : "pointer" }),
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
105
|
+
}, hitSlop: DEFAULT_HIT_SLOP, children: _jsx(View, { style: {
|
|
106
|
+
...styles.radio,
|
|
107
|
+
borderColor,
|
|
108
|
+
backgroundColor: theme.colors.background,
|
|
109
|
+
borderRadius: sizeConfig.outer / 2,
|
|
110
|
+
borderWidth: sizeConfig.borderWidth,
|
|
111
|
+
width: sizeConfig.outer,
|
|
112
|
+
height: sizeConfig.outer,
|
|
113
|
+
opacity: disabled ? 0.5 : 1,
|
|
114
|
+
...(focused && !disabled ? focusRingStyle : null),
|
|
115
|
+
...(flattenedStyle || {}),
|
|
116
|
+
}, children: _jsx(Animated.View, { style: [
|
|
117
|
+
{ transform: [{ scale: dotScale }] },
|
|
118
|
+
{
|
|
119
|
+
width: sizeConfig.inner,
|
|
120
|
+
height: sizeConfig.inner,
|
|
121
|
+
borderRadius: sizeConfig.inner / 2,
|
|
122
|
+
backgroundColor: theme.colors.primary,
|
|
123
|
+
},
|
|
124
|
+
] }) }) }) }));
|
|
125
125
|
// If no label, return just the radio button
|
|
126
126
|
if (!label) {
|
|
127
127
|
return radioElement;
|
|
128
128
|
}
|
|
129
|
-
// With label,
|
|
129
|
+
// With a label, a non-accessible wrapper extends the tap area to the text;
|
|
130
|
+
// the primitive Item above stays the only radio a screen reader meets, and
|
|
131
|
+
// it carries the label text as its name.
|
|
130
132
|
return (_jsxs(Pressable, { onPress: () => {
|
|
131
133
|
if (!disabled) {
|
|
132
134
|
hapticLight();
|
|
133
135
|
onValueChange(itemValue);
|
|
134
136
|
}
|
|
135
|
-
}, style: [styles.container, labelStyle], disabled: disabled,
|
|
136
|
-
checked: isChecked,
|
|
137
|
-
disabled: !!disabled,
|
|
138
|
-
}, accessibilityLabel: label, children: [radioElement, _jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, style: [
|
|
137
|
+
}, style: [styles.container, labelStyle], disabled: disabled, accessible: false, focusable: false, children: [radioElement, _jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, style: [
|
|
139
138
|
styles.label,
|
|
140
139
|
{ color: theme.colors.text },
|
|
141
140
|
disabled && styles.disabledLabel,
|
|
142
141
|
error && { color: theme.colors.destructive },
|
|
143
142
|
], children: [label, required && (_jsx(StyledText, { selectable: false, style: [styles.required, { color: theme.colors.destructive }], children: " *" }))] }) })] }));
|
|
144
143
|
}
|
|
145
|
-
const styles = StyleSheet.create({
|
|
144
|
+
const styles = /*#__PURE__*/ StyleSheet.create({
|
|
145
|
+
control: {
|
|
146
|
+
justifyContent: "center",
|
|
147
|
+
alignItems: "center",
|
|
148
|
+
},
|
|
146
149
|
radio: {
|
|
147
150
|
justifyContent: "center",
|
|
148
151
|
alignItems: "center",
|
|
@@ -160,7 +163,7 @@ const styles = StyleSheet.create({
|
|
|
160
163
|
fontSize: 14,
|
|
161
164
|
},
|
|
162
165
|
disabledLabel: {
|
|
163
|
-
opacity:
|
|
166
|
+
opacity: interaction.disabledOpacity,
|
|
164
167
|
},
|
|
165
168
|
required: {
|
|
166
169
|
fontWeight: "bold",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type ScrollViewProps, type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
export type ScreenEdge = "top" | "bottom" | "left" | "right";
|
|
4
|
+
export type ScreenEdges = ReadonlyArray<ScreenEdge>;
|
|
5
|
+
export interface ScreenProps {
|
|
6
|
+
/**
|
|
7
|
+
* Which safe-area edges this screen owns. Required, with no default, so a
|
|
8
|
+
* screen never pads an edge a navigator already insets: under a Stack header
|
|
9
|
+
* pass `["bottom"]`; above a tab bar `["top"]`; inside a modal or form sheet
|
|
10
|
+
* `["bottom"]`; a headerless full screen `["top", "bottom"]`; `[]` when the
|
|
11
|
+
* navigator owns both.
|
|
12
|
+
*/
|
|
13
|
+
edges: ScreenEdges;
|
|
14
|
+
/**
|
|
15
|
+
* `true` renders a `ScrollView` and puts the inset padding on its content
|
|
16
|
+
* container, so content starts clear of the island / home indicator but
|
|
17
|
+
* scrolls under them. `false` (default) renders a `View` padded on itself.
|
|
18
|
+
*/
|
|
19
|
+
scroll?: boolean;
|
|
20
|
+
/** Extra `ScrollView` props when `scroll` is set (`refreshControl`, `keyboardShouldPersistTaps`, …). */
|
|
21
|
+
scrollProps?: Omit<ScrollViewProps, "style" | "contentContainerStyle" | "children">;
|
|
22
|
+
/**
|
|
23
|
+
* The screen's one horizontal inset, `spacing.screenPadding` (16), plus the
|
|
24
|
+
* left/right safe-area insets when those edges are named. `false` for
|
|
25
|
+
* full-bleed content such as `ItemGroup` rows, which carry their own 16.
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
padded?: boolean;
|
|
29
|
+
/** Style of the outer View / ScrollView (defaults to `flex: 1` on the theme background). */
|
|
30
|
+
style?: StyleProp<ViewStyle>;
|
|
31
|
+
/**
|
|
32
|
+
* Content padding and layout. Its `padding*` values are added to the safe-area
|
|
33
|
+
* insets (`paddingBottom: 48` on a 34 pt home indicator gives 82), never
|
|
34
|
+
* replaced by them. On the `View` variant it merges into `style`.
|
|
35
|
+
*/
|
|
36
|
+
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
37
|
+
children?: React.ReactNode;
|
|
38
|
+
testID?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Screen
|
|
42
|
+
*
|
|
43
|
+
* The page container: safe-area insets on the edges the screen owns, the one
|
|
44
|
+
* 16 pt horizontal inset, and the theme background under all of it. Insets
|
|
45
|
+
* come from `useWindowInsets`, so a screen inside a modal or a native sheet
|
|
46
|
+
* still clears the home indicator.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* // Under a Stack header, above no tab bar
|
|
51
|
+
* <Screen edges={["bottom"]} scroll contentContainerStyle={{ paddingBottom: spacing.xxl }}>
|
|
52
|
+
* …
|
|
53
|
+
* </Screen>
|
|
54
|
+
*
|
|
55
|
+
* // Headerless, full-bleed rows
|
|
56
|
+
* <Screen edges={["top", "bottom"]} padded={false}>
|
|
57
|
+
* <ItemGroup title="Account">…</ItemGroup>
|
|
58
|
+
* </Screen>
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function Screen({ edges, scroll, scrollProps, padded, style, contentContainerStyle, children, testID, }: ScreenProps): React.JSX.Element;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ScrollView, StyleSheet, View, } from "react-native";
|
|
3
|
+
import { useTheme } from "../hooks/useTheme.js";
|
|
4
|
+
import { useWindowInsets } from "../hooks/useWindowInsets.js";
|
|
5
|
+
import { spacing } from "../constants/spacing.js";
|
|
6
|
+
/** The caller's padding on one edge, honouring the `padding` / `paddingVertical` / `paddingHorizontal` shorthands. */
|
|
7
|
+
function ownPadding(style, key) {
|
|
8
|
+
if (!style)
|
|
9
|
+
return 0;
|
|
10
|
+
const axis = key === "paddingTop" || key === "paddingBottom" ? style.paddingVertical : style.paddingHorizontal;
|
|
11
|
+
const value = style[key] ?? axis ?? style.padding;
|
|
12
|
+
return typeof value === "number" ? value : 0;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Screen
|
|
16
|
+
*
|
|
17
|
+
* The page container: safe-area insets on the edges the screen owns, the one
|
|
18
|
+
* 16 pt horizontal inset, and the theme background under all of it. Insets
|
|
19
|
+
* come from `useWindowInsets`, so a screen inside a modal or a native sheet
|
|
20
|
+
* still clears the home indicator.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* // Under a Stack header, above no tab bar
|
|
25
|
+
* <Screen edges={["bottom"]} scroll contentContainerStyle={{ paddingBottom: spacing.xxl }}>
|
|
26
|
+
* …
|
|
27
|
+
* </Screen>
|
|
28
|
+
*
|
|
29
|
+
* // Headerless, full-bleed rows
|
|
30
|
+
* <Screen edges={["top", "bottom"]} padded={false}>
|
|
31
|
+
* <ItemGroup title="Account">…</ItemGroup>
|
|
32
|
+
* </Screen>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function Screen({ edges, scroll = false, scrollProps, padded = true, style, contentContainerStyle, children, testID, }) {
|
|
36
|
+
const { theme } = useTheme();
|
|
37
|
+
const insets = useWindowInsets();
|
|
38
|
+
const has = (edge) => edges.includes(edge);
|
|
39
|
+
const own = StyleSheet.flatten(contentContainerStyle);
|
|
40
|
+
const horizontal = padded ? spacing.screenPadding : 0;
|
|
41
|
+
// Every edge the caller pads or the screen owns gets one resolved value:
|
|
42
|
+
// the caller's spacing plus the inset. Edges neither side touches stay
|
|
43
|
+
// unset so a caller style can still use shorthands for them.
|
|
44
|
+
const padding = {};
|
|
45
|
+
const top = ownPadding(own, "paddingTop") + (has("top") ? insets.top : 0);
|
|
46
|
+
const bottom = ownPadding(own, "paddingBottom") + (has("bottom") ? insets.bottom : 0);
|
|
47
|
+
const left = ownPadding(own, "paddingLeft") + horizontal + (has("left") ? insets.left : 0);
|
|
48
|
+
const right = ownPadding(own, "paddingRight") + horizontal + (has("right") ? insets.right : 0);
|
|
49
|
+
if (has("top") || own?.paddingTop !== undefined || own?.paddingVertical !== undefined || own?.padding !== undefined)
|
|
50
|
+
padding.paddingTop = top;
|
|
51
|
+
if (has("bottom") || own?.paddingBottom !== undefined || own?.paddingVertical !== undefined || own?.padding !== undefined)
|
|
52
|
+
padding.paddingBottom = bottom;
|
|
53
|
+
if (left)
|
|
54
|
+
padding.paddingLeft = left;
|
|
55
|
+
if (right)
|
|
56
|
+
padding.paddingRight = right;
|
|
57
|
+
const base = { flex: 1, backgroundColor: theme.colors.background };
|
|
58
|
+
if (scroll) {
|
|
59
|
+
return (_jsx(ScrollView, { style: [base, style], contentContainerStyle: [own, padding], testID: testID, ...scrollProps, children: children }));
|
|
60
|
+
}
|
|
61
|
+
return (_jsx(View, { style: [base, own, padding, style], testID: testID, children: children }));
|
|
62
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { StyleProp, ViewStyle, TextStyle } from "react-native";
|
|
3
|
-
import { type TextProps } from "./StyledText";
|
|
3
|
+
import { type TextProps } from "./StyledText.js";
|
|
4
4
|
type TextPassthroughProps = Omit<TextProps, "style" | "children"> & {
|
|
5
5
|
style?: StyleProp<TextStyle>;
|
|
6
6
|
};
|
|
@@ -7,18 +7,26 @@ import { type StyleProp, type ViewStyle } from "react-native";
|
|
|
7
7
|
*
|
|
8
8
|
* - iOS: SwiftUI segmented `Picker` (system-styled).
|
|
9
9
|
* - Android: a Material segmented control, accent-tinted via `tintColor`.
|
|
10
|
-
* - Web: the
|
|
11
|
-
* JS implementation, accent-tinted via `tintColor`.
|
|
10
|
+
* - Web: the kit's own themed control (see `WebSegmentedControl`).
|
|
12
11
|
*
|
|
13
12
|
* The API is value-based to match the rest of the design system (RadioGroup /
|
|
14
13
|
* Tabs / Select): pass the segment `values` plus a controlled `value` (or
|
|
15
14
|
* `defaultValue` for uncontrolled), and read selections back as the chosen
|
|
16
|
-
* string. A
|
|
15
|
+
* string. A selection haptic fires on each change, matching Switch / Checkbox
|
|
16
|
+
* (see `hapticSelection`).
|
|
17
17
|
*
|
|
18
18
|
* Theming: the accent color tints the selected segment on Android and web. iOS
|
|
19
19
|
* draws the system segmented control, which ignores a custom tint — pass
|
|
20
20
|
* `appearance` to force light/dark there.
|
|
21
21
|
*
|
|
22
|
+
* Web: `@expo/ui`'s vendored web control paints every label white once a
|
|
23
|
+
* `tintColor` is passed, so unselected labels vanished on the light track and
|
|
24
|
+
* the disabled state was unreadable; it also exposes no label styling. The
|
|
25
|
+
* kit draws the control itself on web from theme tokens (`muted` track,
|
|
26
|
+
* accent pill, `accentForeground` / `mutedForeground` labels, the shared
|
|
27
|
+
* disabled opacity and focus ring). `appearance` is ignored there; the theme
|
|
28
|
+
* decides.
|
|
29
|
+
*
|
|
22
30
|
* @example
|
|
23
31
|
* ```tsx
|
|
24
32
|
* <SegmentedControl
|
|
@@ -44,7 +52,7 @@ export interface SegmentedControlProps {
|
|
|
44
52
|
* Applied on Android and web; iOS uses the system style.
|
|
45
53
|
*/
|
|
46
54
|
tintColor?: string;
|
|
47
|
-
/** Force a color scheme irrespective of the system theme. */
|
|
55
|
+
/** Force a color scheme irrespective of the system theme (iOS/Android; web follows the theme). */
|
|
48
56
|
appearance?: "light" | "dark";
|
|
49
57
|
/** Style override for the control. */
|
|
50
58
|
style?: StyleProp<ViewStyle>;
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import React, { useCallback, useRef } from "react";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Animated, Easing, Platform, Pressable, View, } from "react-native";
|
|
3
4
|
import { SegmentedControl as NativeSegmentedControl } from "@expo/ui/community/segmented-control";
|
|
4
5
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
|
-
import {
|
|
6
|
+
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
7
|
+
import { useFocusVisible } from "../hooks/useFocusVisible.js";
|
|
8
|
+
import { hapticSelection } from "../lib/haptics.js";
|
|
9
|
+
import { useAnimatedValue } from "../lib/useAnimatedValue.js";
|
|
10
|
+
import { spacing } from "../constants/spacing.js";
|
|
11
|
+
import { interaction } from "../constants/interaction.js";
|
|
12
|
+
import { shouldUseNativeDriver } from "../lib/animations.js";
|
|
13
|
+
import { StyledText } from "./StyledText.js";
|
|
6
14
|
function SegmentedControl({ values, value: controlledValue, defaultValue, onValueChange, disabled = false, tintColor, appearance, style, }) {
|
|
7
15
|
const { theme } = useTheme();
|
|
8
16
|
const [internalValue, setInternalValue] = React.useState(defaultValue);
|
|
@@ -14,12 +22,94 @@ function SegmentedControl({ values, value: controlledValue, defaultValue, onValu
|
|
|
14
22
|
const nextIndex = values.indexOf(next);
|
|
15
23
|
if (nextIndex !== lastIndex.current) {
|
|
16
24
|
lastIndex.current = nextIndex;
|
|
17
|
-
|
|
25
|
+
hapticSelection();
|
|
18
26
|
}
|
|
19
27
|
if (!isControlled)
|
|
20
28
|
setInternalValue(next);
|
|
21
29
|
onValueChange?.(next);
|
|
22
30
|
}, [isControlled, onValueChange, values]);
|
|
31
|
+
if (Platform.OS === "web") {
|
|
32
|
+
return (_jsx(WebSegmentedControl, { values: values, selectedIndex: selectedIndex, disabled: disabled, tintColor: tintColor, onSelect: handleValueChange, style: style }));
|
|
33
|
+
}
|
|
23
34
|
return (_jsx(NativeSegmentedControl, { values: values, selectedIndex: selectedIndex, enabled: !disabled, onValueChange: handleValueChange, tintColor: tintColor ?? theme.colors.accent, appearance: appearance ?? (theme.dark ? "dark" : "light"), style: style }));
|
|
24
35
|
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Web
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
/** Matches the Button `md` / vendored control height. */
|
|
40
|
+
const TRACK_HEIGHT = 32;
|
|
41
|
+
/** Gap between the track edge and the selected pill. */
|
|
42
|
+
const PILL_INSET = 2;
|
|
43
|
+
function WebSegmentedControl({ values, selectedIndex, disabled, tintColor, onSelect, style }) {
|
|
44
|
+
const { theme, getContrastingColor } = useTheme();
|
|
45
|
+
const reduceMotion = useReducedMotion();
|
|
46
|
+
const [trackWidth, setTrackWidth] = useState(0);
|
|
47
|
+
const position = useAnimatedValue(selectedIndex);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const slide = Animated.timing(position, {
|
|
50
|
+
toValue: selectedIndex,
|
|
51
|
+
duration: reduceMotion ? 0 : 200,
|
|
52
|
+
easing: Easing.out(Easing.quad),
|
|
53
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
54
|
+
});
|
|
55
|
+
slide.start();
|
|
56
|
+
// Stop on unmount (and before the next selection starts its own slide) so
|
|
57
|
+
// no animation frame outlives the control.
|
|
58
|
+
return () => slide.stop();
|
|
59
|
+
}, [position, reduceMotion, selectedIndex]);
|
|
60
|
+
const onLayout = (event) => setTrackWidth(event.nativeEvent.layout.width);
|
|
61
|
+
const count = Math.max(1, values.length);
|
|
62
|
+
const segmentWidth = Math.max(0, trackWidth - PILL_INSET * 2) / count;
|
|
63
|
+
const travel = Math.max(1, count - 1);
|
|
64
|
+
const pillColor = tintColor ?? theme.colors.accent;
|
|
65
|
+
// A custom tint may not pair with `accentForeground`; pick the readable one.
|
|
66
|
+
const selectedLabelColor = tintColor ? getContrastingColor(tintColor) : theme.colors.accentForeground;
|
|
67
|
+
return (_jsxs(View, { testID: "segmented-control", accessibilityState: { disabled }, onLayout: onLayout, style: [
|
|
68
|
+
{
|
|
69
|
+
height: TRACK_HEIGHT,
|
|
70
|
+
borderRadius: spacing.radiusMd,
|
|
71
|
+
backgroundColor: theme.colors.muted,
|
|
72
|
+
opacity: disabled ? interaction.disabledOpacity : 1,
|
|
73
|
+
overflow: "hidden",
|
|
74
|
+
alignSelf: "stretch",
|
|
75
|
+
},
|
|
76
|
+
style,
|
|
77
|
+
], children: [segmentWidth > 0 && (_jsx(Animated.View, { style: {
|
|
78
|
+
pointerEvents: "none",
|
|
79
|
+
position: "absolute",
|
|
80
|
+
top: PILL_INSET,
|
|
81
|
+
bottom: PILL_INSET,
|
|
82
|
+
left: PILL_INSET,
|
|
83
|
+
width: segmentWidth,
|
|
84
|
+
borderRadius: spacing.radiusSm,
|
|
85
|
+
backgroundColor: pillColor,
|
|
86
|
+
transform: [
|
|
87
|
+
{
|
|
88
|
+
translateX: position.interpolate({
|
|
89
|
+
inputRange: [0, travel],
|
|
90
|
+
outputRange: [0, segmentWidth * travel],
|
|
91
|
+
}),
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
} })), _jsx(View, { style: { position: "absolute", top: PILL_INSET, bottom: PILL_INSET, left: PILL_INSET, right: PILL_INSET, flexDirection: "row" }, children: values.map((label, index) => (_jsx(WebSegment, { label: label, selected: index === selectedIndex, disabled: disabled, color: index === selectedIndex ? selectedLabelColor : theme.colors.mutedForeground, onPress: () => onSelect(label) }, `${index}-${label}`))) })] }));
|
|
95
|
+
}
|
|
96
|
+
function WebSegment({ label, selected, disabled, color, onPress }) {
|
|
97
|
+
const { getFocusRingStyle } = useTheme();
|
|
98
|
+
const focus = useFocusVisible();
|
|
99
|
+
return (_jsx(Pressable, { accessibilityRole: "button", accessibilityState: { selected, disabled }, disabled: disabled, onPress: disabled ? undefined : onPress, onFocus: focus.onFocus, onBlur: focus.onBlur, style: [
|
|
100
|
+
{
|
|
101
|
+
flex: 1,
|
|
102
|
+
alignItems: "center",
|
|
103
|
+
justifyContent: "center",
|
|
104
|
+
paddingHorizontal: spacing.sm,
|
|
105
|
+
borderRadius: spacing.radiusSm,
|
|
106
|
+
...(Platform.OS === "web" && {
|
|
107
|
+
cursor: (disabled ? "not-allowed" : "pointer"),
|
|
108
|
+
userSelect: "none",
|
|
109
|
+
outlineStyle: "none",
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
focus.focused && !disabled && getFocusRingStyle(),
|
|
113
|
+
], children: _jsx(StyledText, { selectable: false, numberOfLines: 1, fontWeight: "medium", style: { fontSize: 14, lineHeight: 18, color }, children: label }) }));
|
|
114
|
+
}
|
|
25
115
|
export { SegmentedControl };
|