@vvedantb/native-ui 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +84 -2
- package/package.json +68 -4
- package/src/AmbientBackground.tsx +29 -0
- package/src/BottomSheetContainer.tsx +92 -0
- package/src/Button.tsx +111 -0
- package/src/Card.tsx +64 -0
- package/src/Chip.tsx +49 -0
- package/src/DateTimeField.tsx +78 -0
- package/src/EmptyState.tsx +51 -0
- package/src/FloatingFAB.tsx +62 -0
- package/src/FloatingSurface.tsx +114 -0
- package/src/FloatingTabBar.tsx +107 -0
- package/src/HeaderIconButton.tsx +29 -0
- package/src/Input.tsx +32 -0
- package/src/LiquidGlass.tsx +14 -0
- package/src/ListRow.tsx +85 -0
- package/src/PillTabs.tsx +51 -0
- package/src/SafeAreaContainer.tsx +26 -0
- package/src/ScreenContainer.tsx +36 -0
- package/src/Select.tsx +132 -0
- package/src/Skeleton.tsx +37 -0
- package/src/Switch.tsx +45 -0
- package/src/cn.ts +6 -0
- package/src/colors.ts +17 -0
- package/src/floating.ts +40 -0
- package/src/haptics.ts +9 -0
- package/src/index.ts +58 -0
- package/src/primitives/button.tsx +101 -0
- package/src/primitives/card.tsx +102 -0
- package/src/primitives/icon.tsx +14 -0
- package/src/primitives/input.tsx +37 -0
- package/src/primitives/native-only-animated-view.tsx +33 -0
- package/src/primitives/select.tsx +265 -0
- package/src/primitives/switch.tsx +77 -0
- package/src/primitives/text.tsx +95 -0
- package/src/settings/SettingsField.tsx +33 -0
- package/src/settings/SettingsSection.tsx +75 -0
- package/src/settings/SettingsStack.tsx +5 -0
- package/src/settings/SettingsToggleRow.tsx +58 -0
- package/src/theme.tsx +44 -0
- package/src/useReduceTransparency.ts +24 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import * as SelectPrimitive from "@rn-primitives/select";
|
|
2
|
+
import {
|
|
3
|
+
Check,
|
|
4
|
+
ChevronDown,
|
|
5
|
+
ChevronDownIcon,
|
|
6
|
+
ChevronUpIcon,
|
|
7
|
+
} from "lucide-react-native";
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
import { Platform, ScrollView, StyleSheet, View } from "react-native";
|
|
10
|
+
import { FadeIn, FadeOut } from "react-native-reanimated";
|
|
11
|
+
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
12
|
+
import { NativeOnlyAnimatedView } from "./native-only-animated-view";
|
|
13
|
+
import { TextClassContext } from "./text";
|
|
14
|
+
import { LiquidGlass } from "../LiquidGlass";
|
|
15
|
+
import { cn } from "../cn";
|
|
16
|
+
|
|
17
|
+
type Option = SelectPrimitive.Option;
|
|
18
|
+
|
|
19
|
+
const Select = SelectPrimitive.Root;
|
|
20
|
+
const SelectGroup = SelectPrimitive.Group;
|
|
21
|
+
const FullWindowOverlay =
|
|
22
|
+
Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment;
|
|
23
|
+
|
|
24
|
+
type SelectValueProps = React.ComponentPropsWithoutRef<
|
|
25
|
+
typeof SelectPrimitive.Value
|
|
26
|
+
> & {
|
|
27
|
+
className?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const SelectValue = React.forwardRef<
|
|
31
|
+
React.ElementRef<typeof SelectPrimitive.Value>,
|
|
32
|
+
SelectValueProps
|
|
33
|
+
>(({ className, ...props }, ref) => {
|
|
34
|
+
return (
|
|
35
|
+
<SelectPrimitive.Value
|
|
36
|
+
ref={ref}
|
|
37
|
+
className={cn("line-clamp-1 text-sm text-foreground", className)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
type SelectTriggerProps = React.ComponentPropsWithoutRef<
|
|
44
|
+
typeof SelectPrimitive.Trigger
|
|
45
|
+
> & {
|
|
46
|
+
className?: string;
|
|
47
|
+
size?: "default" | "sm";
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const SelectTrigger = React.forwardRef<
|
|
51
|
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
52
|
+
SelectTriggerProps
|
|
53
|
+
>(({ className, children, size = "default", disabled, ...props }, ref) => {
|
|
54
|
+
return (
|
|
55
|
+
<SelectPrimitive.Trigger
|
|
56
|
+
ref={ref}
|
|
57
|
+
disabled={disabled}
|
|
58
|
+
className={cn(
|
|
59
|
+
"h-10 flex-row items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2",
|
|
60
|
+
size === "sm" && "h-8 py-1.5",
|
|
61
|
+
disabled && "opacity-50",
|
|
62
|
+
className,
|
|
63
|
+
)}
|
|
64
|
+
{...props}
|
|
65
|
+
>
|
|
66
|
+
<>{children}</>
|
|
67
|
+
<ChevronDown size={16} color="#737373" />
|
|
68
|
+
</SelectPrimitive.Trigger>
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
type SelectContentProps = React.ComponentPropsWithoutRef<
|
|
73
|
+
typeof SelectPrimitive.Content
|
|
74
|
+
> & {
|
|
75
|
+
className?: string;
|
|
76
|
+
portalHost?: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const SelectContent = React.forwardRef<
|
|
80
|
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
81
|
+
SelectContentProps
|
|
82
|
+
>(({ className, children, position = "popper", portalHost, ...props }, ref) => {
|
|
83
|
+
return (
|
|
84
|
+
<SelectPrimitive.Portal hostName={portalHost}>
|
|
85
|
+
<FullWindowOverlay>
|
|
86
|
+
<SelectPrimitive.Overlay
|
|
87
|
+
style={Platform.select({ native: StyleSheet.absoluteFill })}
|
|
88
|
+
>
|
|
89
|
+
<TextClassContext.Provider value="text-popover-foreground">
|
|
90
|
+
<NativeOnlyAnimatedView
|
|
91
|
+
entering={FadeIn}
|
|
92
|
+
exiting={FadeOut}
|
|
93
|
+
className="z-50"
|
|
94
|
+
>
|
|
95
|
+
<SelectPrimitive.Content
|
|
96
|
+
ref={ref}
|
|
97
|
+
position={position}
|
|
98
|
+
className={cn(
|
|
99
|
+
"z-50 min-w-[8rem] rounded-lg",
|
|
100
|
+
Platform.select({
|
|
101
|
+
native: "p-1",
|
|
102
|
+
web: "max-h-52 overflow-hidden",
|
|
103
|
+
}),
|
|
104
|
+
className,
|
|
105
|
+
)}
|
|
106
|
+
{...props}
|
|
107
|
+
>
|
|
108
|
+
<LiquidGlass
|
|
109
|
+
pointerEvents="none"
|
|
110
|
+
elevated={false}
|
|
111
|
+
className="absolute inset-0 rounded-lg"
|
|
112
|
+
/>
|
|
113
|
+
<SelectScrollUpButton />
|
|
114
|
+
<SelectPrimitive.Viewport
|
|
115
|
+
className={cn("p-1", position === "popper" && "w-full")}
|
|
116
|
+
>
|
|
117
|
+
{children}
|
|
118
|
+
</SelectPrimitive.Viewport>
|
|
119
|
+
<SelectScrollDownButton />
|
|
120
|
+
</SelectPrimitive.Content>
|
|
121
|
+
</NativeOnlyAnimatedView>
|
|
122
|
+
</TextClassContext.Provider>
|
|
123
|
+
</SelectPrimitive.Overlay>
|
|
124
|
+
</FullWindowOverlay>
|
|
125
|
+
</SelectPrimitive.Portal>
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
type SelectLabelProps = React.ComponentPropsWithoutRef<
|
|
130
|
+
typeof SelectPrimitive.Label
|
|
131
|
+
> & {
|
|
132
|
+
className?: string;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const SelectLabel = React.forwardRef<
|
|
136
|
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
|
137
|
+
SelectLabelProps
|
|
138
|
+
>(({ className, ...props }, ref) => {
|
|
139
|
+
return (
|
|
140
|
+
<SelectPrimitive.Label
|
|
141
|
+
ref={ref}
|
|
142
|
+
className={cn("px-2 py-2 text-xs text-muted-foreground", className)}
|
|
143
|
+
{...props}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
type SelectItemProps = React.ComponentPropsWithoutRef<
|
|
149
|
+
typeof SelectPrimitive.Item
|
|
150
|
+
> & {
|
|
151
|
+
className?: string;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const SelectItem = React.forwardRef<
|
|
155
|
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
156
|
+
SelectItemProps
|
|
157
|
+
>(({ className, disabled, ...props }, ref) => {
|
|
158
|
+
return (
|
|
159
|
+
<SelectPrimitive.Item
|
|
160
|
+
ref={ref}
|
|
161
|
+
disabled={disabled}
|
|
162
|
+
className={cn(
|
|
163
|
+
"relative flex-row items-center gap-2 rounded-sm py-3 pl-2 pr-8 active:opacity-70",
|
|
164
|
+
disabled && "opacity-50",
|
|
165
|
+
className,
|
|
166
|
+
)}
|
|
167
|
+
{...props}
|
|
168
|
+
>
|
|
169
|
+
<View className="absolute right-2 size-3.5 items-center justify-center">
|
|
170
|
+
<SelectPrimitive.ItemIndicator>
|
|
171
|
+
<Check size={16} color="#737373" />
|
|
172
|
+
</SelectPrimitive.ItemIndicator>
|
|
173
|
+
</View>
|
|
174
|
+
<SelectPrimitive.ItemText className="select-none text-sm font-instrument-sans-medium text-foreground" />
|
|
175
|
+
</SelectPrimitive.Item>
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
type SelectSeparatorProps = React.ComponentPropsWithoutRef<
|
|
180
|
+
typeof SelectPrimitive.Separator
|
|
181
|
+
> & {
|
|
182
|
+
className?: string;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const SelectSeparator = React.forwardRef<
|
|
186
|
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
|
187
|
+
SelectSeparatorProps
|
|
188
|
+
>(({ className, ...props }, ref) => {
|
|
189
|
+
return (
|
|
190
|
+
<SelectPrimitive.Separator
|
|
191
|
+
ref={ref}
|
|
192
|
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
193
|
+
{...props}
|
|
194
|
+
/>
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
function SelectScrollUpButton(
|
|
199
|
+
props: React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
200
|
+
) {
|
|
201
|
+
if (Platform.OS !== "web") {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<SelectPrimitive.ScrollUpButton
|
|
207
|
+
className="items-center justify-center py-1"
|
|
208
|
+
{...props}
|
|
209
|
+
>
|
|
210
|
+
<ChevronUpIcon size={16} color="#737373" />
|
|
211
|
+
</SelectPrimitive.ScrollUpButton>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function SelectScrollDownButton(
|
|
216
|
+
props: React.ComponentPropsWithoutRef<
|
|
217
|
+
typeof SelectPrimitive.ScrollDownButton
|
|
218
|
+
>,
|
|
219
|
+
) {
|
|
220
|
+
if (Platform.OS !== "web") {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<SelectPrimitive.ScrollDownButton
|
|
226
|
+
className="items-center justify-center py-1"
|
|
227
|
+
{...props}
|
|
228
|
+
>
|
|
229
|
+
<ChevronDownIcon size={16} color="#737373" />
|
|
230
|
+
</SelectPrimitive.ScrollDownButton>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function NativeSelectScrollView({
|
|
235
|
+
className,
|
|
236
|
+
...props
|
|
237
|
+
}: React.ComponentPropsWithoutRef<typeof ScrollView> & { className?: string }) {
|
|
238
|
+
if (Platform.OS === "web") {
|
|
239
|
+
return <>{props.children}</>;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return <ScrollView className={cn("max-h-52", className)} {...props} />;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
SelectValue.displayName = "SelectValue";
|
|
246
|
+
SelectTrigger.displayName = "SelectTrigger";
|
|
247
|
+
SelectContent.displayName = "SelectContent";
|
|
248
|
+
SelectLabel.displayName = "SelectLabel";
|
|
249
|
+
SelectItem.displayName = "SelectItem";
|
|
250
|
+
SelectSeparator.displayName = "SelectSeparator";
|
|
251
|
+
|
|
252
|
+
export {
|
|
253
|
+
NativeSelectScrollView,
|
|
254
|
+
Select,
|
|
255
|
+
SelectContent,
|
|
256
|
+
SelectGroup,
|
|
257
|
+
SelectItem,
|
|
258
|
+
SelectLabel,
|
|
259
|
+
SelectScrollDownButton,
|
|
260
|
+
SelectScrollUpButton,
|
|
261
|
+
SelectSeparator,
|
|
262
|
+
SelectTrigger,
|
|
263
|
+
SelectValue,
|
|
264
|
+
type Option,
|
|
265
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as SwitchPrimitives from "@rn-primitives/switch";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Platform } from "react-native";
|
|
4
|
+
import { cn } from "../cn";
|
|
5
|
+
|
|
6
|
+
type SwitchProps = React.ComponentPropsWithoutRef<
|
|
7
|
+
typeof SwitchPrimitives.Root
|
|
8
|
+
> & {
|
|
9
|
+
className?: string;
|
|
10
|
+
thumbClassName?: string;
|
|
11
|
+
onColor?: string;
|
|
12
|
+
offColor?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const Switch = React.forwardRef<
|
|
16
|
+
React.ElementRef<typeof SwitchPrimitives.Root>,
|
|
17
|
+
SwitchProps
|
|
18
|
+
>(
|
|
19
|
+
(
|
|
20
|
+
{
|
|
21
|
+
className,
|
|
22
|
+
thumbClassName,
|
|
23
|
+
onColor,
|
|
24
|
+
offColor,
|
|
25
|
+
checked,
|
|
26
|
+
style,
|
|
27
|
+
disabled,
|
|
28
|
+
...props
|
|
29
|
+
},
|
|
30
|
+
ref,
|
|
31
|
+
) => {
|
|
32
|
+
const isChecked = Boolean(checked);
|
|
33
|
+
const trackColor =
|
|
34
|
+
Platform.OS !== "web" && (onColor || offColor)
|
|
35
|
+
? { backgroundColor: isChecked ? onColor : offColor }
|
|
36
|
+
: null;
|
|
37
|
+
const resolvedStyle =
|
|
38
|
+
Platform.OS === "web" || typeof style === "function"
|
|
39
|
+
? undefined
|
|
40
|
+
: trackColor
|
|
41
|
+
? [trackColor, style]
|
|
42
|
+
: style;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<SwitchPrimitives.Root
|
|
46
|
+
ref={ref}
|
|
47
|
+
checked={checked}
|
|
48
|
+
disabled={disabled}
|
|
49
|
+
style={resolvedStyle}
|
|
50
|
+
className={cn(
|
|
51
|
+
"h-[1.15rem] w-8 shrink-0 rounded-full border border-transparent",
|
|
52
|
+
Platform.select({
|
|
53
|
+
web: "outline-none transition-all",
|
|
54
|
+
}),
|
|
55
|
+
isChecked ? "bg-primary" : "bg-input dark:bg-input/80",
|
|
56
|
+
disabled && "opacity-50",
|
|
57
|
+
className,
|
|
58
|
+
)}
|
|
59
|
+
{...props}
|
|
60
|
+
>
|
|
61
|
+
<SwitchPrimitives.Thumb
|
|
62
|
+
className={cn(
|
|
63
|
+
"h-4 w-4 rounded-full bg-background transition-transform",
|
|
64
|
+
isChecked
|
|
65
|
+
? "translate-x-3.5 dark:bg-primary-foreground"
|
|
66
|
+
: "translate-x-0 dark:bg-foreground",
|
|
67
|
+
thumbClassName,
|
|
68
|
+
)}
|
|
69
|
+
/>
|
|
70
|
+
</SwitchPrimitives.Root>
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
Switch.displayName = "Switch";
|
|
76
|
+
|
|
77
|
+
export { Switch };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as Slot from "@rn-primitives/slot";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Platform, Text as RNText, type Role } from "react-native";
|
|
5
|
+
import { cn } from "../cn";
|
|
6
|
+
|
|
7
|
+
const textVariants = cva(
|
|
8
|
+
cn(
|
|
9
|
+
"text-foreground text-base",
|
|
10
|
+
Platform.select({
|
|
11
|
+
web: "select-text",
|
|
12
|
+
}),
|
|
13
|
+
),
|
|
14
|
+
{
|
|
15
|
+
variants: {
|
|
16
|
+
variant: {
|
|
17
|
+
default: "",
|
|
18
|
+
h1: cn(
|
|
19
|
+
"text-4xl font-extrabold tracking-tight",
|
|
20
|
+
Platform.select({ web: "scroll-m-20" }),
|
|
21
|
+
),
|
|
22
|
+
h2: cn(
|
|
23
|
+
"text-3xl font-semibold tracking-tight border-border border-b pb-2",
|
|
24
|
+
Platform.select({ web: "scroll-m-20" }),
|
|
25
|
+
),
|
|
26
|
+
h3: cn(
|
|
27
|
+
"text-2xl font-semibold tracking-tight",
|
|
28
|
+
Platform.select({ web: "scroll-m-20" }),
|
|
29
|
+
),
|
|
30
|
+
h4: cn(
|
|
31
|
+
"text-xl font-semibold tracking-tight",
|
|
32
|
+
Platform.select({ web: "scroll-m-20" }),
|
|
33
|
+
),
|
|
34
|
+
p: "leading-7",
|
|
35
|
+
blockquote: "border-l-2 border-border pl-4 italic",
|
|
36
|
+
code: "bg-muted rounded px-[0.3rem] py-[0.2rem] text-sm font-semibold",
|
|
37
|
+
lead: "text-xl text-muted-foreground",
|
|
38
|
+
large: "text-lg font-semibold",
|
|
39
|
+
small: "text-sm font-medium",
|
|
40
|
+
muted: "text-sm text-muted-foreground",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
defaultVariants: {
|
|
44
|
+
variant: "default",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
type TextVariantProps = VariantProps<typeof textVariants>;
|
|
50
|
+
type TextVariant = NonNullable<TextVariantProps["variant"]>;
|
|
51
|
+
|
|
52
|
+
const ROLE: Partial<Record<TextVariant, Role>> = {
|
|
53
|
+
h1: "heading",
|
|
54
|
+
h2: "heading",
|
|
55
|
+
h3: "heading",
|
|
56
|
+
h4: "heading",
|
|
57
|
+
blockquote: Platform.select({ web: "blockquote" as Role }),
|
|
58
|
+
code: Platform.select({ web: "code" as Role }),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const ARIA_LEVEL: Partial<Record<TextVariant, string>> = {
|
|
62
|
+
h1: "1",
|
|
63
|
+
h2: "2",
|
|
64
|
+
h3: "3",
|
|
65
|
+
h4: "4",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const TextClassContext = React.createContext<string | undefined>(undefined);
|
|
69
|
+
|
|
70
|
+
type TextProps = React.ComponentPropsWithoutRef<typeof RNText> &
|
|
71
|
+
TextVariantProps & {
|
|
72
|
+
asChild?: boolean;
|
|
73
|
+
className?: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const Text = React.forwardRef<React.ElementRef<typeof RNText>, TextProps>(
|
|
77
|
+
({ className, asChild = false, variant = "default", ...props }, ref) => {
|
|
78
|
+
const textClass = React.useContext(TextClassContext);
|
|
79
|
+
const Component = asChild ? Slot.Text : RNText;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Component
|
|
83
|
+
ref={ref}
|
|
84
|
+
className={cn(textVariants({ variant }), textClass, className)}
|
|
85
|
+
role={variant ? ROLE[variant] : undefined}
|
|
86
|
+
aria-level={variant ? ARIA_LEVEL[variant] : undefined}
|
|
87
|
+
{...props}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
Text.displayName = "Text";
|
|
94
|
+
|
|
95
|
+
export { Text, TextClassContext };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { View, Text } from "react-native";
|
|
2
|
+
|
|
3
|
+
export function SettingsField({
|
|
4
|
+
label,
|
|
5
|
+
description,
|
|
6
|
+
children,
|
|
7
|
+
}: {
|
|
8
|
+
label: React.ReactNode;
|
|
9
|
+
description?: React.ReactNode;
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<View>
|
|
14
|
+
{typeof label === "string" ? (
|
|
15
|
+
<Text className="mb-2 text-base font-instrument-sans-semibold text-foreground">
|
|
16
|
+
{label}
|
|
17
|
+
</Text>
|
|
18
|
+
) : (
|
|
19
|
+
label
|
|
20
|
+
)}
|
|
21
|
+
{children}
|
|
22
|
+
{description != null ? (
|
|
23
|
+
typeof description === "string" ? (
|
|
24
|
+
<Text className="mt-1.5 text-sm font-instrument-sans text-muted-foreground">
|
|
25
|
+
{description}
|
|
26
|
+
</Text>
|
|
27
|
+
) : (
|
|
28
|
+
description
|
|
29
|
+
)
|
|
30
|
+
) : null}
|
|
31
|
+
</View>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { View, Text } from "react-native";
|
|
2
|
+
import { cn } from "../cn";
|
|
3
|
+
import { FloatingSurface } from "../FloatingSurface";
|
|
4
|
+
|
|
5
|
+
type SettingsSectionBodyVariant = "form" | "list" | "compact";
|
|
6
|
+
|
|
7
|
+
export function SettingsSection({
|
|
8
|
+
title,
|
|
9
|
+
description,
|
|
10
|
+
action,
|
|
11
|
+
footer,
|
|
12
|
+
children,
|
|
13
|
+
bodyVariant = "form",
|
|
14
|
+
className,
|
|
15
|
+
}: {
|
|
16
|
+
title: React.ReactNode;
|
|
17
|
+
description?: React.ReactNode;
|
|
18
|
+
action?: React.ReactNode;
|
|
19
|
+
footer?: React.ReactNode;
|
|
20
|
+
children?: React.ReactNode;
|
|
21
|
+
bodyVariant?: SettingsSectionBodyVariant;
|
|
22
|
+
className?: string;
|
|
23
|
+
}) {
|
|
24
|
+
const hasBody = children != null;
|
|
25
|
+
const hasCard = hasBody || footer != null;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<View className={cn("flex-col gap-2", className)}>
|
|
29
|
+
<View className="flex-row items-start justify-between gap-4 px-4">
|
|
30
|
+
<View className="min-w-0 flex-1">
|
|
31
|
+
{typeof title === "string" ? (
|
|
32
|
+
<Text className="text-lg font-instrument-sans-bold text-foreground">
|
|
33
|
+
{title}
|
|
34
|
+
</Text>
|
|
35
|
+
) : (
|
|
36
|
+
title
|
|
37
|
+
)}
|
|
38
|
+
{description != null ? (
|
|
39
|
+
typeof description === "string" ? (
|
|
40
|
+
<Text className="mt-0.5 text-base font-instrument-sans text-muted-foreground">
|
|
41
|
+
{description}
|
|
42
|
+
</Text>
|
|
43
|
+
) : (
|
|
44
|
+
description
|
|
45
|
+
)
|
|
46
|
+
) : null}
|
|
47
|
+
</View>
|
|
48
|
+
{action != null ? <View className="shrink-0">{action}</View> : null}
|
|
49
|
+
</View>
|
|
50
|
+
|
|
51
|
+
{hasCard ? (
|
|
52
|
+
<FloatingSurface>
|
|
53
|
+
{hasBody ? (
|
|
54
|
+
<View
|
|
55
|
+
className={
|
|
56
|
+
bodyVariant === "list"
|
|
57
|
+
? "flex-col"
|
|
58
|
+
: bodyVariant === "compact"
|
|
59
|
+
? "px-4 py-3"
|
|
60
|
+
: "px-4 py-5"
|
|
61
|
+
}
|
|
62
|
+
>
|
|
63
|
+
{children}
|
|
64
|
+
</View>
|
|
65
|
+
) : null}
|
|
66
|
+
{footer != null ? (
|
|
67
|
+
<View className="flex-row items-center justify-end gap-2 bg-background px-4 py-3">
|
|
68
|
+
{footer}
|
|
69
|
+
</View>
|
|
70
|
+
) : null}
|
|
71
|
+
</FloatingSurface>
|
|
72
|
+
) : null}
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { View, Text } from "react-native";
|
|
2
|
+
import { cn } from "../cn";
|
|
3
|
+
import { useLargerText } from "../theme";
|
|
4
|
+
|
|
5
|
+
export function SettingsToggleRow({
|
|
6
|
+
title,
|
|
7
|
+
description,
|
|
8
|
+
action,
|
|
9
|
+
className,
|
|
10
|
+
}: {
|
|
11
|
+
title: React.ReactNode;
|
|
12
|
+
description?: React.ReactNode;
|
|
13
|
+
action: React.ReactNode;
|
|
14
|
+
className?: string;
|
|
15
|
+
}) {
|
|
16
|
+
const largerText = useLargerText();
|
|
17
|
+
return (
|
|
18
|
+
<View
|
|
19
|
+
className={cn(
|
|
20
|
+
"flex-row items-center justify-between gap-4 px-4 py-3",
|
|
21
|
+
largerText ? "min-h-[72px]" : "min-h-16",
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
>
|
|
25
|
+
<View className="min-w-0 flex-1">
|
|
26
|
+
{typeof title === "string" ? (
|
|
27
|
+
<Text
|
|
28
|
+
className={cn(
|
|
29
|
+
"font-instrument-sans-semibold text-foreground",
|
|
30
|
+
largerText ? "text-xl" : "text-lg",
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
{title}
|
|
34
|
+
</Text>
|
|
35
|
+
) : (
|
|
36
|
+
title
|
|
37
|
+
)}
|
|
38
|
+
{description != null ? (
|
|
39
|
+
typeof description === "string" ? (
|
|
40
|
+
<Text
|
|
41
|
+
className={cn(
|
|
42
|
+
"mt-1 font-instrument-sans text-muted-foreground",
|
|
43
|
+
largerText ? "text-lg" : "text-base",
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
{description}
|
|
47
|
+
</Text>
|
|
48
|
+
) : (
|
|
49
|
+
description
|
|
50
|
+
)
|
|
51
|
+
) : null}
|
|
52
|
+
</View>
|
|
53
|
+
<View className={cn("shrink-0 items-center justify-center", "min-h-12")}>
|
|
54
|
+
{action}
|
|
55
|
+
</View>
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
}
|
package/src/theme.tsx
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
+
import { tokens } from "@vvedantb/tokens/native";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_ACCENT = "#db2777";
|
|
5
|
+
|
|
6
|
+
export type NativeUiTheme = {
|
|
7
|
+
largerText: boolean;
|
|
8
|
+
accentColor: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const NativeUiContext = createContext<NativeUiTheme>({
|
|
12
|
+
largerText: false,
|
|
13
|
+
accentColor: DEFAULT_ACCENT,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function NativeUiProvider({
|
|
17
|
+
largerText = false,
|
|
18
|
+
accentColor = DEFAULT_ACCENT,
|
|
19
|
+
children,
|
|
20
|
+
}: {
|
|
21
|
+
largerText?: boolean;
|
|
22
|
+
accentColor?: string;
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
}) {
|
|
25
|
+
return (
|
|
26
|
+
<NativeUiContext.Provider value={{ largerText, accentColor }}>
|
|
27
|
+
{children}
|
|
28
|
+
</NativeUiContext.Provider>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useNativeUi() {
|
|
33
|
+
return useContext(NativeUiContext);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useLargerText() {
|
|
37
|
+
return useContext(NativeUiContext).largerText;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useAccentColor() {
|
|
41
|
+
return useContext(NativeUiContext).accentColor;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { tokens };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { AccessibilityInfo, Platform } from "react-native";
|
|
3
|
+
|
|
4
|
+
export function useReduceTransparency() {
|
|
5
|
+
const [enabled, setEnabled] = useState(false);
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (
|
|
9
|
+
Platform.OS !== "ios" ||
|
|
10
|
+
typeof AccessibilityInfo.isReduceTransparencyEnabled !== "function"
|
|
11
|
+
) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
void AccessibilityInfo.isReduceTransparencyEnabled().then(setEnabled);
|
|
16
|
+
const subscription = AccessibilityInfo.addEventListener(
|
|
17
|
+
"reduceTransparencyChanged",
|
|
18
|
+
setEnabled,
|
|
19
|
+
);
|
|
20
|
+
return () => subscription.remove();
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
return enabled;
|
|
24
|
+
}
|