@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,37 @@
1
+ import { View } from "react-native";
2
+ import Animated, {
3
+ useAnimatedStyle,
4
+ useSharedValue,
5
+ withRepeat,
6
+ withTiming,
7
+ } from "react-native-reanimated";
8
+ import { useEffect } from "react";
9
+ import { cn } from "./cn";
10
+
11
+ export function Skeleton({ className }: { className?: string }) {
12
+ const opacity = useSharedValue(0.4);
13
+
14
+ useEffect(() => {
15
+ opacity.value = withRepeat(withTiming(1, { duration: 800 }), -1, true);
16
+ }, [opacity]);
17
+
18
+ const animatedStyle = useAnimatedStyle(() => ({
19
+ opacity: opacity.value,
20
+ }));
21
+
22
+ return (
23
+ <Animated.View
24
+ style={animatedStyle}
25
+ className={cn("bg-neutral-200 dark:bg-neutral-700 rounded-lg", className)}
26
+ />
27
+ );
28
+ }
29
+
30
+ export function CardSkeleton() {
31
+ return (
32
+ <View className="rounded-lg bg-surface p-5 gap-3">
33
+ <Skeleton className="h-4 w-32" />
34
+ <Skeleton className="h-16 w-full" />
35
+ </View>
36
+ );
37
+ }
package/src/Switch.tsx ADDED
@@ -0,0 +1,45 @@
1
+ import * as React from "react";
2
+ import { Switch as PrimitiveSwitch } from "./primitives/switch";
3
+ import { cn } from "./cn";
4
+
5
+ type SwitchProps = Omit<
6
+ React.ComponentPropsWithoutRef<typeof PrimitiveSwitch>,
7
+ "checked" | "onCheckedChange"
8
+ > & {
9
+ value?: boolean;
10
+ checked?: boolean;
11
+ onValueChange?: (value: boolean) => void;
12
+ onCheckedChange?: (value: boolean) => void;
13
+ };
14
+
15
+ const Switch = React.forwardRef<
16
+ React.ElementRef<typeof PrimitiveSwitch>,
17
+ SwitchProps
18
+ >(({ value, checked, onValueChange, onCheckedChange, ...props }, ref) => {
19
+ const resolvedValue = checked ?? value ?? false;
20
+
21
+ const handleCheckedChange = (nextValue: boolean) => {
22
+ onCheckedChange?.(nextValue);
23
+ onValueChange?.(nextValue);
24
+ };
25
+
26
+ return (
27
+ <PrimitiveSwitch
28
+ ref={ref}
29
+ checked={resolvedValue}
30
+ onCheckedChange={handleCheckedChange}
31
+ hitSlop={12}
32
+ className="h-8 w-14"
33
+ thumbClassName={cn(
34
+ "h-7 w-7",
35
+ resolvedValue ? "translate-x-6" : "translate-x-0",
36
+ )}
37
+ {...props}
38
+ />
39
+ );
40
+ });
41
+
42
+ Switch.displayName = "Switch";
43
+
44
+ export { Switch };
45
+ export type { SwitchProps };
package/src/cn.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
package/src/colors.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { tokens } from "./tokens";
2
+
3
+ export const NEUTRAL_100 = "#f5f5f5";
4
+ export const NEUTRAL_500 = tokens.color.muted.light;
5
+ export const NEUTRAL_900 = "#171717";
6
+ export const PINK_50 = "#fdf2f8";
7
+ export const PINK_100 = "#fce7f3";
8
+ export const PINK_600 = "#db2777";
9
+ export const PINK_700 = "#be185d";
10
+
11
+ export const Colors = {
12
+ white: tokens.color.white,
13
+ black: tokens.color.black,
14
+ neutral: {
15
+ 800: "#262626",
16
+ },
17
+ } as const;
@@ -0,0 +1,40 @@
1
+ import { Platform, type ViewStyle } from "react-native";
2
+ import { tokens } from "./tokens";
3
+
4
+ export const FLOATING_INSET = tokens.spacing.unit;
5
+ export const FLOATING_TAB_BAR_HEIGHT = 64;
6
+ export const FLOATING_TAB_BAR_HEIGHT_LARGE = 72;
7
+ export const FLOATING_RADIUS = tokens.radius.card;
8
+ export const FLOATING_ROW_RADIUS = tokens.radius.field;
9
+ export const FLOATING_PILL_RADIUS = tokens.radius.pill;
10
+
11
+ const FLOATING_BOX_SHADOW = "0px 8px 24px rgba(10, 10, 10, 0.12)";
12
+
13
+ export const floatingShadow: ViewStyle =
14
+ Platform.select({
15
+ ios: {
16
+ shadowColor: "#0a0a0a",
17
+ shadowOffset: { width: 0, height: 8 },
18
+ shadowOpacity: 0.12,
19
+ shadowRadius: 20,
20
+ },
21
+ default: {
22
+ boxShadow: FLOATING_BOX_SHADOW,
23
+ },
24
+ }) ?? {};
25
+
26
+ export const floatingHairline = tokens.color.hairline;
27
+
28
+ export const floatingFill = tokens.color.fill;
29
+
30
+ export const floatingSolidFill = {
31
+ light: tokens.color.surface.light,
32
+ dark: tokens.color.surface.dark,
33
+ } as const;
34
+
35
+ export function tabBarReserve(largerText: boolean, bottomInset: number) {
36
+ const bar = largerText
37
+ ? FLOATING_TAB_BAR_HEIGHT_LARGE
38
+ : FLOATING_TAB_BAR_HEIGHT;
39
+ return bar + FLOATING_INSET + Math.max(bottomInset, 8) + 28;
40
+ }
package/src/haptics.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { Platform } from "react-native";
2
+ import * as Haptics from "expo-haptics";
3
+
4
+ export function selectionHaptic() {
5
+ if (Platform.OS === "web") {
6
+ return;
7
+ }
8
+ void Haptics.selectionAsync();
9
+ }
package/src/index.ts ADDED
@@ -0,0 +1,58 @@
1
+ export { cn } from "./cn";
2
+ export {
3
+ tokens,
4
+ NativeUiProvider,
5
+ useNativeUi,
6
+ useLargerText,
7
+ useAccentColor,
8
+ DEFAULT_ACCENT,
9
+ } from "./theme";
10
+ export type { NativeUiTheme } from "./theme";
11
+ export {
12
+ FLOATING_INSET,
13
+ FLOATING_TAB_BAR_HEIGHT,
14
+ FLOATING_TAB_BAR_HEIGHT_LARGE,
15
+ FLOATING_RADIUS,
16
+ FLOATING_ROW_RADIUS,
17
+ FLOATING_PILL_RADIUS,
18
+ floatingShadow,
19
+ floatingHairline,
20
+ floatingFill,
21
+ floatingSolidFill,
22
+ tabBarReserve,
23
+ } from "./floating";
24
+ export { selectionHaptic } from "./haptics";
25
+ export { useReduceTransparency } from "./useReduceTransparency";
26
+ export { FloatingSurface, GlassSheetBackground } from "./FloatingSurface";
27
+ export {
28
+ LiquidGlass,
29
+ GlassBlurProvider,
30
+ GlassBlurTarget,
31
+ } from "./LiquidGlass";
32
+ export { AmbientBackground } from "./AmbientBackground";
33
+ export { FloatingTabBar } from "./FloatingTabBar";
34
+ export { FloatingFAB } from "./FloatingFAB";
35
+ export { Button } from "./Button";
36
+ export type { ButtonProps } from "./Button";
37
+ export { Input } from "./Input";
38
+ export type { InputProps } from "./Input";
39
+ export { Select } from "./Select";
40
+ export type { SelectOption, SelectProps } from "./Select";
41
+ export { Switch } from "./Switch";
42
+ export type { SwitchProps } from "./Switch";
43
+ export { Chip } from "./Chip";
44
+ export { PillTabs } from "./PillTabs";
45
+ export { ListRow } from "./ListRow";
46
+ export { HeaderIconButton } from "./HeaderIconButton";
47
+ export { Card } from "./Card";
48
+ export { Skeleton, CardSkeleton } from "./Skeleton";
49
+ export { EmptyState } from "./EmptyState";
50
+ export { DateTimeField } from "./DateTimeField";
51
+ export { BottomSheetContainer } from "./BottomSheetContainer";
52
+ export { SafeAreaContainer } from "./SafeAreaContainer";
53
+ export { ScreenContainer } from "./ScreenContainer";
54
+ export { SettingsSection } from "./settings/SettingsSection";
55
+ export { SettingsField } from "./settings/SettingsField";
56
+ export { SettingsToggleRow } from "./settings/SettingsToggleRow";
57
+ export { SettingsStack } from "./settings/SettingsStack";
58
+ export { PINK_50, PINK_100, PINK_600, PINK_700, NEUTRAL_100, NEUTRAL_500, NEUTRAL_900, Colors } from "./colors";
@@ -0,0 +1,101 @@
1
+ import { TextClassContext } from "./text";
2
+ import { cn } from "../cn";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import * as React from "react";
5
+ import { Platform, Pressable } from "react-native";
6
+
7
+ const buttonVariants = cva(
8
+ cn(
9
+ "group flex-row items-center justify-center gap-2 rounded-lg shadow-none active:opacity-80",
10
+ Platform.select({
11
+ web: "outline-none transition-all",
12
+ }),
13
+ ),
14
+ {
15
+ variants: {
16
+ variant: {
17
+ default: cn(
18
+ "bg-primary",
19
+ Platform.select({ web: "hover:bg-primary/90" }),
20
+ ),
21
+ destructive: cn(
22
+ "bg-destructive",
23
+ Platform.select({ web: "hover:bg-destructive/90" }),
24
+ ),
25
+ outline: cn(
26
+ "border border-border bg-background",
27
+ Platform.select({ web: "hover:bg-accent" }),
28
+ ),
29
+ secondary: cn(
30
+ "bg-secondary",
31
+ Platform.select({ web: "hover:bg-secondary/80" }),
32
+ ),
33
+ ghost: cn(Platform.select({ web: "hover:bg-accent" })),
34
+ link: "",
35
+ },
36
+ size: {
37
+ default: "h-10 px-4 py-2",
38
+ sm: "h-8 px-3",
39
+ lg: "h-11 px-6",
40
+ icon: "h-10 w-10",
41
+ },
42
+ },
43
+ defaultVariants: {
44
+ variant: "default",
45
+ size: "default",
46
+ },
47
+ },
48
+ );
49
+
50
+ const buttonTextVariants = cva("text-sm font-medium text-foreground", {
51
+ variants: {
52
+ variant: {
53
+ default: "text-primary-foreground",
54
+ destructive: "text-white",
55
+ outline: "text-accent-foreground",
56
+ secondary: "text-secondary-foreground",
57
+ ghost: "text-accent-foreground",
58
+ link: "text-primary underline",
59
+ },
60
+ size: {
61
+ default: "",
62
+ sm: "",
63
+ lg: "",
64
+ icon: "",
65
+ },
66
+ },
67
+ defaultVariants: {
68
+ variant: "default",
69
+ size: "default",
70
+ },
71
+ });
72
+
73
+ type ButtonProps = React.ComponentPropsWithoutRef<typeof Pressable> &
74
+ VariantProps<typeof buttonVariants> & {
75
+ className?: string;
76
+ };
77
+
78
+ const Button = React.forwardRef<
79
+ React.ElementRef<typeof Pressable>,
80
+ ButtonProps
81
+ >(({ className, variant, size, ...props }, ref) => {
82
+ return (
83
+ <TextClassContext.Provider value={buttonTextVariants({ variant, size })}>
84
+ <Pressable
85
+ ref={ref}
86
+ className={cn(
87
+ buttonVariants({ variant, size }),
88
+ props.disabled && "opacity-45",
89
+ className,
90
+ )}
91
+ role="button"
92
+ {...props}
93
+ />
94
+ </TextClassContext.Provider>
95
+ );
96
+ });
97
+
98
+ Button.displayName = "Button";
99
+
100
+ export { Button, buttonTextVariants, buttonVariants };
101
+ export type { ButtonProps };
@@ -0,0 +1,102 @@
1
+ import * as React from "react";
2
+ import { View } from "react-native";
3
+ import { Text, TextClassContext } from "./text";
4
+ import { cn } from "../cn";
5
+
6
+ type CardViewProps = React.ComponentPropsWithoutRef<typeof View> & {
7
+ className?: string;
8
+ };
9
+
10
+ const Card = React.forwardRef<React.ElementRef<typeof View>, CardViewProps>(
11
+ ({ className, ...props }, ref) => {
12
+ return (
13
+ <TextClassContext.Provider value="text-card-foreground">
14
+ <View
15
+ ref={ref}
16
+ className={cn(
17
+ "flex flex-col gap-6 rounded-lg bg-surface py-6",
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ </TextClassContext.Provider>
23
+ );
24
+ },
25
+ );
26
+
27
+ const CardHeader = React.forwardRef<
28
+ React.ElementRef<typeof View>,
29
+ CardViewProps
30
+ >(({ className, ...props }, ref) => {
31
+ return (
32
+ <View
33
+ ref={ref}
34
+ className={cn("flex flex-col gap-1.5 px-6", className)}
35
+ {...props}
36
+ />
37
+ );
38
+ });
39
+
40
+ const CardTitle = React.forwardRef<
41
+ React.ElementRef<typeof Text>,
42
+ React.ComponentPropsWithoutRef<typeof Text> & { className?: string }
43
+ >(({ className, ...props }, ref) => {
44
+ return (
45
+ <Text
46
+ ref={ref}
47
+ role="heading"
48
+ aria-level={3}
49
+ className={cn("font-semibold leading-none", className)}
50
+ {...props}
51
+ />
52
+ );
53
+ });
54
+
55
+ const CardDescription = React.forwardRef<
56
+ React.ElementRef<typeof Text>,
57
+ React.ComponentPropsWithoutRef<typeof Text> & { className?: string }
58
+ >(({ className, ...props }, ref) => {
59
+ return (
60
+ <Text
61
+ ref={ref}
62
+ className={cn("text-sm text-muted-foreground", className)}
63
+ {...props}
64
+ />
65
+ );
66
+ });
67
+
68
+ const CardContent = React.forwardRef<
69
+ React.ElementRef<typeof View>,
70
+ CardViewProps
71
+ >(({ className, ...props }, ref) => {
72
+ return <View ref={ref} className={cn("px-6", className)} {...props} />;
73
+ });
74
+
75
+ const CardFooter = React.forwardRef<
76
+ React.ElementRef<typeof View>,
77
+ CardViewProps
78
+ >(({ className, ...props }, ref) => {
79
+ return (
80
+ <View
81
+ ref={ref}
82
+ className={cn("flex flex-row items-center px-6", className)}
83
+ {...props}
84
+ />
85
+ );
86
+ });
87
+
88
+ Card.displayName = "Card";
89
+ CardHeader.displayName = "CardHeader";
90
+ CardTitle.displayName = "CardTitle";
91
+ CardDescription.displayName = "CardDescription";
92
+ CardContent.displayName = "CardContent";
93
+ CardFooter.displayName = "CardFooter";
94
+
95
+ export {
96
+ Card,
97
+ CardContent,
98
+ CardDescription,
99
+ CardFooter,
100
+ CardHeader,
101
+ CardTitle,
102
+ };
@@ -0,0 +1,14 @@
1
+ import * as React from "react";
2
+ import type { LucideIcon, LucideProps } from "lucide-react-native";
3
+
4
+ type IconProps = LucideProps & {
5
+ as: LucideIcon;
6
+ className?: string;
7
+ };
8
+
9
+ function Icon({ as: Component, className: _className, ...props }: IconProps) {
10
+ return <Component {...props} />;
11
+ }
12
+
13
+ export { Icon };
14
+ export type { IconProps };
@@ -0,0 +1,37 @@
1
+ import * as React from "react";
2
+ import { Platform, TextInput } from "react-native";
3
+ import { cn } from "../cn";
4
+
5
+ type InputProps = React.ComponentPropsWithoutRef<typeof TextInput> & {
6
+ className?: string;
7
+ };
8
+
9
+ const Input = React.forwardRef<React.ElementRef<typeof TextInput>, InputProps>(
10
+ ({ className, ...props }, ref) => {
11
+ return (
12
+ <TextInput
13
+ ref={ref}
14
+ className={cn(
15
+ "flex w-full flex-row items-center rounded-lg border-0 bg-secondary px-3 py-2 text-base text-foreground",
16
+ props.editable === false &&
17
+ cn(
18
+ "opacity-50",
19
+ Platform.select({
20
+ web: "cursor-not-allowed pointer-events-none",
21
+ }),
22
+ ),
23
+ Platform.select({
24
+ web: "outline-none transition-[color,box-shadow] md:text-sm",
25
+ native: "placeholder:text-muted-foreground/50",
26
+ }),
27
+ className,
28
+ )}
29
+ {...props}
30
+ />
31
+ );
32
+ },
33
+ );
34
+
35
+ Input.displayName = "Input";
36
+
37
+ export { Input };
@@ -0,0 +1,33 @@
1
+ import { Platform, View, type ViewProps } from "react-native";
2
+ import Animated, { type AnimatedProps } from "react-native-reanimated";
3
+
4
+ type NativeOnlyAnimatedViewProps = ViewProps & {
5
+ entering?: AnimatedProps<ViewProps>["entering"];
6
+ exiting?: AnimatedProps<ViewProps>["exiting"];
7
+ layout?: AnimatedProps<ViewProps>["layout"];
8
+ };
9
+
10
+ function NativeOnlyAnimatedView({
11
+ children,
12
+ entering,
13
+ exiting,
14
+ layout,
15
+ ...props
16
+ }: NativeOnlyAnimatedViewProps) {
17
+ if (Platform.OS === "web") {
18
+ return <View {...props}>{children}</View>;
19
+ }
20
+
21
+ return (
22
+ <Animated.View
23
+ entering={entering}
24
+ exiting={exiting}
25
+ layout={layout}
26
+ {...props}
27
+ >
28
+ {children}
29
+ </Animated.View>
30
+ );
31
+ }
32
+
33
+ export { NativeOnlyAnimatedView };