@vvedantb/native-ui 0.2.1 → 0.3.12
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/README.md +160 -9
- package/package.json +80 -2
- package/src/AlertDialog.tsx +73 -0
- package/src/AmbientBackground.tsx +26 -11
- package/src/Avatar.tsx +141 -0
- package/src/Badge.tsx +108 -0
- package/src/BottomSheetContainer.tsx +7 -1
- package/src/Button.tsx +153 -52
- package/src/Card.tsx +111 -36
- package/src/Checkbox.tsx +78 -0
- package/src/Chip.tsx +5 -3
- package/src/ConfirmDialog.tsx +105 -0
- package/src/DateTimeField.tsx +6 -2
- package/src/Dialog.tsx +253 -0
- package/src/EmptyState.tsx +122 -7
- package/src/FloatingFAB.tsx +2 -4
- package/src/FloatingSurface.tsx +31 -9
- package/src/FloatingTabBar.tsx +39 -11
- package/src/Input.tsx +15 -4
- package/src/Label.tsx +31 -0
- package/src/LabeledSwitchRow.tsx +96 -0
- package/src/LiquidGlass.tsx +2 -10
- package/src/ListRow.tsx +7 -2
- package/src/PillTabs.tsx +28 -14
- package/src/Progress.tsx +67 -0
- package/src/SearchInput.tsx +163 -0
- package/src/Select.tsx +32 -6
- package/src/Separator.tsx +38 -0
- package/src/Sheet.tsx +107 -0
- package/src/Skeleton.tsx +25 -5
- package/src/Spinner.tsx +92 -0
- package/src/StatusDot.tsx +66 -0
- package/src/Switch.tsx +24 -7
- package/src/Tabs.tsx +190 -0
- package/src/Textarea.tsx +47 -0
- package/src/Toast.tsx +170 -0
- package/src/blur.ts +48 -0
- package/src/brand/AppIcons.tsx +106 -0
- package/src/brand/AppMark.tsx +83 -0
- package/src/brand/AppWordmark.tsx +97 -0
- package/src/brand/familyApps.ts +24 -0
- package/src/brand/index.ts +34 -0
- package/src/brand/svg.ts +263 -0
- package/src/brand/vProAppBenefits.ts +22 -0
- package/src/controlStyles.ts +360 -0
- package/src/dialogLayout.ts +20 -0
- package/src/entries/ambient.ts +3 -0
- package/src/entries/bottom-sheet.ts +2 -0
- package/src/entries/brand.ts +2 -0
- package/src/entries/button.ts +3 -0
- package/src/entries/date-time.ts +3 -0
- package/src/entries/floating.ts +24 -0
- package/src/entries/layout.ts +7 -0
- package/src/entries/marketing.ts +10 -0
- package/src/entries/primitives.ts +99 -0
- package/src/entries/search-input.ts +4 -0
- package/src/entries/tab-bar.ts +10 -0
- package/src/entries/theme.ts +15 -0
- package/src/entries/toast.ts +6 -0
- package/src/examples/ParityGallery.tsx +496 -0
- package/src/floating.ts +5 -1
- package/src/glassBlur.tsx +95 -0
- package/src/gradient.ts +21 -0
- package/src/haptics.ts +22 -2
- package/src/index.ts +99 -2
- package/src/initials.ts +10 -0
- package/src/marketing/PricingModal.tsx +410 -0
- package/src/marketing/UpgradeToProCard.tsx +103 -0
- package/src/marketing/pricing.ts +76 -0
- package/src/palette.ts +43 -0
- package/src/primitives/input.tsx +9 -2
- package/src/primitives/native-only-animated-view.tsx +10 -3
- package/src/primitives/overlay.tsx +179 -0
- package/src/primitives/select.tsx +25 -12
- package/src/primitives/switch.tsx +12 -13
- package/src/primitives/use-controllable-state.ts +24 -0
- package/src/settings/SettingsField.tsx +11 -2
- package/src/settings/SettingsSection.tsx +20 -6
- package/src/settings/SettingsToggleRow.tsx +7 -2
- package/src/tabBarRoutes.ts +52 -0
- package/src/toastChrome.ts +103 -0
- package/src/toastStore.ts +86 -0
- package/src/tokens.ts +19 -14
- package/src/typography.ts +24 -0
- package/src/useReduceMotion.ts +24 -0
- package/src/useScheme.ts +6 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Platform, View, type ViewProps } from "react-native";
|
|
2
2
|
import Animated, { type AnimatedProps } from "react-native-reanimated";
|
|
3
|
+
import { useReduceMotion } from "../useReduceMotion";
|
|
3
4
|
|
|
4
5
|
type NativeOnlyAnimatedViewProps = ViewProps & {
|
|
5
6
|
entering?: AnimatedProps<ViewProps>["entering"];
|
|
@@ -7,6 +8,11 @@ type NativeOnlyAnimatedViewProps = ViewProps & {
|
|
|
7
8
|
layout?: AnimatedProps<ViewProps>["layout"];
|
|
8
9
|
};
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* `Animated.View` with layout animations on iOS and Android, a plain `View`
|
|
13
|
+
* on web. With Reduce Motion on, it drops the animations: sheets, toasts and
|
|
14
|
+
* the `Select` popover appear and leave in place.
|
|
15
|
+
*/
|
|
10
16
|
function NativeOnlyAnimatedView({
|
|
11
17
|
children,
|
|
12
18
|
entering,
|
|
@@ -14,15 +20,16 @@ function NativeOnlyAnimatedView({
|
|
|
14
20
|
layout,
|
|
15
21
|
...props
|
|
16
22
|
}: NativeOnlyAnimatedViewProps) {
|
|
23
|
+
const reduceMotion = useReduceMotion();
|
|
17
24
|
if (Platform.OS === "web") {
|
|
18
25
|
return <View {...props}>{children}</View>;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
return (
|
|
22
29
|
<Animated.View
|
|
23
|
-
entering={entering}
|
|
24
|
-
exiting={exiting}
|
|
25
|
-
layout={layout}
|
|
30
|
+
entering={reduceMotion ? undefined : entering}
|
|
31
|
+
exiting={reduceMotion ? undefined : exiting}
|
|
32
|
+
layout={reduceMotion ? undefined : layout}
|
|
26
33
|
{...props}
|
|
27
34
|
>
|
|
28
35
|
{children}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import * as Slot from "@rn-primitives/slot";
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
type ComponentPropsWithoutRef,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react";
|
|
8
|
+
import {
|
|
9
|
+
KeyboardAvoidingView,
|
|
10
|
+
Modal,
|
|
11
|
+
Pressable,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
View,
|
|
14
|
+
type GestureResponderEvent,
|
|
15
|
+
} from "react-native";
|
|
16
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
17
|
+
import { palette } from "../palette";
|
|
18
|
+
import { useReduceMotion } from "../useReduceMotion";
|
|
19
|
+
import { useScheme } from "../useScheme";
|
|
20
|
+
import { useControllableState } from "./use-controllable-state";
|
|
21
|
+
|
|
22
|
+
type OverlayContextValue = {
|
|
23
|
+
open: boolean;
|
|
24
|
+
setOpen: (open: boolean) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const OverlayContext = createContext<OverlayContextValue | null>(null);
|
|
28
|
+
|
|
29
|
+
export function useOverlay(part: string): OverlayContextValue {
|
|
30
|
+
const context = useContext(OverlayContext);
|
|
31
|
+
if (!context) {
|
|
32
|
+
throw new Error(`${part} must be inside its Dialog, AlertDialog or Sheet root.`);
|
|
33
|
+
}
|
|
34
|
+
return context;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type OverlayRootProps = {
|
|
38
|
+
/** Open state when the parent controls it. */
|
|
39
|
+
open?: boolean;
|
|
40
|
+
/** Initial open state when `open` is not set. Default `false`. */
|
|
41
|
+
defaultOpen?: boolean;
|
|
42
|
+
/** Called with the next open state (trigger, close, backdrop, Android back). */
|
|
43
|
+
onOpenChange?: (open: boolean) => void;
|
|
44
|
+
children?: ReactNode;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Root shared by `Dialog`, `AlertDialog` and `Sheet`: holds the open state, renders no view. */
|
|
48
|
+
export function OverlayRoot({
|
|
49
|
+
open,
|
|
50
|
+
defaultOpen = false,
|
|
51
|
+
onOpenChange,
|
|
52
|
+
children,
|
|
53
|
+
}: OverlayRootProps) {
|
|
54
|
+
const [value, setValue] = useControllableState({
|
|
55
|
+
value: open,
|
|
56
|
+
defaultValue: defaultOpen,
|
|
57
|
+
onChange: onOpenChange,
|
|
58
|
+
});
|
|
59
|
+
return (
|
|
60
|
+
<OverlayContext.Provider value={{ open: value, setOpen: setValue }}>
|
|
61
|
+
{children}
|
|
62
|
+
</OverlayContext.Provider>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type OverlayPressableProps = ComponentPropsWithoutRef<typeof Pressable> & {
|
|
67
|
+
/** Merge the press handler into the single child (for example a `Button`) instead of wrapping it. */
|
|
68
|
+
asChild?: boolean;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function OverlayPressable({
|
|
72
|
+
asChild,
|
|
73
|
+
next,
|
|
74
|
+
part,
|
|
75
|
+
onPress,
|
|
76
|
+
...props
|
|
77
|
+
}: OverlayPressableProps & { next: boolean; part: string }) {
|
|
78
|
+
const { setOpen } = useOverlay(part);
|
|
79
|
+
const Component = asChild ? Slot.Pressable : Pressable;
|
|
80
|
+
return (
|
|
81
|
+
<Component
|
|
82
|
+
accessibilityRole="button"
|
|
83
|
+
{...props}
|
|
84
|
+
onPress={(event: GestureResponderEvent) => {
|
|
85
|
+
onPress?.(event);
|
|
86
|
+
setOpen(next);
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function OverlayTrigger(props: OverlayPressableProps & { part: string }) {
|
|
93
|
+
return <OverlayPressable {...props} next />;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function OverlayClose(props: OverlayPressableProps & { part: string }) {
|
|
97
|
+
return <OverlayPressable {...props} next={false} />;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* RN `Modal` with a tinted backdrop, safe-area padding and keyboard avoidance.
|
|
102
|
+
* `placement="center"` centres the child (dialogs) inside the insets on every
|
|
103
|
+
* side; `"bottom"` pins it to the bottom edge (sheets), which pad their own
|
|
104
|
+
* bottom inset. The backdrop closes the overlay only when `dismissible`;
|
|
105
|
+
* Android back and the VoiceOver escape gesture always close it, as Escape
|
|
106
|
+
* does on the web. With Reduce Motion on, the fade is off.
|
|
107
|
+
*/
|
|
108
|
+
export function OverlayModal({
|
|
109
|
+
part,
|
|
110
|
+
placement,
|
|
111
|
+
dismissible,
|
|
112
|
+
children,
|
|
113
|
+
}: {
|
|
114
|
+
part: string;
|
|
115
|
+
placement: "center" | "bottom";
|
|
116
|
+
dismissible: boolean;
|
|
117
|
+
children: ReactNode;
|
|
118
|
+
}) {
|
|
119
|
+
const { open, setOpen } = useOverlay(part);
|
|
120
|
+
const scheme = useScheme();
|
|
121
|
+
const insets = useSafeAreaInsets();
|
|
122
|
+
const reduceMotion = useReduceMotion();
|
|
123
|
+
const close = () => setOpen(false);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Modal
|
|
127
|
+
visible={open}
|
|
128
|
+
transparent
|
|
129
|
+
animationType={reduceMotion ? "none" : "fade"}
|
|
130
|
+
statusBarTranslucent
|
|
131
|
+
onRequestClose={close}
|
|
132
|
+
>
|
|
133
|
+
{/* Android too: a modal window with `statusBarTranslucent` may not
|
|
134
|
+
resize for the keyboard, which then covers the fields. The padding
|
|
135
|
+
is the measured overlap, so it drops to 0 where the window does
|
|
136
|
+
resize. */}
|
|
137
|
+
<KeyboardAvoidingView behavior="padding" style={styles.fill}>
|
|
138
|
+
<Pressable
|
|
139
|
+
testID={`${part}-backdrop`}
|
|
140
|
+
accessible={dismissible}
|
|
141
|
+
accessibilityRole="button"
|
|
142
|
+
accessibilityLabel="Close"
|
|
143
|
+
disabled={!dismissible}
|
|
144
|
+
onPress={close}
|
|
145
|
+
style={[StyleSheet.absoluteFill, { backgroundColor: palette.backdrop[scheme] }]}
|
|
146
|
+
/>
|
|
147
|
+
<View
|
|
148
|
+
pointerEvents="box-none"
|
|
149
|
+
onAccessibilityEscape={close}
|
|
150
|
+
style={[
|
|
151
|
+
styles.fill,
|
|
152
|
+
placement === "bottom"
|
|
153
|
+
? {
|
|
154
|
+
justifyContent: "flex-end",
|
|
155
|
+
paddingTop: insets.top + 16,
|
|
156
|
+
paddingLeft: insets.left,
|
|
157
|
+
paddingRight: insets.right,
|
|
158
|
+
}
|
|
159
|
+
: {
|
|
160
|
+
justifyContent: "center",
|
|
161
|
+
paddingTop: insets.top + 16,
|
|
162
|
+
paddingBottom: insets.bottom + 16,
|
|
163
|
+
paddingLeft: insets.left + 16,
|
|
164
|
+
paddingRight: insets.right + 16,
|
|
165
|
+
},
|
|
166
|
+
]}
|
|
167
|
+
>
|
|
168
|
+
{children}
|
|
169
|
+
</View>
|
|
170
|
+
</KeyboardAvoidingView>
|
|
171
|
+
</Modal>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const styles = StyleSheet.create({
|
|
176
|
+
fill: {
|
|
177
|
+
flex: 1,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
@@ -6,13 +6,16 @@ import {
|
|
|
6
6
|
ChevronUpIcon,
|
|
7
7
|
} from "lucide-react-native";
|
|
8
8
|
import * as React from "react";
|
|
9
|
-
import { Platform, ScrollView, StyleSheet, View } from "react-native";
|
|
9
|
+
import { Platform, ScrollView, StyleSheet, Text, View } from "react-native";
|
|
10
10
|
import { FadeIn, FadeOut } from "react-native-reanimated";
|
|
11
11
|
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
12
12
|
import { NativeOnlyAnimatedView } from "./native-only-animated-view";
|
|
13
13
|
import { TextClassContext } from "./text";
|
|
14
14
|
import { LiquidGlass } from "../LiquidGlass";
|
|
15
15
|
import { cn } from "../cn";
|
|
16
|
+
import { textStyle, typeStyle } from "../controlStyles";
|
|
17
|
+
import { tokens } from "../tokens";
|
|
18
|
+
import { useScheme } from "../useScheme";
|
|
16
19
|
|
|
17
20
|
type Option = SelectPrimitive.Option;
|
|
18
21
|
|
|
@@ -30,14 +33,18 @@ type SelectValueProps = React.ComponentPropsWithoutRef<
|
|
|
30
33
|
const SelectValue = React.forwardRef<
|
|
31
34
|
React.ElementRef<typeof SelectPrimitive.Value>,
|
|
32
35
|
SelectValueProps
|
|
33
|
-
>(({ className, ...props }, ref) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
>(({ className, style, ...props }, ref) => {
|
|
37
|
+
const classes = cn("line-clamp-1 text-sm text-foreground", className);
|
|
38
|
+
if (Platform.OS === "web") {
|
|
39
|
+
// Radix `Select.Value` drops `className` and `style`, so the placeholder
|
|
40
|
+
// took the page colour. Its span inherits them from this Text instead.
|
|
41
|
+
return (
|
|
42
|
+
<Text className={classes} style={style}>
|
|
43
|
+
<SelectPrimitive.Value ref={ref} {...props} />
|
|
44
|
+
</Text>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return <SelectPrimitive.Value ref={ref} className={classes} style={style} {...props} />;
|
|
41
48
|
});
|
|
42
49
|
|
|
43
50
|
type SelectTriggerProps = React.ComponentPropsWithoutRef<
|
|
@@ -51,6 +58,7 @@ const SelectTrigger = React.forwardRef<
|
|
|
51
58
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
52
59
|
SelectTriggerProps
|
|
53
60
|
>(({ className, children, size = "default", disabled, ...props }, ref) => {
|
|
61
|
+
const scheme = useScheme();
|
|
54
62
|
return (
|
|
55
63
|
<SelectPrimitive.Trigger
|
|
56
64
|
ref={ref}
|
|
@@ -64,7 +72,7 @@ const SelectTrigger = React.forwardRef<
|
|
|
64
72
|
{...props}
|
|
65
73
|
>
|
|
66
74
|
<>{children}</>
|
|
67
|
-
<ChevronDown size={16} color=
|
|
75
|
+
<ChevronDown size={16} color={tokens.color.muted[scheme]} />
|
|
68
76
|
</SelectPrimitive.Trigger>
|
|
69
77
|
);
|
|
70
78
|
});
|
|
@@ -155,6 +163,7 @@ const SelectItem = React.forwardRef<
|
|
|
155
163
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
156
164
|
SelectItemProps
|
|
157
165
|
>(({ className, disabled, ...props }, ref) => {
|
|
166
|
+
const scheme = useScheme();
|
|
158
167
|
return (
|
|
159
168
|
<SelectPrimitive.Item
|
|
160
169
|
ref={ref}
|
|
@@ -168,10 +177,14 @@ const SelectItem = React.forwardRef<
|
|
|
168
177
|
>
|
|
169
178
|
<View className="absolute right-2 size-3.5 items-center justify-center">
|
|
170
179
|
<SelectPrimitive.ItemIndicator>
|
|
171
|
-
<Check size={16} color=
|
|
180
|
+
<Check size={16} color={tokens.color.muted[scheme]} />
|
|
172
181
|
</SelectPrimitive.ItemIndicator>
|
|
173
182
|
</View>
|
|
174
|
-
<SelectPrimitive.ItemText
|
|
183
|
+
<SelectPrimitive.ItemText
|
|
184
|
+
className="select-none text-sm font-instrument-sans-medium"
|
|
185
|
+
// One object: on web the item text is a Radix slot, which drops arrays.
|
|
186
|
+
style={{ ...textStyle("foreground", scheme), ...typeStyle("medium") }}
|
|
187
|
+
/>
|
|
175
188
|
</SelectPrimitive.Item>
|
|
176
189
|
);
|
|
177
190
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as SwitchPrimitives from "@rn-primitives/switch";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import { Platform } from "react-native";
|
|
3
|
+
import { Platform, type ViewStyle } from "react-native";
|
|
4
4
|
import { cn } from "../cn";
|
|
5
5
|
|
|
6
6
|
type SwitchProps = React.ComponentPropsWithoutRef<
|
|
@@ -8,8 +8,12 @@ type SwitchProps = React.ComponentPropsWithoutRef<
|
|
|
8
8
|
> & {
|
|
9
9
|
className?: string;
|
|
10
10
|
thumbClassName?: string;
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Track and thumb colours. Flat objects: on web each part is a Radix slot,
|
|
13
|
+
* which spreads `style`, so these are the only styles applied there.
|
|
14
|
+
*/
|
|
15
|
+
trackStyle?: ViewStyle;
|
|
16
|
+
thumbStyle?: ViewStyle;
|
|
13
17
|
};
|
|
14
18
|
|
|
15
19
|
const Switch = React.forwardRef<
|
|
@@ -20,8 +24,8 @@ const Switch = React.forwardRef<
|
|
|
20
24
|
{
|
|
21
25
|
className,
|
|
22
26
|
thumbClassName,
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
trackStyle,
|
|
28
|
+
thumbStyle,
|
|
25
29
|
checked,
|
|
26
30
|
style,
|
|
27
31
|
disabled,
|
|
@@ -30,16 +34,10 @@ const Switch = React.forwardRef<
|
|
|
30
34
|
ref,
|
|
31
35
|
) => {
|
|
32
36
|
const isChecked = Boolean(checked);
|
|
33
|
-
const trackColor =
|
|
34
|
-
Platform.OS !== "web" && (onColor || offColor)
|
|
35
|
-
? { backgroundColor: isChecked ? onColor : offColor }
|
|
36
|
-
: null;
|
|
37
37
|
const resolvedStyle =
|
|
38
38
|
Platform.OS === "web" || typeof style === "function"
|
|
39
|
-
?
|
|
40
|
-
:
|
|
41
|
-
? [trackColor, style]
|
|
42
|
-
: style;
|
|
39
|
+
? trackStyle
|
|
40
|
+
: [trackStyle, style];
|
|
43
41
|
|
|
44
42
|
return (
|
|
45
43
|
<SwitchPrimitives.Root
|
|
@@ -59,6 +57,7 @@ const Switch = React.forwardRef<
|
|
|
59
57
|
{...props}
|
|
60
58
|
>
|
|
61
59
|
<SwitchPrimitives.Thumb
|
|
60
|
+
style={thumbStyle}
|
|
62
61
|
className={cn(
|
|
63
62
|
"h-4 w-4 rounded-full bg-background transition-transform",
|
|
64
63
|
isChecked
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/** `value` when the parent controls it, else local state seeded by `defaultValue`. `onChange` fires either way. */
|
|
4
|
+
export function useControllableState<T>({
|
|
5
|
+
value,
|
|
6
|
+
defaultValue,
|
|
7
|
+
onChange,
|
|
8
|
+
}: {
|
|
9
|
+
value: T | undefined;
|
|
10
|
+
defaultValue: T;
|
|
11
|
+
onChange?: (next: T) => void;
|
|
12
|
+
}) {
|
|
13
|
+
const [local, setLocal] = useState(defaultValue);
|
|
14
|
+
const controlled = value !== undefined;
|
|
15
|
+
const current = controlled ? value : local;
|
|
16
|
+
const setValue = useCallback(
|
|
17
|
+
(next: T) => {
|
|
18
|
+
if (!controlled) setLocal(next);
|
|
19
|
+
onChange?.(next);
|
|
20
|
+
},
|
|
21
|
+
[controlled, onChange],
|
|
22
|
+
);
|
|
23
|
+
return [current, setValue] as const;
|
|
24
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { View, Text } from "react-native";
|
|
2
|
+
import { textStyle, typeStyle } from "../controlStyles";
|
|
3
|
+
import { useScheme } from "../useScheme";
|
|
2
4
|
|
|
3
5
|
export function SettingsField({
|
|
4
6
|
label,
|
|
@@ -9,10 +11,14 @@ export function SettingsField({
|
|
|
9
11
|
description?: React.ReactNode;
|
|
10
12
|
children: React.ReactNode;
|
|
11
13
|
}) {
|
|
14
|
+
const scheme = useScheme();
|
|
12
15
|
return (
|
|
13
16
|
<View>
|
|
14
17
|
{typeof label === "string" ? (
|
|
15
|
-
<Text
|
|
18
|
+
<Text
|
|
19
|
+
className="mb-2 text-base font-instrument-sans-semibold"
|
|
20
|
+
style={[textStyle("foreground", scheme), typeStyle("semibold")]}
|
|
21
|
+
>
|
|
16
22
|
{label}
|
|
17
23
|
</Text>
|
|
18
24
|
) : (
|
|
@@ -21,7 +27,10 @@ export function SettingsField({
|
|
|
21
27
|
{children}
|
|
22
28
|
{description != null ? (
|
|
23
29
|
typeof description === "string" ? (
|
|
24
|
-
<Text
|
|
30
|
+
<Text
|
|
31
|
+
className="mt-1.5 text-sm font-instrument-sans"
|
|
32
|
+
style={textStyle("muted", scheme)}
|
|
33
|
+
>
|
|
25
34
|
{description}
|
|
26
35
|
</Text>
|
|
27
36
|
) : (
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { View, Text } from "react-native";
|
|
2
2
|
import { cn } from "../cn";
|
|
3
|
-
import {
|
|
3
|
+
import { cardStyle, textStyle, typeStyle } from "../controlStyles";
|
|
4
|
+
import { tokens } from "../tokens";
|
|
5
|
+
import { useScheme } from "../useScheme";
|
|
4
6
|
|
|
5
7
|
type SettingsSectionBodyVariant = "form" | "list" | "compact";
|
|
6
8
|
|
|
@@ -21,6 +23,7 @@ export function SettingsSection({
|
|
|
21
23
|
bodyVariant?: SettingsSectionBodyVariant;
|
|
22
24
|
className?: string;
|
|
23
25
|
}) {
|
|
26
|
+
const scheme = useScheme();
|
|
24
27
|
const hasBody = children != null;
|
|
25
28
|
const hasCard = hasBody || footer != null;
|
|
26
29
|
|
|
@@ -29,7 +32,11 @@ export function SettingsSection({
|
|
|
29
32
|
<View className="flex-row items-start justify-between gap-4 px-4">
|
|
30
33
|
<View className="min-w-0 flex-1">
|
|
31
34
|
{typeof title === "string" ? (
|
|
32
|
-
<Text
|
|
35
|
+
<Text
|
|
36
|
+
accessibilityRole="header"
|
|
37
|
+
className="text-lg font-instrument-sans-semibold"
|
|
38
|
+
style={[textStyle("foreground", scheme), typeStyle("semibold")]}
|
|
39
|
+
>
|
|
33
40
|
{title}
|
|
34
41
|
</Text>
|
|
35
42
|
) : (
|
|
@@ -37,7 +44,10 @@ export function SettingsSection({
|
|
|
37
44
|
)}
|
|
38
45
|
{description != null ? (
|
|
39
46
|
typeof description === "string" ? (
|
|
40
|
-
<Text
|
|
47
|
+
<Text
|
|
48
|
+
className="mt-0.5 text-base font-instrument-sans"
|
|
49
|
+
style={textStyle("muted", scheme)}
|
|
50
|
+
>
|
|
41
51
|
{description}
|
|
42
52
|
</Text>
|
|
43
53
|
) : (
|
|
@@ -49,7 +59,8 @@ export function SettingsSection({
|
|
|
49
59
|
</View>
|
|
50
60
|
|
|
51
61
|
{hasCard ? (
|
|
52
|
-
|
|
62
|
+
// The web section card (`rounded-lg bg-surface-card`): flat, no shadow.
|
|
63
|
+
<View className="overflow-hidden" style={cardStyle(scheme)}>
|
|
53
64
|
{hasBody ? (
|
|
54
65
|
<View
|
|
55
66
|
className={
|
|
@@ -64,11 +75,14 @@ export function SettingsSection({
|
|
|
64
75
|
</View>
|
|
65
76
|
) : null}
|
|
66
77
|
{footer != null ? (
|
|
67
|
-
<View
|
|
78
|
+
<View
|
|
79
|
+
className="flex-row items-center justify-end gap-2 px-4 py-3"
|
|
80
|
+
style={{ backgroundColor: tokens.color.surfaceSecondary[scheme] }}
|
|
81
|
+
>
|
|
68
82
|
{footer}
|
|
69
83
|
</View>
|
|
70
84
|
) : null}
|
|
71
|
-
</
|
|
85
|
+
</View>
|
|
72
86
|
) : null}
|
|
73
87
|
</View>
|
|
74
88
|
);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { View, Text } from "react-native";
|
|
2
2
|
import { cn } from "../cn";
|
|
3
|
+
import { textStyle, typeStyle } from "../controlStyles";
|
|
3
4
|
import { useLargerText } from "../theme";
|
|
5
|
+
import { useScheme } from "../useScheme";
|
|
4
6
|
|
|
5
7
|
export function SettingsToggleRow({
|
|
6
8
|
title,
|
|
@@ -14,6 +16,7 @@ export function SettingsToggleRow({
|
|
|
14
16
|
className?: string;
|
|
15
17
|
}) {
|
|
16
18
|
const largerText = useLargerText();
|
|
19
|
+
const scheme = useScheme();
|
|
17
20
|
return (
|
|
18
21
|
<View
|
|
19
22
|
className={cn(
|
|
@@ -26,9 +29,10 @@ export function SettingsToggleRow({
|
|
|
26
29
|
{typeof title === "string" ? (
|
|
27
30
|
<Text
|
|
28
31
|
className={cn(
|
|
29
|
-
"font-instrument-sans-semibold
|
|
32
|
+
"font-instrument-sans-semibold",
|
|
30
33
|
largerText ? "text-xl" : "text-lg",
|
|
31
34
|
)}
|
|
35
|
+
style={[textStyle("foreground", scheme), typeStyle("semibold")]}
|
|
32
36
|
>
|
|
33
37
|
{title}
|
|
34
38
|
</Text>
|
|
@@ -39,9 +43,10 @@ export function SettingsToggleRow({
|
|
|
39
43
|
typeof description === "string" ? (
|
|
40
44
|
<Text
|
|
41
45
|
className={cn(
|
|
42
|
-
"mt-1 font-instrument-sans
|
|
46
|
+
"mt-1 font-instrument-sans",
|
|
43
47
|
largerText ? "text-lg" : "text-base",
|
|
44
48
|
)}
|
|
49
|
+
style={textStyle("muted", scheme)}
|
|
45
50
|
>
|
|
46
51
|
{description}
|
|
47
52
|
</Text>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Which routes `FloatingTabBar` draws. No React Native runtime import, so the
|
|
2
|
+
// tests can load this file.
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { ColorValue, StyleProp, ViewStyle } from "react-native";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The fields `FloatingTabBar` reads from a route's options (React
|
|
8
|
+
* Navigation's `BottomTabNavigationOptions`, and Expo Router's `href`).
|
|
9
|
+
*/
|
|
10
|
+
export type FloatingTabOptions = {
|
|
11
|
+
title?: string;
|
|
12
|
+
tabBarLabel?: string | ((props: never) => ReactNode);
|
|
13
|
+
tabBarIcon?: (props: {
|
|
14
|
+
focused: boolean;
|
|
15
|
+
color: ColorValue;
|
|
16
|
+
size: number;
|
|
17
|
+
}) => ReactNode;
|
|
18
|
+
tabBarActiveTintColor?: ColorValue;
|
|
19
|
+
tabBarInactiveTintColor?: ColorValue;
|
|
20
|
+
tabBarButtonTestID?: string;
|
|
21
|
+
tabBarAccessibilityLabel?: string;
|
|
22
|
+
/** Expo Router's `href`. `null` hides the tab. */
|
|
23
|
+
href?: string | null;
|
|
24
|
+
/** Only `display: "none"` is read: it hides the tab, as in React Navigation's own bar. */
|
|
25
|
+
tabBarItemStyle?: StyleProp<ViewStyle>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** The `display` a style resolves to: in an array the last one set wins, as in `StyleSheet.flatten`. */
|
|
29
|
+
function displayOf(style: unknown): unknown {
|
|
30
|
+
if (Array.isArray(style)) {
|
|
31
|
+
let display: unknown;
|
|
32
|
+
for (const item of style) {
|
|
33
|
+
const value = displayOf(item);
|
|
34
|
+
if (value !== undefined) display = value;
|
|
35
|
+
}
|
|
36
|
+
return display;
|
|
37
|
+
}
|
|
38
|
+
return style && typeof style === "object" ? (style as { display?: unknown }).display : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether `FloatingTabBar` draws a tab for a route with these options. A route
|
|
43
|
+
* without a descriptor has none. Apps hide a tab with Expo Router's
|
|
44
|
+
* `options={{ href: null }}`. Expo Router's `Tabs` can drop `href` and pass
|
|
45
|
+
* `tabBarItemStyle: { display: "none" }` in its place, so both hide the tab.
|
|
46
|
+
* Hiding is display-only: `state.index` still counts every route.
|
|
47
|
+
*/
|
|
48
|
+
export function isTabBarRouteVisible(
|
|
49
|
+
options: FloatingTabOptions | undefined,
|
|
50
|
+
): options is FloatingTabOptions {
|
|
51
|
+
return options !== undefined && options.href !== null && displayOf(options.tabBarItemStyle) !== "none";
|
|
52
|
+
}
|