@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.
Files changed (83) hide show
  1. package/dist/index.d.mts +568 -0
  2. package/dist/index.d.ts +568 -0
  3. package/dist/index.js +1 -0
  4. package/dist/index.mjs +1 -0
  5. package/global.css +53 -0
  6. package/nativewind-env.d.ts +2 -0
  7. package/package.json +88 -0
  8. package/src/MAlert.tsx +38 -0
  9. package/src/MAnimation.tsx +55 -0
  10. package/src/MAvatar.tsx +111 -0
  11. package/src/MBadge.tsx +72 -0
  12. package/src/MButton.tsx +90 -0
  13. package/src/MCard.tsx +15 -0
  14. package/src/MChevron.tsx +47 -0
  15. package/src/MConfirmation.tsx +68 -0
  16. package/src/MCountDown.tsx +120 -0
  17. package/src/MDateTimePicker.tsx +124 -0
  18. package/src/MDivider.tsx +69 -0
  19. package/src/MDrawerRightPanel.tsx +187 -0
  20. package/src/MDropdown.tsx +277 -0
  21. package/src/MInput.tsx +162 -0
  22. package/src/MLabel.tsx +3 -0
  23. package/src/MLucideIcon.tsx +21 -0
  24. package/src/MModal.tsx +287 -0
  25. package/src/MNativeAlert.tsx +33 -0
  26. package/src/MNumpad.tsx +520 -0
  27. package/src/MPicker.tsx +150 -0
  28. package/src/MPinPadKeys.tsx +104 -0
  29. package/src/MPortal.tsx +4 -0
  30. package/src/MProgressBar.tsx +74 -0
  31. package/src/MRadioGroup.tsx +4 -0
  32. package/src/MRequiredLabel.tsx +21 -0
  33. package/src/MResponsiveContainer.tsx +74 -0
  34. package/src/MSearch.tsx +138 -0
  35. package/src/MSelector.tsx +48 -0
  36. package/src/MSkeleton.tsx +3 -0
  37. package/src/MSwitch.tsx +13 -0
  38. package/src/MTable.tsx +17 -0
  39. package/src/MTabs.tsx +198 -0
  40. package/src/MText.tsx +51 -0
  41. package/src/MTimerUp.tsx +88 -0
  42. package/src/MToggle.tsx +51 -0
  43. package/src/constants.ts +19 -0
  44. package/src/hooks/useColorScheme.tsx +12 -0
  45. package/src/hooks/useIconColors.ts +19 -0
  46. package/src/index.ts +124 -0
  47. package/src/primitives/accordion.tsx +143 -0
  48. package/src/primitives/alert-dialog.tsx +181 -0
  49. package/src/primitives/alert.tsx +94 -0
  50. package/src/primitives/aspect-ratio.tsx +5 -0
  51. package/src/primitives/avatar.tsx +47 -0
  52. package/src/primitives/badge.tsx +57 -0
  53. package/src/primitives/button.tsx +92 -0
  54. package/src/primitives/card.tsx +86 -0
  55. package/src/primitives/checkbox.tsx +35 -0
  56. package/src/primitives/collapsible.tsx +9 -0
  57. package/src/primitives/context-menu.tsx +255 -0
  58. package/src/primitives/dialog.tsx +166 -0
  59. package/src/primitives/dropdown-menu.tsx +264 -0
  60. package/src/primitives/hover-card.tsx +45 -0
  61. package/src/primitives/input.tsx +25 -0
  62. package/src/primitives/label.tsx +33 -0
  63. package/src/primitives/menubar.tsx +266 -0
  64. package/src/primitives/navigation-menu.tsx +192 -0
  65. package/src/primitives/popover.tsx +46 -0
  66. package/src/primitives/progress.tsx +82 -0
  67. package/src/primitives/radio-group.tsx +42 -0
  68. package/src/primitives/select.tsx +192 -0
  69. package/src/primitives/separator.tsx +28 -0
  70. package/src/primitives/skeleton.tsx +39 -0
  71. package/src/primitives/switch.tsx +102 -0
  72. package/src/primitives/table.tsx +107 -0
  73. package/src/primitives/tabs.tsx +66 -0
  74. package/src/primitives/text.tsx +28 -0
  75. package/src/primitives/textarea.tsx +39 -0
  76. package/src/primitives/toggle-group.tsx +89 -0
  77. package/src/primitives/toggle.tsx +91 -0
  78. package/src/primitives/tooltip.tsx +40 -0
  79. package/src/primitives/typography.tsx +214 -0
  80. package/src/theme.ts +43 -0
  81. package/src/tokens.ts +7 -0
  82. package/src/utils.ts +14 -0
  83. package/tailwind.config.ts +112 -0
