@vvedantb/native-ui 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +89 -2
  3. package/package.json +72 -3
  4. package/src/AmbientBackground.tsx +29 -0
  5. package/src/BottomSheetContainer.tsx +92 -0
  6. package/src/Button.tsx +111 -0
  7. package/src/Card.tsx +64 -0
  8. package/src/Chip.tsx +49 -0
  9. package/src/DateTimeField.tsx +78 -0
  10. package/src/EmptyState.tsx +51 -0
  11. package/src/FloatingFAB.tsx +62 -0
  12. package/src/FloatingSurface.tsx +114 -0
  13. package/src/FloatingTabBar.tsx +107 -0
  14. package/src/HeaderIconButton.tsx +29 -0
  15. package/src/Input.tsx +32 -0
  16. package/src/LiquidGlass.tsx +14 -0
  17. package/src/ListRow.tsx +85 -0
  18. package/src/PillTabs.tsx +51 -0
  19. package/src/SafeAreaContainer.tsx +26 -0
  20. package/src/ScreenContainer.tsx +36 -0
  21. package/src/Select.tsx +132 -0
  22. package/src/Skeleton.tsx +37 -0
  23. package/src/Switch.tsx +45 -0
  24. package/src/cn.ts +6 -0
  25. package/src/colors.ts +17 -0
  26. package/src/floating.ts +40 -0
  27. package/src/haptics.ts +9 -0
  28. package/src/index.ts +58 -0
  29. package/src/primitives/button.tsx +101 -0
  30. package/src/primitives/card.tsx +102 -0
  31. package/src/primitives/icon.tsx +14 -0
  32. package/src/primitives/input.tsx +37 -0
  33. package/src/primitives/native-only-animated-view.tsx +33 -0
  34. package/src/primitives/select.tsx +265 -0
  35. package/src/primitives/switch.tsx +77 -0
  36. package/src/primitives/text.tsx +95 -0
  37. package/src/settings/SettingsField.tsx +33 -0
  38. package/src/settings/SettingsSection.tsx +75 -0
  39. package/src/settings/SettingsStack.tsx +5 -0
  40. package/src/settings/SettingsToggleRow.tsx +58 -0
  41. package/src/theme.tsx +44 -0
  42. package/src/tokens.ts +43 -0
  43. package/src/useReduceTransparency.ts +24 -0
