getlotui 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 (128) hide show
  1. package/README.md +78 -0
  2. package/dist/bin.d.ts +2 -0
  3. package/dist/bin.js +5 -0
  4. package/dist/commands/add.d.ts +1 -0
  5. package/dist/commands/add.js +37 -0
  6. package/dist/commands/init.d.ts +1 -0
  7. package/dist/commands/init.js +93 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +22 -0
  10. package/dist/templates/expo/Accordion.d.ts +14 -0
  11. package/dist/templates/expo/Accordion.js +118 -0
  12. package/dist/templates/expo/Accordion.tsx +152 -0
  13. package/dist/templates/expo/AlertDialog.d.ts +12 -0
  14. package/dist/templates/expo/AlertDialog.js +126 -0
  15. package/dist/templates/expo/AlertDialog.tsx +147 -0
  16. package/dist/templates/expo/Avatar.d.ts +8 -0
  17. package/dist/templates/expo/Avatar.js +81 -0
  18. package/dist/templates/expo/Avatar.tsx +78 -0
  19. package/dist/templates/expo/Badge.d.ts +6 -0
  20. package/dist/templates/expo/Badge.js +60 -0
  21. package/dist/templates/expo/Badge.tsx +67 -0
  22. package/dist/templates/expo/Button.d.ts +9 -0
  23. package/dist/templates/expo/Button.js +37 -0
  24. package/dist/templates/expo/Button.tsx +53 -0
  25. package/dist/templates/expo/Dropdown.d.ts +12 -0
  26. package/dist/templates/expo/Dropdown.js +91 -0
  27. package/dist/templates/expo/Dropdown.tsx +100 -0
  28. package/dist/templates/expo/Input.d.ts +11 -0
  29. package/dist/templates/expo/Input.js +43 -0
  30. package/dist/templates/expo/Input.tsx +67 -0
  31. package/dist/templates/expo/Toast.d.ts +16 -0
  32. package/dist/templates/expo/Toast.js +142 -0
  33. package/dist/templates/expo/Toast.tsx +161 -0
  34. package/dist/templates/expo/utils.d.ts +4 -0
  35. package/dist/templates/expo/utils.js +10 -0
  36. package/dist/templates/expo/utils.ts +8 -0
  37. package/dist/templates/flutter/Accordion.dart +142 -0
  38. package/dist/templates/flutter/Alert.dart +96 -0
  39. package/dist/templates/flutter/AlertDialog.dart +175 -0
  40. package/dist/templates/flutter/Avatar.dart +82 -0
  41. package/dist/templates/flutter/Badge.dart +89 -0
  42. package/dist/templates/flutter/Button.dart +116 -0
  43. package/dist/templates/flutter/Card.dart +91 -0
  44. package/dist/templates/flutter/Input.dart +73 -0
  45. package/dist/templates/flutter/Text.dart +87 -0
  46. package/dist/templates/flutter/utils.dart +13 -0
  47. package/dist/templates/templates/expo/Button.tsx +50 -0
  48. package/dist/templates/templates/expo/Input.tsx +67 -0
  49. package/dist/templates/web/Accordion.d.ts +7 -0
  50. package/dist/templates/web/Accordion.js +59 -0
  51. package/dist/templates/web/Accordion.tsx +64 -0
  52. package/dist/templates/web/Alert.d.ts +9 -0
  53. package/dist/templates/web/Alert.js +64 -0
  54. package/dist/templates/web/Alert.tsx +71 -0
  55. package/dist/templates/web/AlertDialog.d.ts +14 -0
  56. package/dist/templates/web/AlertDialog.js +85 -0
  57. package/dist/templates/web/AlertDialog.tsx +164 -0
  58. package/dist/templates/web/Avatar.d.ts +6 -0
  59. package/dist/templates/web/Avatar.js +50 -0
  60. package/dist/templates/web/Avatar.tsx +51 -0
  61. package/dist/templates/web/Badge.d.ts +9 -0
  62. package/dist/templates/web/Badge.js +59 -0
  63. package/dist/templates/web/Badge.tsx +38 -0
  64. package/dist/templates/web/Button.d.ts +10 -0
  65. package/dist/templates/web/Button.js +70 -0
  66. package/dist/templates/web/Button.tsx +60 -0
  67. package/dist/templates/web/Card.d.ts +9 -0
  68. package/dist/templates/web/Card.js +65 -0
  69. package/dist/templates/web/Card.tsx +92 -0
  70. package/dist/templates/web/Dropdown.d.ts +27 -0
  71. package/dist/templates/web/Dropdown.js +95 -0
  72. package/dist/templates/web/Dropdown.tsx +198 -0
  73. package/dist/templates/web/Input.d.ts +3 -0
  74. package/dist/templates/web/Input.js +41 -0
  75. package/dist/templates/web/Input.tsx +21 -0
  76. package/dist/templates/web/Tabs.d.ts +7 -0
  77. package/dist/templates/web/Tabs.js +55 -0
  78. package/dist/templates/web/Tabs.tsx +66 -0
  79. package/dist/templates/web/Toast.d.ts +15 -0
  80. package/dist/templates/web/Toast.js +75 -0
  81. package/dist/templates/web/Toast.tsx +126 -0
  82. package/dist/templates/web/utils.d.ts +2 -0
  83. package/dist/templates/web/utils.js +8 -0
  84. package/dist/templates/web/utils.ts +6 -0
  85. package/dist/utils/detect.d.ts +19 -0
  86. package/dist/utils/detect.js +90 -0
  87. package/dist/utils/fs.d.ts +5 -0
  88. package/dist/utils/fs.js +35 -0
  89. package/getlotui.config.json +4 -0
  90. package/package.json +31 -0
  91. package/src/bin.ts +5 -0
  92. package/src/commands/add.ts +50 -0
  93. package/src/commands/init.ts +108 -0
  94. package/src/index.ts +23 -0
  95. package/src/templates/expo/Accordion.tsx +152 -0
  96. package/src/templates/expo/AlertDialog.tsx +147 -0
  97. package/src/templates/expo/Avatar.tsx +78 -0
  98. package/src/templates/expo/Badge.tsx +67 -0
  99. package/src/templates/expo/Button.tsx +53 -0
  100. package/src/templates/expo/Dropdown.tsx +100 -0
  101. package/src/templates/expo/Input.tsx +67 -0
  102. package/src/templates/expo/Toast.tsx +161 -0
  103. package/src/templates/expo/utils.ts +8 -0
  104. package/src/templates/flutter/Accordion.dart +142 -0
  105. package/src/templates/flutter/Alert.dart +96 -0
  106. package/src/templates/flutter/AlertDialog.dart +175 -0
  107. package/src/templates/flutter/Avatar.dart +82 -0
  108. package/src/templates/flutter/Badge.dart +89 -0
  109. package/src/templates/flutter/Button.dart +116 -0
  110. package/src/templates/flutter/Card.dart +91 -0
  111. package/src/templates/flutter/Input.dart +73 -0
  112. package/src/templates/flutter/Text.dart +87 -0
  113. package/src/templates/flutter/utils.dart +13 -0
  114. package/src/templates/web/Accordion.tsx +64 -0
  115. package/src/templates/web/Alert.tsx +71 -0
  116. package/src/templates/web/AlertDialog.tsx +164 -0
  117. package/src/templates/web/Avatar.tsx +51 -0
  118. package/src/templates/web/Badge.tsx +38 -0
  119. package/src/templates/web/Button.tsx +60 -0
  120. package/src/templates/web/Card.tsx +92 -0
  121. package/src/templates/web/Dropdown.tsx +198 -0
  122. package/src/templates/web/Input.tsx +21 -0
  123. package/src/templates/web/Tabs.tsx +66 -0
  124. package/src/templates/web/Toast.tsx +126 -0
  125. package/src/templates/web/utils.ts +6 -0
  126. package/src/utils/detect.ts +81 -0
  127. package/src/utils/fs.ts +32 -0
  128. package/tsconfig.json +17 -0
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { Command } from "commander";
2
+ import { initCommand } from "./commands/init";
3
+ import { addCommand } from "./commands/add";
4
+
5
+ export function runCLI() {
6
+ const program = new Command();
7
+
8
+ program
9
+ .name("getlotui")
10
+ .description("GetLotUI CLI for scaffolding components")
11
+ .version("0.1.0");
12
+
13
+ program
14
+ .command("init")
15
+ .description("Initialize GetLotUI config")
16
+ .action(initCommand);
17
+ program
18
+ .command("add <component>")
19
+ .description("Add a UI component")
20
+ .action(addCommand);
21
+
22
+ program.parse(process.argv);
23
+ }
@@ -0,0 +1,152 @@
1
+ import React, { useState } from "react";
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ StyleSheet,
7
+ LayoutAnimation,
8
+ Platform,
9
+ UIManager,
10
+ } from "react-native";
11
+
12
+ if (
13
+ Platform.OS === "android" &&
14
+ UIManager.setLayoutAnimationEnabledExperimental
15
+ ) {
16
+ UIManager.setLayoutAnimationEnabledExperimental(true);
17
+ }
18
+
19
+ interface AccordionItemProps {
20
+ value: string;
21
+ trigger: string;
22
+ content: string;
23
+ isOpen: boolean;
24
+ onToggle: () => void;
25
+ }
26
+
27
+ const AccordionItem: React.FC<AccordionItemProps> = ({
28
+ trigger,
29
+ content,
30
+ isOpen,
31
+ onToggle,
32
+ }) => {
33
+ const handleToggle = () => {
34
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
35
+ onToggle();
36
+ };
37
+
38
+ return (
39
+ <View style={styles.item}>
40
+ <TouchableOpacity
41
+ style={styles.trigger}
42
+ onPress={handleToggle}
43
+ activeOpacity={0.7}
44
+ >
45
+ <Text style={styles.triggerText}>{trigger}</Text>
46
+ <Text style={[styles.chevron, isOpen && styles.chevronOpen]}>›</Text>
47
+ </TouchableOpacity>
48
+ {isOpen && (
49
+ <View style={styles.content}>
50
+ <Text style={styles.contentText}>{content}</Text>
51
+ </View>
52
+ )}
53
+ </View>
54
+ );
55
+ };
56
+
57
+ interface AccordionProps {
58
+ type?: "single" | "multiple";
59
+ collapsible?: boolean;
60
+ defaultValue?: string | string[];
61
+ children?: React.ReactNode;
62
+ items: Array<{
63
+ value: string;
64
+ trigger: string;
65
+ content: string;
66
+ }>;
67
+ }
68
+
69
+ export const Accordion: React.FC<AccordionProps> = ({
70
+ type = "single",
71
+ collapsible = false,
72
+ defaultValue,
73
+ items,
74
+ }) => {
75
+ const [openItems, setOpenItems] = useState<string[]>(() => {
76
+ if (defaultValue) {
77
+ return Array.isArray(defaultValue) ? defaultValue : [defaultValue];
78
+ }
79
+ return [];
80
+ });
81
+
82
+ const handleToggle = (value: string) => {
83
+ if (type === "single") {
84
+ if (openItems.includes(value)) {
85
+ setOpenItems(collapsible ? [] : openItems);
86
+ } else {
87
+ setOpenItems([value]);
88
+ }
89
+ } else {
90
+ setOpenItems((prev) =>
91
+ prev.includes(value)
92
+ ? prev.filter((item) => item !== value)
93
+ : [...prev, value]
94
+ );
95
+ }
96
+ };
97
+
98
+ return (
99
+ <View style={styles.container}>
100
+ {items.map((item) => (
101
+ <AccordionItem
102
+ key={item.value}
103
+ value={item.value}
104
+ trigger={item.trigger}
105
+ content={item.content}
106
+ isOpen={openItems.includes(item.value)}
107
+ onToggle={() => handleToggle(item.value)}
108
+ />
109
+ ))}
110
+ </View>
111
+ );
112
+ };
113
+
114
+ const styles = StyleSheet.create({
115
+ container: {
116
+ width: "100%",
117
+ },
118
+ item: {
119
+ borderBottomWidth: 1,
120
+ borderBottomColor: "#e5e7eb",
121
+ },
122
+ trigger: {
123
+ flexDirection: "row",
124
+ justifyContent: "space-between",
125
+ alignItems: "center",
126
+ paddingVertical: 16,
127
+ paddingHorizontal: 0,
128
+ },
129
+ triggerText: {
130
+ fontSize: 15,
131
+ fontWeight: "500",
132
+ color: "#111827",
133
+ flex: 1,
134
+ },
135
+ chevron: {
136
+ fontSize: 20,
137
+ color: "#6b7280",
138
+ transform: [{ rotate: "90deg" }],
139
+ marginLeft: 8,
140
+ },
141
+ chevronOpen: {
142
+ transform: [{ rotate: "270deg" }],
143
+ },
144
+ content: {
145
+ paddingBottom: 16,
146
+ },
147
+ contentText: {
148
+ fontSize: 14,
149
+ color: "#6b7280",
150
+ lineHeight: 20,
151
+ },
152
+ });
@@ -0,0 +1,147 @@
1
+ import React, { useState } from "react";
2
+ import {
3
+ View,
4
+ Text,
5
+ Modal,
6
+ StyleSheet,
7
+ Pressable,
8
+ Dimensions,
9
+ } from "react-native";
10
+
11
+ export interface AlertDialogProps {
12
+ open?: boolean;
13
+ onOpenChange?: (open: boolean) => void;
14
+ trigger?: React.ReactNode;
15
+ title: string;
16
+ description: string;
17
+ cancelText?: string;
18
+ actionText?: string;
19
+ onAction?: () => void;
20
+ }
21
+
22
+ export const AlertDialog = ({
23
+ open: controlledOpen,
24
+ onOpenChange,
25
+ trigger,
26
+ title,
27
+ description,
28
+ cancelText = "Cancel",
29
+ actionText = "Continue",
30
+ onAction,
31
+ }: AlertDialogProps) => {
32
+ const [internalOpen, setInternalOpen] = useState(false);
33
+ const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
34
+
35
+ const setOpen = (val: boolean) => {
36
+ if (onOpenChange) onOpenChange(val);
37
+ setInternalOpen(val);
38
+ };
39
+
40
+ return (
41
+ <>
42
+ {trigger && (
43
+ <Pressable onPress={() => setOpen(true)}>{trigger}</Pressable>
44
+ )}
45
+
46
+ <Modal
47
+ transparent
48
+ visible={isOpen}
49
+ animationType="fade"
50
+ onRequestClose={() => setOpen(false)}
51
+ >
52
+ <Pressable style={styles.overlay} onPress={() => setOpen(false)}>
53
+ <Pressable
54
+ style={styles.content}
55
+ onPress={(e) => e.stopPropagation()}
56
+ >
57
+ <Text style={styles.title}>{title}</Text>
58
+ <Text style={styles.description}>{description}</Text>
59
+
60
+ <View style={styles.footer}>
61
+ <Pressable
62
+ style={[styles.button, styles.cancelButton]}
63
+ onPress={() => setOpen(false)}
64
+ >
65
+ <Text style={styles.cancelButtonText}>{cancelText}</Text>
66
+ </Pressable>
67
+
68
+ <Pressable
69
+ style={[styles.button, styles.actionButton]}
70
+ onPress={() => {
71
+ onAction?.();
72
+ setOpen(false);
73
+ }}
74
+ >
75
+ <Text style={styles.actionButtonText}>{actionText}</Text>
76
+ </Pressable>
77
+ </View>
78
+ </Pressable>
79
+ </Pressable>
80
+ </Modal>
81
+ </>
82
+ );
83
+ };
84
+
85
+ const styles = StyleSheet.create({
86
+ overlay: {
87
+ flex: 1,
88
+ backgroundColor: "rgba(0,0,0,0.5)",
89
+ justifyContent: "center",
90
+ alignItems: "center",
91
+ padding: 20,
92
+ },
93
+ content: {
94
+ backgroundColor: "white",
95
+ borderRadius: 12,
96
+ padding: 24,
97
+ width: "100%",
98
+ maxWidth: 400,
99
+ shadowColor: "#000",
100
+ shadowOffset: { width: 0, height: 2 },
101
+ shadowOpacity: 0.25,
102
+ shadowRadius: 4,
103
+ elevation: 5,
104
+ },
105
+ title: {
106
+ fontSize: 18,
107
+ fontWeight: "600",
108
+ color: "#18181b",
109
+ marginBottom: 8,
110
+ },
111
+ description: {
112
+ fontSize: 14,
113
+ color: "#71717a",
114
+ lineHeight: 20,
115
+ marginBottom: 24,
116
+ },
117
+ footer: {
118
+ flexDirection: "row",
119
+ justifyContent: "flex-end",
120
+ gap: 12,
121
+ },
122
+ button: {
123
+ paddingHorizontal: 16,
124
+ paddingVertical: 10,
125
+ borderRadius: 6,
126
+ minWidth: 80,
127
+ alignItems: "center",
128
+ },
129
+ cancelButton: {
130
+ backgroundColor: "white",
131
+ borderWidth: 1,
132
+ borderColor: "#e4e4e7",
133
+ },
134
+ actionButton: {
135
+ backgroundColor: "#18181b",
136
+ },
137
+ cancelButtonText: {
138
+ fontSize: 14,
139
+ fontWeight: "500",
140
+ color: "#18181b",
141
+ },
142
+ actionButtonText: {
143
+ fontSize: 14,
144
+ fontWeight: "500",
145
+ color: "white",
146
+ },
147
+ });
@@ -0,0 +1,78 @@
1
+ import React, { useState } from "react";
2
+ import { View, Image, Text, StyleSheet, Pressable } from "react-native";
3
+
4
+ export interface AvatarProps {
5
+ src?: string;
6
+ fallback?: string;
7
+ size?: "sm" | "md" | "lg";
8
+ shape?: "circle" | "square";
9
+ }
10
+
11
+ export const Avatar = ({
12
+ src,
13
+ fallback,
14
+ size = "md",
15
+ shape = "circle",
16
+ }: AvatarProps) => {
17
+ const [hasError, setHasError] = useState(false);
18
+
19
+ const sizes = {
20
+ sm: 32,
21
+ md: 40,
22
+ lg: 56,
23
+ };
24
+
25
+ const currentSize = sizes[size];
26
+
27
+ return (
28
+ <View
29
+ style={[
30
+ styles.container,
31
+ {
32
+ width: currentSize,
33
+ height: currentSize,
34
+ borderRadius: shape === "circle" ? currentSize / 2 : 8,
35
+ },
36
+ ]}
37
+ >
38
+ {src && !hasError ? (
39
+ <Image
40
+ source={{ uri: src }}
41
+ style={styles.image}
42
+ onError={() => setHasError(true)}
43
+ />
44
+ ) : (
45
+ <View style={styles.fallback}>
46
+ <Text style={[styles.fallbackText, { fontSize: currentSize * 0.4 }]}>
47
+ {fallback?.substring(0, 2).toUpperCase() || "??"}
48
+ </Text>
49
+ </View>
50
+ )}
51
+ </View>
52
+ );
53
+ };
54
+
55
+ const styles = StyleSheet.create({
56
+ container: {
57
+ overflow: "hidden",
58
+ backgroundColor: "#f3f4f6",
59
+ justifyContent: "center",
60
+ alignItems: "center",
61
+ borderWidth: 1,
62
+ borderColor: "rgba(0,0,0,0.05)",
63
+ },
64
+ image: {
65
+ width: "100%",
66
+ height: "100%",
67
+ },
68
+ fallback: {
69
+ width: "100%",
70
+ height: "100%",
71
+ justifyContent: "center",
72
+ alignItems: "center",
73
+ },
74
+ fallbackText: {
75
+ fontWeight: "600",
76
+ color: "#4b5563",
77
+ },
78
+ });
@@ -0,0 +1,67 @@
1
+ import React from "react";
2
+ import { View, Text, StyleSheet } from "react-native";
3
+
4
+ export interface BadgeProps {
5
+ children: React.ReactNode;
6
+ variant?: "default" | "secondary" | "outline" | "destructive" | "success";
7
+ }
8
+
9
+ export const Badge = ({ children, variant = "default" }: BadgeProps) => {
10
+ const variantStyles = {
11
+ default: {
12
+ bg: "#18181b",
13
+ text: "#ffffff",
14
+ border: "transparent",
15
+ },
16
+ secondary: {
17
+ bg: "#f4f4f5",
18
+ text: "#18181b",
19
+ border: "transparent",
20
+ },
21
+ outline: {
22
+ bg: "transparent",
23
+ text: "#18181b",
24
+ border: "#e4e4e7",
25
+ },
26
+ destructive: {
27
+ bg: "#ef4444",
28
+ text: "#ffffff",
29
+ border: "transparent",
30
+ },
31
+ success: {
32
+ bg: "#22c55e",
33
+ text: "#ffffff",
34
+ border: "transparent",
35
+ },
36
+ };
37
+
38
+ const style = variantStyles[variant];
39
+
40
+ return (
41
+ <View
42
+ style={[
43
+ styles.badge,
44
+ {
45
+ backgroundColor: style.bg,
46
+ borderColor: style.border,
47
+ borderWidth: style.border === "transparent" ? 0 : 1,
48
+ },
49
+ ]}
50
+ >
51
+ <Text style={[styles.text, { color: style.text }]}>{children}</Text>
52
+ </View>
53
+ );
54
+ };
55
+
56
+ const styles = StyleSheet.create({
57
+ badge: {
58
+ paddingHorizontal: 8,
59
+ paddingVertical: 2,
60
+ borderRadius: 9999,
61
+ alignSelf: "flex-start",
62
+ },
63
+ text: {
64
+ fontSize: 12,
65
+ fontWeight: "600",
66
+ },
67
+ });
@@ -0,0 +1,53 @@
1
+ import React from "react";
2
+ import { TouchableOpacity, Text, StyleSheet } from "react-native";
3
+ import { theme } from "../../theme/config";
4
+
5
+ interface ButtonProps {
6
+ title: string;
7
+ onPress: () => void;
8
+ variant?: "default" | "primary" | "secondary";
9
+ disabled?: boolean;
10
+ }
11
+
12
+ export const Button: React.FC<ButtonProps> = ({
13
+ title,
14
+ onPress,
15
+ variant = "default",
16
+ disabled = false,
17
+ }) => {
18
+ const backgroundColor =
19
+ variant === "primary"
20
+ ? theme.colors.primary
21
+ : variant === "secondary"
22
+ ? theme.colors.secondary || "#94a3b8"
23
+ : theme.colors.background === "#ffffff"
24
+ ? "#e5e7eb"
25
+ : theme.colors.background;
26
+
27
+ return (
28
+ <TouchableOpacity
29
+ style={[styles.button, { backgroundColor }, disabled && styles.disabled]}
30
+ onPress={onPress}
31
+ disabled={disabled}
32
+ >
33
+ <Text style={styles.text}>{title}</Text>
34
+ </TouchableOpacity>
35
+ );
36
+ };
37
+
38
+ const styles = StyleSheet.create({
39
+ button: {
40
+ paddingVertical: 12,
41
+ paddingHorizontal: 24,
42
+ borderRadius: 4,
43
+ alignItems: "center",
44
+ justifyContent: "center",
45
+ },
46
+ text: {
47
+ color: "#fff",
48
+ fontWeight: "600",
49
+ },
50
+ disabled: {
51
+ opacity: 0.6,
52
+ },
53
+ });
@@ -0,0 +1,100 @@
1
+ import React, { useState } from "react";
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ Pressable,
7
+ Modal,
8
+ Dimensions,
9
+ FlatList,
10
+ } from "react-native";
11
+
12
+ export interface DropdownItem {
13
+ label: string;
14
+ value: string;
15
+ icon?: React.ReactNode;
16
+ }
17
+
18
+ export interface DropdownProps {
19
+ trigger: React.ReactNode;
20
+ items: DropdownItem[];
21
+ onSelect: (value: string) => void;
22
+ }
23
+
24
+ export const Dropdown = ({ trigger, items, onSelect }: DropdownProps) => {
25
+ const [open, setOpen] = useState(false);
26
+ const [layout, setLayout] = useState({ x: 0, y: 0, width: 0, height: 0 });
27
+
28
+ const onTriggerLayout = (event: any) => {
29
+ // In a real implementation, we'd use measure() to get page position
30
+ // For the template, we'll simplify
31
+ };
32
+
33
+ return (
34
+ <View>
35
+ <Pressable onPress={() => setOpen(true)} onLayout={onTriggerLayout}>
36
+ {trigger}
37
+ </Pressable>
38
+
39
+ <Modal
40
+ visible={open}
41
+ transparent
42
+ animationType="none"
43
+ onRequestClose={() => setOpen(false)}
44
+ >
45
+ <Pressable style={styles.overlay} onPress={() => setOpen(false)}>
46
+ <View style={styles.menu}>
47
+ {items.map((item) => (
48
+ <Pressable
49
+ key={item.value}
50
+ style={styles.item}
51
+ onPress={() => {
52
+ onSelect(item.value);
53
+ setOpen(false);
54
+ }}
55
+ >
56
+ {item.icon && <View style={styles.icon}>{item.icon}</View>}
57
+ <Text style={styles.label}>{item.label}</Text>
58
+ </Pressable>
59
+ ))}
60
+ </View>
61
+ </Pressable>
62
+ </Modal>
63
+ </View>
64
+ );
65
+ };
66
+
67
+ const styles = StyleSheet.create({
68
+ overlay: {
69
+ flex: 1,
70
+ backgroundColor: "transparent",
71
+ justifyContent: "center", // Should be positioned near trigger
72
+ alignItems: "center",
73
+ },
74
+ menu: {
75
+ backgroundColor: "white",
76
+ borderRadius: 8,
77
+ padding: 4,
78
+ minWidth: 160,
79
+ shadowColor: "#000",
80
+ shadowOffset: { width: 0, height: 4 },
81
+ shadowOpacity: 0.1,
82
+ shadowRadius: 8,
83
+ elevation: 8,
84
+ borderWidth: 1,
85
+ borderColor: "#e4e4e7",
86
+ },
87
+ item: {
88
+ flexDirection: "row",
89
+ alignItems: "center",
90
+ padding: 8,
91
+ borderRadius: 4,
92
+ },
93
+ icon: {
94
+ marginRight: 8,
95
+ },
96
+ label: {
97
+ fontSize: 14,
98
+ color: "#18181b",
99
+ },
100
+ });
@@ -0,0 +1,67 @@
1
+ import React from "react";
2
+ import { TextInput, StyleSheet, View, Text } from "react-native";
3
+
4
+ type InputProps = {
5
+ label?: string;
6
+ placeholder?: string;
7
+ value: string;
8
+ onChangeText: (text: string) => void;
9
+ secureTextEntry?: boolean;
10
+ disabled?: boolean;
11
+ };
12
+
13
+ export const Input: React.FC<InputProps> = ({
14
+ label,
15
+ placeholder,
16
+ value,
17
+ onChangeText,
18
+ secureTextEntry = false,
19
+ disabled = false,
20
+ }) => {
21
+ const colors = {
22
+ text: "#111827", // gray-900
23
+ border: "#d1d5db", // gray-300
24
+ muted: "#9ca3af", // gray-400
25
+ };
26
+ return (
27
+ <View style={styles.container}>
28
+ {label && (
29
+ <Text style={[styles.label, { color: colors.text }]}>{label}</Text>
30
+ )}
31
+ <TextInput
32
+ style={[
33
+ styles.input,
34
+ { borderColor: colors.border, color: colors.text },
35
+ disabled && styles.disabled,
36
+ ]}
37
+ placeholder={placeholder}
38
+ placeholderTextColor={colors.muted}
39
+ value={value}
40
+ onChangeText={onChangeText}
41
+ secureTextEntry={secureTextEntry}
42
+ editable={!disabled}
43
+ />
44
+ </View>
45
+ );
46
+ };
47
+
48
+ const styles = StyleSheet.create({
49
+ container: {
50
+ marginVertical: 8,
51
+ },
52
+ label: {
53
+ marginBottom: 4,
54
+ fontSize: 14,
55
+ fontWeight: "500",
56
+ },
57
+ input: {
58
+ borderWidth: 1,
59
+ borderRadius: 4,
60
+ paddingHorizontal: 12,
61
+ paddingVertical: 8,
62
+ fontSize: 16,
63
+ },
64
+ disabled: {
65
+ opacity: 0.6,
66
+ },
67
+ });