@idealyst/components 1.2.7 → 1.2.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.
@@ -1,5 +1,6 @@
1
- import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
1
+ import React, { useState, useRef, useMemo, useCallback } from 'react';
2
2
  import { getWebProps } from 'react-native-unistyles/web';
3
+ import { useUnistyles } from 'react-native-unistyles';
3
4
  import { accordionStyles } from './Accordion.styles';
4
5
  import type { AccordionProps, AccordionItem as AccordionItemType } from './types';
5
6
  import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
@@ -23,15 +24,17 @@ const AccordionItem: React.FC<AccordionItemProps> = ({
23
24
  isExpanded,
24
25
  onToggle,
25
26
  type,
26
- size,
27
+ size = 'md',
27
28
  isLast,
28
29
  testID,
29
30
  headerId,
30
31
  panelId,
31
32
  onKeyDown,
32
33
  }) => {
33
- const contentInnerRef = useRef<HTMLDivElement>(null);
34
- const [contentHeight, setContentHeight] = useState(0);
34
+ // Get theme for icon size
35
+ const { theme } = useUnistyles();
36
+ const iconSize = theme.sizes.accordion[size].iconSize;
37
+ const iconColor = theme.intents.primary.primary;
35
38
 
36
39
  // Apply item-specific variants (for size, expanded, disabled)
37
40
  accordionStyles.useVariants({
@@ -46,22 +49,9 @@ const AccordionItem: React.FC<AccordionItemProps> = ({
46
49
  const headerProps = getWebProps([(accordionStyles.header as any)({})]);
47
50
  const titleProps = getWebProps([accordionStyles.title]);
48
51
  const iconProps = getWebProps([(accordionStyles.icon as any)({})]);
49
- const contentProps = getWebProps([
50
- (accordionStyles.content as any)({}),
51
- {
52
- height: isExpanded ? contentHeight : 0,
53
- overflow: 'hidden' as const,
54
- }
55
- ]);
56
- const contentInnerProps = getWebProps([accordionStyles.contentInner]);
57
-
58
- useEffect(() => {
59
- if (isExpanded) {
60
- setContentHeight(contentInnerRef.current.getBoundingClientRect().height);
61
- } else {
62
- setContentHeight(0);
63
- }
64
- }, [isExpanded]);
52
+ // Pass expanded state to get correct maxHeight from styles
53
+ const contentProps = getWebProps([(accordionStyles.content as any)({ expanded: isExpanded })]);
54
+ const contentInnerProps = getWebProps([(accordionStyles.contentInner as any)({})]);
65
55
 
66
56
  return (
67
57
  <div
@@ -83,8 +73,9 @@ const AccordionItem: React.FC<AccordionItemProps> = ({
83
73
  </span>
84
74
  <span {...iconProps}>
85
75
  <IconSvg
86
- style={{ width: 12, height: 12 }}
87
76
  name="chevron-down"
77
+ size={iconSize}
78
+ color={iconColor}
88
79
  aria-label="chevron-down"
89
80
  />
90
81
  </span>
@@ -97,10 +88,8 @@ const AccordionItem: React.FC<AccordionItemProps> = ({
97
88
  aria-labelledby={headerId}
98
89
  aria-hidden={!isExpanded}
99
90
  >
100
- <div ref={contentInnerRef}>
101
- <div {...contentInnerProps}>
102
- {item.content}
103
- </div>
91
+ <div {...contentInnerProps}>
92
+ {item.content}
104
93
  </div>
105
94
  </div>
106
95
  </div>
@@ -154,16 +143,18 @@ const Accordion: React.FC<AccordionProps> = ({
154
143
  const currentIndex = enabledItems.findIndex(item => item.id === itemId);
155
144
  let nextIndex = -1;
156
145
 
157
- if (ACCORDION_KEYS.next.includes(key)) {
146
+ // ArrowDown moves to next item
147
+ if (key === 'ArrowDown') {
158
148
  e.preventDefault();
159
149
  nextIndex = currentIndex < enabledItems.length - 1 ? currentIndex + 1 : 0;
160
- } else if (ACCORDION_KEYS.prev.includes(key)) {
150
+ // ArrowUp moves to previous item
151
+ } else if (key === 'ArrowUp') {
161
152
  e.preventDefault();
162
153
  nextIndex = currentIndex > 0 ? currentIndex - 1 : enabledItems.length - 1;
163
- } else if (ACCORDION_KEYS.first.includes(key)) {
154
+ } else if (ACCORDION_KEYS.first.includes(key as 'Home')) {
164
155
  e.preventDefault();
165
156
  nextIndex = 0;
166
- } else if (ACCORDION_KEYS.last.includes(key)) {
157
+ } else if (ACCORDION_KEYS.last.includes(key as 'End')) {
167
158
  e.preventDefault();
168
159
  nextIndex = enabledItems.length - 1;
169
160
  }
@@ -199,7 +190,7 @@ const Accordion: React.FC<AccordionProps> = ({
199
190
  marginHorizontal,
200
191
  });
201
192
 
202
- const containerProps = getWebProps([(accordionStyles.container as any)({}), style as any]);
193
+ const containerProps = getWebProps([(accordionStyles.container as any)({ type }), style as any]);
203
194
 
204
195
  const toggleItem = (itemId: string, disabled?: boolean) => {
205
196
  if (disabled) return;
@@ -59,9 +59,11 @@ export const alertStyles = defineStyle('Alert', (theme: Theme) => ({
59
59
  display: 'flex' as const,
60
60
  alignItems: 'center' as const,
61
61
  justifyContent: 'center' as const,
62
+ alignSelf: 'flex-start' as const,
62
63
  flexShrink: 0,
63
64
  width: 24,
64
65
  height: 24,
66
+ marginTop: 2,
65
67
  color,
66
68
  } as const;
67
69
  },
@@ -105,27 +107,38 @@ export const alertStyles = defineStyle('Alert', (theme: Theme) => ({
105
107
 
106
108
  closeButton: (_props: AlertDynamicProps) => ({
107
109
  padding: 4,
108
- backgroundColor: 'transparent' as const,
109
110
  borderRadius: 4,
110
111
  display: 'flex' as const,
111
112
  alignItems: 'center' as const,
112
113
  justifyContent: 'center' as const,
113
114
  flexShrink: 0,
115
+ alignSelf: 'flex-start' as const,
116
+ marginTop: 2,
114
117
  _web: {
118
+ appearance: 'none',
119
+ background: 'none',
120
+ backgroundColor: 'transparent',
115
121
  border: 'none',
116
122
  cursor: 'pointer',
117
123
  outline: 'none',
124
+ margin: 0,
118
125
  _hover: {
119
126
  backgroundColor: 'rgba(0, 0, 0, 0.1)',
120
127
  },
121
128
  },
122
129
  }),
123
130
 
124
- closeIcon: (_props: AlertDynamicProps) => ({
125
- display: 'flex' as const,
126
- alignItems: 'center' as const,
127
- justifyContent: 'center' as const,
128
- width: 16,
129
- height: 16,
130
- }),
131
+ closeIcon: ({ intent = 'neutral', type = 'soft' }: AlertDynamicProps) => {
132
+ const intentValue = theme.intents[intent];
133
+ const color = type === 'filled' ? intentValue.contrast : intentValue.primary;
134
+
135
+ return {
136
+ display: 'flex' as const,
137
+ alignItems: 'center' as const,
138
+ justifyContent: 'center' as const,
139
+ width: 16,
140
+ height: 16,
141
+ color,
142
+ } as const;
143
+ },
131
144
  }));
@@ -39,12 +39,12 @@ const Alert = forwardRef<HTMLDivElement, AlertProps>(({
39
39
  const dynamicProps = { intent, type };
40
40
  const containerProps = getWebProps([(alertStyles.container as any)(dynamicProps), style as any]);
41
41
  const iconContainerProps = getWebProps([(alertStyles.iconContainer as any)(dynamicProps)]);
42
- const contentProps = getWebProps([alertStyles.content]);
42
+ const contentProps = getWebProps([(alertStyles.content as any)({})]);
43
43
  const titleProps = getWebProps([(alertStyles.title as any)(dynamicProps)]);
44
44
  const messageProps = getWebProps([(alertStyles.message as any)(dynamicProps)]);
45
- const actionsProps = getWebProps([alertStyles.actions]);
46
- const closeButtonProps = getWebProps([alertStyles.closeButton]);
47
- const closeIconProps = getWebProps([alertStyles.closeIcon]);
45
+ const actionsProps = getWebProps([(alertStyles.actions as any)({})]);
46
+ const closeButtonProps = getWebProps([(alertStyles.closeButton as any)(dynamicProps)]);
47
+ const closeIconProps = getWebProps([(alertStyles.closeIcon as any)(dynamicProps)]);
48
48
 
49
49
  const displayIcon = icon !== undefined ? icon : (showIcon ? defaultIcons[intent] : null);
50
50
 
@@ -1,5 +1,5 @@
1
1
  import React, { ComponentRef, forwardRef, isValidElement, useMemo } from 'react';
2
- import { StyleSheet as RNStyleSheet, Text, TouchableOpacity, View } from 'react-native';
2
+ import { ActivityIndicator, StyleSheet as RNStyleSheet, Text, TouchableOpacity, View } from 'react-native';
3
3
  import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
4
4
  import Svg, { Defs, LinearGradient, Stop, Rect } from 'react-native-svg';
5
5
  import { buttonStyles } from './Button.styles';
@@ -13,6 +13,7 @@ const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>((p
13
13
  title,
14
14
  onPress,
15
15
  disabled = false,
16
+ loading = false,
16
17
  type = 'contained',
17
18
  intent = 'primary',
18
19
  size = 'md',
@@ -35,19 +36,23 @@ const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>((p
35
36
  accessibilityPressed,
36
37
  } = props;
37
38
 
39
+ // Button is effectively disabled when loading
40
+ const isDisabled = disabled || loading;
41
+
38
42
  // Apply variants for size, disabled, gradient
39
43
  buttonStyles.useVariants({
40
44
  size,
41
- disabled,
45
+ disabled: isDisabled,
42
46
  gradient,
43
47
  });
44
48
 
45
49
  // Compute dynamic styles with all props for full flexibility
46
- const dynamicProps = { intent, type, size, disabled, gradient };
50
+ const dynamicProps = { intent, type, size, disabled: isDisabled, gradient };
47
51
  const buttonStyle = (buttonStyles.button as any)(dynamicProps);
48
52
  const textStyle = (buttonStyles.text as any)(dynamicProps);
49
53
  const iconStyle = (buttonStyles.icon as any)(dynamicProps);
50
54
  const iconContainerStyle = (buttonStyles.iconContainer as any)(dynamicProps);
55
+ const spinnerStyle = (buttonStyles.spinner as any)(dynamicProps);
51
56
 
52
57
  // Gradient is only applicable to contained buttons
53
58
  const showGradient = gradient && type === 'contained';
@@ -87,7 +92,7 @@ const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>((p
87
92
  return getNativeInteractiveAccessibilityProps({
88
93
  accessibilityLabel: computedLabel,
89
94
  accessibilityHint,
90
- accessibilityDisabled: accessibilityDisabled ?? disabled,
95
+ accessibilityDisabled: accessibilityDisabled ?? isDisabled,
91
96
  accessibilityHidden,
92
97
  accessibilityRole: accessibilityRole ?? 'button',
93
98
  accessibilityLabelledBy,
@@ -103,7 +108,7 @@ const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>((p
103
108
  rightIcon,
104
109
  accessibilityHint,
105
110
  accessibilityDisabled,
106
- disabled,
111
+ isDisabled,
107
112
  accessibilityHidden,
108
113
  accessibilityRole,
109
114
  accessibilityLabelledBy,
@@ -166,7 +171,7 @@ const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>((p
166
171
  const touchableProps = {
167
172
  ref,
168
173
  onPress,
169
- disabled,
174
+ disabled: isDisabled,
170
175
  testID,
171
176
  nativeID: id,
172
177
  activeOpacity: 0.7,
@@ -175,32 +180,53 @@ const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>((p
175
180
  showGradient && { overflow: 'hidden' },
176
181
  style,
177
182
  ],
183
+ accessibilityState: loading ? { busy: true } : undefined,
178
184
  ...nativeA11yProps,
179
185
  };
180
186
 
187
+ // Get spinner color from the spinner style (matches text color)
188
+ const spinnerColor = spinnerStyle?.color || (type === 'contained' ? '#fff' : undefined);
189
+
190
+ // Content opacity - hide when loading but keep for sizing
191
+ const contentOpacity = loading ? 0 : 1;
192
+
181
193
  return (
182
194
  <TouchableOpacity {...touchableProps as any}>
183
195
  {renderGradientLayer()}
196
+ {/* Centered spinner overlay */}
197
+ {loading && (
198
+ <View style={RNStyleSheet.absoluteFill}>
199
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
200
+ <ActivityIndicator
201
+ size="small"
202
+ color={spinnerColor}
203
+ />
204
+ </View>
205
+ </View>
206
+ )}
207
+ {/* Content with opacity 0 when loading to maintain size */}
184
208
  {hasIcons ? (
185
- <View style={iconContainerStyle}>
186
- {leftIcon &&
187
- <MaterialCommunityIcons
188
- name={leftIcon}
189
- size={iconSize}
190
- style={iconStyle}
191
- />}
209
+ <View style={[iconContainerStyle, { opacity: contentOpacity }]}>
210
+ {leftIcon && (
211
+ <MaterialCommunityIcons
212
+ name={leftIcon}
213
+ size={iconSize}
214
+ style={iconStyle}
215
+ />
216
+ )}
192
217
  <Text style={textStyle}>
193
218
  {buttonContent}
194
219
  </Text>
195
- {rightIcon &&
196
- <MaterialCommunityIcons
197
- name={rightIcon}
198
- size={iconSize}
199
- style={iconStyle}
200
- />}
220
+ {rightIcon && (
221
+ <MaterialCommunityIcons
222
+ name={rightIcon}
223
+ size={iconSize}
224
+ style={iconStyle}
225
+ />
226
+ )}
201
227
  </View>
202
228
  ) : (
203
- <Text style={textStyle}>
229
+ <Text style={[textStyle, { opacity: contentOpacity }]}>
204
230
  {buttonContent}
205
231
  </Text>
206
232
  )}
@@ -137,4 +137,19 @@ export const buttonStyles = defineStyle('Button', (theme: Theme) => ({
137
137
  justifyContent: 'center' as const,
138
138
  gap: 4,
139
139
  }),
140
+ spinner: ({ intent = 'primary', type = 'contained' }: ButtonDynamicProps) => ({
141
+ display: 'flex',
142
+ alignItems: 'center',
143
+ justifyContent: 'center',
144
+ // Match the text color based on button type
145
+ color: type === 'contained'
146
+ ? theme.intents[intent].contrast
147
+ : theme.intents[intent].primary,
148
+ variants: {
149
+ size: {
150
+ width: theme.sizes.$button.iconSize,
151
+ height: theme.sizes.$button.iconSize,
152
+ },
153
+ },
154
+ }),
140
155
  }));
@@ -16,6 +16,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
16
16
  children,
17
17
  onPress,
18
18
  disabled = false,
19
+ loading = false,
19
20
  type = 'contained',
20
21
  intent = 'primary',
21
22
  size = 'md',
@@ -40,10 +41,13 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
40
41
  accessibilityHasPopup,
41
42
  } = props;
42
43
 
44
+ // Button is effectively disabled when loading
45
+ const isDisabled = disabled || loading;
46
+
43
47
  // Apply variants for size, disabled, gradient
44
48
  buttonStyles.useVariants({
45
49
  size,
46
- disabled,
50
+ disabled: isDisabled,
47
51
  gradient,
48
52
  });
49
53
 
@@ -51,7 +55,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
51
55
  e.preventDefault();
52
56
  // Only stop propagation if we have an onPress handler
53
57
  // Otherwise, let clicks bubble up to parent handlers (e.g., Menu triggers)
54
- if (!disabled && onPress) {
58
+ if (!isDisabled && onPress) {
55
59
  e.stopPropagation();
56
60
  onPress();
57
61
  }
@@ -70,7 +74,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
70
74
  return getWebInteractiveAriaProps({
71
75
  accessibilityLabel: computedLabel,
72
76
  accessibilityHint,
73
- accessibilityDisabled: accessibilityDisabled ?? disabled,
77
+ accessibilityDisabled: accessibilityDisabled ?? isDisabled,
74
78
  accessibilityHidden,
75
79
  accessibilityRole: accessibilityRole ?? 'button',
76
80
  accessibilityLabelledBy,
@@ -89,7 +93,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
89
93
  rightIcon,
90
94
  accessibilityHint,
91
95
  accessibilityDisabled,
92
- disabled,
96
+ isDisabled,
93
97
  accessibilityHidden,
94
98
  accessibilityRole,
95
99
  accessibilityLabelledBy,
@@ -102,7 +106,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
102
106
  ]);
103
107
 
104
108
  // Compute dynamic styles with all props for full flexibility
105
- const dynamicProps = { intent, type, size, disabled, gradient };
109
+ const dynamicProps = { intent, type, size, disabled: isDisabled, gradient };
106
110
  const buttonStyleArray = [
107
111
  (buttonStyles.button as any)(dynamicProps),
108
112
  (buttonStyles.text as any)(dynamicProps),
@@ -119,6 +123,10 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
119
123
  const iconStyleArray = [(buttonStyles.icon as any)(dynamicProps)];
120
124
  const iconProps = getWebProps(iconStyleArray);
121
125
 
126
+ // Spinner styles that match the text color
127
+ const spinnerStyleArray = [(buttonStyles.spinner as any)(dynamicProps)];
128
+ const spinnerProps = getWebProps(spinnerStyleArray);
129
+
122
130
  // Helper to render icon - now uses icon name directly
123
131
  const renderIcon = (icon: string | React.ReactNode) => {
124
132
  if (typeof icon === 'string') {
@@ -143,9 +151,41 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
143
151
  // Determine if we need to wrap content in icon container
144
152
  const hasIcons = leftIcon || rightIcon;
145
153
 
154
+ // Render spinner with inline CSS animation (absolutely centered)
155
+ const renderSpinner = () => (
156
+ <>
157
+ <style>
158
+ {`
159
+ @keyframes button-spin {
160
+ from { transform: rotate(0deg); }
161
+ to { transform: rotate(360deg); }
162
+ }
163
+ `}
164
+ </style>
165
+ <span
166
+ {...spinnerProps}
167
+ style={{
168
+ position: 'absolute',
169
+ display: 'inline-block',
170
+ width: '1em',
171
+ height: '1em',
172
+ border: '2px solid currentColor',
173
+ borderTopColor: 'transparent',
174
+ borderRadius: '50%',
175
+ animation: 'button-spin 0.8s linear infinite',
176
+ }}
177
+ role="status"
178
+ aria-label="Loading"
179
+ />
180
+ </>
181
+ );
182
+
146
183
  // Merge unistyles web ref with forwarded ref
147
184
  const mergedRef = useMergeRefs(ref, webProps.ref);
148
185
 
186
+ // Content opacity - hide when loading but keep for sizing
187
+ const contentStyle = loading ? { opacity: 0 } : undefined;
188
+
149
189
  return (
150
190
  <button
151
191
  {...webProps}
@@ -153,17 +193,20 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
153
193
  ref={mergedRef}
154
194
  id={buttonId}
155
195
  onClick={handleClick}
156
- disabled={disabled}
196
+ disabled={isDisabled}
157
197
  data-testid={testID}
198
+ aria-busy={loading ? 'true' : undefined}
199
+ style={{ position: 'relative' }}
158
200
  >
201
+ {loading && renderSpinner()}
159
202
  {hasIcons ? (
160
- <div {...iconContainerProps}>
203
+ <div {...iconContainerProps} style={contentStyle}>
161
204
  {leftIcon && renderIcon(leftIcon)}
162
205
  {buttonContent}
163
206
  {rightIcon && renderIcon(rightIcon)}
164
207
  </div>
165
208
  ) : (
166
- buttonContent
209
+ <span style={contentStyle}>{buttonContent}</span>
167
210
  )}
168
211
  </button>
169
212
  );
@@ -75,6 +75,13 @@ export interface ButtonProps extends BaseProps, InteractiveAccessibilityProps {
75
75
  */
76
76
  rightIcon?: IconName | ReactNode;
77
77
 
78
+ /**
79
+ * Whether the button is in a loading state.
80
+ * When true, shows a spinner and disables interaction.
81
+ * The spinner color matches the button text color.
82
+ */
83
+ loading?: boolean;
84
+
78
85
  /**
79
86
  * Additional styles (platform-specific)
80
87
  */
@@ -8,6 +8,7 @@ const Icon = forwardRef<any, IconProps>(({
8
8
  name,
9
9
  size = 'md',
10
10
  color,
11
+ textColor,
11
12
  intent,
12
13
  style,
13
14
  testID,
@@ -17,7 +18,7 @@ const Icon = forwardRef<any, IconProps>(({
17
18
  const { theme } = useUnistyles();
18
19
 
19
20
  // Call dynamic style with variants - includes theme-reactive color
20
- const iconStyle = (iconStyles.icon as any)({ color, intent, size });
21
+ const iconStyle = (iconStyles.icon as any)({ color, textColor, intent, size });
21
22
 
22
23
  const iconSize = useMemo(() => {
23
24
  return iconStyle.width;
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { StyleSheet } from 'react-native-unistyles';
5
5
  import { defineStyle, ThemeStyleWrapper, getColorFromString } from '@idealyst/theme';
6
- import type { Theme as BaseTheme, Intent, Color } from '@idealyst/theme';
6
+ import type { Theme as BaseTheme, Intent, Color, Text } from '@idealyst/theme';
7
7
  import { IconSizeVariant } from './types';
8
8
 
9
9
  // Required: Unistyles must see StyleSheet usage in original source to process this file
@@ -16,6 +16,7 @@ export type IconVariants = {
16
16
  size: IconSizeVariant;
17
17
  intent?: Intent;
18
18
  color?: Color;
19
+ textColor?: Text;
19
20
  };
20
21
 
21
22
  export type IconDynamicProps = Partial<IconVariants>;
@@ -24,7 +25,7 @@ export type IconDynamicProps = Partial<IconVariants>;
24
25
  * Icon styles with dynamic color/size handling.
25
26
  */
26
27
  export const iconStyles = defineStyle('Icon', (theme: Theme) => ({
27
- icon: ({ color, intent, size = 'md' }: IconDynamicProps) => {
28
+ icon: ({ color, textColor, intent, size = 'md' }: IconDynamicProps) => {
28
29
  // Handle size - can be a named size or number
29
30
  let iconWidth: number;
30
31
  let iconHeight: number;
@@ -44,12 +45,15 @@ export const iconStyles = defineStyle('Icon', (theme: Theme) => ({
44
45
  }
45
46
  }
46
47
 
47
- // Get color - intent takes priority, then color prop, then default
48
+ // Get color - priority: intent > color > textColor > default
49
+ // color takes precedence over textColor (as per design)
48
50
  const iconColor = intent
49
51
  ? theme.intents[intent]?.primary
50
52
  : color
51
53
  ? getColorFromString(theme as unknown as BaseTheme, color)
52
- : theme.colors.text.primary;
54
+ : textColor
55
+ ? theme.colors.text[textColor]
56
+ : theme.colors.text.primary;
53
57
 
54
58
  return {
55
59
  width: iconWidth,
@@ -5,7 +5,7 @@ import { iconStyles } from './Icon.styles';
5
5
  import { getWebProps } from 'react-native-unistyles/web';
6
6
  import { useUnistyles } from 'react-native-unistyles';
7
7
  import useMergeRefs from '../hooks/useMergeRefs';
8
- import { getColorFromString, Intent, Color } from '@idealyst/theme';
8
+ import { getColorFromString, Intent, Color, Text } from '@idealyst/theme';
9
9
  import { IconRegistry } from './IconRegistry';
10
10
 
11
11
  /**
@@ -17,6 +17,7 @@ const Icon = forwardRef<HTMLSpanElement, IconProps>((props, ref) => {
17
17
  name,
18
18
  size = 'md',
19
19
  color,
20
+ textColor,
20
21
  intent,
21
22
  style,
22
23
  testID,
@@ -47,15 +48,18 @@ const Icon = forwardRef<HTMLSpanElement, IconProps>((props, ref) => {
47
48
  iconSize = typeof themeSize === 'number' ? themeSize : (themeSize?.width ?? 24);
48
49
  }
49
50
 
50
- // Compute color from intent or color prop or default
51
+ // Compute color - priority: intent > color > textColor > default
52
+ // color takes precedence over textColor (as per design)
51
53
  const iconColor = intent
52
54
  ? theme.intents[intent as Intent]?.primary
53
55
  : color
54
56
  ? getColorFromString(theme, color as Color)
55
- : theme.colors.text.primary;
57
+ : textColor
58
+ ? theme.colors.text[textColor as Text]
59
+ : theme.colors.text.primary;
56
60
 
57
61
  // Use getWebProps for className generation but override with computed values
58
- const iconStyle = (iconStyles.icon as any)({ intent, color, size });
62
+ const iconStyle = (iconStyles.icon as any)({ intent, color, textColor, size });
59
63
  const iconProps = getWebProps([iconStyle, style]);
60
64
 
61
65
  const mergedRef = useMergeRefs(ref, iconProps.ref);
package/src/Icon/types.ts CHANGED
@@ -1,12 +1,15 @@
1
1
  import type { StyleProp, ViewStyle } from 'react-native';
2
2
  import type { IconName } from "./icon-types";
3
- import type { Size } from '@idealyst/theme';
3
+ import type { Size, Text } from '@idealyst/theme';
4
4
  import { Color, Intent } from '@idealyst/theme';
5
5
  import { BaseProps } from '../utils/viewStyleProps';
6
6
 
7
7
  export type IconSizeVariant = Size | number;
8
8
 
9
- export interface IconProps extends BaseProps {
9
+ /**
10
+ * Base props shared by all Icon variants
11
+ */
12
+ interface IconBaseProps extends BaseProps {
10
13
  /**
11
14
  * The name of the icon to display
12
15
  */
@@ -17,10 +20,6 @@ export interface IconProps extends BaseProps {
17
20
  */
18
21
  size?: IconSizeVariant;
19
22
 
20
- /**
21
- * Predefined color variant based on theme
22
- */
23
- color?: Color;
24
23
  /**
25
24
  * Intent variant for the icon
26
25
  */
@@ -40,4 +39,32 @@ export interface IconProps extends BaseProps {
40
39
  * Accessibility label for screen readers
41
40
  */
42
41
  accessibilityLabel?: string;
43
- }
42
+ }
43
+
44
+ /**
45
+ * Icon props with palette color (e.g., 'blue.500', 'red.100')
46
+ */
47
+ interface IconWithColor extends IconBaseProps {
48
+ /**
49
+ * Predefined color variant based on theme palette
50
+ */
51
+ color?: Color;
52
+ textColor?: never;
53
+ }
54
+
55
+ /**
56
+ * Icon props with text color (e.g., 'primary', 'secondary')
57
+ */
58
+ interface IconWithTextColor extends IconBaseProps {
59
+ color?: never;
60
+ /**
61
+ * Text color variant from theme (e.g., 'primary', 'secondary')
62
+ * Cannot be used together with `color` prop
63
+ */
64
+ textColor?: Text;
65
+ }
66
+
67
+ /**
68
+ * Icon component props - accepts either `color` (palette) or `textColor` (text colors), but not both
69
+ */
70
+ export type IconProps = IconWithColor | IconWithTextColor;
@@ -171,9 +171,13 @@ export const inputStyles = defineStyle('Input', (theme: Theme) => ({
171
171
  },
172
172
  },
173
173
  _web: {
174
- background: 'transparent',
174
+ appearance: 'none',
175
+ background: 'none',
176
+ backgroundColor: 'transparent',
175
177
  border: 'none',
178
+ outline: 'none',
176
179
  cursor: 'pointer',
180
+ margin: 0,
177
181
  _hover: { opacity: 0.7 },
178
182
  _active: { opacity: 0.5 },
179
183
  },