@@ -0,0 +1,62 @@
1
+ import type { ReactNode } from "react";
2
+ import { Platform, Pressable, View } from "react-native";
3
+ import { cn } from "./cn";
4
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
5
+ import { FloatingSurface } from "./FloatingSurface";
6
+ import { useAccentColor, useLargerText } from "./theme";
7
+ import {
8
+ FLOATING_INSET,
9
+ FLOATING_TAB_BAR_HEIGHT,
10
+ FLOATING_TAB_BAR_HEIGHT_LARGE,
11
+ } from "./floating";
12
+
13
+ export function FloatingFAB({
14
+ onPress,
15
+ label,
16
+ testID,
17
+ children,
18
+ }: {
19
+ onPress: () => void;
20
+ label: string;
21
+ testID?: string;
22
+ children: ReactNode;
23
+ }) {
24
+ const insets = useSafeAreaInsets();
25
+ const largerText = useLargerText();
26
+ const accentColor = useAccentColor();
27
+ const size = largerText ? 64 : 56;
28
+ const tabBar = largerText
29
+ ? FLOATING_TAB_BAR_HEIGHT_LARGE
30
+ : FLOATING_TAB_BAR_HEIGHT;
31
+
32
+ return (
33
+ <View
34
+ pointerEvents="box-none"
35
+ className={cn(
36
+ "right-4 z-10",
37
+ Platform.OS === "web" ? "fixed" : "absolute",
38
+ )}
39
+ style={{
40
+ bottom: FLOATING_INSET + Math.max(insets.bottom, 8) + tabBar + 20,
41
+ }}
42
+ >
43
+ <FloatingSurface radius={size / 2} blur={false}>
44
+ <Pressable
45
+ testID={testID}
46
+ accessibilityRole="button"
47
+ accessibilityLabel={label}
48
+ onPress={onPress}
49
+ className="items-center justify-center"
50
+ style={({ pressed }) => ({
51
+ width: size,
52
+ height: size,
53
+ backgroundColor: accentColor,
54
+ opacity: pressed ? 0.7 : 1,
55
+ })}
56
+ >
57
+ {children}
58
+ </Pressable>
59
+ </FloatingSurface>
60
+ </View>
61
+ );
62
+ }
@@ -0,0 +1,114 @@
1
+ import { Platform, StyleSheet, View, type ViewProps } from "react-native";
2
+ import { BlurView } from "expo-blur";
3
+ import { useColorScheme } from "nativewind";
4
+ import { cn } from "./cn";
5
+ import {
6
+ floatingFill,
7
+ floatingHairline,
8
+ floatingShadow,
9
+ floatingSolidFill,
10
+ FLOATING_RADIUS,
11
+ } from "./floating";
12
+ import { useReduceTransparency } from "./useReduceTransparency";
13
+
14
+ type FloatingSurfaceProps = ViewProps & {
15
+ className?: string;
16
+ radius?: number;
17
+ blur?: boolean;
18
+ elevated?: boolean;
19
+ };
20
+
21
+ export function FloatingSurface({
22
+ className,
23
+ style,
24
+ children,
25
+ radius = FLOATING_RADIUS,
26
+ blur = true,
27
+ elevated = true,
28
+ ...props
29
+ }: FloatingSurfaceProps) {
30
+ const { colorScheme } = useColorScheme();
31
+ const reduceTransparency = useReduceTransparency();
32
+ const isDark = colorScheme === "dark";
33
+ const opaqueFill = isDark ? floatingSolidFill.dark : floatingSolidFill.light;
34
+ const translucentFill = isDark ? floatingFill.dark : floatingFill.light;
35
+ const border = isDark ? floatingHairline.dark : floatingHairline.light;
36
+ const useBlur = blur && !reduceTransparency && Platform.OS === "ios";
37
+ const fill =
38
+ Platform.OS === "android" || reduceTransparency || !blur
39
+ ? opaqueFill
40
+ : translucentFill;
41
+
42
+ return (
43
+ <View
44
+ className={cn(className)}
45
+ {...props}
46
+ style={[
47
+ { borderRadius: radius },
48
+ elevated ? floatingShadow : null,
49
+ style,
50
+ { backgroundColor: fill },
51
+ ]}
52
+ >
53
+ <View
54
+ style={[
55
+ styles.clip,
56
+ {
57
+ borderRadius: radius,
58
+ borderWidth: StyleSheet.hairlineWidth,
59
+ borderColor: border,
60
+ backgroundColor: useBlur ? "transparent" : fill,
61
+ },
62
+ Platform.OS === "ios" ? styles.continuous : null,
63
+ style != null ? styles.fill : null,
64
+ ]}
65
+ >
66
+ {useBlur ? (
67
+ <BlurView
68
+ pointerEvents="none"
69
+ tint={isDark ? "systemMaterialDark" : "systemMaterialLight"}
70
+ intensity={70}
71
+ style={StyleSheet.absoluteFill}
72
+ />
73
+ ) : null}
74
+ {Platform.OS === "web" && blur && !reduceTransparency ? (
75
+ <View
76
+ pointerEvents="none"
77
+ className="absolute inset-0 backdrop-blur-xl"
78
+ style={{ backgroundColor: translucentFill }}
79
+ />
80
+ ) : null}
81
+ {children}
82
+ </View>
83
+ </View>
84
+ );
85
+ }
86
+
87
+ export function GlassSheetBackground({
88
+ style,
89
+ pointerEvents,
90
+ }: {
91
+ style?: ViewProps["style"];
92
+ pointerEvents?: ViewProps["pointerEvents"];
93
+ }) {
94
+ return (
95
+ <FloatingSurface
96
+ style={style}
97
+ pointerEvents={pointerEvents}
98
+ radius={20}
99
+ className="rounded-t-[20px]"
100
+ />
101
+ );
102
+ }
103
+
104
+ const styles = StyleSheet.create({
105
+ clip: {
106
+ overflow: "hidden",
107
+ },
108
+ fill: {
109
+ flex: 1,
110
+ },
111
+ continuous: {
112
+ borderCurve: "continuous",
113
+ },
114
+ });
@@ -0,0 +1,107 @@
1
+ import type { ComponentProps } from "react";
2
+ import { Pressable, Text, View } from "react-native";
3
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
4
+ import { Tabs } from "expo-router";
5
+ import { selectionHaptic } from "./haptics";
6
+ import { FloatingSurface } from "./FloatingSurface";
7
+ import { useLargerText } from "./theme";
8
+ import {
9
+ FLOATING_INSET,
10
+ FLOATING_PILL_RADIUS,
11
+ FLOATING_TAB_BAR_HEIGHT,
12
+ FLOATING_TAB_BAR_HEIGHT_LARGE,
13
+ } from "./floating";
14
+
15
+ type TabBarProps = Parameters<
16
+ NonNullable<ComponentProps<typeof Tabs>["tabBar"]>
17
+ >[0];
18
+
19
+ export function FloatingTabBar({
20
+ state,
21
+ descriptors,
22
+ navigation,
23
+ }: TabBarProps) {
24
+ const insets = useSafeAreaInsets();
25
+ const largerText = useLargerText();
26
+ const height = largerText
27
+ ? FLOATING_TAB_BAR_HEIGHT_LARGE
28
+ : FLOATING_TAB_BAR_HEIGHT;
29
+
30
+ return (
31
+ <View
32
+ pointerEvents="box-none"
33
+ style={{
34
+ position: "absolute",
35
+ left: FLOATING_INSET,
36
+ right: FLOATING_INSET,
37
+ bottom: FLOATING_INSET + Math.max(insets.bottom, 8),
38
+ }}
39
+ >
40
+ <FloatingSurface radius={FLOATING_PILL_RADIUS}>
41
+ <View
42
+ className="flex-row items-center justify-around px-2"
43
+ style={{ height }}
44
+ accessibilityRole="tablist"
45
+ >
46
+ {state.routes.map((route, index) => {
47
+ const focused = state.index === index;
48
+ const options = descriptors[route.key]?.options;
49
+ if (!options) {
50
+ return null;
51
+ }
52
+ const label =
53
+ typeof options.tabBarLabel === "string"
54
+ ? options.tabBarLabel
55
+ : typeof options.title === "string"
56
+ ? options.title
57
+ : route.name;
58
+ const color = focused
59
+ ? options.tabBarActiveTintColor
60
+ : options.tabBarInactiveTintColor;
61
+ const onPress = () => {
62
+ const event = navigation.emit({
63
+ type: "tabPress",
64
+ target: route.key,
65
+ canPreventDefault: true,
66
+ });
67
+ if (!focused && !event.defaultPrevented) {
68
+ selectionHaptic();
69
+ navigation.navigate(route.name, route.params);
70
+ }
71
+ };
72
+
73
+ return (
74
+ <Pressable
75
+ key={route.key}
76
+ testID={options.tabBarButtonTestID}
77
+ accessibilityRole="tab"
78
+ accessibilityState={{ selected: focused }}
79
+ accessibilityLabel={options.tabBarAccessibilityLabel ?? label}
80
+ onPress={onPress}
81
+ className="min-h-12 min-w-12 flex-1 items-center justify-center gap-1"
82
+ style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })}
83
+ >
84
+ {options.tabBarIcon
85
+ ? options.tabBarIcon({
86
+ focused,
87
+ color: color ?? "#737373",
88
+ size: largerText ? 28 : 24,
89
+ })
90
+ : null}
91
+ <Text
92
+ numberOfLines={1}
93
+ className={`font-instrument-sans-semibold ${
94
+ largerText ? "text-base" : "text-sm"
95
+ }`}
96
+ style={{ color }}
97
+ >
98
+ {label}
99
+ </Text>
100
+ </Pressable>
101
+ );
102
+ })}
103
+ </View>
104
+ </FloatingSurface>
105
+ </View>
106
+ );
107
+ }
@@ -0,0 +1,29 @@
1
+ import { TouchableOpacity } from "react-native";
2
+ import { useLargerText } from "./theme";
3
+
4
+ export function HeaderIconButton({
5
+ onPress,
6
+ label,
7
+ testID,
8
+ children,
9
+ }: {
10
+ onPress: () => void;
11
+ label: string;
12
+ testID?: string;
13
+ children: React.ReactNode;
14
+ }) {
15
+ const largerText = useLargerText();
16
+ return (
17
+ <TouchableOpacity
18
+ testID={testID}
19
+ className={`${largerText ? "h-14 w-14" : "h-12 w-12"} items-center justify-center rounded-full border border-white/40 bg-surface dark:border-white/10`}
20
+ onPress={onPress}
21
+ accessibilityRole="button"
22
+ accessibilityLabel={label}
23
+ hitSlop={8}
24
+ activeOpacity={0.7}
25
+ >
26
+ {children}
27
+ </TouchableOpacity>
28
+ );
29
+ }
package/src/Input.tsx ADDED
@@ -0,0 +1,32 @@
1
+ import * as React from "react";
2
+ import { Input as PrimitiveInput } from "./primitives/input";
3
+ import { cn } from "./cn";
4
+ import { useLargerText } from "./theme";
5
+
6
+ type InputProps = React.ComponentPropsWithoutRef<typeof PrimitiveInput> & {
7
+ className?: string;
8
+ };
9
+
10
+ const Input = React.forwardRef<
11
+ React.ElementRef<typeof PrimitiveInput>,
12
+ InputProps
13
+ >(({ className, placeholderTextColor, ...props }, ref) => {
14
+ const largerText = useLargerText();
15
+ return (
16
+ <PrimitiveInput
17
+ ref={ref}
18
+ placeholderTextColor={placeholderTextColor ?? "#737373"}
19
+ className={cn(
20
+ "min-h-14 rounded-full border border-white/40 bg-secondary px-4 py-3 text-lg font-instrument-sans-medium text-foreground dark:border-white/10",
21
+ largerText && "min-h-16 py-4 text-xl",
22
+ className,
23
+ )}
24
+ {...props}
25
+ />
26
+ );
27
+ });
28
+
29
+ Input.displayName = "Input";
30
+
31
+ export { Input };
32
+ export type { InputProps };
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export {
4
+ FloatingSurface as LiquidGlass,
5
+ GlassSheetBackground,
6
+ } from "./FloatingSurface";
7
+
8
+ export function GlassBlurProvider({ children }: { children: ReactNode }) {
9
+ return children;
10
+ }
11
+
12
+ export function GlassBlurTarget({ children }: { children: ReactNode }) {
13
+ return children;
14
+ }
@@ -0,0 +1,85 @@
1
+ import type { ComponentType, ReactNode } from "react";
2
+ import { Pressable, Text, View } from "react-native";
3
+ import type { IconProps } from "@tabler/icons-react-native";
4
+ import { NEUTRAL_500 } from "./colors";
5
+ import { cn } from "./cn";
6
+ import { FLOATING_ROW_RADIUS } from "./floating";
7
+ import { useLargerText } from "./theme";
8
+
9
+ export function ListRow({
10
+ title,
11
+ meta,
12
+ detail,
13
+ icon: Icon,
14
+ iconColor = NEUTRAL_500,
15
+ right,
16
+ onPress,
17
+ onCanvas = false,
18
+ testID,
19
+ }: {
20
+ title: string;
21
+ meta?: string;
22
+ detail?: ReactNode;
23
+ icon?: ComponentType<IconProps>;
24
+ iconColor?: string;
25
+ right?: ReactNode;
26
+ onPress?: () => void;
27
+ onCanvas?: boolean;
28
+ testID?: string;
29
+ }) {
30
+ const largerText = useLargerText();
31
+ const content = (
32
+ <>
33
+ {Icon ? <Icon size={largerText ? 30 : 26} color={iconColor} /> : null}
34
+ <View className="min-w-0 flex-1">
35
+ <Text
36
+ numberOfLines={2}
37
+ className={cn(
38
+ "font-instrument-sans-semibold text-foreground",
39
+ largerText ? "text-xl" : "text-lg",
40
+ )}
41
+ >
42
+ {title}
43
+ </Text>
44
+ {meta ? (
45
+ <Text
46
+ className={cn(
47
+ "font-instrument-sans text-muted-foreground",
48
+ largerText ? "text-lg" : "text-base",
49
+ )}
50
+ >
51
+ {meta}
52
+ </Text>
53
+ ) : null}
54
+ {detail}
55
+ </View>
56
+ {right}
57
+ </>
58
+ );
59
+ const className = cn(
60
+ "flex-row items-center gap-3 px-4",
61
+ largerText ? "min-h-[72px] py-4" : "min-h-16 py-3",
62
+ onCanvas ? "bg-surface" : "bg-surface-secondary",
63
+ );
64
+ const rowStyle = { borderRadius: FLOATING_ROW_RADIUS };
65
+
66
+ if (!onPress) {
67
+ return (
68
+ <View testID={testID} className={className} style={rowStyle}>
69
+ {content}
70
+ </View>
71
+ );
72
+ }
73
+ return (
74
+ <Pressable
75
+ testID={testID}
76
+ accessibilityRole="button"
77
+ accessibilityLabel={title}
78
+ onPress={onPress}
79
+ className={cn(className, "active:opacity-70")}
80
+ style={rowStyle}
81
+ >
82
+ {content}
83
+ </Pressable>
84
+ );
85
+ }
@@ -0,0 +1,51 @@
1
+ import { View, Text, Pressable } from "react-native";
2
+ import { useLargerText } from "./theme";
3
+
4
+ export function PillTabs<T extends string>({
5
+ options,
6
+ value,
7
+ onChange,
8
+ }: {
9
+ options: ReadonlyArray<{ id: T; label: string; testID?: string }>;
10
+ value: T;
11
+ onChange: (id: T) => void;
12
+ }) {
13
+ const largerText = useLargerText();
14
+ return (
15
+ <View className="flex-row gap-2">
16
+ {options.map((tab) => {
17
+ const isSelected = value === tab.id;
18
+
19
+ return (
20
+ <Pressable
21
+ key={tab.id}
22
+ testID={tab.testID}
23
+ accessibilityLabel={tab.label}
24
+ onPress={() => onChange(tab.id)}
25
+ accessibilityRole="tab"
26
+ accessibilityState={{ selected: isSelected }}
27
+ className={`flex-1 justify-center rounded-full border border-white/40 px-3 dark:border-white/10 ${
28
+ largerText ? "min-h-16 py-3" : "min-h-14 py-3"
29
+ } ${
30
+ isSelected ? "bg-pink-100 dark:bg-pink-900/40" : "bg-secondary"
31
+ }`}
32
+ style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })}
33
+ >
34
+ <Text
35
+ numberOfLines={1}
36
+ className={`text-center font-instrument-sans-semibold ${
37
+ largerText ? "text-xl" : "text-lg"
38
+ } ${
39
+ isSelected
40
+ ? "text-pink-700 dark:text-pink-300"
41
+ : "text-muted-foreground"
42
+ }`}
43
+ >
44
+ {tab.label}
45
+ </Text>
46
+ </Pressable>
47
+ );
48
+ })}
49
+ </View>
50
+ );
51
+ }
@@ -0,0 +1,26 @@
1
+ import { View } from "react-native";
2
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
3
+
4
+ export const SafeAreaContainer = ({
5
+ children,
6
+ }: {
7
+ children: React.ReactNode;
8
+ }) => {
9
+ const insets = useSafeAreaInsets();
10
+
11
+ return (
12
+ <View
13
+ className="bg-transparent"
14
+ style={{
15
+ flex: 1,
16
+ justifyContent: "space-between",
17
+ paddingTop: insets.top,
18
+ paddingBottom: insets.bottom,
19
+ paddingLeft: insets.left,
20
+ paddingRight: insets.right,
21
+ }}
22
+ >
23
+ {children}
24
+ </View>
25
+ );
26
+ };
@@ -0,0 +1,36 @@
1
+ import { ScrollView, View, type ViewProps } from "react-native";
2
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
3
+ import { useLargerText } from "./theme";
4
+ import { tabBarReserve } from "./floating";
5
+
6
+ interface ScreenContainerProps extends ViewProps {
7
+ children: React.ReactNode;
8
+ reserveTabBar?: boolean;
9
+ footer?: React.ReactNode;
10
+ }
11
+
12
+ export function ScreenContainer({
13
+ children,
14
+ reserveTabBar = false,
15
+ footer,
16
+ ...props
17
+ }: ScreenContainerProps) {
18
+ const insets = useSafeAreaInsets();
19
+ const largerText = useLargerText();
20
+ const bottomPad = reserveTabBar
21
+ ? tabBarReserve(largerText, insets.bottom)
22
+ : 40;
23
+
24
+ return (
25
+ <View className="flex-1 bg-transparent" {...props}>
26
+ <ScrollView
27
+ showsVerticalScrollIndicator={false}
28
+ className="flex-1"
29
+ contentContainerStyle={{ paddingBottom: bottomPad }}
30
+ >
31
+ <View className="flex-col gap-4 p-4">{children}</View>
32
+ {footer ? <View className="pt-4">{footer}</View> : null}
33
+ </ScrollView>
34
+ </View>
35
+ );
36
+ }
package/src/Select.tsx ADDED
@@ -0,0 +1,132 @@
1
+ import * as React from "react";
2
+ import {
3
+ NativeSelectScrollView,
4
+ Select as PrimitiveSelect,
5
+ SelectContent,
6
+ SelectGroup,
7
+ SelectItem,
8
+ SelectTrigger,
9
+ SelectValue,
10
+ } from "./primitives/select";
11
+ import { cn } from "./cn";
12
+ import { useLargerText } from "./theme";
13
+
14
+ interface SelectOption<TValue = string | number | null> {
15
+ label: string;
16
+ value: TValue;
17
+ color?: string;
18
+ }
19
+
20
+ interface SelectProps<TValue = string | number | null> {
21
+ value?: TValue;
22
+ onValueChange: (value: TValue) => void;
23
+ options: Array<SelectOption<TValue>>;
24
+ placeholder?: string;
25
+ disabled?: boolean;
26
+ className?: string;
27
+ triggerClassName?: string;
28
+ contentClassName?: string;
29
+ testID?: string;
30
+ }
31
+
32
+ const NULL_SENTINEL = "__null__";
33
+
34
+ function toPrimitiveValue<TValue>(
35
+ value: TValue | undefined,
36
+ options: Array<SelectOption<TValue>>,
37
+ ) {
38
+ if (value === undefined || value === "") {
39
+ return undefined;
40
+ }
41
+
42
+ const expectedValue = value === null ? NULL_SENTINEL : String(value);
43
+ const option = options.find(
44
+ (item) => String(item.value ?? NULL_SENTINEL) === expectedValue,
45
+ );
46
+
47
+ if (!option) return undefined;
48
+
49
+ return {
50
+ label: option.label,
51
+ value: String(option.value ?? NULL_SENTINEL),
52
+ };
53
+ }
54
+
55
+ function Select<TValue = string | number | null>({
56
+ value,
57
+ onValueChange,
58
+ options,
59
+ placeholder,
60
+ disabled,
61
+ className,
62
+ triggerClassName,
63
+ contentClassName,
64
+ testID,
65
+ }: SelectProps<TValue>) {
66
+ const largerText = useLargerText();
67
+ const primitiveValue = toPrimitiveValue(value, options);
68
+
69
+ return (
70
+ <PrimitiveSelect
71
+ value={primitiveValue}
72
+ onValueChange={(nextOption) => {
73
+ if (!nextOption) {
74
+ return;
75
+ }
76
+
77
+ const selectedValue =
78
+ nextOption.value === NULL_SENTINEL ? null : nextOption.value;
79
+ const match = options.find(
80
+ (option) =>
81
+ String(option.value ?? NULL_SENTINEL) === String(selectedValue),
82
+ );
83
+
84
+ if (match) {
85
+ onValueChange(match.value);
86
+ }
87
+ }}
88
+ disabled={disabled}
89
+ >
90
+ <SelectTrigger
91
+ testID={testID}
92
+ className={cn(
93
+ "h-14 w-full rounded-lg border-0 bg-secondary px-4 py-0 shadow-none",
94
+ largerText && "h-16",
95
+ triggerClassName,
96
+ className,
97
+ )}
98
+ >
99
+ <SelectValue
100
+ placeholder={placeholder ?? "Select option"}
101
+ className={cn(
102
+ "font-instrument-sans-medium text-foreground",
103
+ largerText ? "text-xl" : "text-lg",
104
+ )}
105
+ />
106
+ </SelectTrigger>
107
+ <SelectContent className={cn("w-full", contentClassName)}>
108
+ <NativeSelectScrollView>
109
+ <SelectGroup>
110
+ {options.map((option) => {
111
+ const optionId = testID
112
+ ? `${testID}-${String(option.value ?? "null")}`
113
+ : undefined;
114
+ return (
115
+ <SelectItem
116
+ key={`${String(option.value)}-${option.label}`}
117
+ testID={optionId}
118
+ accessibilityLabel={option.label}
119
+ label={option.label}
120
+ value={String(option.value ?? NULL_SENTINEL)}
121
+ />
122
+ );
123
+ })}
124
+ </SelectGroup>
125
+ </NativeSelectScrollView>
126
+ </SelectContent>
127
+ </PrimitiveSelect>
128
+ );
129
+ }
130
+
131
+ export { Select };
132
+ export type { SelectOption, SelectProps };