@retray-dev/ui-kit 5.1.0 → 5.4.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.
@@ -1,8 +1,9 @@
1
1
  import React, { useEffect, useRef } from 'react'
2
- import { View, Text, StyleSheet, ViewStyle } from 'react-native'
2
+ import { View, Text, StyleSheet, ViewStyle, KeyboardAvoidingView, Platform } from 'react-native'
3
3
  import {
4
4
  BottomSheetModal,
5
5
  BottomSheetView,
6
+ BottomSheetScrollView,
6
7
  BottomSheetBackdrop,
7
8
  BottomSheetModalProvider,
8
9
  type BottomSheetBackdropProps,
@@ -19,8 +20,16 @@ export interface SheetProps {
19
20
  title?: string
20
21
  description?: string
21
22
  children?: React.ReactNode
22
- /** Style for the inner `BottomSheetView` content container. */
23
+ /** Style for the inner content container. */
23
24
  style?: ViewStyle
25
+ /** Render children inside BottomSheetScrollView so gestures are handled correctly on both platforms. */
26
+ scrollable?: boolean
27
+ /** Cap sheet height (dp). Children scroll when content exceeds this value. */
28
+ maxHeight?: number
29
+ /** Wrap content in KeyboardAvoidingView. Defaults to platform-appropriate behavior when set. */
30
+ keyboardBehavior?: 'padding' | 'height' | 'position' | 'none'
31
+ /** Extra vertical offset for the keyboard avoiding view. */
32
+ keyboardOffset?: number
24
33
  }
25
34
 
26
35
  export function Sheet({
@@ -30,6 +39,10 @@ export function Sheet({
30
39
  description,
31
40
  children,
32
41
  style,
42
+ scrollable,
43
+ maxHeight,
44
+ keyboardBehavior,
45
+ keyboardOffset = 0,
33
46
  }: SheetProps) {
34
47
  const { colors } = useTheme()
35
48
  const ref = useRef<BottomSheetModal>(null)
@@ -52,6 +65,31 @@ export function Sheet({
52
65
  />
53
66
  )
54
67
 
68
+ const headerNode = (title || description) ? (
69
+ <View style={styles.header}>
70
+ {title ? (
71
+ <Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>{title}</Text>
72
+ ) : null}
73
+ {description ? (
74
+ <Text style={[styles.description, { color: colors.foregroundMuted }]} allowFontScaling={true}>
75
+ {description}
76
+ </Text>
77
+ ) : null}
78
+ </View>
79
+ ) : null
80
+
81
+ const useScroll = scrollable || !!maxHeight
82
+
83
+ const wrapKeyboard = (node: React.ReactNode) => {
84
+ if (!keyboardBehavior || keyboardBehavior === 'none') return node
85
+ const behavior = keyboardBehavior ?? Platform.select({ ios: 'padding', android: 'height' }) as 'padding' | 'height'
86
+ return (
87
+ <KeyboardAvoidingView behavior={behavior} keyboardVerticalOffset={keyboardOffset}>
88
+ {node}
89
+ </KeyboardAvoidingView>
90
+ )
91
+ }
92
+
55
93
  return (
56
94
  <BottomSheetModal
57
95
  ref={ref}
@@ -62,20 +100,18 @@ export function Sheet({
62
100
  handleIndicatorStyle={[styles.handle, { backgroundColor: colors.border }]}
63
101
  enablePanDownToClose
64
102
  >
65
- <BottomSheetView style={[styles.content, style]}>
66
- {title || description ? (
67
- <View style={styles.header}>
68
- {title ? (
69
- <Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>{title}</Text>
70
- ) : null}
71
- {description ? (
72
- <Text style={[styles.description, { color: colors.foregroundMuted }]} allowFontScaling={true}>
73
- {description}
74
- </Text>
75
- ) : null}
76
- </View>
77
- ) : null}
78
- {children}
103
+ <BottomSheetView style={maxHeight ? { maxHeight } : undefined}>
104
+ {wrapKeyboard(useScroll ? (
105
+ <BottomSheetScrollView contentContainerStyle={[styles.content, style]}>
106
+ {headerNode}
107
+ {children}
108
+ </BottomSheetScrollView>
109
+ ) : (
110
+ <BottomSheetView style={[styles.content, style]}>
111
+ {headerNode}
112
+ {children}
113
+ </BottomSheetView>
114
+ ))}
79
115
  </BottomSheetView>
80
116
  </BottomSheetModal>
81
117
  )
@@ -2,6 +2,7 @@ import React, { useState } from 'react'
2
2
  import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, Platform } from 'react-native'
3
3
  import { useTheme } from '../../theme'
4
4
  import { s, vs, ms } from '../../utils/scaling'
5
+ import { renderIcon } from '../../utils/icons'
5
6
 
6
7
  const webInputResetStyle: any =
7
8
  Platform.OS === 'web'
@@ -16,6 +17,12 @@ export interface TextareaProps extends TextInputProps {
16
17
  hint?: string
17
18
  /** Number of visible text rows. Defaults to `4`. Controls `numberOfLines` and `minHeight`. */
18
19
  rows?: number
20
+ /** Icon name from @expo/vector-icons rendered inside top-left corner. */
21
+ prefixIcon?: string
22
+ /** Custom icon node rendered top-left. */
23
+ prefixIconNode?: React.ReactNode
24
+ /** Override prefix icon color. Defaults to foregroundMuted. */
25
+ prefixIconColor?: string
19
26
  /** Style for the outer container `View`. Use `style` (from `TextInputProps`) to style the `TextInput` itself. */
20
27
  containerStyle?: ViewStyle
21
28
  }
@@ -25,6 +32,9 @@ export function Textarea({
25
32
  error,
26
33
  hint,
27
34
  rows = 4,
35
+ prefixIcon,
36
+ prefixIconNode,
37
+ prefixIconColor,
28
38
  containerStyle,
29
39
  style,
30
40
  onFocus,
@@ -34,40 +44,53 @@ export function Textarea({
34
44
  const { colors } = useTheme()
35
45
  const [focused, setFocused] = useState(false)
36
46
 
47
+ const resolvedPrefixIcon = prefixIcon
48
+ ? renderIcon(prefixIcon, ms(16), prefixIconColor ?? colors.foregroundMuted)
49
+ : prefixIconNode
50
+
37
51
  return (
38
52
  <View style={[styles.container, containerStyle]}>
39
53
  {label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
40
- <TextInput
41
- multiline
42
- numberOfLines={rows}
43
- textAlignVertical="top"
54
+ <View
44
55
  style={[
45
- styles.input,
56
+ styles.inputWrapper,
46
57
  {
47
58
  borderColor: error
48
59
  ? colors.destructive
49
60
  : focused
50
61
  ? (colors.ring ?? colors.primary)
51
62
  : colors.border,
52
- color: colors.foreground,
53
63
  backgroundColor: colors.background,
54
- minHeight: rows * vs(30),
55
64
  },
56
- webInputResetStyle,
57
- style,
58
65
  ]}
59
- onFocus={(e) => {
60
- setFocused(true)
61
- onFocus?.(e)
62
- }}
63
- onBlur={(e) => {
64
- setFocused(false)
65
- onBlur?.(e)
66
- }}
67
- placeholderTextColor={colors.foregroundMuted}
68
- allowFontScaling={true}
69
- {...props}
70
- />
66
+ >
67
+ {resolvedPrefixIcon ? <View style={styles.prefixIcon}>{resolvedPrefixIcon}</View> : null}
68
+ <TextInput
69
+ multiline
70
+ numberOfLines={rows}
71
+ textAlignVertical="top"
72
+ style={[
73
+ styles.input,
74
+ {
75
+ color: colors.foreground,
76
+ minHeight: rows * vs(30),
77
+ },
78
+ webInputResetStyle,
79
+ style,
80
+ ]}
81
+ onFocus={(e) => {
82
+ setFocused(true)
83
+ onFocus?.(e)
84
+ }}
85
+ onBlur={(e) => {
86
+ setFocused(false)
87
+ onBlur?.(e)
88
+ }}
89
+ placeholderTextColor={colors.foregroundMuted}
90
+ allowFontScaling={true}
91
+ {...props}
92
+ />
93
+ </View>
71
94
  {error ? (
72
95
  <Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
73
96
  ) : null}
@@ -80,23 +103,37 @@ export function Textarea({
80
103
 
81
104
  const styles = StyleSheet.create({
82
105
  container: {
83
- gap: vs(8),
106
+ gap: vs(4),
84
107
  },
85
108
  label: {
86
109
  fontFamily: 'Poppins-Medium',
87
110
  fontSize: ms(13),
111
+ lineHeight: vs(18),
112
+ marginBottom: vs(2),
88
113
  },
89
- input: {
90
- fontFamily: 'Poppins-Regular',
91
- borderWidth: 2,
92
- borderRadius: ms(8),
114
+ inputWrapper: {
115
+ borderWidth: 1,
116
+ borderRadius: 8,
93
117
  paddingHorizontal: s(14),
94
118
  paddingVertical: vs(11),
95
- fontSize: ms(15),
96
- includeFontPadding: false,
119
+ gap: s(8),
120
+ },
121
+ prefixIcon: {
122
+ alignItems: 'flex-start',
123
+ justifyContent: 'flex-start',
124
+ paddingTop: vs(2),
125
+ },
126
+ input: {
127
+ fontFamily: 'Poppins-Regular',
128
+ fontSize: ms(14),
129
+ lineHeight: vs(22),
130
+ padding: 0,
131
+ margin: 0,
97
132
  },
98
133
  helperText: {
99
134
  fontFamily: 'Poppins-Regular',
100
- fontSize: ms(13),
135
+ fontSize: ms(12),
136
+ lineHeight: vs(16),
137
+ marginTop: vs(4),
101
138
  },
102
139
  })
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export { defaultLight, defaultDark, deriveColors } from './theme'
5
5
 
6
6
  // Components
7
7
  export * from './components/Button'
8
+ export * from './components/ButtonGroup'
8
9
  export * from './components/IconButton'
9
10
  export * from './components/Text'
10
11
  export * from './components/Input'
@@ -40,9 +41,13 @@ export * from './components/MonthPicker'
40
41
  export * from './components/MediaCard'
41
42
  export * from './components/CategoryStrip'
42
43
  export * from './components/Pressable'
44
+ export * from './components/DetailRow'
43
45
 
44
46
  // Icon utility
45
47
  export { Icon, renderIcon } from './utils/icons'
48
+
49
+ // Typography utilities
50
+ export { getResponsiveFontSize } from './utils/typography'
46
51
  export type { IconProps, IconFamily } from './utils/icons'
47
52
 
48
53
  // Design tokens
package/src/tokens.ts CHANGED
@@ -180,7 +180,7 @@ export const TYPOGRAPHY = {
180
180
  fontFamily: 'Poppins-Medium',
181
181
  fontSize: 16,
182
182
  fontWeight: '500' as const,
183
- lineHeight: 20,
183
+ lineHeight: 22,
184
184
  letterSpacing: 0,
185
185
  },
186
186
  'button-sm': {
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Returns a font size that steps down as formatted text grows longer.
3
+ * Designed for currency/numeric displays where long values overflow.
4
+ *
5
+ * @param text - The formatted string (e.g. "$1.250.000")
6
+ * @param maxSize - Base (maximum) font size in points
7
+ * @param steps - Custom step config [{ maxLen, subtract }] — sorted ascending by maxLen
8
+ */
9
+ export function getResponsiveFontSize(
10
+ text: string,
11
+ maxSize: number,
12
+ steps: { maxLen: number; subtract: number }[] = [
13
+ { maxLen: 10, subtract: 0 },
14
+ { maxLen: 12, subtract: 4 },
15
+ { maxLen: 14, subtract: 6 },
16
+ ],
17
+ ): number {
18
+ const len = text.length
19
+ const sorted = [...steps].sort((a, b) => a.maxLen - b.maxLen)
20
+ for (const step of sorted) {
21
+ if (len <= step.maxLen) return maxSize - step.subtract
22
+ }
23
+ return maxSize - 8
24
+ }