@lunar-kit/core 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 (59) hide show
  1. package/dist/index.d.ts +5 -0
  2. package/dist/index.js +14 -0
  3. package/package.json +31 -0
  4. package/src/components/ui/accordion.tsx +334 -0
  5. package/src/components/ui/avatar.tsx +326 -0
  6. package/src/components/ui/badge.tsx +84 -0
  7. package/src/components/ui/banner.tsx +151 -0
  8. package/src/components/ui/bottom-sheet.tsx +579 -0
  9. package/src/components/ui/button.tsx +142 -0
  10. package/src/components/ui/calendar.tsx +502 -0
  11. package/src/components/ui/card.tsx +163 -0
  12. package/src/components/ui/checkbox.tsx +129 -0
  13. package/src/components/ui/date-picker.tsx +190 -0
  14. package/src/components/ui/date-range-picker.tsx +262 -0
  15. package/src/components/ui/dialog.tsx +204 -0
  16. package/src/components/ui/form.tsx +139 -0
  17. package/src/components/ui/input.tsx +107 -0
  18. package/src/components/ui/radio-group.tsx +123 -0
  19. package/src/components/ui/radio.tsx +109 -0
  20. package/src/components/ui/select-sheet.tsx +814 -0
  21. package/src/components/ui/select.tsx +547 -0
  22. package/src/components/ui/tabs.tsx +254 -0
  23. package/src/components/ui/text.tsx +229 -0
  24. package/src/components/ui/textarea.tsx +77 -0
  25. package/src/components/v0/accordion.tsx +199 -0
  26. package/src/components/v1/accordion.tsx +234 -0
  27. package/src/components/v1/avatar.tsx +259 -0
  28. package/src/components/v1/bottom-sheet.tsx +1090 -0
  29. package/src/components/v1/button.tsx +61 -0
  30. package/src/components/v1/calendar.tsx +498 -0
  31. package/src/components/v1/card.tsx +86 -0
  32. package/src/components/v1/checkbox.tsx +46 -0
  33. package/src/components/v1/date-picker.tsx +135 -0
  34. package/src/components/v1/date-range-picker.tsx +218 -0
  35. package/src/components/v1/dialog.tsx +211 -0
  36. package/src/components/v1/radio-group.tsx +76 -0
  37. package/src/components/v1/select.tsx +217 -0
  38. package/src/components/v1/tabs.tsx +253 -0
  39. package/src/registry/ui/accordion.json +30 -0
  40. package/src/registry/ui/avatar.json +41 -0
  41. package/src/registry/ui/badge.json +26 -0
  42. package/src/registry/ui/banner.json +27 -0
  43. package/src/registry/ui/bottom-sheet.json +29 -0
  44. package/src/registry/ui/button.json +24 -0
  45. package/src/registry/ui/calendar.json +29 -0
  46. package/src/registry/ui/card.json +25 -0
  47. package/src/registry/ui/checkbox.json +25 -0
  48. package/src/registry/ui/date-picker.json +30 -0
  49. package/src/registry/ui/date-range-picker.json +33 -0
  50. package/src/registry/ui/dialog.json +25 -0
  51. package/src/registry/ui/form.json +27 -0
  52. package/src/registry/ui/input.json +22 -0
  53. package/src/registry/ui/radio-group.json +26 -0
  54. package/src/registry/ui/radio.json +23 -0
  55. package/src/registry/ui/select-sheet.json +29 -0
  56. package/src/registry/ui/select.json +26 -0
  57. package/src/registry/ui/tabs.json +29 -0
  58. package/src/registry/ui/text.json +22 -0
  59. package/src/registry/ui/textarea.json +24 -0
