@idealyst/components 1.2.29 → 1.2.31

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 (143) hide show
  1. package/README.md +3 -3
  2. package/package.json +4 -4
  3. package/plugin/__tests__/web.test.ts +2 -2
  4. package/plugin/web.js +2 -0
  5. package/src/Accordion/Accordion.native.tsx +3 -2
  6. package/src/ActivityIndicator/ActivityIndicator.native.tsx +4 -2
  7. package/src/ActivityIndicator/ActivityIndicator.styles.tsx +22 -27
  8. package/src/ActivityIndicator/ActivityIndicator.web.tsx +17 -29
  9. package/src/Alert/Alert.native.tsx +20 -10
  10. package/src/Alert/Alert.styles.tsx +173 -86
  11. package/src/Alert/Alert.web.tsx +34 -30
  12. package/src/Alert/types.ts +53 -3
  13. package/src/Avatar/Avatar.native.tsx +3 -2
  14. package/src/Avatar/Avatar.web.tsx +2 -1
  15. package/src/Avatar/types.ts +1 -1
  16. package/src/Badge/Badge.native.tsx +18 -6
  17. package/src/Badge/Badge.styles.tsx +22 -5
  18. package/src/Badge/Badge.web.tsx +12 -4
  19. package/src/Badge/types.ts +14 -2
  20. package/src/Breadcrumb/Breadcrumb.native.tsx +3 -2
  21. package/src/Button/Button.native.tsx +16 -6
  22. package/src/Button/Button.styles.tsx +2 -2
  23. package/src/Button/Button.web.tsx +19 -15
  24. package/src/Button/types.ts +6 -10
  25. package/src/Card/Card.native.tsx +27 -3
  26. package/src/Card/Card.web.tsx +30 -4
  27. package/src/Card/types.ts +15 -0
  28. package/src/Checkbox/Checkbox.native.tsx +5 -4
  29. package/src/Checkbox/Checkbox.styles.tsx +62 -52
  30. package/src/Checkbox/Checkbox.web.tsx +4 -3
  31. package/src/Checkbox/types.ts +1 -1
  32. package/src/Chip/Chip.native.tsx +30 -7
  33. package/src/Chip/Chip.web.tsx +28 -5
  34. package/src/Chip/types.ts +15 -0
  35. package/src/Dialog/Dialog.native.tsx +6 -6
  36. package/src/Dialog/Dialog.web.tsx +5 -5
  37. package/src/Dialog/types.ts +2 -2
  38. package/src/Divider/Divider.native.tsx +20 -17
  39. package/src/Divider/Divider.styles.tsx +51 -29
  40. package/src/Divider/Divider.web.tsx +5 -4
  41. package/src/Divider/types.ts +3 -3
  42. package/src/Icon/Icon.native.tsx +3 -2
  43. package/src/Icon/Icon.web.tsx +2 -1
  44. package/src/Icon/IconSvg/IconSvg.native.tsx +3 -2
  45. package/src/IconButton/IconButton.native.tsx +219 -0
  46. package/src/IconButton/IconButton.styles.tsx +127 -0
  47. package/src/IconButton/IconButton.web.tsx +198 -0
  48. package/src/IconButton/index.native.ts +5 -0
  49. package/src/IconButton/index.ts +5 -0
  50. package/src/IconButton/index.web.ts +5 -0
  51. package/src/IconButton/types.ts +84 -0
  52. package/src/Image/Image.native.tsx +3 -2
  53. package/src/Input/Input.native.tsx +42 -290
  54. package/src/Input/Input.styles.tsx +1 -1
  55. package/src/Input/Input.web.tsx +37 -288
  56. package/src/Input/index.native.ts +9 -2
  57. package/src/Input/index.ts +8 -1
  58. package/src/Input/index.web.ts +8 -1
  59. package/src/Input/types.ts +1 -1
  60. package/src/List/List.native.tsx +3 -2
  61. package/src/List/ListItem.native.tsx +3 -2
  62. package/src/List/ListSection.native.tsx +3 -2
  63. package/src/Menu/Menu.native.tsx +2 -1
  64. package/src/Menu/Menu.styles.tsx +79 -29
  65. package/src/Menu/Menu.web.tsx +2 -1
  66. package/src/Menu/MenuItem.native.tsx +4 -3
  67. package/src/Menu/MenuItem.styles.tsx +81 -32
  68. package/src/Menu/MenuItem.web.tsx +2 -1
  69. package/src/Menu/docs.ts +1 -1
  70. package/src/Popover/Popover.native.tsx +2 -1
  71. package/src/Popover/Popover.web.tsx +2 -1
  72. package/src/Popover/types.ts +15 -4
  73. package/src/Pressable/Pressable.native.tsx +3 -2
  74. package/src/Pressable/Pressable.web.tsx +3 -5
  75. package/src/Progress/Progress.native.tsx +5 -4
  76. package/src/Progress/Progress.web.tsx +3 -3
  77. package/src/Progress/types.ts +3 -3
  78. package/src/RadioButton/RadioButton.native.tsx +4 -3
  79. package/src/RadioButton/RadioButton.styles.tsx +53 -33
  80. package/src/RadioButton/RadioGroup.native.tsx +3 -2
  81. package/src/SVGImage/SVGImage.native.tsx +5 -4
  82. package/src/SVGImage/SVGImage.styles.tsx +44 -10
  83. package/src/SVGImage/SVGImage.web.tsx +2 -1
  84. package/src/Screen/Screen.native.tsx +2 -1
  85. package/src/Screen/Screen.web.tsx +2 -1
  86. package/src/Select/Select.native.tsx +6 -5
  87. package/src/Select/Select.styles.tsx +1 -1
  88. package/src/Select/Select.web.tsx +4 -3
  89. package/src/Select/types.ts +1 -1
  90. package/src/Skeleton/Skeleton.native.tsx +2 -1
  91. package/src/Skeleton/Skeleton.web.tsx +1 -1
  92. package/src/Slider/Slider.native.tsx +9 -8
  93. package/src/Slider/Slider.web.tsx +10 -9
  94. package/src/Slider/types.ts +9 -2
  95. package/src/Switch/Switch.native.tsx +7 -6
  96. package/src/Switch/Switch.styles.tsx +52 -17
  97. package/src/Switch/Switch.web.tsx +15 -16
  98. package/src/Switch/types.ts +44 -4
  99. package/src/TabBar/TabBar.native.tsx +3 -2
  100. package/src/Text/Text.native.tsx +3 -2
  101. package/src/Text/Text.web.tsx +2 -1
  102. package/src/TextArea/TextArea.native.tsx +3 -2
  103. package/src/TextArea/TextArea.styles.tsx +2 -2
  104. package/src/TextArea/TextArea.web.tsx +2 -1
  105. package/src/TextInput/TextInput.native.tsx +300 -0
  106. package/src/TextInput/TextInput.styles.tsx +207 -0
  107. package/src/TextInput/TextInput.web.tsx +301 -0
  108. package/src/TextInput/index.native.ts +3 -0
  109. package/src/TextInput/index.ts +5 -0
  110. package/src/TextInput/index.web.ts +5 -0
  111. package/src/TextInput/types.ts +163 -0
  112. package/src/Tooltip/Tooltip.native.tsx +3 -2
  113. package/src/Video/Video.native.tsx +4 -3
  114. package/src/View/View.native.tsx +2 -1
  115. package/src/View/View.styles.tsx +1 -0
  116. package/src/View/View.web.tsx +9 -2
  117. package/src/examples/ActivityIndicatorExamples.tsx +177 -0
  118. package/src/examples/AlertExamples.tsx +5 -5
  119. package/src/examples/ButtonExamples.tsx +12 -12
  120. package/src/examples/CardExamples.tsx +1 -1
  121. package/src/examples/CheckboxExamples.tsx +2 -2
  122. package/src/examples/ChipExamples.tsx +6 -6
  123. package/src/examples/DialogExamples.tsx +1 -1
  124. package/src/examples/DividerExamples.tsx +1 -1
  125. package/src/examples/InputExamples.tsx +1 -1
  126. package/src/examples/LinkExamples.tsx +1 -1
  127. package/src/examples/ListExamples.tsx +1 -1
  128. package/src/examples/MenuExamples.tsx +2 -2
  129. package/src/examples/ProgressExamples.tsx +1 -1
  130. package/src/examples/RadioButtonExamples.tsx +5 -5
  131. package/src/examples/SVGImageExamples.tsx +1 -1
  132. package/src/examples/SelectExamples.tsx +1 -1
  133. package/src/examples/SliderExamples.tsx +5 -5
  134. package/src/examples/SwitchExamples.tsx +26 -26
  135. package/src/examples/TableExamples.tsx +1 -1
  136. package/src/examples/TooltipExamples.tsx +2 -2
  137. package/src/examples/index.ts +1 -0
  138. package/src/extensions/index.ts +1 -0
  139. package/src/extensions/types.ts +22 -3
  140. package/src/index.native.ts +4 -0
  141. package/src/index.ts +27 -2
  142. package/src/utils/index.ts +12 -0
  143. package/src/utils/refTypes.ts +50 -0
