@retray-dev/ui-kit 1.8.0 → 2.5.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 (34) hide show
  1. package/COMPONENTS.md +150 -43
  2. package/dist/index.d.mts +80 -44
  3. package/dist/index.d.ts +80 -44
  4. package/dist/index.js +627 -457
  5. package/dist/index.mjs +626 -457
  6. package/package.json +8 -2
  7. package/src/components/Accordion/Accordion.tsx +4 -6
  8. package/src/components/Alert/Alert.tsx +3 -3
  9. package/src/components/AlertBanner/AlertBanner.tsx +85 -0
  10. package/src/components/{Alert → AlertBanner}/index.ts +2 -2
  11. package/src/components/Avatar/Avatar.tsx +1 -0
  12. package/src/components/Badge/Badge.tsx +45 -9
  13. package/src/components/Button/Button.tsx +5 -5
  14. package/src/components/Card/Card.tsx +90 -18
  15. package/src/components/Checkbox/Checkbox.tsx +4 -4
  16. package/src/components/Chip/Chip.tsx +36 -5
  17. package/src/components/CurrencyInput/CurrencyInput.tsx +9 -1
  18. package/src/components/EmptyState/EmptyState.tsx +2 -1
  19. package/src/components/Input/Input.tsx +107 -26
  20. package/src/components/ListItem/ListItem.tsx +157 -21
  21. package/src/components/MonthPicker/MonthPicker.tsx +3 -6
  22. package/src/components/RadioGroup/RadioGroup.tsx +2 -2
  23. package/src/components/Select/Select.tsx +200 -132
  24. package/src/components/Slider/Slider.tsx +64 -100
  25. package/src/components/Switch/Switch.tsx +22 -20
  26. package/src/components/Textarea/Textarea.tsx +16 -7
  27. package/src/components/Toast/Toast.tsx +23 -18
  28. package/src/components/Toggle/Toggle.tsx +36 -49
  29. package/src/index.ts +3 -2
  30. package/src/theme/ThemeProvider.tsx +11 -8
  31. package/src/theme/colors.ts +19 -18
  32. package/src/theme/types.ts +2 -0
  33. package/src/components/CurrencyInputLarge/CurrencyInputLarge.tsx +0 -66
  34. package/src/components/CurrencyInputLarge/index.ts +0 -1
@@ -1,7 +1,12 @@
1
1
  import React, { useState } from 'react'
2
- import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle } from 'react-native'
2
+ import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, Platform } from 'react-native'
3
3
  import { useTheme } from '../../theme'
4
4
 
