@idealyst/components 1.1.6 → 1.1.8

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 (144) hide show
  1. package/package.json +8 -3
  2. package/src/Accordion/Accordion.native.tsx +22 -14
  3. package/src/Accordion/Accordion.styles.old.tsx +298 -0
  4. package/src/Accordion/Accordion.styles.tsx +139 -248
  5. package/src/Accordion/Accordion.web.tsx +12 -7
  6. package/src/ActivityIndicator/ActivityIndicator.native.tsx +3 -2
  7. package/src/ActivityIndicator/ActivityIndicator.styles.old.tsx +94 -0
  8. package/src/ActivityIndicator/ActivityIndicator.styles.tsx +43 -62
  9. package/src/ActivityIndicator/ActivityIndicator.web.tsx +2 -2
  10. package/src/Alert/Alert.native.tsx +26 -15
  11. package/src/Alert/Alert.styles.old.tsx +209 -0
  12. package/src/Alert/Alert.styles.tsx +108 -281
  13. package/src/Alert/Alert.web.tsx +6 -10
  14. package/src/Avatar/Avatar.native.tsx +5 -2
  15. package/src/Avatar/Avatar.styles.old.tsx +99 -0
  16. package/src/Avatar/Avatar.styles.tsx +47 -62
  17. package/src/Avatar/Avatar.web.tsx +2 -2
  18. package/src/Badge/Badge.native.tsx +2 -2
  19. package/src/Badge/Badge.styles.old.tsx +157 -0
  20. package/src/Badge/Badge.styles.tsx +69 -108
  21. package/src/Badge/Badge.web.tsx +6 -6
  22. package/src/Breadcrumb/Breadcrumb.native.tsx +12 -5
  23. package/src/Breadcrumb/Breadcrumb.styles.old.tsx +231 -0
  24. package/src/Breadcrumb/Breadcrumb.styles.tsx +93 -209
  25. package/src/Breadcrumb/Breadcrumb.web.tsx +39 -27
  26. package/src/Button/Button.native.tsx +39 -14
  27. package/src/Button/Button.styles.tsx +99 -253
  28. package/src/Button/Button.web.tsx +10 -8
  29. package/src/Card/Card.native.tsx +8 -4
  30. package/src/Card/Card.styles.old.tsx +160 -0
  31. package/src/Card/Card.styles.tsx +107 -142
  32. package/src/Card/Card.web.tsx +6 -4
  33. package/src/Checkbox/Checkbox.native.tsx +14 -6
  34. package/src/Checkbox/Checkbox.styles.old.tsx +271 -0
  35. package/src/Checkbox/Checkbox.styles.tsx +109 -197
  36. package/src/Checkbox/Checkbox.web.tsx +7 -7
  37. package/src/Chip/Chip.native.tsx +5 -5
  38. package/src/Chip/Chip.styles.old.tsx +184 -0
  39. package/src/Chip/Chip.styles.tsx +34 -22
  40. package/src/Chip/Chip.web.tsx +5 -5
  41. package/src/Dialog/Dialog.native.tsx +16 -7
  42. package/src/Dialog/Dialog.styles.old.tsx +202 -0
  43. package/src/Dialog/Dialog.styles.tsx +108 -132
  44. package/src/Dialog/Dialog.web.tsx +4 -4
  45. package/src/Divider/Divider.native.tsx +29 -42
  46. package/src/Divider/Divider.styles.old.tsx +172 -0
  47. package/src/Divider/Divider.styles.tsx +116 -242
  48. package/src/Divider/Divider.web.tsx +17 -14
  49. package/src/Icon/Icon.native.tsx +12 -4
  50. package/src/Icon/Icon.styles.old.tsx +81 -0
  51. package/src/Icon/Icon.styles.tsx +52 -60
  52. package/src/Icon/Icon.web.tsx +43 -7
  53. package/src/Icon/IconSvg/IconSvg.web.tsx +2 -0
  54. package/src/Image/Image.styles.old.tsx +69 -0
  55. package/src/Image/Image.styles.tsx +45 -43
  56. package/src/Input/Input.native.tsx +140 -56
  57. package/src/Input/Input.styles.old.tsx +289 -0
  58. package/src/Input/Input.styles.tsx +177 -228
  59. package/src/Input/Input.web.tsx +5 -8
  60. package/src/Link/Link.native.tsx +4 -1
  61. package/src/List/List.native.tsx +5 -2
  62. package/src/List/List.styles.old.tsx +242 -0
  63. package/src/List/List.styles.tsx +178 -240
  64. package/src/List/ListItem.native.tsx +16 -8
  65. package/src/List/ListItem.web.tsx +26 -15
  66. package/src/Menu/Menu.native.tsx +1 -1
  67. package/src/Menu/Menu.styles.old.tsx +197 -0
  68. package/src/Menu/Menu.styles.tsx +90 -156
  69. package/src/Menu/Menu.web.tsx +2 -2
  70. package/src/Menu/MenuItem.native.tsx +9 -5
  71. package/src/Menu/MenuItem.styles.old.tsx +114 -0
  72. package/src/Menu/MenuItem.styles.tsx +71 -104
  73. package/src/Menu/MenuItem.web.tsx +23 -5
  74. package/src/Popover/Popover.native.tsx +10 -4
  75. package/src/Popover/Popover.styles.old.tsx +135 -0
  76. package/src/Popover/Popover.styles.tsx +46 -96
  77. package/src/Popover/Popover.web.tsx +1 -1
  78. package/src/Pressable/Pressable.native.tsx +3 -1
  79. package/src/Pressable/Pressable.styles.old.tsx +27 -0
  80. package/src/Pressable/Pressable.styles.tsx +35 -20
  81. package/src/Pressable/Pressable.web.tsx +1 -1
  82. package/src/Progress/Progress.native.tsx +15 -6
  83. package/src/Progress/Progress.styles.old.tsx +200 -0
  84. package/src/Progress/Progress.styles.tsx +69 -118
  85. package/src/Progress/Progress.web.tsx +10 -9
  86. package/src/RadioButton/RadioButton.native.tsx +10 -4
  87. package/src/RadioButton/RadioButton.styles.old.tsx +175 -0
  88. package/src/RadioButton/RadioButton.styles.tsx +81 -145
  89. package/src/RadioButton/RadioButton.web.tsx +4 -4
  90. package/src/SVGImage/SVGImage.styles.old.tsx +86 -0
  91. package/src/SVGImage/SVGImage.styles.tsx +35 -66
  92. package/src/Screen/Screen.native.tsx +30 -27
  93. package/src/Screen/Screen.styles.old.tsx +87 -0
  94. package/src/Screen/Screen.styles.tsx +120 -71
  95. package/src/Screen/Screen.web.tsx +2 -2
  96. package/src/Select/Select.native.tsx +44 -29
  97. package/src/Select/Select.styles.old.tsx +353 -0
  98. package/src/Select/Select.styles.tsx +244 -293
  99. package/src/Select/Select.web.tsx +5 -5
  100. package/src/Skeleton/Skeleton.styles.old.tsx +67 -0
  101. package/src/Skeleton/Skeleton.styles.tsx +31 -43
  102. package/src/Slider/Slider.native.tsx +9 -5
  103. package/src/Slider/Slider.styles.old.tsx +259 -0
  104. package/src/Slider/Slider.styles.tsx +157 -227
  105. package/src/Slider/Slider.web.tsx +5 -5
  106. package/src/Switch/Switch.native.tsx +11 -5
  107. package/src/Switch/Switch.styles.old.tsx +203 -0
  108. package/src/Switch/Switch.styles.tsx +103 -149
  109. package/src/Switch/Switch.web.tsx +8 -8
  110. package/src/TabBar/TabBar.native.tsx +24 -31
  111. package/src/TabBar/TabBar.styles.old.tsx +343 -0
  112. package/src/TabBar/TabBar.styles.tsx +204 -494
  113. package/src/TabBar/TabBar.web.tsx +21 -33
  114. package/src/Table/Table.native.tsx +18 -9
  115. package/src/Table/Table.styles.old.tsx +311 -0
  116. package/src/Table/Table.styles.tsx +151 -278
  117. package/src/Table/Table.web.tsx +1 -1
  118. package/src/Text/Text.native.tsx +1 -4
  119. package/src/Text/Text.style.demo.tsx +16 -0
  120. package/src/Text/Text.styles.old.tsx +219 -0
  121. package/src/Text/Text.styles.tsx +94 -78
  122. package/src/Text/Text.web.tsx +2 -2
  123. package/src/Text/index.ts +1 -0
  124. package/src/TextArea/TextArea.styles.old.tsx +213 -0
  125. package/src/TextArea/TextArea.styles.tsx +101 -157
  126. package/src/Tooltip/Tooltip.native.tsx +2 -2
  127. package/src/Tooltip/Tooltip.styles.old.tsx +82 -0
  128. package/src/Tooltip/Tooltip.styles.tsx +38 -53
  129. package/src/Tooltip/Tooltip.web.tsx +2 -2
  130. package/src/Video/Video.styles.old.tsx +51 -0
  131. package/src/Video/Video.styles.tsx +32 -28
  132. package/src/View/View.native.tsx +12 -12
  133. package/src/View/View.styles.old.tsx +125 -0
  134. package/src/View/View.styles.tsx +84 -103
  135. package/src/View/View.web.tsx +14 -2
  136. package/src/examples/CardExamples.tsx +0 -6
  137. package/src/extensions/applyExtension.ts +210 -0
  138. package/src/extensions/extendComponent.ts +438 -0
  139. package/src/extensions/index.ts +102 -0
  140. package/src/extensions/types.ts +497 -0
  141. package/src/globals.ts +16 -0
  142. package/src/index.native.ts +4 -0
  143. package/src/index.ts +28 -0
  144. package/src/utils/deepMerge.ts +54 -2