@@ -0,0 +1,207 @@
1
+ /**
2
+ * TextInput styles using defineStyle with $iterator expansion.
3
+ */
4
+ import { StyleSheet } from 'react-native-unistyles';
5
+ import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
6
+ import type { Theme as BaseTheme } from '@idealyst/theme';
7
+ import { ViewStyleSize } from '../utils/viewStyleProps';
8
+
9
+ // Required: Unistyles must see StyleSheet usage in original source to process this file
10
+ void StyleSheet;
11
+
12
+ // Wrap theme for $iterator support
13
+ type Theme = ThemeStyleWrapper<BaseTheme>;
14
+
15
+ type TextInputType = 'outlined' | 'filled' | 'bare';
16
+
17
+ export type TextInputDynamicProps = {
18
+ type?: TextInputType;
19
+ focused?: boolean;
20
+ hasError?: boolean;
21
+ disabled?: boolean;
22
+ margin?: ViewStyleSize;
23
+ marginVertical?: ViewStyleSize;
24
+ marginHorizontal?: ViewStyleSize;
25
+ };
26
+
27
+ /**
28
+ * TextInput styles with type/state handling.
29
+ */
30
+ export const textInputStyles = defineStyle('TextInput', (theme: Theme) => ({
31
+ container: ({ type = 'outlined', focused = false, hasError = false, disabled = false }: TextInputDynamicProps) => ({
32
+ display: 'flex' as const,
33
+ flexDirection: 'row' as const,
34
+ alignItems: 'center' as const,
35
+ width: '100%',
36
+ minWidth: 0,
37
+ borderRadius: theme.radii.md,
38
+ borderWidth: 1,
39
+ borderStyle: 'solid' as const,
40
+ borderColor: theme.colors.border.primary,
41
+ variants: {
42
+ type: {
43
+ outlined: {
44
+ backgroundColor: theme.colors.surface.primary,
45
+ borderColor: theme.colors.border.primary,
46
+ },
47
+ filled: {
48
+ backgroundColor: theme.colors.surface.secondary,
49
+ borderColor: 'transparent',
50
+ },
51
+ bare: {
52
+ backgroundColor: 'transparent',
53
+ borderWidth: 0,
54
+ borderColor: 'transparent',
55
+ },
56
+ },
57
+ focused: {
58
+ true: {
59
+ borderColor: theme.intents.primary.primary,
60
+ },
61
+ false: {
62
+
63
+ },
64
+ },
65
+ disabled: {
66
+ true: { opacity: 0.8, _web: { cursor: 'not-allowed' } },
67
+ false: { opacity: 1 }
68
+ },
69
+ hasError: {
70
+ true: { borderColor: theme.intents.danger.primary },
71
+ false: { borderColor: theme.colors.border.primary },
72
+ },
73
+ // $iterator expands for each input size
74
+ size: {
75
+ height: theme.sizes.$input.height,
76
+ paddingHorizontal: theme.sizes.$input.paddingHorizontal,
77
+ },
78
+ margin: {
79
+ margin: theme.sizes.$view.padding,
80
+ },
81
+ marginVertical: {
82
+ marginVertical: theme.sizes.$view.padding,
83
+ },
84
+ marginHorizontal: {
85
+ marginHorizontal: theme.sizes.$view.padding,
86
+ },
87
+ },
88
+ _web: {
89
+ boxSizing: 'border-box',
90
+ transition: 'border-color 0.2s ease, box-shadow 0.2s ease',
91
+ }
92
+ }),
93
+
94
+ input: (_props: TextInputDynamicProps) => ({
95
+ flex: 1,
96
+ minWidth: 0,
97
+ backgroundColor: 'transparent' as const,
98
+ color: theme.colors.text.primary,
99
+ fontWeight: '400' as const,
100
+ variants: {
101
+ size: {
102
+ fontSize: theme.sizes.$input.fontSize,
103
+ },
104
+ },
105
+ _web: {
106
+ border: 'none',
107
+ outline: 'none',
108
+ fontFamily: 'inherit',
109
+ },
110
+ }),
111
+
112
+ leftIconContainer: (_props: TextInputDynamicProps) => ({
113
+ display: 'flex' as const,
114
+ alignItems: 'center' as const,
115
+ justifyContent: 'center' as const,
116
+ flexShrink: 0,
117
+ variants: {
118
+ size: {
119
+ width: theme.sizes.$input.iconSize,
120
+ marginRight: theme.sizes.$input.iconMargin,
121
+ },
122
+ },
123
+ }),
124
+
125
+ rightIconContainer: (_props: TextInputDynamicProps) => ({
126
+ display: 'flex' as const,
127
+ alignItems: 'center' as const,
128
+ justifyContent: 'center' as const,
129
+ flexShrink: 0,
130
+ variants: {
131
+ size: {
132
+ width: theme.sizes.$input.iconSize,
133
+ marginLeft: theme.sizes.$input.iconMargin,
134
+ },
135
+ },
136
+ }),
137
+
138
+ leftIcon: (_props: TextInputDynamicProps) => ({
139
+ color: theme.colors.text.secondary,
140
+ variants: {
141
+ size: {
142
+ fontSize: theme.sizes.$input.iconSize,
143
+ width: theme.sizes.$input.iconSize,
144
+ height: theme.sizes.$input.iconSize,
145
+ },
146
+ },
147
+ }),
148
+
149
+ rightIcon: (_props: TextInputDynamicProps) => ({
150
+ display: 'flex' as const,
151
+ alignItems: 'center' as const,
152
+ justifyContent: 'center' as const,
153
+ flexShrink: 0,
154
+ color: theme.colors.text.secondary,
155
+ variants: {
156
+ size: {
157
+ fontSize: theme.sizes.$input.iconSize,
158
+ width: theme.sizes.$input.iconSize,
159
+ height: theme.sizes.$input.iconSize,
160
+ },
161
+ },
162
+ }),
163
+
164
+ passwordToggle: (_props: TextInputDynamicProps) => ({
165
+ display: 'flex' as const,
166
+ alignItems: 'center' as const,
167
+ justifyContent: 'center' as const,
168
+ flexShrink: 0,
169
+ padding: 0,
170
+ variants: {
171
+ size: {
172
+ marginLeft: theme.sizes.$input.iconMargin,
173
+ width: theme.sizes.$input.iconSize,
174
+ height: theme.sizes.$input.iconSize,
175
+ },
176
+ },
177
+ _web: {
178
+ appearance: 'none',
179
+ background: 'none',
180
+ backgroundColor: 'transparent',
181
+ border: 'none',
182
+ outline: 'none',
183
+ cursor: 'pointer',
184
+ margin: 0,
185
+ _hover: { opacity: 0.7 },
186
+ _active: { opacity: 0.5 },
187
+ },
188
+ }),
189
+
190
+ passwordToggleIcon: (_props: TextInputDynamicProps) => ({
191
+ display: 'flex' as const,
192
+ alignItems: 'center' as const,
193
+ justifyContent: 'center' as const,
194
+ flexShrink: 0,
195
+ color: theme.colors.text.secondary,
196
+ variants: {
197
+ size: {
198
+ fontSize: theme.sizes.$input.iconSize,
199
+ width: theme.sizes.$input.iconSize,
200
+ height: theme.sizes.$input.iconSize,
201
+ },
202
+ },
203
+ }),
204
+ }));
205
+
206
+ // Legacy export for backwards compatibility
207
+ export { textInputStyles as inputStyles };
@@ -0,0 +1,301 @@
1
+ import React, { isValidElement, useState, useMemo, useRef } from 'react';
2
+ import { getWebProps } from 'react-native-unistyles/web';
3
+ import { useUnistyles } from 'react-native-unistyles';
4
+ import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
5
+ import { isIconName } from '../Icon/icon-resolver';
6
+ import useMergeRefs from '../hooks/useMergeRefs';
7
+ import { textInputStyles } from './TextInput.styles';
8
+ import { TextInputProps } from './types';
9
+ import { getWebFormAriaProps } from '../utils/accessibility';
10
+ import type { IdealystElement } from '../utils/refTypes';
11
+
12
+ /**
13
+ * Single-line text input field with support for icons, password visibility toggle, and validation states.
14
+ * Available in outlined and filled variants with multiple sizes.
15
+ */
16
+ const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
17
+ value,
18
+ onChangeText,
19
+ onFocus,
20
+ onBlur,
21
+ onPress,
22
+ placeholder,
23
+ disabled = false,
24
+ inputMode = 'text',
25
+ secureTextEntry = false,
26
+ leftIcon,
27
+ rightIcon,
28
+ showPasswordToggle,
29
+ autoCapitalize = 'sentences',
30
+ size = 'md',
31
+ type = 'outlined',
32
+ hasError = false,
33
+ // Spacing variants from FormInputStyleProps
34
+ margin,
35
+ marginVertical,
36
+ marginHorizontal,
37
+ style,
38
+ testID,
39
+ id,
40
+ // Accessibility props
41
+ accessibilityLabel,
42
+ accessibilityHint,
43
+ accessibilityDisabled,
44
+ accessibilityHidden,
45
+ accessibilityRole,
46
+ accessibilityLabelledBy,
47
+ accessibilityDescribedBy,
48
+ accessibilityControls,
49
+ accessibilityExpanded,
50
+ accessibilityPressed,
51
+ accessibilityOwns,
52
+ accessibilityHasPopup,
53
+ accessibilityRequired,
54
+ accessibilityInvalid,
55
+ accessibilityErrorMessage,
56
+ accessibilityAutoComplete,
57
+ }, ref) => {
58
+ const [isPasswordVisible, setIsPasswordVisible] = useState(false);
59
+
60
+ // Determine if we should show password toggle
61
+ const isPasswordField = inputMode === 'password' || secureTextEntry;
62
+ const shouldShowPasswordToggle = isPasswordField && (showPasswordToggle !== false);
63
+
64
+ // Get theme for icon sizes and colors
65
+ const { theme } = useUnistyles();
66
+ const iconSize = theme.sizes.input[size].iconSize;
67
+ const iconColor = theme.colors.text.secondary;
68
+
69
+ const [isFocused, setIsFocused] = useState(false);
70
+
71
+ const getInputType = () => {
72
+ // Handle password visibility
73
+ if (isPasswordField && !isPasswordVisible) {
74
+ return 'password';
75
+ }
76
+
77
+ switch (inputMode) {
78
+ case 'email':
79
+ return 'email';
80
+ case 'number':
81
+ return 'number';
82
+ case 'password':
83
+ return 'text'; // When visible
84
+ case 'text':
85
+ default:
86
+ return 'text';
87
+ }
88
+ };
89
+
90
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
91
+ if (onChangeText) {
92
+ onChangeText(e.target.value);
93
+ }
94
+ };
95
+
96
+ const handlePress = (e: React.MouseEvent<HTMLDivElement>) => {
97
+ // For web compatibility, we can trigger onFocus when pressed
98
+ e.preventDefault();
99
+ e.stopPropagation();
100
+ if (onPress) {
101
+ onPress();
102
+ }
103
+ }
104
+
105
+ const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
106
+ e.preventDefault();
107
+ e.stopPropagation();
108
+ setIsFocused(true);
109
+ if (onFocus) {
110
+ onFocus();
111
+ }
112
+ };
113
+
114
+ const handleBlur = () => {
115
+ setIsFocused(false);
116
+ if (onBlur) {
117
+ onBlur();
118
+ }
119
+ };
120
+
121
+ const togglePasswordVisibility = () => {
122
+ setIsPasswordVisible(!isPasswordVisible);
123
+ };
124
+
125
+ // Apply variants (for size and spacing)
126
+ textInputStyles.useVariants({
127
+ size,
128
+ margin,
129
+ marginVertical,
130
+ marginHorizontal,
131
+ });
132
+
133
+ // Get web props for all styled elements (all styles are dynamic functions)
134
+ const dynamicContainerStyle = (textInputStyles.container as any)({ type, focused: isFocused, hasError, disabled });
135
+ const {ref: containerStyleRef, ...containerProps} = getWebProps([dynamicContainerStyle, style]);
136
+ const leftIconContainerProps = getWebProps([(textInputStyles.leftIconContainer as any)({})]);
137
+ const rightIconContainerProps = getWebProps([(textInputStyles.rightIconContainer as any)({})]);
138
+ const passwordToggleProps = getWebProps([(textInputStyles.passwordToggle as any)({})]);
139
+
140
+ // Get input props
141
+ const inputWebProps = getWebProps([(textInputStyles.input as any)({})]);
142
+
143
+ // Generate accessibility props
144
+ const ariaProps = useMemo(() => {
145
+ // Derive invalid state from hasError or explicit accessibilityInvalid
146
+ const isInvalid = accessibilityInvalid ?? hasError;
147
+
148
+ return getWebFormAriaProps({
149
+ accessibilityLabel,
150
+ accessibilityHint,
151
+ accessibilityDisabled: accessibilityDisabled ?? disabled,
152
+ accessibilityHidden,
153
+ accessibilityRole: accessibilityRole ?? 'textbox',
154
+ accessibilityLabelledBy,
155
+ accessibilityDescribedBy,
156
+ accessibilityControls,
157
+ accessibilityExpanded,
158
+ accessibilityPressed,
159
+ accessibilityOwns,
160
+ accessibilityHasPopup,
161
+ accessibilityRequired,
162
+ accessibilityInvalid: isInvalid,
163
+ accessibilityErrorMessage,
164
+ accessibilityAutoComplete,
165
+ });
166
+ }, [
167
+ accessibilityLabel,
168
+ accessibilityHint,
169
+ accessibilityDisabled,
170
+ disabled,
171
+ accessibilityHidden,
172
+ accessibilityRole,
173
+ accessibilityLabelledBy,
174
+ accessibilityDescribedBy,
175
+ accessibilityControls,
176
+ accessibilityExpanded,
177
+ accessibilityPressed,
178
+ accessibilityOwns,
179
+ accessibilityHasPopup,
180
+ accessibilityRequired,
181
+ accessibilityInvalid,
182
+ hasError,
183
+ accessibilityErrorMessage,
184
+ accessibilityAutoComplete,
185
+ ]);
186
+
187
+ // Merge the forwarded ref with unistyles ref for the input
188
+ const mergedInputRef = useMergeRefs(ref, inputWebProps.ref);
189
+
190
+ // Helper to render left icon
191
+ const renderLeftIcon = () => {
192
+ if (!leftIcon) return null;
193
+
194
+ if (isIconName(leftIcon)) {
195
+ return (
196
+ <IconSvg
197
+ name={leftIcon}
198
+ size={iconSize}
199
+ color={iconColor}
200
+ aria-label={leftIcon}
201
+ />
202
+ );
203
+ } else if (isValidElement(leftIcon)) {
204
+ return <span {...leftIconContainerProps}>{leftIcon}</span>;
205
+ }
206
+
207
+ return null;
208
+ };
209
+
210
+ // Helper to render right icon (not password toggle)
211
+ const renderRightIcon = () => {
212
+ if (!rightIcon) return null;
213
+
214
+ if (isIconName(rightIcon)) {
215
+ return (
216
+ <IconSvg
217
+ name={rightIcon}
218
+ size={iconSize}
219
+ color={iconColor}
220
+ aria-label={rightIcon}
221
+ />
222
+ );
223
+ } else if (isValidElement(rightIcon)) {
224
+ return <span {...rightIconContainerProps}>{rightIcon}</span>;
225
+ }
226
+
227
+ return null;
228
+ };
229
+
230
+ // Helper to render password toggle icon
231
+ const renderPasswordToggleIcon = () => {
232
+ const iconName = isPasswordVisible ? 'eye-off' : 'eye';
233
+ return (
234
+ <IconSvg
235
+ name={iconName}
236
+ size={iconSize}
237
+ color={iconColor}
238
+ aria-label={iconName}
239
+ />
240
+ );
241
+ };
242
+
243
+ const containerRef = useRef<HTMLDivElement>(null);
244
+
245
+ const handleContainerPress = (e: React.MouseEvent<HTMLDivElement>) => {
246
+ e.preventDefault();
247
+ e.stopPropagation();
248
+ containerRef.current?.focus();
249
+ }
250
+
251
+ const mergedContainerRef = useMergeRefs(containerRef, containerStyleRef);
252
+
253
+ return (
254
+ <div onClick={handleContainerPress} ref={mergedContainerRef} {...containerProps} id={id} data-testid={testID}>
255
+ {/* Left Icon */}
256
+ {leftIcon && (
257
+ <span {...leftIconContainerProps}>
258
+ {renderLeftIcon()}
259
+ </span>
260
+ )}
261
+
262
+ {/* Input */}
263
+ <input
264
+ {...inputWebProps}
265
+ {...ariaProps}
266
+ ref={mergedInputRef}
267
+ type={getInputType()}
268
+ value={value}
269
+ onClick={handlePress}
270
+ onChange={handleChange}
271
+ onFocus={handleFocus}
272
+ onBlur={handleBlur}
273
+ placeholder={placeholder}
274
+ disabled={disabled}
275
+ autoCapitalize={autoCapitalize}
276
+ />
277
+
278
+ {/* Right Icon or Password Toggle */}
279
+ {shouldShowPasswordToggle ? (
280
+ <button
281
+ {...passwordToggleProps}
282
+ onClick={togglePasswordVisibility}
283
+ disabled={disabled}
284
+ aria-label={isPasswordVisible ? 'Hide password' : 'Show password'}
285
+ type="button"
286
+ tabIndex={-1}
287
+ >
288
+ {renderPasswordToggleIcon()}
289
+ </button>
290
+ ) : rightIcon ? (
291
+ <span {...rightIconContainerProps}>
292
+ {renderRightIcon()}
293
+ </span>
294
+ ) : null}
295
+ </div>
296
+ );
297
+ });
298
+
299
+ TextInput.displayName = 'TextInput';
300
+
301
+ export default TextInput;
@@ -0,0 +1,3 @@
1
+ // React Native-specific TextInput export
2
+ export { default } from './TextInput.native';
3
+ export * from './types';
@@ -0,0 +1,5 @@
1
+ import TextInputComponent from './TextInput.web';
2
+
3
+ export default TextInputComponent;
4
+ export { TextInputComponent as TextInput };
5
+ export * from './types';
@@ -0,0 +1,5 @@
1
+ import TextInputComponent from './TextInput.web';
2
+
3
+ export default TextInputComponent;
4
+ export { TextInputComponent as TextInput };
5
+ export * from './types';
@@ -0,0 +1,163 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
2
+ import type { IconName } from '../Icon/icon-types';
3
+ import { Intent, Size } from '@idealyst/theme';
4
+ import { FormInputStyleProps } from '../utils/viewStyleProps';
5
+ import { FormAccessibilityProps } from '../utils/accessibility';
6
+
7
+ // Component-specific type aliases for future extensibility
8
+ export type TextInputIntent = Intent;
9
+ export type TextInputSize = Size;
10
+ export type TextInputType = 'outlined' | 'filled' | 'bare';
11
+
12
+ /**
13
+ * Input mode for keyboard type on mobile platforms.
14
+ * This prop only affects React Native - on web, browser handles keyboard automatically.
15
+ * @platform native
16
+ */
17
+ export type TextInputMode = 'text' | 'email' | 'password' | 'number';
18
+
19
+ /**
20
+ * Single-line text input field with support for icons, validation states, and multiple visual styles.
21
+ * Includes built-in password visibility toggle and platform-specific keyboard handling.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * // Basic usage
26
+ * <TextInput
27
+ * value={email}
28
+ * onChangeText={setEmail}
29
+ * placeholder="Enter email"
30
+ * inputMode="email"
31
+ * />
32
+ *
33
+ * // Password with toggle
34
+ * <TextInput
35
+ * value={password}
36
+ * onChangeText={setPassword}
37
+ * inputMode="password"
38
+ * secureTextEntry
39
+ * />
40
+ *
41
+ * // With icons and validation
42
+ * <TextInput
43
+ * value={search}
44
+ * onChangeText={setSearch}
45
+ * leftIcon="magnify"
46
+ * intent="danger"
47
+ * />
48
+ * ```
49
+ */
50
+ export interface TextInputProps extends FormInputStyleProps, FormAccessibilityProps {
51
+ /**
52
+ * The current value of the input
53
+ */
54
+ value?: string;
55
+
56
+ /**
57
+ * Called when the text changes
58
+ */
59
+ onChangeText?: (text: string) => void;
60
+
61
+ /**
62
+ * Called when the input receives focus
63
+ */
64
+ onFocus?: () => void;
65
+
66
+ /**
67
+ * Called when the input loses focus
68
+ */
69
+ onBlur?: () => void;
70
+
71
+ /**
72
+ * Called when the input is pressed
73
+ */
74
+ onPress?: () => void;
75
+
76
+ /**
77
+ * Placeholder text shown when the input is empty
78
+ */
79
+ placeholder?: string;
80
+
81
+ /**
82
+ * Whether the input is disabled
83
+ */
84
+ disabled?: boolean;
85
+
86
+ /**
87
+ * The type of input keyboard to show on mobile platforms.
88
+ * This prop only affects React Native - on web, browser handles keyboard automatically.
89
+ * @platform native
90
+ */
91
+ inputMode?: TextInputMode;
92
+
93
+ /**
94
+ * Whether to hide the text (for passwords)
95
+ */
96
+ secureTextEntry?: boolean;
97
+
98
+ /**
99
+ * Icon to display on the left side of the input.
100
+ * Can be an icon name string or a custom React node.
101
+ */
102
+ leftIcon?: IconName | React.ReactNode;
103
+
104
+ /**
105
+ * Icon to display on the right side of the input.
106
+ * Can be an icon name string or a custom React node.
107
+ */
108
+ rightIcon?: IconName | React.ReactNode;
109
+
110
+ /**
111
+ * Show password visibility toggle for password inputs.
112
+ * Defaults to true when inputMode="password" or secureTextEntry is true.
113
+ */
114
+ showPasswordToggle?: boolean;
115
+
116
+ /**
117
+ * Auto-capitalization behavior
118
+ */
119
+ autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
120
+
121
+ /**
122
+ * Size variant of the input
123
+ */
124
+ size?: TextInputSize;
125
+
126
+ /**
127
+ * Visual style type of the input
128
+ */
129
+ type?: TextInputType;
130
+
131
+ /**
132
+ * The intent/color scheme of the input (for focus states, validation, etc.)
133
+ */
134
+ intent?: TextInputIntent;
135
+
136
+ /**
137
+ * Whether the input has an error state
138
+ * @deprecated Use intent="danger" instead
139
+ */
140
+ hasError?: boolean;
141
+
142
+ /**
143
+ * Additional styles (platform-specific)
144
+ */
145
+ style?: StyleProp<ViewStyle>;
146
+
147
+ /**
148
+ * Test ID for testing
149
+ */
150
+ testID?: string;
151
+ }
152
+
153
+ // Legacy type aliases for backwards compatibility
154
+ /** @deprecated Use TextInputIntent instead */
155
+ export type InputIntent = TextInputIntent;
156
+ /** @deprecated Use TextInputSize instead */
157
+ export type InputSize = TextInputSize;
158
+ /** @deprecated Use TextInputType instead */
159
+ export type InputType = TextInputType;
160
+ /** @deprecated Use TextInputMode instead */
161
+ export type InputInputType = TextInputMode;
162
+ /** @deprecated Use TextInputProps instead */
163
+ export type InputProps = TextInputProps;
@@ -3,8 +3,9 @@ import { View, Modal, Text, Pressable } from 'react-native';
3
3
  import { tooltipStyles } from './Tooltip.styles';
4
4
  import type { TooltipProps } from './types';
5
5
  import { getNativeAccessibilityProps } from '../utils/accessibility';
6
+ import type { IdealystElement } from '../utils/refTypes';
6
7
 
7
- const Tooltip = forwardRef<View, TooltipProps>(({
8
+ const Tooltip = forwardRef<IdealystElement, TooltipProps>(({
8
9
  content,
9
10
  children,
10
11
  placement = 'top',
@@ -144,7 +145,7 @@ const Tooltip = forwardRef<View, TooltipProps>(({
144
145
 
145
146
  return (
146
147
  <>
147
- <View ref={ref} nativeID={id} collapsable={false} style={style} {...nativeA11yProps}>
148
+ <View ref={ref as any} nativeID={id} collapsable={false} style={style} {...nativeA11yProps}>
148
149
  {trigger}
149
150
  </View>
150
151