@@ -0,0 +1,254 @@
1
+ // components/ui/tabs.tsx
2
+ import * as React from 'react';
3
+ import { View, Pressable, Animated, LayoutChangeEvent } from 'react-native';
4
+ import { cn } from '@/lib/utils';
5
+ import { Text } from './text';
6
+
7
+ // Context untuk share state
8
+ interface TabsContextValue {
9
+ value: string;
10
+ onValueChange: (value: string) => void;
11
+ variant?: 'pill' | 'underline';
12
+ }
13
+
14
+ const TabsContext = React.createContext<TabsContextValue | null>(null);
15
+
16
+ const useTabsContext = () => {
17
+ const context = React.useContext(TabsContext);
18
+ if (!context) {
19
+ throw new Error('Tabs components must be used within Tabs');
20
+ }
21
+ return context;
22
+ };
23
+
24
+ // Props Types
25
+ export interface TabsProps {
26
+ value: string;
27
+ onValueChange: (value: string) => void;
28
+ children: React.ReactNode;
29
+ variant?: 'pill' | 'underline';
30
+ className?: string;
31
+ }
32
+
33
+ export interface TabsListProps {
34
+ children: React.ReactNode;
35
+ className?: string;
36
+ }
37
+
38
+ export interface TabsTriggerProps {
39
+ value: string;
40
+ children: string;
41
+ className?: string;
42
+ disabled?: boolean;
43
+ }
44
+
45
+ export interface TabsContentProps {
46
+ value: string;
47
+ children: React.ReactNode;
48
+ className?: string;
49
+ }
50
+
51
+ interface TabLayout {
52
+ x: number;
53
+ width: number;
54
+ height: number;
55
+ }
56
+
57
+ // Extended Context for pill animation
58
+ interface TabsListContextValue extends TabsContextValue {
59
+ registerTab: (value: string, layout: TabLayout) => void;
60
+ tabLayouts: Map<string, TabLayout>;
61
+ }
62
+
63
+ const TabsListContext = React.createContext<TabsListContextValue | null>(null);
64
+
65
+ const useTabsListContext = () => {
66
+ const context = React.useContext(TabsListContext);
67
+ if (!context) {
68
+ throw new Error('TabsTrigger must be used within TabsList');
69
+ }
70
+ return context;
71
+ };
72
+
73
+ // Tabs Root Component
74
+ export function Tabs({
75
+ value,
76
+ onValueChange,
77
+ children,
78
+ variant = 'underline',
79
+ className,
80
+ }: TabsProps) {
81
+ const contextValue: TabsContextValue = {
82
+ value,
83
+ onValueChange,
84
+ variant,
85
+ };
86
+
87
+ return (
88
+ <TabsContext.Provider value={contextValue}>
89
+ <View className={cn('flex', className)}>{children}</View>
90
+ </TabsContext.Provider>
91
+ );
92
+ }
93
+
94
+ // TabsList Component
95
+ export function TabsList({ children, className }: TabsListProps) {
96
+ const { value, onValueChange, variant } = useTabsContext();
97
+ const [tabLayouts, setTabLayouts] = React.useState<Map<string, TabLayout>>(new Map());
98
+
99
+ const indicatorPosition = React.useRef(new Animated.Value(0)).current;
100
+ const indicatorWidth = React.useRef(new Animated.Value(0)).current;
101
+ const indicatorHeight = React.useRef(new Animated.Value(0)).current;
102
+
103
+ const registerTab = React.useCallback((tabValue: string, layout: TabLayout) => {
104
+ setTabLayouts((prev) => {
105
+ const newMap = new Map(prev);
106
+ newMap.set(tabValue, layout);
107
+ return newMap;
108
+ });
109
+ }, []);
110
+
111
+ // Animate pill indicator when active tab changes
112
+ React.useEffect(() => {
113
+ const activeLayout = tabLayouts.get(value);
114
+ if (activeLayout && variant === 'pill') {
115
+ Animated.parallel([
116
+ Animated.spring(indicatorPosition, {
117
+ toValue: activeLayout.x,
118
+ useNativeDriver: false,
119
+ tension: 100,
120
+ friction: 10,
121
+ }),
122
+ Animated.spring(indicatorWidth, {
123
+ toValue: activeLayout.width,
124
+ useNativeDriver: false,
125
+ tension: 100,
126
+ friction: 10,
127
+ }),
128
+ Animated.spring(indicatorHeight, {
129
+ toValue: activeLayout.height,
130
+ useNativeDriver: false,
131
+ tension: 100,
132
+ friction: 10,
133
+ }),
134
+ ]).start();
135
+ }
136
+ }, [value, tabLayouts, variant]);
137
+
138
+ const contextValue: TabsListContextValue = {
139
+ value,
140
+ onValueChange,
141
+ variant,
142
+ registerTab,
143
+ tabLayouts,
144
+ };
145
+
146
+ return (
147
+ <TabsListContext.Provider value={contextValue}>
148
+ <View
149
+ className={cn(
150
+ 'relative flex-row items-center',
151
+ variant === 'pill' && 'bg-muted rounded-lg p-1',
152
+ variant === 'underline' && 'border-b border-border',
153
+ className
154
+ )}
155
+ >
156
+ {/* Animated Pill Background */}
157
+ {variant === 'pill' && (
158
+ <Animated.View
159
+ className="absolute bg-background rounded-md shadow-sm"
160
+ style={{
161
+ left: indicatorPosition,
162
+ width: indicatorWidth,
163
+ height: indicatorHeight,
164
+ }}
165
+ />
166
+ )}
167
+
168
+ {children}
169
+ </View>
170
+ </TabsListContext.Provider>
171
+ );
172
+ }
173
+
174
+ // TabsTrigger Component
175
+ export function TabsTrigger({ value: triggerValue, children, className, disabled = false }: TabsTriggerProps) {
176
+ const { value, onValueChange, variant, registerTab } = useTabsListContext();
177
+ const isActive = value === triggerValue;
178
+
179
+ // Animation for underline
180
+ const scaleAnim = React.useRef(new Animated.Value(isActive ? 1 : 0)).current;
181
+ const opacityAnim = React.useRef(new Animated.Value(isActive ? 1 : 0)).current;
182
+
183
+ const handleLayout = (event: LayoutChangeEvent) => {
184
+ const { x, width, height } = event.nativeEvent.layout;
185
+ registerTab(triggerValue, { x, width, height });
186
+ };
187
+
188
+ React.useEffect(() => {
189
+ if (variant === 'underline') {
190
+ Animated.parallel([
191
+ Animated.spring(scaleAnim, {
192
+ toValue: isActive ? 1 : 0,
193
+ useNativeDriver: true,
194
+ tension: 100,
195
+ friction: 10,
196
+ }),
197
+ Animated.timing(opacityAnim, {
198
+ toValue: isActive ? 1 : 0,
199
+ duration: 200,
200
+ useNativeDriver: true,
201
+ }),
202
+ ]).start();
203
+ }
204
+ }, [isActive, variant, scaleAnim, opacityAnim]);
205
+
206
+ return (
207
+ <Pressable
208
+ onPress={() => onValueChange(triggerValue)}
209
+ onLayout={handleLayout}
210
+ disabled={disabled}
211
+ className={cn(
212
+ 'flex-1 items-center justify-center',
213
+ variant === 'pill' && 'px-4 py-2 rounded-md z-10',
214
+ disabled && 'opacity-50',
215
+ className
216
+ )}
217
+ >
218
+ <View className="items-center w-full">
219
+ <Text
220
+ size={variant === 'pill' ? 'sm' : 'md'}
221
+ variant="label"
222
+ className={cn(
223
+ isActive ? 'text-foreground' : 'text-muted-foreground'
224
+ )}
225
+ >
226
+ {children}
227
+ </Text>
228
+
229
+ {/* Animated Underline Indicator */}
230
+ {variant === 'underline' && (
231
+ <Animated.View
232
+ className="h-0.5 mt-3 rounded-full bg-primary border-b-2 border-primary"
233
+ style={{
234
+ width: '100%',
235
+ transform: [{ scaleX: scaleAnim }],
236
+ opacity: opacityAnim,
237
+ }}
238
+ />
239
+ )}
240
+ </View>
241
+ </Pressable>
242
+ );
243
+ }
244
+
245
+ // TabsContent Component
246
+ export function TabsContent({ value: contentValue, children, className }: TabsContentProps) {
247
+ const { value } = useTabsContext();
248
+
249
+ if (value !== contentValue) {
250
+ return null;
251
+ }
252
+
253
+ return <View className={cn('pt-4', className)}>{children}</View>;
254
+ }
@@ -0,0 +1,229 @@
1
+ // components/ui/text.tsx
2
+ import React from 'react';
3
+ import { Text as RNText, TextProps as RNTextProps } from 'react-native';
4
+ import { cva, type VariantProps } from 'class-variance-authority';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ const textVariants = cva(
8
+ 'text-foreground', // base style
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: '',
13
+ header: 'font-bold',
14
+ title: 'font-semibold',
15
+ label: 'font-medium',
16
+ body: 'font-normal',
17
+ caption: 'font-normal text-muted-foreground',
18
+ muted: 'text-muted-foreground',
19
+ error: 'text-destructive',
20
+ success: 'text-green-600 dark:text-green-400',
21
+ },
22
+ size: {
23
+ sm: '', // akan di-override di compound variants
24
+ md: '', // akan di-override di compound variants
25
+ lg: '', // akan di-override di compound variants
26
+ xl: '', // akan di-override di compound variants
27
+ },
28
+ align: {
29
+ left: 'text-left',
30
+ center: 'text-center',
31
+ right: 'text-right',
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ variant: 'default',
36
+ size: 'md',
37
+ align: 'left',
38
+ },
39
+ // Compound variants - setiap variant punya size mapping sendiri
40
+ compoundVariants: [
41
+ // HEADER sizes
42
+ {
43
+ variant: 'header',
44
+ size: 'sm',
45
+ class: 'text-2xl', // 24px
46
+ },
47
+ {
48
+ variant: 'header',
49
+ size: 'md',
50
+ class: 'text-3xl', // 30px
51
+ },
52
+ {
53
+ variant: 'header',
54
+ size: 'lg',
55
+ class: 'text-4xl', // 36px
56
+ },
57
+ {
58
+ variant: 'header',
59
+ size: 'xl',
60
+ class: 'text-5xl', // 48px
61
+ },
62
+
63
+ // TITLE sizes
64
+ {
65
+ variant: 'title',
66
+ size: 'sm',
67
+ class: 'text-lg', // 18px
68
+ },
69
+ {
70
+ variant: 'title',
71
+ size: 'md',
72
+ class: 'text-xl', // 20px
73
+ },
74
+ {
75
+ variant: 'title',
76
+ size: 'lg',
77
+ class: 'text-2xl', // 24px
78
+ },
79
+ {
80
+ variant: 'title',
81
+ size: 'xl',
82
+ class: 'text-3xl', // 30px
83
+ },
84
+
85
+ // LABEL sizes
86
+ {
87
+ variant: 'label',
88
+ size: 'sm',
89
+ class: 'text-xs uppercase tracking-wider', // 12px
90
+ },
91
+ {
92
+ variant: 'label',
93
+ size: 'md',
94
+ class: 'text-sm uppercase tracking-wide', // 14px
95
+ },
96
+ {
97
+ variant: 'label',
98
+ size: 'lg',
99
+ class: 'text-base uppercase tracking-wide', // 16px
100
+ },
101
+
102
+ // BODY sizes
103
+ {
104
+ variant: 'body',
105
+ size: 'sm',
106
+ class: 'text-sm', // 14px
107
+ },
108
+ {
109
+ variant: 'body',
110
+ size: 'md',
111
+ class: 'text-base', // 16px
112
+ },
113
+ {
114
+ variant: 'body',
115
+ size: 'lg',
116
+ class: 'text-lg', // 18px
117
+ },
118
+
119
+ // CAPTION sizes
120
+ {
121
+ variant: 'caption',
122
+ size: 'sm',
123
+ class: 'text-xs', // 12px
124
+ },
125
+ {
126
+ variant: 'caption',
127
+ size: 'md',
128
+ class: 'text-sm', // 14px
129
+ },
130
+ {
131
+ variant: 'caption',
132
+ size: 'lg',
133
+ class: 'text-base', // 16px
134
+ },
135
+
136
+ // MUTED sizes (sama dengan body)
137
+ {
138
+ variant: 'muted',
139
+ size: 'sm',
140
+ class: 'text-sm',
141
+ },
142
+ {
143
+ variant: 'muted',
144
+ size: 'md',
145
+ class: 'text-base',
146
+ },
147
+ {
148
+ variant: 'muted',
149
+ size: 'lg',
150
+ class: 'text-lg',
151
+ },
152
+
153
+ // ERROR sizes (sama dengan body)
154
+ {
155
+ variant: 'error',
156
+ size: 'sm',
157
+ class: 'text-sm',
158
+ },
159
+ {
160
+ variant: 'error',
161
+ size: 'md',
162
+ class: 'text-base',
163
+ },
164
+ {
165
+ variant: 'error',
166
+ size: 'lg',
167
+ class: 'text-lg',
168
+ },
169
+
170
+ // SUCCESS sizes (sama dengan body)
171
+ {
172
+ variant: 'success',
173
+ size: 'sm',
174
+ class: 'text-sm',
175
+ },
176
+ {
177
+ variant: 'success',
178
+ size: 'md',
179
+ class: 'text-base',
180
+ },
181
+ {
182
+ variant: 'success',
183
+ size: 'lg',
184
+ class: 'text-lg',
185
+ },
186
+
187
+ // DEFAULT sizes
188
+ {
189
+ variant: 'default',
190
+ size: 'sm',
191
+ class: 'text-sm',
192
+ },
193
+ {
194
+ variant: 'default',
195
+ size: 'md',
196
+ class: 'text-base',
197
+ },
198
+ {
199
+ variant: 'default',
200
+ size: 'lg',
201
+ class: 'text-lg',
202
+ },
203
+ ],
204
+ }
205
+ );
206
+
207
+ export interface TextProps
208
+ extends RNTextProps,
209
+ VariantProps<typeof textVariants> {
210
+ className?: string;
211
+ variant?: 'default' | 'header' | 'title' | 'label' | 'body' | 'caption' | 'muted' | 'error' | 'success';
212
+ size?: 'sm' | 'md' | 'lg' | 'xl';
213
+ align?: 'left' | 'center' | 'right';
214
+ }
215
+
216
+ export function Text({
217
+ variant,
218
+ size,
219
+ align,
220
+ className,
221
+ ...props
222
+ }: TextProps) {
223
+ return (
224
+ <RNText
225
+ className={cn(textVariants({ variant, size, align }), className)}
226
+ {...props}
227
+ />
228
+ );
229
+ }
@@ -0,0 +1,77 @@
1
+ // components/ui/textarea.tsx
2
+ import * as React from 'react';
3
+ import { TextInput, View } from 'react-native';
4
+ import { cn } from '@/lib/utils';
5
+ import { Text } from './text';
6
+ import { useThemeColors } from '@/hooks/useThemeColors';
7
+
8
+ export interface TextareaProps extends React.ComponentPropsWithoutRef<typeof TextInput> {
9
+ label?: string;
10
+ error?: string;
11
+ containerClassName?: string;
12
+ rows?: number;
13
+ }
14
+
15
+ export const Textarea = React.forwardRef<TextInput, TextareaProps>(
16
+ (
17
+ {
18
+ className,
19
+ containerClassName,
20
+ label,
21
+ error,
22
+ editable = true,
23
+ rows = 4,
24
+ ...props
25
+ },
26
+ ref
27
+ ) => {
28
+ const { colors } = useThemeColors();
29
+
30
+ return (
31
+ <View className={cn('w-full', containerClassName)}>
32
+ {label && (
33
+ <Text
34
+ size="sm"
35
+ variant="label"
36
+ className={cn(
37
+ 'mb-2',
38
+ error && 'text-destructive'
39
+ )}
40
+ >
41
+ {label}
42
+ </Text>
43
+ )}
44
+
45
+ <TextInput
46
+ ref={ref}
47
+ className={cn(
48
+ 'border rounded-lg bg-background px-3 py-3 text-base text-foreground',
49
+ error ? 'border-destructive' : 'border-input',
50
+ 'focus:border-ring',
51
+ !editable && 'bg-muted opacity-60',
52
+ className
53
+ )}
54
+ placeholderTextColor={colors.mutedForeground}
55
+ multiline
56
+ numberOfLines={rows}
57
+ textAlignVertical="top"
58
+ editable={editable}
59
+ style={[
60
+ {
61
+ minHeight: rows * 24,
62
+ color: colors.foreground,
63
+ }
64
+ ]}
65
+ {...props}
66
+ />
67
+
68
+ {error && (
69
+ <Text size="sm" className="text-destructive mt-2">
70
+ {error}
71
+ </Text>
72
+ )}
73
+ </View>
74
+ );
75
+ }
76
+ );
77
+ Textarea.displayName = 'Textarea';