5
+ const webInputResetStyle: any =
6
+ Platform.OS === 'web'
7
+ ? { outlineStyle: 'none', outlineWidth: 0, outlineColor: 'transparent', boxShadow: 'none' }
8
+ : {}
9
+
5
10
  export interface TextareaProps extends TextInputProps {
6
11
  label?: string
7
12
  /** Red helper text below the textarea; also changes border to `destructive` color. Takes priority over `hint`. */
@@ -38,11 +43,16 @@ export function Textarea({
38
43
  style={[
39
44
  styles.input,
40
45
  {
41
- borderColor: error ? colors.destructive : focused ? colors.ring : colors.border,
46
+ borderColor: error
47
+ ? colors.destructive
48
+ : focused
49
+ ? (colors.ring ?? colors.primary)
50
+ : colors.border,
42
51
  color: colors.foreground,
43
52
  backgroundColor: colors.background,
44
53
  minHeight: rows * 30,
45
54
  },
55
+ webInputResetStyle,
46
56
  style,
47
57
  ]}
48
58
  onFocus={(e) => {
@@ -69,18 +79,17 @@ export function Textarea({
69
79
 
70
80
  const styles = StyleSheet.create({
71
81
  container: {
72
- gap: 6,
82
+ gap: 8,
73
83
  },
74
84
  label: {
75
85
  fontSize: 15,
76
86
  fontWeight: '500',
77
- marginBottom: 6,
78
87
  },
79
88
  input: {
80
89
  borderWidth: 1.5,
81
- borderRadius: 14,
82
- paddingHorizontal: 20,
83
- paddingVertical: 16,
90
+ borderRadius: 8,
91
+ paddingHorizontal: 16,
92
+ paddingVertical: 14,
84
93
  fontSize: 17,
85
94
  },
86
95
  helperText: {
@@ -1,5 +1,6 @@
1
1
  import React, { createContext, useContext, useState, useCallback, useEffect } from 'react'
2
- import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'
2
+ import { View, Text, TouchableOpacity, StyleSheet, Platform } from 'react-native'
3
+ import { FontAwesome5, MaterialIcons, Entypo, AntDesign } from '@expo/vector-icons'
3
4
  import Animated, {
4
5
  useSharedValue,
5
6
  useAnimatedStyle,
@@ -98,11 +99,16 @@ function ToastNotification({ item, onDismiss }: { item: ToastItem; onDismiss: ()
98
99
  success: colors.successForeground,
99
100
  }[item.variant ?? 'default']
100
101
 
101
- const leftIcon = item.icon ?? (
102
- <Text style={[styles.defaultIcon, { color: textColor }]}>
103
- {item.variant === 'success' ? '✓' : item.variant === 'destructive' ? '✖' : 'ℹ'}
104
- </Text>
105
- )
102
+ const defaultIcon =
103
+ item.variant === 'success' ? (
104
+ <FontAwesome5 name="check-circle" size={22} color={textColor} />
105
+ ) : item.variant === 'destructive' ? (
106
+ <MaterialIcons name="error-outline" size={24} color={textColor} />
107
+ ) : (
108
+ <Entypo name="info-with-circle" size={22} color={textColor} />
109
+ )
110
+
111
+ const leftIcon = item.icon ?? defaultIcon
106
112
 
107
113
  return (
108
114
  <GestureDetector gesture={panGesture}>
@@ -113,13 +119,13 @@ function ToastNotification({ item, onDismiss }: { item: ToastItem; onDismiss: ()
113
119
  <Text style={[styles.toastTitle, { color: textColor }]}>{item.title}</Text>
114
120
  ) : null}
115
121
  {item.description ? (
116
- <Text style={[styles.toastDescription, { color: textColor, opacity: 0.85 }]}>
122
+ <Text style={[styles.toastDescription, { color: textColor, opacity: 0.85 }]}>
117
123
  {item.description}
118
124
  </Text>
119
125
  ) : null}
120
126
  </View>
121
127
  <TouchableOpacity onPress={onDismiss} style={styles.dismissButton} touchSoundDisabled={true}>
122
- <Text style={[styles.dismissIcon, { color: textColor }]}>✕</Text>
128
+ <AntDesign name="close-circle" size={18} color={textColor} />
123
129
  </TouchableOpacity>
124
130
  </Animated.View>
125
131
  </GestureDetector>
@@ -158,7 +164,7 @@ export function ToastProvider({ children }: ToastProviderProps) {
158
164
  return (
159
165
  <ToastContext.Provider value={{ toast, dismiss }}>
160
166
  {children}
161
- <View style={[styles.container, { top: insets.top + 8 }]} pointerEvents="box-none">
167
+ <View style={[styles.container, Platform.OS === 'web' && styles.containerWeb, { top: insets.top + 8 }]} pointerEvents="box-none">
162
168
  {toasts.map((item) => (
163
169
  <ToastNotification key={item.id} item={item} onDismiss={() => dismiss(item.id)} />
164
170
  ))}
@@ -175,6 +181,12 @@ const styles = StyleSheet.create({
175
181
  gap: 8,
176
182
  zIndex: 9999,
177
183
  },
184
+ containerWeb: {
185
+ left: undefined,
186
+ right: undefined,
187
+ alignSelf: 'center',
188
+ width: 400,
189
+ },
178
190
  toast: {
179
191
  flexDirection: 'row',
180
192
  alignItems: 'center',
@@ -192,15 +204,11 @@ const styles = StyleSheet.create({
192
204
  gap: 4,
193
205
  },
194
206
  leftIconContainer: {
195
- width: 36,
207
+ width: 40,
196
208
  alignItems: 'center',
197
209
  justifyContent: 'center',
198
210
  marginRight: 8,
199
211
  },
200
- defaultIcon: {
201
- fontSize: 22,
202
- fontWeight: '700',
203
- },
204
212
  toastTitle: {
205
213
  fontSize: 15,
206
214
  fontWeight: '600',
@@ -209,10 +217,7 @@ const styles = StyleSheet.create({
209
217
  fontSize: 14,
210
218
  },
211
219
  dismissButton: {
212
- padding: 12,
220
+ padding: 8,
213
221
  marginLeft: 4,
214
222
  },
215
- dismissIcon: {
216
- fontSize: 14,
217
- },
218
223
  })
@@ -1,5 +1,6 @@
1
1
  import React, { useRef, useEffect } from 'react'
2
- import { TouchableOpacity, Animated, Text, StyleSheet, TouchableOpacityProps, ViewStyle, View, Easing } from 'react-native'
2
+ import { TouchableOpacity, Animated, StyleSheet, TouchableOpacityProps, ViewStyle, View, Easing } from 'react-native'
3
+ import { FontAwesome5 } from '@expo/vector-icons'
3
4
  import * as Haptics from 'expo-haptics'
4
5
  import { useTheme } from '../../theme'
5
6
 
@@ -83,46 +84,46 @@ export function Toggle({
83
84
  return prop
84
85
  }
85
86
 
86
- if (!pressed) return renderProp(icon)
87
+ if (pressed) {
88
+ const active = renderProp(activeIcon)
89
+ if (active) return <>{active}</>
90
+ return <FontAwesome5 name="check-circle" size={20} color={colors.primary} />
91
+ }
87
92
 
88
- const active = renderProp(activeIcon)
89
- if (active) return <>{active}</>
93
+ const custom = renderProp(icon)
94
+ if (custom) return <>{custom}</>
90
95
 
91
- return (
92
- <View style={[styles.checkContainer, { borderColor: colors.primary }]}>
93
- <Text style={[styles.checkMark, { color: colors.primary }]}>✓</Text>
94
- </View>
95
- )
96
+ // Default: empty circle to signal an action is available
97
+ return <FontAwesome5 name="circle" size={20} color={colors.mutedForeground} />
96
98
  }
97
99
 
98
100
  return (
99
- <Animated.View style={{ transform: [{ scale }] }}>
100
- <Animated.View
101
- style={[
102
- styles.base,
103
- sizeStyles[size],
104
- { borderColor, backgroundColor, borderWidth: 2 },
105
- disabled && styles.disabled,
106
- style,
107
- ]}
101
+ <Animated.View style={[{ transform: [{ scale }] }, disabled && styles.disabled, style]}>
102
+ <TouchableOpacity
103
+ onPress={() => {
104
+ Haptics.selectionAsync()
105
+ onPressedChange?.(!pressed)
106
+ }}
107
+ onPressIn={handlePressIn}
108
+ onPressOut={handlePressOut}
109
+ disabled={disabled}
110
+ activeOpacity={1}
111
+ touchSoundDisabled={true}
112
+ {...props}
108
113
  >
109
- <TouchableOpacity
110
- style={styles.touchable}
111
- onPress={() => {
112
- Haptics.selectionAsync()
113
- onPressedChange?.(!pressed)
114
- }}
115
- onPressIn={handlePressIn}
116
- onPressOut={handlePressOut}
117
- disabled={disabled}
118
- activeOpacity={1}
119
- touchSoundDisabled={true}
120
- {...props}
114
+ <Animated.View
115
+ style={[
116
+ styles.base,
117
+ sizeStyles[size],
118
+ { borderColor, backgroundColor, borderWidth: 2 },
119
+ ]}
121
120
  >
122
- <LeftIcon />
123
- {label ? <Animated.Text style={[styles.label, { color: textColor }]}>{label}</Animated.Text> : null}
124
- </TouchableOpacity>
125
- </Animated.View>
121
+ <View style={styles.inner}>
122
+ <LeftIcon />
123
+ {label ? <Animated.Text style={[styles.label, { color: textColor }]}>{label}</Animated.Text> : null}
124
+ </View>
125
+ </Animated.View>
126
+ </TouchableOpacity>
126
127
  </Animated.View>
127
128
  )
128
129
  }
@@ -130,14 +131,12 @@ export function Toggle({
130
131
  const styles = StyleSheet.create({
131
132
  base: {
132
133
  borderRadius: 8,
133
- overflow: 'hidden',
134
134
  },
135
- touchable: {
135
+ inner: {
136
136
  flexDirection: 'row',
137
137
  alignItems: 'center',
138
138
  justifyContent: 'center',
139
139
  gap: 8,
140
- flex: 1,
141
140
  },
142
141
  disabled: {
143
142
  opacity: 0.45,
@@ -146,16 +145,4 @@ const styles = StyleSheet.create({
146
145
  fontSize: 14,
147
146
  fontWeight: '500',
148
147
  },
149
- checkContainer: {
150
- width: 24,
151
- height: 24,
152
- borderRadius: 12,
153
- borderWidth: 2,
154
- alignItems: 'center',
155
- justifyContent: 'center',
156
- },
157
- checkMark: {
158
- fontSize: 14,
159
- fontWeight: '700',
160
- },
161
148
  })
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@ export * from './components/Separator'
13
13
  export * from './components/Spinner'
14
14
  export * from './components/Skeleton'
15
15
  export * from './components/Avatar'
16
- export * from './components/Alert'
16
+ export * from './components/AlertBanner'
17
17
  export * from './components/Progress'
18
18
  export * from './components/EmptyState'
19
19
  export * from './components/Textarea'
@@ -29,7 +29,8 @@ export * from './components/Select'
29
29
  export * from './components/Toast'
30
30
  export * from './components/CurrencyInput'
31
31
  export * from './components/CurrencyDisplay'
32
- export * from './components/CurrencyInputLarge'
32
+ // CurrencyInputLarge is deprecated — use <CurrencyInput size="large" /> instead
33
+ export { CurrencyInput as CurrencyInputLarge } from './components/CurrencyInput'
33
34
  export * from './components/ListItem'
34
35
  export * from './components/Chip'
35
36
  export * from './components/ConfirmDialog'
@@ -11,11 +11,10 @@ const ThemeContext = createContext<ThemeContextValue>({
11
11
  export interface ThemeProviderProps {
12
12
  children: React.ReactNode
13
13
  /**
14
- * Override individual color tokens per scheme. Only provide the tokens you want
15
- * to change the rest fall back to the defaults.
16
- * @example
17
- * { light: { primary: '#6366f1', primaryForeground: '#fff' },
18
- * dark: { primary: '#818cf8', primaryForeground: '#fff' } }
14
+ * Optional full-palette overrides per scheme. Supply a partial or full `ThemeColors` object
15
+ * for `light` and/or `dark` to override the defaults.
16
+ * @example
17
+ * { light: { primary: '#6366f1', card: '#fff' }, dark: { primary: '#818cf8' } }
19
18
  */
20
19
  theme?: Theme
21
20
  /**
@@ -31,8 +30,8 @@ export function ThemeProvider({ children, theme, colorScheme = 'system' }: Theme
31
30
 
32
31
  const colors = useMemo<ThemeColors>(() => {
33
32
  const base = resolvedScheme === 'dark' ? defaultDark : defaultLight
34
- const overrides = resolvedScheme === 'dark' ? theme?.dark : theme?.light
35
- return { ...base, ...overrides }
33
+ const override = resolvedScheme === 'dark' ? theme?.dark : theme?.light
34
+ return override ? { ...base, ...override } : base
36
35
  }, [resolvedScheme, theme])
37
36
 
38
37
  return (
@@ -43,5 +42,9 @@ export function ThemeProvider({ children, theme, colorScheme = 'system' }: Theme
43
42
  }
44
43
 
45
44
  export function useTheme(): ThemeContextValue {
46
- return useContext(ThemeContext)
45
+ const context = useContext(ThemeContext)
46
+ if (!context) {
47
+ throw new Error('useTheme must be used within a ThemeProvider')
48
+ }
49
+ return context
47
50
  }
@@ -1,45 +1,46 @@
1
1
  import { ThemeColors } from './types'
2
2
 
3
+ // Full, explicit theme palettes. No derivation — palettes are direct and fully customizable.
3
4
  export const defaultLight: ThemeColors = {
4
5
  background: '#ffffff',
5
6
  foreground: '#171717',
6
7
  card: '#ffffff',
7
8
  cardForeground: '#171717',
8
9
  primary: '#1a1a1a',
9
- primaryForeground: '#fafafa',
10
- secondary: '#f5f5f5',
11
- secondaryForeground: '#1a1a1a',
12
- muted: '#f5f5f5',
13
- mutedForeground: '#646464',
14
- accent: '#f5f5f5',
15
- accentForeground: '#1a1a1a',
10
+ primaryForeground: '#ffffff',
11
+ secondary: '#f1f1f1',
12
+ secondaryForeground: '#171717',
13
+ muted: '#f1f1f1',
14
+ mutedForeground: '#a2a2a2',
15
+ accent: '#e4e4e4',
16
+ accentForeground: '#171717',
16
17
  destructive: '#ef4444',
17
- destructiveForeground: '#fafafa',
18
+ destructiveForeground: '#1a1a1a',
18
19
  border: '#e5e5e5',
19
20
  input: '#e5e5e5',
20
- ring: '#a3a3a3',
21
+ ring: '#1a1a1a',
21
22
  success: '#16a34a',
22
- successForeground: '#ffffff',
23
+ successForeground: '#1a1a1a',
23
24
  }
24
25
 
25
26
  export const defaultDark: ThemeColors = {
26
27
  background: '#171717',
27
28
  foreground: '#fafafa',
28
- card: '#1f1f1f',
29
+ card: '#222222',
29
30
  cardForeground: '#fafafa',
30
31
  primary: '#fafafa',
31
32
  primaryForeground: '#1a1a1a',
32
- secondary: '#2a2a2a',
33
+ secondary: '#323232',
33
34
  secondaryForeground: '#fafafa',
34
- muted: '#2a2a2a',
35
- mutedForeground: '#a3a3a3',
36
- accent: '#2a2a2a',
35
+ muted: '#323232',
36
+ mutedForeground: '#888888',
37
+ accent: '#323232',
37
38
  accentForeground: '#fafafa',
38
39
  destructive: '#dc2626',
39
- destructiveForeground: '#fafafa',
40
+ destructiveForeground: '#1a1a1a',
40
41
  border: '#2a2a2a',
41
42
  input: '#2a2a2a',
42
- ring: '#d4d4d4',
43
+ ring: '#fafafa',
43
44
  success: '#22c55e',
44
- successForeground: '#ffffff',
45
+ successForeground: '#1a1a1a',
45
46
  }
@@ -20,6 +20,8 @@ export type ThemeColors = {
20
20
  successForeground: string
21
21
  }
22
22
 
23
+ // Theme overrides: consumers may supply partial or full `ThemeColors` objects
24
+ // for `theme.light` / `theme.dark` to override only the tokens they want.
23
25
  export type Theme = {
24
26
  light?: Partial<ThemeColors>
25
27
  dark?: Partial<ThemeColors>
@@ -1,66 +0,0 @@
1
- import React from 'react'
2
- import { ViewStyle } from 'react-native'
3
- import { Input } from '../Input'
4
-
5
- export interface CurrencyInputLargeProps {
6
- value?: string
7
- onChangeText?: (formatted: string) => void
8
- /** Called with the parsed numeric value (no separators, no prefix). */
9
- onChangeValue?: (raw: number) => void
10
- /** Symbol prepended to the formatted value. Defaults to `'$'`. */
11
- prefix?: string
12
- /** Character used to separate groups of three digits. Defaults to `'.'`. */
13
- thousandsSeparator?: '.' | ','
14
- label?: string
15
- /** Red helper text; also changes input border to destructive color. */
16
- error?: string
17
- hint?: string
18
- placeholder?: string
19
- editable?: boolean
20
- containerStyle?: ViewStyle
21
- }
22
-
23
- function formatCurrency(raw: string, separator: '.' | ','): string {
24
- const digits = raw.replace(/\D/g, '')
25
- if (!digits) return ''
26
- return digits.replace(/\B(?=(\d{3})+(?!\d))/g, separator)
27
- }
28
-
29
- export function CurrencyInputLarge({
30
- value,
31
- onChangeText,
32
- onChangeValue,
33
- prefix = '$',
34
- thousandsSeparator = '.',
35
- label,
36
- error,
37
- hint,
38
- placeholder,
39
- editable,
40
- containerStyle,
41
- }: CurrencyInputLargeProps) {
42
- const handleChange = (text: string) => {
43
- const withoutPrefix = prefix && text.startsWith(prefix) ? text.slice(prefix.length) : text
44
- const formatted = formatCurrency(withoutPrefix, thousandsSeparator)
45
- const display = formatted ? `${prefix}${formatted}` : ''
46
- onChangeText?.(display)
47
- const separatorRegex = new RegExp(`\\${thousandsSeparator}`, 'g')
48
- const raw = parseFloat(formatted.replace(separatorRegex, '') || '0')
49
- onChangeValue?.(isNaN(raw) ? 0 : raw)
50
- }
51
-
52
- return (
53
- <Input
54
- value={value}
55
- onChangeText={handleChange}
56
- keyboardType="numeric"
57
- label={label}
58
- error={error}
59
- hint={hint}
60
- placeholder={placeholder ?? `${prefix}0`}
61
- editable={editable}
62
- containerStyle={containerStyle}
63
- style={{ fontSize: 36 }}
64
- />
65
- )
66
- }
@@ -1 +0,0 @@
1
- export * from './CurrencyInputLarge'