@@ -0,0 +1,187 @@
1
+ import type { ReactNode } from "react";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import {
4
+ Modal,
5
+ Platform,
6
+ ScrollView,
7
+ TouchableWithoutFeedback,
8
+ View,
9
+ useWindowDimensions
10
+ } from "react-native";
11
+ import Animated, {
12
+ useAnimatedStyle,
13
+ useSharedValue,
14
+ withSpring,
15
+ withTiming
16
+ } from "react-native-reanimated";
17
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
18
+ import { NAV_THEME } from "./constants";
19
+ import { useColorScheme } from "./hooks/useColorScheme";
20
+ import { cn } from "./utils";
21
+
22
+ export interface MDrawerRightPanelProps {
23
+ isVisible: boolean;
24
+ onClose: () => void;
25
+ header?: ReactNode;
26
+ footer?: ReactNode;
27
+ children: ReactNode;
28
+ closable?: boolean;
29
+ widthFraction?: number;
30
+ className?: string;
31
+ scrollable?: boolean;
32
+ }
33
+
34
+ const OPEN_SPRING = {
35
+ mass: 0.4,
36
+ stiffness: 110,
37
+ damping: 16,
38
+ overshootClamping: false
39
+ } as const;
40
+
41
+ const CLOSE_SPRING = {
42
+ mass: 0.4,
43
+ stiffness: 140,
44
+ damping: 22,
45
+ overshootClamping: true
46
+ } as const;
47
+
48
+ const CLOSE_DURATION_MS = 340;
49
+
50
+ export const MDrawerRightPanel = ({
51
+ isVisible,
52
+ onClose,
53
+ header,
54
+ footer,
55
+ children,
56
+ closable = true,
57
+ widthFraction = 0.45,
58
+ className,
59
+ scrollable = true
60
+ }: MDrawerRightPanelProps) => {
61
+ const { colorScheme } = useColorScheme();
62
+ const theme = NAV_THEME[colorScheme];
63
+ const { width } = useWindowDimensions();
64
+ const insets = useSafeAreaInsets();
65
+ const panelWidth = width * widthFraction;
66
+
67
+ const [modalVisible, setModalVisible] = useState(false);
68
+ const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
69
+ const isAnimatingClose = useRef(false);
70
+
71
+ const translateX = useSharedValue(panelWidth);
72
+ const backdropOpacity = useSharedValue(0);
73
+
74
+ const runCloseAnimation = (callback?: () => void) => {
75
+ if (isAnimatingClose.current) return;
76
+ isAnimatingClose.current = true;
77
+
78
+ translateX.value = withSpring(panelWidth, CLOSE_SPRING);
79
+ backdropOpacity.value = withTiming(0, { duration: CLOSE_DURATION_MS });
80
+
81
+ closeTimerRef.current = setTimeout(() => {
82
+ setModalVisible(false);
83
+ isAnimatingClose.current = false;
84
+ callback?.();
85
+ }, CLOSE_DURATION_MS);
86
+ };
87
+
88
+ useEffect(() => {
89
+ if (isVisible) {
90
+ if (closeTimerRef.current) {
91
+ clearTimeout(closeTimerRef.current);
92
+ closeTimerRef.current = null;
93
+ isAnimatingClose.current = false;
94
+ }
95
+ setModalVisible(true);
96
+ translateX.value = withSpring(0, OPEN_SPRING);
97
+ backdropOpacity.value = withTiming(1, { duration: 300 });
98
+ } else {
99
+ runCloseAnimation();
100
+ }
101
+
102
+ return () => {
103
+ if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
104
+ };
105
+ }, [isVisible]);
106
+
107
+ const handleBackdropPress = () => {
108
+ if (!closable) return;
109
+ runCloseAnimation(onClose);
110
+ };
111
+
112
+ const handleCloseButton = () => {
113
+ runCloseAnimation(onClose);
114
+ };
115
+
116
+ const panelStyle = useAnimatedStyle(() => ({
117
+ transform: [{ translateX: translateX.value }]
118
+ }));
119
+
120
+ const backdropStyle = useAnimatedStyle(() => ({
121
+ opacity: backdropOpacity.value
122
+ }));
123
+
124
+ return (
125
+ <Modal
126
+ transparent
127
+ visible={modalVisible}
128
+ animationType="none"
129
+ onRequestClose={closable ? handleCloseButton : undefined}
130
+ >
131
+ <View
132
+ className="flex-1 flex-row justify-end"
133
+ style={{ paddingTop: insets.top, paddingBottom: insets.bottom }}
134
+ >
135
+ <TouchableWithoutFeedback onPress={handleBackdropPress}>
136
+ <Animated.View
137
+ className="absolute inset-0 bg-black/60"
138
+ style={backdropStyle}
139
+ />
140
+ </TouchableWithoutFeedback>
141
+
142
+ <Animated.View
143
+ style={[panelStyle, { width: panelWidth }]}
144
+ className={cn(
145
+ "h-full overflow-hidden border rounded-2xl border-border",
146
+ className
147
+ )}
148
+ >
149
+ <View
150
+ className="flex-1"
151
+ style={{ backgroundColor: theme.background }}
152
+ >
153
+ {header && (
154
+ <View className="px-6 pt-6 flex-shrink-0">{header}</View>
155
+ )}
156
+
157
+ {scrollable ? (
158
+ <ScrollView
159
+ className="flex-1 px-6"
160
+ showsVerticalScrollIndicator={false}
161
+ contentContainerStyle={{
162
+ paddingTop: 16,
163
+ paddingBottom: 24
164
+ }}
165
+ keyboardShouldPersistTaps="handled"
166
+ automaticallyAdjustKeyboardInsets={Platform.OS === "ios"}
167
+ keyboardDismissMode="interactive"
168
+ >
169
+ {children}
170
+ </ScrollView>
171
+ ) : (
172
+ <View className="flex-1 overflow-hidden">{children}</View>
173
+ )}
174
+
175
+ {footer && (
176
+ <View className="px-6 pb-6 pt-4 border-t-2 border-border flex-shrink-0">
177
+ {footer}
178
+ </View>
179
+ )}
180
+ </View>
181
+ </Animated.View>
182
+ </View>
183
+ </Modal>
184
+ );
185
+ };
186
+
187
+ MDrawerRightPanel.displayName = "MDrawerRightPanel";
@@ -0,0 +1,277 @@
1
+ import { ArrowDown } from "lucide-react-native";
2
+ import { useMemo } from "react";
3
+ import { type StyleProp, StyleSheet, type ViewStyle } from "react-native";
4
+ import { Dropdown } from "react-native-element-dropdown";
5
+ import type { DropdownProps } from "react-native-element-dropdown/lib/typescript/components/Dropdown/model";
6
+ import { NAV_THEME } from "./constants";
7
+ import { useColorScheme } from "./hooks/useColorScheme";
8
+ import { useIconColors } from "./hooks/useIconColors";
9
+ import { darkVars, lightVars } from "./theme";
10
+
11
+ export type MDropdownFieldFrame =
12
+ | "none"
13
+ | "mInputSm2xl"
14
+ | "mInputCompact"
15
+ | "pill";
16
+
17
+ export type MDropdownProps<T = unknown> = DropdownProps<T> & {
18
+ customStyles?: StyleProp<ViewStyle>;
19
+ fieldFrame?: MDropdownFieldFrame;
20
+ };
21
+
22
+ function mInputSm2xlFieldPreset(args: {
23
+ cardColor: string;
24
+ borderColor: string;
25
+ textColor: string;
26
+ mutedColor: string;
27
+ }): {
28
+ trigger: ViewStyle;
29
+ containerStyle: ViewStyle;
30
+ itemContainerStyle: ViewStyle;
31
+ activeColor: string;
32
+ } {
33
+ const r = 16;
34
+ return {
35
+ trigger: {
36
+ width: "100%",
37
+ paddingHorizontal: 16,
38
+ paddingVertical: 12,
39
+ borderRadius: r,
40
+ borderWidth: 1,
41
+ borderColor: args.borderColor,
42
+ backgroundColor: args.cardColor
43
+ },
44
+ containerStyle: {
45
+ marginTop: 6,
46
+ borderRadius: r,
47
+ borderWidth: 1,
48
+ borderColor: args.borderColor,
49
+ backgroundColor: args.cardColor,
50
+ shadowColor: args.textColor,
51
+ shadowOpacity: 0.06,
52
+ shadowRadius: 8,
53
+ shadowOffset: { width: 0, height: 2 },
54
+ elevation: 3,
55
+ overflow: "hidden"
56
+ },
57
+ itemContainerStyle: {
58
+ backgroundColor: args.cardColor,
59
+ padding: 0
60
+ },
61
+ activeColor: args.mutedColor
62
+ };
63
+ }
64
+
65
+ function mInputCompactFieldPreset(args: {
66
+ cardColor: string;
67
+ borderColor: string;
68
+ textColor: string;
69
+ mutedColor: string;
70
+ }): {
71
+ trigger: ViewStyle;
72
+ containerStyle: ViewStyle;
73
+ itemContainerStyle: ViewStyle;
74
+ activeColor: string;
75
+ } {
76
+ const r = 12;
77
+ return {
78
+ trigger: {
79
+ width: "100%",
80
+ paddingHorizontal: 12,
81
+ paddingVertical: 8,
82
+ borderRadius: r,
83
+ borderWidth: 1,
84
+ borderColor: args.borderColor,
85
+ backgroundColor: args.cardColor
86
+ },
87
+ containerStyle: {
88
+ marginTop: 4,
89
+ borderRadius: r,
90
+ borderWidth: 1,
91
+ borderColor: args.borderColor,
92
+ backgroundColor: args.cardColor,
93
+ shadowColor: args.textColor,
94
+ shadowOpacity: 0.06,
95
+ shadowRadius: 6,
96
+ shadowOffset: { width: 0, height: 1 },
97
+ elevation: 2,
98
+ overflow: "hidden"
99
+ },
100
+ itemContainerStyle: {
101
+ backgroundColor: args.cardColor,
102
+ padding: 0
103
+ },
104
+ activeColor: args.mutedColor
105
+ };
106
+ }
107
+
108
+ function pillFieldPreset(args: {
109
+ bgColor: string;
110
+ cardColor: string;
111
+ borderColor: string;
112
+ textColor: string;
113
+ mutedColor: string;
114
+ }): {
115
+ trigger: ViewStyle;
116
+ containerStyle: ViewStyle;
117
+ itemContainerStyle: ViewStyle;
118
+ activeColor: string;
119
+ } {
120
+ return {
121
+ trigger: {
122
+ paddingHorizontal: 16,
123
+ paddingVertical: 8,
124
+ borderRadius: 999,
125
+ borderWidth: 0,
126
+ backgroundColor: args.bgColor,
127
+ minWidth: 160
128
+ },
129
+ containerStyle: {
130
+ marginTop: 4,
131
+ borderRadius: 16,
132
+ borderWidth: 1,
133
+ borderColor: args.borderColor,
134
+ backgroundColor: args.cardColor,
135
+ shadowColor: args.textColor,
136
+ shadowOpacity: 0.06,
137
+ shadowRadius: 6,
138
+ shadowOffset: { width: 0, height: 1 },
139
+ elevation: 2,
140
+ overflow: "hidden"
141
+ },
142
+ itemContainerStyle: {
143
+ backgroundColor: args.cardColor,
144
+ padding: 0
145
+ },
146
+ activeColor: args.mutedColor
147
+ };
148
+ }
149
+
150
+ export const MDropdown = <T = unknown>({
151
+ data = [],
152
+ placeholder = "",
153
+ value,
154
+ onFocus,
155
+ onBlur,
156
+ onChange,
157
+ customStyles,
158
+ fieldFrame = "none",
159
+ style,
160
+ containerStyle,
161
+ itemContainerStyle,
162
+ selectedTextStyle,
163
+ placeholderStyle,
164
+ activeColor: activeColorProp,
165
+ maxHeight = 300,
166
+ renderRightIcon: renderRightIconProp,
167
+ selectedTextProps: selectedTextPropsProp,
168
+ mode = "auto",
169
+ ...rest
170
+ }: MDropdownProps<T>) => {
171
+ const { colorScheme } = useColorScheme();
172
+ const { foreground, muted } = useIconColors();
173
+ const isEmpty = data.length === 0;
174
+ const isDark = colorScheme === "dark";
175
+ const theme = NAV_THEME[isDark ? "dark" : "light"];
176
+ const vars = isDark ? darkVars : lightVars;
177
+ const mutedColor = `hsl(${vars["--muted"]})`;
178
+
179
+ const framePreset = useMemo(() => {
180
+ const args = {
181
+ cardColor: theme.card,
182
+ borderColor: theme.border,
183
+ textColor: theme.text,
184
+ mutedColor
185
+ };
186
+ if (fieldFrame === "mInputSm2xl") return mInputSm2xlFieldPreset(args);
187
+ if (fieldFrame === "mInputCompact") return mInputCompactFieldPreset(args);
188
+ if (fieldFrame === "pill") {
189
+ return pillFieldPreset({
190
+ ...args,
191
+ bgColor: mutedColor
192
+ });
193
+ }
194
+ return null;
195
+ }, [fieldFrame, theme.card, theme.border, theme.text, mutedColor]);
196
+
197
+ const mergedStyle = StyleSheet.flatten([
198
+ framePreset?.trigger,
199
+ customStyles,
200
+ style
201
+ ]);
202
+
203
+ const mergedContainerStyle = StyleSheet.flatten([
204
+ framePreset?.containerStyle,
205
+ containerStyle
206
+ ]);
207
+
208
+ const mergedItemContainerStyle = StyleSheet.flatten([
209
+ framePreset?.itemContainerStyle,
210
+ itemContainerStyle
211
+ ]);
212
+
213
+ const textSize =
214
+ fieldFrame === "mInputSm2xl"
215
+ ? 18
216
+ : fieldFrame === "mInputCompact"
217
+ ? 15
218
+ : 14;
219
+
220
+ const mergedSelectedTextStyle = StyleSheet.flatten([
221
+ { fontSize: textSize, color: foreground },
222
+ selectedTextStyle
223
+ ]);
224
+
225
+ const mergedPlaceholderStyle = StyleSheet.flatten([
226
+ {
227
+ fontSize: textSize,
228
+ color: isEmpty ? muted : foreground
229
+ },
230
+ placeholderStyle
231
+ ]);
232
+
233
+ const listActiveColor = framePreset
234
+ ? framePreset.activeColor
235
+ : (activeColorProp ?? mutedColor);
236
+
237
+ const rightIconSize = fieldFrame === "mInputCompact" ? 16 : 20;
238
+
239
+ return (
240
+ <Dropdown
241
+ {...rest}
242
+ mode={mode}
243
+ data={data}
244
+ maxHeight={maxHeight}
245
+ placeholder={placeholder}
246
+ value={value}
247
+ onFocus={onFocus}
248
+ onBlur={onBlur}
249
+ onChange={onChange}
250
+ style={mergedStyle}
251
+ containerStyle={mergedContainerStyle}
252
+ itemContainerStyle={mergedItemContainerStyle}
253
+ activeColor={listActiveColor}
254
+ fontFamily="DMSans_400Regular"
255
+ selectedTextStyle={mergedSelectedTextStyle}
256
+ selectedTextProps={{
257
+ numberOfLines: 1,
258
+ ellipsizeMode: "tail",
259
+ ...(selectedTextPropsProp ?? {})
260
+ }}
261
+ placeholderStyle={mergedPlaceholderStyle}
262
+ renderRightIcon={
263
+ renderRightIconProp ??
264
+ (() => (
265
+ <ArrowDown
266
+ color={isEmpty ? muted : foreground}
267
+ size={rightIconSize}
268
+ />
269
+ ))
270
+ }
271
+ />
272
+ );
273
+ };
274
+
275
+ MDropdown.displayName = "MDropdown";
276
+
277
+ export default MDropdown;
package/src/MInput.tsx ADDED
@@ -0,0 +1,162 @@
1
+ import { type VariantProps, cva } from "class-variance-authority";
2
+ import { AlertCircle } from "lucide-react-native";
3
+ import { type ReactNode, useRef, useState } from "react";
4
+ import {
5
+ Pressable,
6
+ TextInput,
7
+ type TextInputProps,
8
+ View,
9
+ type ViewProps
10
+ } from "react-native";
11
+ import { MText } from "./MText";
12
+ import { useIconColors } from "./hooks/useIconColors";
13
+ import { cn } from "./utils";
14
+
15
+ const containerVariants = cva("w-full", {
16
+ variants: {
17
+ size: {
18
+ default: "gap-1.5",
19
+ sm: "gap-1",
20
+ lg: "gap-2"
21
+ }
22
+ },
23
+ defaultVariants: { size: "default" }
24
+ });
25
+
26
+ const inputWrapperVariants = cva(
27
+ "flex-row items-center bg-card border border-border",
28
+ {
29
+ variants: {
30
+ size: {
31
+ default: "px-4 py-3",
32
+ sm: "px-3 py-2",
33
+ lg: "px-5 py-4"
34
+ },
35
+ radius: {
36
+ xl: "rounded-xl",
37
+ "2xl": "rounded-2xl"
38
+ },
39
+ hasError: {
40
+ true: "border border-destructive",
41
+ false: ""
42
+ },
43
+ focused: {
44
+ true: "border border-primary",
45
+ false: ""
46
+ }
47
+ },
48
+ compoundVariants: [
49
+ { hasError: true, focused: true, class: "border border-destructive" }
50
+ ],
51
+ defaultVariants: {
52
+ size: "default",
53
+ radius: "xl",
54
+ hasError: false,
55
+ focused: false
56
+ }
57
+ }
58
+ );
59
+
60
+ const inputVariants = cva("flex-1 min-w-0 font-munchi text-foreground p-0", {
61
+ variants: {
62
+ size: {
63
+ default: "text-lg",
64
+ sm: "text-base",
65
+ lg: "text-xl"
66
+ }
67
+ },
68
+ defaultVariants: { size: "default" }
69
+ });
70
+
71
+ const INPUT_FONT_SIZE: Record<string, number> = {
72
+ default: 18,
73
+ sm: 16,
74
+ lg: 20
75
+ };
76
+
77
+ export interface MInputProps
78
+ extends Omit<TextInputProps, "className">,
79
+ VariantProps<typeof containerVariants> {
80
+ label?: string;
81
+ errorMessage?: string;
82
+ rightIcon?: ReactNode;
83
+ className?: ViewProps["className"];
84
+ inputClassName?: string;
85
+ radius?: VariantProps<typeof inputWrapperVariants>["radius"];
86
+ }
87
+
88
+ export const MInput = ({
89
+ label,
90
+ errorMessage,
91
+ rightIcon,
92
+ size = "default",
93
+ radius = "xl",
94
+ className,
95
+ inputClassName,
96
+ onFocus,
97
+ onBlur,
98
+ placeholderTextColor,
99
+ ...textInputProps
100
+ }: MInputProps) => {
101
+ const { foreground, muted, destructive } = useIconColors();
102
+ const [isFocused, setIsFocused] = useState(false);
103
+ const inputRef = useRef<TextInput>(null);
104
+ const hasError = !!errorMessage;
105
+
106
+ return (
107
+ <View className={cn(containerVariants({ size }), className)}>
108
+ {label && (
109
+ <MText
110
+ size={size === "lg" ? "lg" : size === "sm" ? "sm" : "base"}
111
+ className="font-munchi-semibold text-foreground"
112
+ >
113
+ {label}
114
+ </MText>
115
+ )}
116
+
117
+ <Pressable onPress={() => inputRef.current?.focus()}>
118
+ <View
119
+ className={inputWrapperVariants({
120
+ size,
121
+ radius,
122
+ hasError,
123
+ focused: isFocused
124
+ })}
125
+ >
126
+ <TextInput
127
+ ref={inputRef}
128
+ placeholderTextColor={placeholderTextColor ?? muted}
129
+ onFocus={(e) => {
130
+ setIsFocused(true);
131
+ onFocus?.(e);
132
+ }}
133
+ onBlur={(e) => {
134
+ setIsFocused(false);
135
+ onBlur?.(e);
136
+ }}
137
+ style={{
138
+ color: foreground,
139
+ fontSize: INPUT_FONT_SIZE[size ?? "default"],
140
+ paddingVertical: 0,
141
+ includeFontPadding: false
142
+ }}
143
+ className={cn(inputVariants({ size }), inputClassName)}
144
+ {...textInputProps}
145
+ />
146
+ {rightIcon && <View className="ml-2 shrink-0">{rightIcon}</View>}
147
+ </View>
148
+ </Pressable>
149
+
150
+ {hasError && (
151
+ <View className="flex-row items-center gap-1.5">
152
+ <AlertCircle size={14} color={destructive} />
153
+ <MText size="sm" className="text-destructive flex-1">
154
+ {errorMessage}
155
+ </MText>
156
+ </View>
157
+ )}
158
+ </View>
159
+ );
160
+ };
161
+
162
+ MInput.displayName = "MInput";
package/src/MLabel.tsx ADDED
@@ -0,0 +1,3 @@
1
+ import { Label } from "./primitives/label";
2
+
3
+ export { Label as MLabel };
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import * as LucideIcons from "lucide-react-native";
3
+ import type { LucideProps } from "lucide-react-native";
4
+
5
+ export type IconName = keyof typeof LucideIcons;
6
+
7
+ export interface MLucideIconProps extends LucideProps {
8
+ name: IconName;
9
+ }
10
+
11
+ export const MLucideIcon = ({ name, ...props }: MLucideIconProps) => {
12
+ const Icon = LucideIcons[name] as React.ComponentType<LucideProps> | undefined;
13
+
14
+ if (!Icon) {
15
+ return null;
16
+ }
17
+
18
+ return <Icon {...props} />;
19
+ };
20
+
21
+ MLucideIcon.displayName = "MLucideIcon";