@munchi_oy/native-ui 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/dist/index.d.mts +568 -0
- package/dist/index.d.ts +568 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +1 -0
- package/global.css +53 -0
- package/nativewind-env.d.ts +2 -0
- package/package.json +88 -0
- package/src/MAlert.tsx +38 -0
- package/src/MAnimation.tsx +55 -0
- package/src/MAvatar.tsx +111 -0
- package/src/MBadge.tsx +72 -0
- package/src/MButton.tsx +90 -0
- package/src/MCard.tsx +15 -0
- package/src/MChevron.tsx +47 -0
- package/src/MConfirmation.tsx +68 -0
- package/src/MCountDown.tsx +120 -0
- package/src/MDateTimePicker.tsx +124 -0
- package/src/MDivider.tsx +69 -0
- package/src/MDrawerRightPanel.tsx +187 -0
- package/src/MDropdown.tsx +277 -0
- package/src/MInput.tsx +162 -0
- package/src/MLabel.tsx +3 -0
- package/src/MLucideIcon.tsx +21 -0
- package/src/MModal.tsx +287 -0
- package/src/MNativeAlert.tsx +33 -0
- package/src/MNumpad.tsx +520 -0
- package/src/MPicker.tsx +150 -0
- package/src/MPinPadKeys.tsx +104 -0
- package/src/MPortal.tsx +4 -0
- package/src/MProgressBar.tsx +74 -0
- package/src/MRadioGroup.tsx +4 -0
- package/src/MRequiredLabel.tsx +21 -0
- package/src/MResponsiveContainer.tsx +74 -0
- package/src/MSearch.tsx +138 -0
- package/src/MSelector.tsx +48 -0
- package/src/MSkeleton.tsx +3 -0
- package/src/MSwitch.tsx +13 -0
- package/src/MTable.tsx +17 -0
- package/src/MTabs.tsx +198 -0
- package/src/MText.tsx +51 -0
- package/src/MTimerUp.tsx +88 -0
- package/src/MToggle.tsx +51 -0
- package/src/constants.ts +19 -0
- package/src/hooks/useColorScheme.tsx +12 -0
- package/src/hooks/useIconColors.ts +19 -0
- package/src/index.ts +124 -0
- package/src/primitives/accordion.tsx +143 -0
- package/src/primitives/alert-dialog.tsx +181 -0
- package/src/primitives/alert.tsx +94 -0
- package/src/primitives/aspect-ratio.tsx +5 -0
- package/src/primitives/avatar.tsx +47 -0
- package/src/primitives/badge.tsx +57 -0
- package/src/primitives/button.tsx +92 -0
- package/src/primitives/card.tsx +86 -0
- package/src/primitives/checkbox.tsx +35 -0
- package/src/primitives/collapsible.tsx +9 -0
- package/src/primitives/context-menu.tsx +255 -0
- package/src/primitives/dialog.tsx +166 -0
- package/src/primitives/dropdown-menu.tsx +264 -0
- package/src/primitives/hover-card.tsx +45 -0
- package/src/primitives/input.tsx +25 -0
- package/src/primitives/label.tsx +33 -0
- package/src/primitives/menubar.tsx +266 -0
- package/src/primitives/navigation-menu.tsx +192 -0
- package/src/primitives/popover.tsx +46 -0
- package/src/primitives/progress.tsx +82 -0
- package/src/primitives/radio-group.tsx +42 -0
- package/src/primitives/select.tsx +192 -0
- package/src/primitives/separator.tsx +28 -0
- package/src/primitives/skeleton.tsx +39 -0
- package/src/primitives/switch.tsx +102 -0
- package/src/primitives/table.tsx +107 -0
- package/src/primitives/tabs.tsx +66 -0
- package/src/primitives/text.tsx +28 -0
- package/src/primitives/textarea.tsx +39 -0
- package/src/primitives/toggle-group.tsx +89 -0
- package/src/primitives/toggle.tsx +91 -0
- package/src/primitives/tooltip.tsx +40 -0
- package/src/primitives/typography.tsx +214 -0
- package/src/theme.ts +43 -0
- package/src/tokens.ts +7 -0
- package/src/utils.ts +14 -0
- package/tailwind.config.ts +112 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Delete } from "lucide-react-native";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
import { Pressable, View } from "react-native";
|
|
4
|
+
import { MText } from "./MText";
|
|
5
|
+
import { useIconColors } from "./hooks/useIconColors";
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
|
|
8
|
+
type KeyProps = {
|
|
9
|
+
label?: string;
|
|
10
|
+
onPress: () => void;
|
|
11
|
+
testID?: string;
|
|
12
|
+
icon?: React.ReactNode;
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
iconTint: string;
|
|
15
|
+
labelClassName: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const PinPadKey: React.FC<KeyProps> = ({
|
|
19
|
+
label,
|
|
20
|
+
onPress,
|
|
21
|
+
testID,
|
|
22
|
+
icon,
|
|
23
|
+
disabled,
|
|
24
|
+
iconTint,
|
|
25
|
+
labelClassName
|
|
26
|
+
}) => {
|
|
27
|
+
return (
|
|
28
|
+
<Pressable
|
|
29
|
+
testID={testID}
|
|
30
|
+
onPress={onPress}
|
|
31
|
+
className="w-20 h-20 md:w-24 md:h-24 rounded-full items-center justify-center active:bg-accent"
|
|
32
|
+
disabled={disabled}
|
|
33
|
+
>
|
|
34
|
+
{label ? (
|
|
35
|
+
<MText
|
|
36
|
+
className={cn("text-3xl md:text-4xl font-light", labelClassName)}
|
|
37
|
+
>
|
|
38
|
+
{label}
|
|
39
|
+
</MText>
|
|
40
|
+
) : (
|
|
41
|
+
(icon ?? <Delete size={26} color={iconTint} />)
|
|
42
|
+
)}
|
|
43
|
+
</Pressable>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export interface MPinPadKeysProps {
|
|
48
|
+
onDigit: (d: string) => void;
|
|
49
|
+
onBackspace: () => void;
|
|
50
|
+
isLoading: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const MPinPadKeys: React.FC<MPinPadKeysProps> = ({
|
|
54
|
+
onDigit,
|
|
55
|
+
onBackspace,
|
|
56
|
+
isLoading
|
|
57
|
+
}) => {
|
|
58
|
+
const { foreground } = useIconColors();
|
|
59
|
+
|
|
60
|
+
const keyRows = [
|
|
61
|
+
["1", "2", "3"],
|
|
62
|
+
["4", "5", "6"],
|
|
63
|
+
["7", "8", "9"]
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<View className="w-full items-center gap-0">
|
|
68
|
+
{keyRows.map((row, rowIndex) => (
|
|
69
|
+
<View key={rowIndex} className="flex-row justify-center gap-24">
|
|
70
|
+
{row.map((digit) => (
|
|
71
|
+
<PinPadKey
|
|
72
|
+
key={digit}
|
|
73
|
+
label={digit}
|
|
74
|
+
onPress={() => onDigit(digit)}
|
|
75
|
+
disabled={isLoading}
|
|
76
|
+
iconTint={foreground}
|
|
77
|
+
labelClassName="text-foreground"
|
|
78
|
+
/>
|
|
79
|
+
))}
|
|
80
|
+
</View>
|
|
81
|
+
))}
|
|
82
|
+
<View className="flex-row justify-center gap-24">
|
|
83
|
+
<View className="w-20 h-20 md:w-24 md:h-24" />
|
|
84
|
+
<PinPadKey
|
|
85
|
+
label="0"
|
|
86
|
+
onPress={() => onDigit("0")}
|
|
87
|
+
disabled={isLoading}
|
|
88
|
+
iconTint={foreground}
|
|
89
|
+
labelClassName="text-foreground"
|
|
90
|
+
/>
|
|
91
|
+
<PinPadKey
|
|
92
|
+
onPress={onBackspace}
|
|
93
|
+
testID="backspace"
|
|
94
|
+
disabled={isLoading}
|
|
95
|
+
iconTint={foreground}
|
|
96
|
+
labelClassName="text-foreground"
|
|
97
|
+
icon={<Delete size={26} color={foreground} />}
|
|
98
|
+
/>
|
|
99
|
+
</View>
|
|
100
|
+
</View>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
MPinPadKeys.displayName = "MPinPadKeys";
|
package/src/MPortal.tsx
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type VariantProps, cva } from "class-variance-authority";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
import { View, type ViewProps } from "react-native";
|
|
4
|
+
import { cn } from "./utils";
|
|
5
|
+
|
|
6
|
+
const progressBarVariants = cva("w-full h-2 rounded-full relative", {
|
|
7
|
+
variants: {
|
|
8
|
+
size: {
|
|
9
|
+
sm: "h-1",
|
|
10
|
+
md: "h-2",
|
|
11
|
+
lg: "h-3"
|
|
12
|
+
},
|
|
13
|
+
variant: {
|
|
14
|
+
default: "bg-muted",
|
|
15
|
+
accent: "bg-accent/30",
|
|
16
|
+
destructive: "bg-destructive/30",
|
|
17
|
+
secondary: "bg-secondary",
|
|
18
|
+
gray: "bg-gray",
|
|
19
|
+
pink: "bg-pink",
|
|
20
|
+
blue: "bg-blue-200",
|
|
21
|
+
green: "bg-green-200"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
size: "md",
|
|
26
|
+
variant: "default"
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const progressFillVariants = cva("h-full rounded-full absolute top-0 left-0", {
|
|
31
|
+
variants: {
|
|
32
|
+
variant: {
|
|
33
|
+
default: "bg-foreground",
|
|
34
|
+
accent: "bg-accent",
|
|
35
|
+
destructive: "bg-destructive",
|
|
36
|
+
secondary: "bg-secondary-foreground",
|
|
37
|
+
gray: "bg-gray-600",
|
|
38
|
+
pink: "bg-pink",
|
|
39
|
+
blue: "bg-blue-600",
|
|
40
|
+
green: "bg-green-600"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
defaultVariants: {
|
|
44
|
+
variant: "default"
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export interface MProgressBarProps
|
|
49
|
+
extends Pick<ViewProps, "className">,
|
|
50
|
+
VariantProps<typeof progressBarVariants> {
|
|
51
|
+
progress: number;
|
|
52
|
+
fillClassName?: ViewProps["className"];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const MProgressBar: React.FC<MProgressBarProps> = ({
|
|
56
|
+
progress,
|
|
57
|
+
size = "md",
|
|
58
|
+
variant = "default",
|
|
59
|
+
className,
|
|
60
|
+
fillClassName
|
|
61
|
+
}) => {
|
|
62
|
+
const clampedProgress = Math.min(Math.max(progress, 0), 100);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<View className={cn(progressBarVariants({ size, variant }), className)}>
|
|
66
|
+
<View
|
|
67
|
+
style={{ width: `${clampedProgress}%` }}
|
|
68
|
+
className={cn(progressFillVariants({ variant }), fillClassName)}
|
|
69
|
+
/>
|
|
70
|
+
</View>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
MProgressBar.displayName = "MProgressBar";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { View, type ViewProps } from "react-native";
|
|
2
|
+
import { MText } from "./MText";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
type MRequiredLabelProps = ViewProps & {
|
|
6
|
+
label: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const MRequiredLabel = ({
|
|
10
|
+
label,
|
|
11
|
+
className,
|
|
12
|
+
...props
|
|
13
|
+
}: MRequiredLabelProps) => (
|
|
14
|
+
<View
|
|
15
|
+
className={cn("flex-row items-center gap-1 mb-3", className)}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
<MText className="font-munchi-bold text-xl">{label}</MText>
|
|
19
|
+
<MText className="font-munchi-bold text-xl text-destructive">*</MText>
|
|
20
|
+
</View>
|
|
21
|
+
);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
interface MResponsiveContainerProps extends Omit<ViewProps, "children"> {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
/**
|
|
8
|
+
* Responsive padding values. Keys are Tailwind breakpoints; values must be
|
|
9
|
+
* valid Tailwind spacing scale steps (0, 1, 2, 3, 4, 5, 6, 8, 10, 12, …).
|
|
10
|
+
*/
|
|
11
|
+
padding?: {
|
|
12
|
+
xs?: number;
|
|
13
|
+
sm?: number;
|
|
14
|
+
md?: number;
|
|
15
|
+
lg?: number;
|
|
16
|
+
xl?: number;
|
|
17
|
+
"2xl"?: number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Responsive margin values. Keys are Tailwind breakpoints; values must be
|
|
21
|
+
* valid Tailwind spacing scale steps (0, 1, 2, 3, 4, 5, 6, 8, 10, 12, …).
|
|
22
|
+
*/
|
|
23
|
+
margin?: {
|
|
24
|
+
xs?: number;
|
|
25
|
+
sm?: number;
|
|
26
|
+
md?: number;
|
|
27
|
+
lg?: number;
|
|
28
|
+
xl?: number;
|
|
29
|
+
"2xl"?: number;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const MResponsiveContainer: React.FC<MResponsiveContainerProps> = ({
|
|
34
|
+
children,
|
|
35
|
+
className,
|
|
36
|
+
style,
|
|
37
|
+
padding,
|
|
38
|
+
margin,
|
|
39
|
+
...rest
|
|
40
|
+
}) => {
|
|
41
|
+
const getResponsiveClasses = () => {
|
|
42
|
+
const classes: string[] = [];
|
|
43
|
+
|
|
44
|
+
if (padding) {
|
|
45
|
+
for (const [size, value] of Object.entries(padding)) {
|
|
46
|
+
if (value !== undefined) {
|
|
47
|
+
classes.push(`${size}:p-${value}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (margin) {
|
|
53
|
+
for (const [size, value] of Object.entries(margin)) {
|
|
54
|
+
if (value !== undefined) {
|
|
55
|
+
classes.push(`${size}:m-${value}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return classes.join(" ");
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<View
|
|
65
|
+
className={cn(getResponsiveClasses(), className)}
|
|
66
|
+
style={style}
|
|
67
|
+
{...rest}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</View>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
MResponsiveContainer.displayName = "MResponsiveContainer";
|
package/src/MSearch.tsx
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { type VariantProps, cva } from "class-variance-authority";
|
|
2
|
+
import { Search, X } from "lucide-react-native";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import {
|
|
5
|
+
Keyboard,
|
|
6
|
+
Pressable,
|
|
7
|
+
TextInput,
|
|
8
|
+
type TextInputProps,
|
|
9
|
+
type TextProps,
|
|
10
|
+
View,
|
|
11
|
+
type ViewProps
|
|
12
|
+
} from "react-native";
|
|
13
|
+
import { useIconColors } from "./hooks/useIconColors";
|
|
14
|
+
import { cn } from "./utils";
|
|
15
|
+
|
|
16
|
+
const containerVariants = cva(
|
|
17
|
+
"flex-row items-center bg-card rounded-full border border-border",
|
|
18
|
+
{
|
|
19
|
+
variants: {
|
|
20
|
+
size: {
|
|
21
|
+
default: "",
|
|
22
|
+
sm: "",
|
|
23
|
+
lg: ""
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
defaultVariants: {
|
|
27
|
+
size: "default"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const INPUT_SIZE: Record<string, number> = {
|
|
33
|
+
default: 18,
|
|
34
|
+
sm: 15,
|
|
35
|
+
lg: 21
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const INPUT_HEIGHT: Record<string, number> = {
|
|
39
|
+
default: 36,
|
|
40
|
+
sm: 30,
|
|
41
|
+
lg: 52
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const inputVariants = cva("flex-1 min-w-0 px-2 font-munchi", {
|
|
45
|
+
variants: {
|
|
46
|
+
size: {
|
|
47
|
+
default: "text-lg",
|
|
48
|
+
sm: "text-base",
|
|
49
|
+
lg: "text-xl"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
defaultVariants: {
|
|
53
|
+
size: "default"
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export interface MSearchProps
|
|
58
|
+
extends Omit<TextInputProps, "className">,
|
|
59
|
+
VariantProps<typeof containerVariants> {
|
|
60
|
+
value: string;
|
|
61
|
+
onChangeText: (text: string) => void;
|
|
62
|
+
onClear?: () => void;
|
|
63
|
+
placeholder?: string;
|
|
64
|
+
className?: ViewProps["className"];
|
|
65
|
+
containerClassName?: ViewProps["className"];
|
|
66
|
+
inputClassName?: TextProps["className"];
|
|
67
|
+
iconSize?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const MSearch = React.forwardRef<TextInput, MSearchProps>(
|
|
71
|
+
(
|
|
72
|
+
{
|
|
73
|
+
value,
|
|
74
|
+
onChangeText,
|
|
75
|
+
onClear,
|
|
76
|
+
placeholder = "Search...",
|
|
77
|
+
size = "default",
|
|
78
|
+
className,
|
|
79
|
+
containerClassName,
|
|
80
|
+
inputClassName,
|
|
81
|
+
iconSize = 20,
|
|
82
|
+
placeholderTextColor: placeholderTextColorProp,
|
|
83
|
+
...textInputProps
|
|
84
|
+
},
|
|
85
|
+
ref
|
|
86
|
+
) => {
|
|
87
|
+
const { foreground, muted } = useIconColors();
|
|
88
|
+
|
|
89
|
+
const handleClear = () => {
|
|
90
|
+
if (onClear) onClear();
|
|
91
|
+
else onChangeText("");
|
|
92
|
+
if (Keyboard.isVisible()) Keyboard.dismiss();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<View
|
|
97
|
+
className={cn(
|
|
98
|
+
containerVariants({ size }),
|
|
99
|
+
className,
|
|
100
|
+
containerClassName
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
<View className="pl-3 shrink-0">
|
|
104
|
+
<Search size={iconSize} color={muted} />
|
|
105
|
+
</View>
|
|
106
|
+
<TextInput
|
|
107
|
+
ref={ref}
|
|
108
|
+
placeholder={placeholder}
|
|
109
|
+
placeholderTextColor={placeholderTextColorProp ?? muted}
|
|
110
|
+
value={value}
|
|
111
|
+
onChangeText={onChangeText}
|
|
112
|
+
textAlignVertical="center"
|
|
113
|
+
style={{
|
|
114
|
+
color: foreground,
|
|
115
|
+
fontSize: INPUT_SIZE[size ?? "default"],
|
|
116
|
+
height: INPUT_HEIGHT[size ?? "default"],
|
|
117
|
+
paddingVertical: 0,
|
|
118
|
+
includeFontPadding: false
|
|
119
|
+
}}
|
|
120
|
+
className={cn(inputVariants({ size }), inputClassName)}
|
|
121
|
+
{...textInputProps}
|
|
122
|
+
/>
|
|
123
|
+
<View className="pr-2 shrink-0" style={{ width: iconSize + 16 }}>
|
|
124
|
+
{value.length > 0 && (
|
|
125
|
+
<Pressable
|
|
126
|
+
onPress={handleClear}
|
|
127
|
+
className="items-center justify-center p-2"
|
|
128
|
+
>
|
|
129
|
+
<X size={iconSize} color={muted} />
|
|
130
|
+
</Pressable>
|
|
131
|
+
)}
|
|
132
|
+
</View>
|
|
133
|
+
</View>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
MSearch.displayName = "MSearch";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Pressable, Text, View } from "react-native";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export interface MSelectorProps {
|
|
6
|
+
label: string;
|
|
7
|
+
isSelected: boolean;
|
|
8
|
+
onPress: () => void;
|
|
9
|
+
description?: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const MSelector = React.memo(
|
|
13
|
+
({ label, isSelected, onPress, description }: MSelectorProps) => {
|
|
14
|
+
return (
|
|
15
|
+
<Pressable
|
|
16
|
+
className="flex flex-row justify-between items-center p-2"
|
|
17
|
+
onPress={onPress}
|
|
18
|
+
>
|
|
19
|
+
<Text
|
|
20
|
+
className={cn("font-munchi", isSelected && "font-munchi-semibold")}
|
|
21
|
+
>
|
|
22
|
+
{label}
|
|
23
|
+
</Text>
|
|
24
|
+
|
|
25
|
+
{isSelected && description && (
|
|
26
|
+
<Text className="text-muted-foreground text-sm mx-2 flex-1 text-right">
|
|
27
|
+
{description}
|
|
28
|
+
</Text>
|
|
29
|
+
)}
|
|
30
|
+
|
|
31
|
+
<View
|
|
32
|
+
className={cn(
|
|
33
|
+
"w-6 h-6 rounded-full border-2 border-primary ml-2",
|
|
34
|
+
!isSelected && "bg-card"
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
{isSelected && (
|
|
38
|
+
<View className="absolute inset-0 m-1 rounded-full bg-primary" />
|
|
39
|
+
)}
|
|
40
|
+
</View>
|
|
41
|
+
</Pressable>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
MSelector.displayName = "MSelector";
|
|
47
|
+
|
|
48
|
+
export default MSelector;
|
package/src/MSwitch.tsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Switch } from "react-native";
|
|
2
|
+
import type { SwitchProps } from "react-native";
|
|
3
|
+
|
|
4
|
+
export type MSwitchProps = Omit<
|
|
5
|
+
SwitchProps,
|
|
6
|
+
"trackColor" | "thumbColor" | "ios_backgroundColor"
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
export const MSwitch = (props: MSwitchProps) => {
|
|
10
|
+
return <Switch {...props} />;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
MSwitch.displayName = "MSwitch";
|
package/src/MTable.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Table,
|
|
3
|
+
TableBody,
|
|
4
|
+
TableCell,
|
|
5
|
+
TableFooter,
|
|
6
|
+
TableHead,
|
|
7
|
+
TableHeader,
|
|
8
|
+
TableRow
|
|
9
|
+
} from "./primitives/table";
|
|
10
|
+
|
|
11
|
+
export { Table as MTable };
|
|
12
|
+
export { TableBody as MTableBody };
|
|
13
|
+
export { TableCell as MTableCell };
|
|
14
|
+
export { TableFooter as MTableFooter };
|
|
15
|
+
export { TableHead as MTableHead };
|
|
16
|
+
export { TableHeader as MTableHeader };
|
|
17
|
+
export { TableRow as MTableRow };
|
package/src/MTabs.tsx
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
type LayoutRectangle,
|
|
5
|
+
Pressable,
|
|
6
|
+
View,
|
|
7
|
+
type ViewProps
|
|
8
|
+
} from "react-native";
|
|
9
|
+
import Animated, {
|
|
10
|
+
useAnimatedStyle,
|
|
11
|
+
useSharedValue,
|
|
12
|
+
withSpring
|
|
13
|
+
} from "react-native-reanimated";
|
|
14
|
+
import { MText } from "./MText";
|
|
15
|
+
import { NAV_THEME } from "./constants";
|
|
16
|
+
import { useColorScheme } from "./hooks/useColorScheme";
|
|
17
|
+
import { cn } from "./utils";
|
|
18
|
+
|
|
19
|
+
export interface Tab<T> {
|
|
20
|
+
key: T;
|
|
21
|
+
label?: string;
|
|
22
|
+
icon?: React.ReactNode;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MTabsProps<T> extends Omit<ViewProps, "children"> {
|
|
27
|
+
tabs: Tab<T>[];
|
|
28
|
+
activeTab: T | null;
|
|
29
|
+
onTabChange: (tab: T) => void;
|
|
30
|
+
tabClassName?: string;
|
|
31
|
+
activeTabClassName?: string;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
size?: "default" | "compact";
|
|
34
|
+
variant?: "default" | "ghost";
|
|
35
|
+
stretch?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SPRING = { damping: 20, stiffness: 200, mass: 0.8 };
|
|
39
|
+
|
|
40
|
+
export const MTabs = <T,>({
|
|
41
|
+
tabs,
|
|
42
|
+
activeTab,
|
|
43
|
+
onTabChange,
|
|
44
|
+
className,
|
|
45
|
+
style,
|
|
46
|
+
disabled = false,
|
|
47
|
+
size = "default",
|
|
48
|
+
variant = "default",
|
|
49
|
+
stretch = false,
|
|
50
|
+
...viewProps
|
|
51
|
+
}: MTabsProps<T>) => {
|
|
52
|
+
const { colorScheme } = useColorScheme();
|
|
53
|
+
const theme = NAV_THEME[colorScheme === "dark" ? "dark" : "light"];
|
|
54
|
+
const isCompact = size === "compact";
|
|
55
|
+
const isGhost = variant === "ghost";
|
|
56
|
+
const indicatorInset = isGhost ? 0 : isCompact ? 2 : 4;
|
|
57
|
+
const indicatorRadius = isGhost ? 20 : isCompact ? 8 : 12;
|
|
58
|
+
const pressPadH = isCompact ? 8 : 12;
|
|
59
|
+
const pressPadV = isGhost ? 7 : isCompact ? 2 : 4;
|
|
60
|
+
const pressRadius = isGhost ? 22 : isCompact ? 8 : 12;
|
|
61
|
+
const innerGap = isCompact ? 2 : 4;
|
|
62
|
+
|
|
63
|
+
const layouts = useRef<Record<string, LayoutRectangle>>({});
|
|
64
|
+
const indicatorX = useSharedValue(0);
|
|
65
|
+
const indicatorW = useSharedValue(0);
|
|
66
|
+
const initialized = useRef(false);
|
|
67
|
+
|
|
68
|
+
const [, forceUpdate] = useState(0);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (activeTab == null) {
|
|
72
|
+
initialized.current = false;
|
|
73
|
+
indicatorW.value = withSpring(0, SPRING);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const keyStr = String(activeTab);
|
|
77
|
+
const rect = layouts.current[keyStr];
|
|
78
|
+
if (rect) {
|
|
79
|
+
indicatorX.value = withSpring(rect.x, SPRING);
|
|
80
|
+
indicatorW.value = withSpring(rect.width, SPRING);
|
|
81
|
+
initialized.current = true;
|
|
82
|
+
forceUpdate((n) => n + 1);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
initialized.current = false;
|
|
86
|
+
}, [activeTab, indicatorW, indicatorX]);
|
|
87
|
+
|
|
88
|
+
const indicatorStyle = useAnimatedStyle(() => ({
|
|
89
|
+
transform: [{ translateX: indicatorX.value }],
|
|
90
|
+
width: indicatorW.value
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
const handleLayout = (key: string, rect: LayoutRectangle) => {
|
|
94
|
+
layouts.current[key] = rect;
|
|
95
|
+
|
|
96
|
+
const activeKey = String(activeTab);
|
|
97
|
+
if (key !== activeKey) return;
|
|
98
|
+
|
|
99
|
+
if (!initialized.current) {
|
|
100
|
+
indicatorX.value = rect.x;
|
|
101
|
+
indicatorW.value = rect.width;
|
|
102
|
+
initialized.current = true;
|
|
103
|
+
forceUpdate((n) => n + 1);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
indicatorX.value = withSpring(rect.x, SPRING);
|
|
107
|
+
indicatorW.value = withSpring(rect.width, SPRING);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleTabPress = (key: T) => {
|
|
111
|
+
const keyStr = String(key);
|
|
112
|
+
const rect = layouts.current[keyStr];
|
|
113
|
+
if (rect) {
|
|
114
|
+
indicatorX.value = withSpring(rect.x, SPRING);
|
|
115
|
+
indicatorW.value = withSpring(rect.width, SPRING);
|
|
116
|
+
}
|
|
117
|
+
onTabChange(key);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<View
|
|
122
|
+
className={cn(
|
|
123
|
+
"flex-row",
|
|
124
|
+
!isGhost && "bg-accent",
|
|
125
|
+
isCompact ? "gap-0.5 rounded-xl p-0.5" : "gap-1 rounded-2xl p-1",
|
|
126
|
+
stretch ? "self-stretch" : "self-start",
|
|
127
|
+
className
|
|
128
|
+
)}
|
|
129
|
+
style={style}
|
|
130
|
+
{...viewProps}
|
|
131
|
+
>
|
|
132
|
+
<Animated.View
|
|
133
|
+
pointerEvents="none"
|
|
134
|
+
style={[
|
|
135
|
+
{
|
|
136
|
+
position: "absolute",
|
|
137
|
+
top: indicatorInset,
|
|
138
|
+
bottom: indicatorInset,
|
|
139
|
+
borderRadius: indicatorRadius,
|
|
140
|
+
backgroundColor: theme.card,
|
|
141
|
+
shadowColor: theme.text,
|
|
142
|
+
shadowOpacity: isGhost ? 0 : colorScheme === "dark" ? 0.08 : 0.14,
|
|
143
|
+
shadowRadius: isCompact ? 4 : 6,
|
|
144
|
+
shadowOffset: { width: 0, height: isCompact ? 1 : 2 },
|
|
145
|
+
elevation: isCompact ? 2 : 3
|
|
146
|
+
},
|
|
147
|
+
indicatorStyle
|
|
148
|
+
]}
|
|
149
|
+
/>
|
|
150
|
+
{tabs.map(({ key, label, icon, disabled: tabDisabled = false }) => {
|
|
151
|
+
const isDisabled = disabled || tabDisabled;
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<Pressable
|
|
155
|
+
key={String(key)}
|
|
156
|
+
onPress={() => handleTabPress(key)}
|
|
157
|
+
disabled={isDisabled}
|
|
158
|
+
onLayout={(e) => handleLayout(String(key), e.nativeEvent.layout)}
|
|
159
|
+
style={{
|
|
160
|
+
...(stretch
|
|
161
|
+
? { flex: 1, minWidth: 0 }
|
|
162
|
+
: { flexShrink: 1, minWidth: 0 }),
|
|
163
|
+
alignItems: "center",
|
|
164
|
+
justifyContent: "center",
|
|
165
|
+
paddingHorizontal: pressPadH,
|
|
166
|
+
paddingVertical: pressPadV,
|
|
167
|
+
borderRadius: pressRadius,
|
|
168
|
+
opacity: isDisabled ? 0.5 : 1
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
<View
|
|
172
|
+
style={{
|
|
173
|
+
flexDirection: "row",
|
|
174
|
+
alignItems: "center",
|
|
175
|
+
gap: innerGap,
|
|
176
|
+
flexShrink: 1,
|
|
177
|
+
overflow: "hidden"
|
|
178
|
+
}}
|
|
179
|
+
>
|
|
180
|
+
{icon}
|
|
181
|
+
{label && (
|
|
182
|
+
<MText
|
|
183
|
+
size={isGhost ? "lg" : "xl"}
|
|
184
|
+
numberOfLines={1}
|
|
185
|
+
style={{ flexShrink: 1 }}
|
|
186
|
+
>
|
|
187
|
+
{label}
|
|
188
|
+
</MText>
|
|
189
|
+
)}
|
|
190
|
+
</View>
|
|
191
|
+
</Pressable>
|
|
192
|
+
);
|
|
193
|
+
})}
|
|
194
|
+
</View>
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
MTabs.displayName = "MTabs";
|