@@ -1,10 +1,55 @@
1
- import React, { useState, isValidElement, useMemo } from 'react';
2
- import { View, TextInput, TouchableOpacity } from 'react-native';
1
+ import React, { useState, isValidElement, useMemo, useEffect, useRef, useCallback } from 'react';
2
+ import { View, TextInput, TouchableOpacity, Platform, TextInputProps } from 'react-native';
3
3
  import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
4
+ import { useUnistyles } from 'react-native-unistyles';
4
5
  import { InputProps } from './types';
5
6
  import { inputStyles } from './Input.styles';
6
7
  import { getNativeFormAccessibilityProps } from '../utils/accessibility';
7
8
 
9
+ // Inner TextInput component that can be memoized to prevent re-renders
10
+ // for Android secure text entry
11
+ type InnerTextInputProps = {
12
+ inputRef: React.ForwardedRef<TextInput>;
13
+ value: string | undefined;
14
+ onChangeText: ((text: string) => void) | undefined;
15
+ isAndroidSecure: boolean;
16
+ textInputProps: Omit<TextInputProps, 'value' | 'defaultValue' | 'onChangeText'>;
17
+ inputStyle: any;
18
+ };
19
+
20
+ const InnerTextInput = React.memo<InnerTextInputProps>(
21
+ ({ inputRef, value, onChangeText, isAndroidSecure, textInputProps, inputStyle }) => {
22
+ return (
23
+ <TextInput
24
+ ref={inputRef}
25
+ // For Android secure text entry, don't pass value prop at all
26
+ // Let TextInput manage its own state to preserve character reveal animation
27
+ {...(isAndroidSecure ? {} : { value })}
28
+ onChangeText={onChangeText}
29
+ style={inputStyle}
30
+ {...textInputProps}
31
+ />
32
+ );
33
+ },
34
+ (prevProps, nextProps) => {
35
+ // For Android secure text entry, skip re-renders when only value changes
36
+ if (nextProps.isAndroidSecure) {
37
+ // Only re-render if non-value props change
38
+ const valueChanged = prevProps.value !== nextProps.value;
39
+ const otherPropsChanged =
40
+ prevProps.onChangeText !== nextProps.onChangeText ||
41
+ prevProps.isAndroidSecure !== nextProps.isAndroidSecure ||
42
+ prevProps.textInputProps !== nextProps.textInputProps ||
43
+ prevProps.inputStyle !== nextProps.inputStyle;
44
+
45
+ if (valueChanged && !otherPropsChanged) {
46
+ return true; // Skip re-render
47
+ }
48
+ }
49
+ return false; // Allow re-render
50
+ }
51
+ );
52
+
8
53
  const Input = React.forwardRef<TextInput, InputProps>(({
9
54
  value,
10
55
  onChangeText,
@@ -41,11 +86,30 @@ const Input = React.forwardRef<TextInput, InputProps>(({
41
86
  const [isFocused, setIsFocused] = useState(false);
42
87
  const [isPasswordVisible, setIsPasswordVisible] = useState(false);
43
88
 
89
+ // Track if this is a secure field that needs Android workaround
90
+ const isSecureField = inputType === 'password' || secureTextEntry;
91
+ const needsAndroidSecureWorkaround = Platform.OS === 'android' && isSecureField && !isPasswordVisible;
92
+
93
+ // For Android secure text entry, we use an internal ref to track value
94
+ const internalValueRef = useRef(value ?? '');
95
+
96
+ // Sync external value changes to internal ref (for programmatic updates)
97
+ useEffect(() => {
98
+ if (value !== undefined) {
99
+ internalValueRef.current = value;
100
+ }
101
+ }, [value]);
102
+
103
+ // Get theme for icon sizes and colors
104
+ const { theme } = useUnistyles();
105
+ const iconSize = theme.sizes.input[size].iconSize;
106
+ const iconColor = theme.colors.text.secondary;
107
+
44
108
  // Determine if we should show password toggle
45
109
  const isPasswordField = inputType === 'password' || secureTextEntry;
46
110
  const shouldShowPasswordToggle = isPasswordField && (showPasswordToggle !== false);
47
111
 
48
- const getKeyboardType = () => {
112
+ const getKeyboardType = useCallback((): 'default' | 'email-address' | 'numeric' => {
49
113
  switch (inputType) {
50
114
  case 'email':
51
115
  return 'email-address';
@@ -56,43 +120,34 @@ const Input = React.forwardRef<TextInput, InputProps>(({
56
120
  default:
57
121
  return 'default';
58
122
  }
59
- };
123
+ }, [inputType]);
60
124
 
61
- const handleFocus = () => {
125
+ const handleFocus = useCallback(() => {
62
126
  setIsFocused(true);
63
- if (onFocus) {
64
- onFocus();
65
- }
66
- };
127
+ onFocus?.();
128
+ }, [onFocus]);
67
129
 
68
- const handlePress = () => {
69
- if (onPress) {
70
- onPress();
71
- }
72
- }
130
+ const handlePress = useCallback(() => {
131
+ onPress?.();
132
+ }, [onPress]);
73
133
 
74
- const handleBlur = () => {
134
+ const handleBlur = useCallback(() => {
75
135
  setIsFocused(false);
76
- if (onBlur) {
77
- onBlur();
78
- }
79
- };
136
+ onBlur?.();
137
+ }, [onBlur]);
80
138
 
81
139
  const togglePasswordVisibility = () => {
82
140
  setIsPasswordVisible(!isPasswordVisible);
83
141
  };
84
142
 
85
- // Apply variants to the stylesheet
86
- inputStyles.useVariants({
87
- size,
88
- type,
89
- focused: isFocused,
90
- hasError,
91
- disabled,
92
- margin,
93
- marginVertical,
94
- marginHorizontal,
95
- });
143
+ // Memoized change handler for InnerTextInput
144
+ const handleChangeText = useCallback((text: string) => {
145
+ internalValueRef.current = text;
146
+ onChangeText?.(text);
147
+ }, [onChangeText]);
148
+
149
+ // Memoized input style
150
+ const inputStyle = useMemo(() => (inputStyles.input as any)({}), []);
96
151
 
97
152
  // Generate native accessibility props
98
153
  const nativeA11yProps = useMemo(() => {
@@ -120,17 +175,55 @@ const Input = React.forwardRef<TextInput, InputProps>(({
120
175
  hasError,
121
176
  ]);
122
177
 
178
+ // Memoized TextInput props (everything except value/onChangeText)
179
+ const textInputProps = useMemo(() => ({
180
+ onPress: handlePress,
181
+ placeholder,
182
+ editable: !disabled,
183
+ keyboardType: getKeyboardType(),
184
+ secureTextEntry: isSecureField && !isPasswordVisible,
185
+ autoCapitalize,
186
+ onFocus: handleFocus,
187
+ onBlur: handleBlur,
188
+ placeholderTextColor: '#999999',
189
+ ...nativeA11yProps,
190
+ }), [
191
+ handlePress,
192
+ placeholder,
193
+ disabled,
194
+ getKeyboardType,
195
+ isSecureField,
196
+ isPasswordVisible,
197
+ autoCapitalize,
198
+ handleFocus,
199
+ handleBlur,
200
+ nativeA11yProps,
201
+ ]);
202
+
203
+ // Apply variants to the stylesheet (for size and spacing)
204
+ inputStyles.useVariants({
205
+ size,
206
+ margin,
207
+ marginVertical,
208
+ marginHorizontal,
209
+ });
210
+
211
+ // Compute dynamic styles - call as functions for theme reactivity
212
+ const containerStyle = (inputStyles.container as any)({ type, focused: isFocused, hasError, disabled });
213
+ const leftIconContainerStyle = (inputStyles.leftIconContainer as any)({});
214
+ const rightIconContainerStyle = (inputStyles.rightIconContainer as any)({});
215
+ const passwordToggleStyle = (inputStyles.passwordToggle as any)({});
216
+
123
217
  // Helper to render left icon
124
218
  const renderLeftIcon = () => {
125
219
  if (!leftIcon) return null;
126
220
 
127
221
  if (typeof leftIcon === 'string') {
128
- const iconStyle = inputStyles.leftIcon;
129
222
  return (
130
223
  <MaterialCommunityIcons
131
224
  name={leftIcon}
132
- size={iconStyle.width}
133
- color={iconStyle.color}
225
+ size={iconSize}
226
+ color={iconColor}
134
227
  />
135
228
  );
136
229
  } else if (isValidElement(leftIcon)) {
@@ -145,12 +238,11 @@ const Input = React.forwardRef<TextInput, InputProps>(({
145
238
  if (!rightIcon) return null;
146
239
 
147
240
  if (typeof rightIcon === 'string') {
148
- const iconStyle = inputStyles.rightIcon;
149
241
  return (
150
242
  <MaterialCommunityIcons
151
243
  name={rightIcon}
152
- size={iconStyle.width}
153
- color={iconStyle.color}
244
+ size={iconSize}
245
+ color={iconColor}
154
246
  />
155
247
  );
156
248
  } else if (isValidElement(rightIcon)) {
@@ -161,48 +253,40 @@ const Input = React.forwardRef<TextInput, InputProps>(({
161
253
  };
162
254
 
163
255
  return (
164
- <View style={[inputStyles.container, style]} testID={testID} nativeID={id}>
256
+ <View style={[containerStyle, style]} testID={testID} nativeID={id}>
165
257
  {/* Left Icon */}
166
258
  {leftIcon && (
167
- <View style={inputStyles.leftIconContainer}>
259
+ <View style={leftIconContainerStyle}>
168
260
  {renderLeftIcon()}
169
261
  </View>
170
262
  )}
171
263
 
172
264
  {/* Input */}
173
- <TextInput
174
- onPress={handlePress}
175
- ref={ref}
265
+ <InnerTextInput
266
+ inputRef={ref}
176
267
  value={value}
177
- onChangeText={onChangeText}
178
- placeholder={placeholder}
179
- editable={!disabled}
180
- keyboardType={getKeyboardType()}
181
- secureTextEntry={(secureTextEntry || inputType === 'password') && !isPasswordVisible}
182
- autoCapitalize={autoCapitalize}
183
- onFocus={handleFocus}
184
- onBlur={handleBlur}
185
- style={inputStyles.input}
186
- placeholderTextColor="#999999"
187
- {...nativeA11yProps}
268
+ onChangeText={handleChangeText}
269
+ isAndroidSecure={needsAndroidSecureWorkaround}
270
+ inputStyle={inputStyle}
271
+ textInputProps={textInputProps}
188
272
  />
189
273
 
190
274
  {/* Right Icon or Password Toggle */}
191
275
  {shouldShowPasswordToggle ? (
192
276
  <TouchableOpacity
193
- style={inputStyles.passwordToggle}
277
+ style={passwordToggleStyle}
194
278
  onPress={togglePasswordVisibility}
195
279
  disabled={disabled}
196
280
  accessibilityLabel={isPasswordVisible ? 'Hide password' : 'Show password'}
197
281
  >
198
282
  <MaterialCommunityIcons
199
283
  name={isPasswordVisible ? 'eye-off' : 'eye'}
200
- size={inputStyles.passwordToggleIcon.width}
201
- color={inputStyles.passwordToggleIcon.color}
284
+ size={iconSize}
285
+ color={iconColor}
202
286
  />
203
287
  </TouchableOpacity>
204
288
  ) : rightIcon ? (
205
- <View style={inputStyles.rightIconContainer}>
289
+ <View style={rightIconContainerStyle}>
206
290
  {renderRightIcon()}
207
291
  </View>
208
292
  ) : null}
@@ -0,0 +1,289 @@
1
+ import { StyleSheet } from 'react-native-unistyles';
2
+ import { Theme, Size } from '@idealyst/theme';
3
+ import { buildSizeVariants } from '../utils/buildSizeVariants';
4
+ import {
5
+ buildMarginVariants,
6
+ buildMarginVerticalVariants,
7
+ buildMarginHorizontalVariants,
8
+ } from '../utils/buildViewStyleVariants';
9
+ import { InputSize, InputType } from './types';
10
+ import { applyExtensions } from '../extensions/applyExtension';
11
+
12
+ export type InputVariants = {
13
+ size: InputSize;
14
+ type: InputType;
15
+ focused: boolean;
16
+ hasError: boolean;
17
+ disabled: boolean;
18
+ }
19
+
20
+ type InputDynamicProps = {
21
+ type?: InputType;
22
+ focused?: boolean;
23
+ hasError?: boolean;
24
+ disabled?: boolean;
25
+ };
26
+
27
+ /**
28
+ * Get container border/background styles based on type, focused, hasError, disabled
29
+ */
30
+ function getContainerDynamicStyles(theme: Theme, props: InputDynamicProps) {
31
+ const { type = 'outlined', focused = false, hasError = false, disabled = false } = props;
32
+ const focusColor = theme.intents.primary.primary;
33
+ const errorColor = theme.intents.error.primary;
34
+
35
+ // Base styles by type
36
+ let backgroundColor = 'transparent';
37
+ let borderWidth = 1;
38
+ let borderColor = theme.colors.border.primary;
39
+ let borderStyle = 'solid' as const;
40
+
41
+ if (type === 'filled') {
42
+ backgroundColor = theme.colors.surface.secondary;
43
+ borderWidth = 0;
44
+ } else if (type === 'bare') {
45
+ backgroundColor = 'transparent';
46
+ borderWidth = 0;
47
+ }
48
+
49
+ // Error state takes precedence
50
+ if (hasError) {
51
+ borderColor = errorColor;
52
+ borderWidth = 1;
53
+ }
54
+
55
+ // Focus state (error still takes precedence for color)
56
+ if (focused && !hasError) {
57
+ borderColor = focusColor;
58
+ borderWidth = 1;
59
+ }
60
+
61
+ // Disabled state
62
+ if (disabled) {
63
+ backgroundColor = theme.colors.surface.secondary;
64
+ }
65
+
66
+ return {
67
+ backgroundColor,
68
+ borderWidth,
69
+ borderColor,
70
+ borderStyle,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Create dynamic container styles
76
+ */
77
+ function createContainerStyles(theme: Theme) {
78
+ return (props: InputDynamicProps) => {
79
+ const { type = 'outlined', focused = false, hasError = false, disabled = false } = props;
80
+ const dynamicStyles = getContainerDynamicStyles(theme, props);
81
+ const focusColor = theme.intents.primary.primary;
82
+ const errorColor = theme.intents.error.primary;
83
+
84
+ // Web-specific border and shadow
85
+ let webBorder = `1px solid ${dynamicStyles.borderColor}`;
86
+ let webBoxShadow = 'none';
87
+
88
+ if (type === 'filled' || type === 'bare') {
89
+ webBorder = 'none';
90
+ }
91
+
92
+ if (hasError) {
93
+ webBorder = `1px solid ${errorColor}`;
94
+ if (focused) {
95
+ webBoxShadow = `0 0 0 2px ${errorColor}20`;
96
+ }
97
+ } else if (focused) {
98
+ webBorder = `1px solid ${focusColor}`;
99
+ webBoxShadow = `0 0 0 2px ${focusColor}20`;
100
+ }
101
+
102
+ return {
103
+ display: 'flex',
104
+ flexDirection: 'row',
105
+ alignItems: 'center',
106
+ width: '100%',
107
+ minWidth: 0,
108
+ borderRadius: 8,
109
+ ...dynamicStyles,
110
+ opacity: disabled ? 0.6 : 1,
111
+ variants: {
112
+ size: buildSizeVariants(theme, 'input', (size) => ({
113
+ height: size.height,
114
+ paddingHorizontal: size.paddingHorizontal,
115
+ })),
116
+ // Spacing variants from FormInputStyleProps
117
+ margin: buildMarginVariants(theme),
118
+ marginVertical: buildMarginVerticalVariants(theme),
119
+ marginHorizontal: buildMarginHorizontalVariants(theme),
120
+ },
121
+ _web: {
122
+ boxSizing: 'border-box',
123
+ transition: 'border-color 0.2s ease, box-shadow 0.2s ease',
124
+ border: webBorder,
125
+ boxShadow: webBoxShadow,
126
+ cursor: disabled ? 'not-allowed' : 'text',
127
+ },
128
+ } as const;
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Create left icon container styles
134
+ */
135
+ function createLeftIconContainerStyles(theme: Theme) {
136
+ return () => ({
137
+ display: 'flex' as const,
138
+ alignItems: 'center' as const,
139
+ justifyContent: 'center' as const,
140
+ flexShrink: 0,
141
+ variants: {
142
+ size: buildSizeVariants(theme, 'input', (size) => ({
143
+ marginRight: size.iconMargin,
144
+ })),
145
+ },
146
+ });
147
+ }
148
+
149
+ /**
150
+ * Create right icon container styles
151
+ */
152
+ function createRightIconContainerStyles(theme: Theme) {
153
+ return () => ({
154
+ display: 'flex' as const,
155
+ alignItems: 'center' as const,
156
+ justifyContent: 'center' as const,
157
+ flexShrink: 0,
158
+ variants: {
159
+ size: buildSizeVariants(theme, 'input', (size) => ({
160
+ marginLeft: size.iconMargin,
161
+ })),
162
+ },
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Create left icon styles
168
+ */
169
+ function createLeftIconStyles(theme: Theme) {
170
+ return () => ({
171
+ color: theme.colors.text.secondary,
172
+ variants: {
173
+ size: buildSizeVariants(theme, 'input', (size) => ({
174
+ fontSize: size.iconSize,
175
+ width: size.iconSize,
176
+ height: size.iconSize,
177
+ })),
178
+ },
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Create right icon styles
184
+ */
185
+ function createRightIconStyles(theme: Theme) {
186
+ return () => ({
187
+ display: 'flex' as const,
188
+ alignItems: 'center' as const,
189
+ justifyContent: 'center' as const,
190
+ flexShrink: 0,
191
+ color: theme.colors.text.secondary,
192
+ variants: {
193
+ size: buildSizeVariants(theme, 'input', (size) => ({
194
+ fontSize: size.iconSize,
195
+ width: size.iconSize,
196
+ height: size.iconSize,
197
+ })),
198
+ },
199
+ });
200
+ }
201
+
202
+ /**
203
+ * Create password toggle styles
204
+ */
205
+ function createPasswordToggleStyles(theme: Theme) {
206
+ return () => ({
207
+ display: 'flex' as const,
208
+ alignItems: 'center' as const,
209
+ justifyContent: 'center' as const,
210
+ flexShrink: 0,
211
+ padding: 0,
212
+ variants: {
213
+ size: buildSizeVariants(theme, 'input', (size) => ({
214
+ marginLeft: size.iconMargin,
215
+ })),
216
+ },
217
+ _web: {
218
+ background: 'transparent',
219
+ border: 'none',
220
+ cursor: 'pointer',
221
+ _hover: {
222
+ opacity: 0.7,
223
+ },
224
+ _active: {
225
+ opacity: 0.5,
226
+ },
227
+ },
228
+ });
229
+ }
230
+
231
+ /**
232
+ * Create password toggle icon styles
233
+ */
234
+ function createPasswordToggleIconStyles(theme: Theme) {
235
+ return () => ({
236
+ display: 'flex' as const,
237
+ alignItems: 'center' as const,
238
+ justifyContent: 'center' as const,
239
+ flexShrink: 0,
240
+ color: theme.colors.text.secondary,
241
+ variants: {
242
+ size: buildSizeVariants(theme, 'input', (size) => ({
243
+ fontSize: size.iconSize,
244
+ width: size.iconSize,
245
+ height: size.iconSize,
246
+ })),
247
+ },
248
+ });
249
+ }
250
+
251
+ /**
252
+ * Create input styles
253
+ */
254
+ function createInputStyles(theme: Theme) {
255
+ return () => ({
256
+ flex: 1,
257
+ minWidth: 0,
258
+ backgroundColor: 'transparent',
259
+ color: theme.colors.text.primary,
260
+ fontWeight: '400' as const,
261
+ variants: {
262
+ size: buildSizeVariants(theme, 'input', (size) => ({
263
+ fontSize: size.fontSize,
264
+ })),
265
+ },
266
+ _web: {
267
+ border: 'none',
268
+ outline: 'none',
269
+ fontFamily: 'inherit',
270
+ },
271
+ });
272
+ }
273
+
274
+ // Styles are inlined here instead of in @idealyst/theme because Unistyles' Babel
275
+ // transform on native cannot resolve function calls to extract variant structures.
276
+ export const inputStyles = StyleSheet.create((theme: Theme) => {
277
+ // Apply extensions to main visual elements
278
+
279
+ return applyExtensions('Input', theme, {container: createContainerStyles(theme),
280
+ input: createInputStyles(theme),
281
+ // Additional styles (merged from return)
282
+ // Minor utility styles (not extended)
283
+ leftIconContainer: createLeftIconContainerStyles(theme)(),
284
+ rightIconContainer: createRightIconContainerStyles(theme)(),
285
+ leftIcon: createLeftIconStyles(theme)(),
286
+ rightIcon: createRightIconStyles(theme)(),
287
+ passwordToggle: createPasswordToggleStyles(theme)(),
288
+ passwordToggleIcon: createPasswordToggleIconStyles(theme)()});
289
+ });