@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
@@ -2,11 +2,12 @@ import { forwardRef } from 'react';
2
2
  import { View, Text } from 'react-native';
3
3
  import { DividerProps } from './types';
4
4
  import { dividerStyles } from './Divider.styles';
5
+ import type { IdealystElement } from '../utils/refTypes';
5
6
 
6
- const Divider = forwardRef<View, DividerProps>(({
7
+ const Divider = forwardRef<IdealystElement, DividerProps>(({
7
8
  orientation = 'horizontal',
8
9
  type = 'solid',
9
- thickness = 'thin',
10
+ size = 'sm',
10
11
  intent = 'neutral',
11
12
  length = 'full',
12
13
  spacing = 'md',
@@ -26,7 +27,7 @@ const Divider = forwardRef<View, DividerProps>(({
26
27
  // Get dynamic divider style
27
28
  const dividerStyle = (dividerStyles.divider as any)({
28
29
  orientation,
29
- thickness,
30
+ size,
30
31
  type,
31
32
  intent,
32
33
  spacing,
@@ -35,15 +36,17 @@ const Divider = forwardRef<View, DividerProps>(({
35
36
  // Get dynamic line style
36
37
  const lineStyle = (dividerStyles.line as any)({
37
38
  orientation,
38
- thickness,
39
+ size,
39
40
  });
40
41
 
41
- // Get thickness value for dashed/dotted border handling on native
42
- const getThicknessValue = () => {
43
- switch (thickness) {
44
- case 'thin': return 1;
42
+ // Get size value for dashed/dotted border handling on native
43
+ const getSizeValue = () => {
44
+ switch (size) {
45
+ case 'xs': return 1;
46
+ case 'sm': return 1;
45
47
  case 'md': return 2;
46
- case 'thick': return 4;
48
+ case 'lg': return 3;
49
+ case 'xl': return 4;
47
50
  default: return 1;
48
51
  }
49
52
  };
@@ -51,19 +54,19 @@ const Divider = forwardRef<View, DividerProps>(({
51
54
  // For dashed/dotted variants on native, we need to use border instead of background
52
55
  const getNativeDashedStyle = () => {
53
56
  if (type === 'dashed' || type === 'dotted') {
54
- const actualThickness = getThicknessValue();
57
+ const actualSize = getSizeValue();
55
58
 
56
59
  return {
57
60
  backgroundColor: 'transparent',
58
61
  borderStyle: type,
59
62
  borderColor: dividerStyle.backgroundColor,
60
63
  ...(orientation === 'horizontal' ? {
61
- borderTopWidth: actualThickness,
64
+ borderTopWidth: actualSize,
62
65
  borderBottomWidth: 0,
63
66
  borderLeftWidth: 0,
64
67
  borderRightWidth: 0,
65
68
  } : {
66
- borderLeftWidth: actualThickness,
69
+ borderLeftWidth: actualSize,
67
70
  borderTopWidth: 0,
68
71
  borderBottomWidth: 0,
69
72
  borderRightWidth: 0,
@@ -77,7 +80,7 @@ const Divider = forwardRef<View, DividerProps>(({
77
80
  if (!children) {
78
81
  return (
79
82
  <View
80
- ref={ref}
83
+ ref={ref as any}
81
84
  nativeID={id}
82
85
  style={[dividerStyle, getNativeDashedStyle(), style]}
83
86
  testID={testID}
@@ -89,7 +92,7 @@ const Divider = forwardRef<View, DividerProps>(({
89
92
  // For lines with content, create line segments
90
93
  const renderLineSegment = () => {
91
94
  if (type === 'dashed' || type === 'dotted') {
92
- const actualThickness = getThicknessValue();
95
+ const actualSize = getSizeValue();
93
96
 
94
97
  return (
95
98
  <View
@@ -100,12 +103,12 @@ const Divider = forwardRef<View, DividerProps>(({
100
103
  borderStyle: type,
101
104
  borderColor: lineStyle.backgroundColor,
102
105
  ...(orientation === 'horizontal' ? {
103
- borderTopWidth: actualThickness,
106
+ borderTopWidth: actualSize,
104
107
  borderBottomWidth: 0,
105
108
  borderLeftWidth: 0,
106
109
  borderRightWidth: 0,
107
110
  } : {
108
- borderLeftWidth: actualThickness,
111
+ borderLeftWidth: actualSize,
109
112
  borderTopWidth: 0,
110
113
  borderBottomWidth: 0,
111
114
  borderRightWidth: 0,
@@ -121,7 +124,7 @@ const Divider = forwardRef<View, DividerProps>(({
121
124
 
122
125
  return (
123
126
  <View
124
- ref={ref}
127
+ ref={ref as any}
125
128
  nativeID={id}
126
129
  style={dividerStyles.container}
127
130
  testID={testID}
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { StyleSheet } from 'react-native-unistyles';
5
5
  import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
6
- import type { Theme as BaseTheme, Intent } from '@idealyst/theme';
6
+ import type { Theme as BaseTheme, Intent, Size } from '@idealyst/theme';
7
7
 
8
8
  // Required: Unistyles must see StyleSheet usage in original source to process this file
9
9
  void StyleSheet;
@@ -12,14 +12,14 @@ void StyleSheet;
12
12
  type Theme = ThemeStyleWrapper<BaseTheme>;
13
13
 
14
14
  type DividerOrientation = 'horizontal' | 'vertical';
15
- type DividerThickness = 'thin' | 'md' | 'thick';
15
+ type DividerSize = Size;
16
16
  type DividerType = 'solid' | 'dashed' | 'dotted';
17
17
  type DividerIntent = Intent | 'secondary' | 'neutral' | 'info';
18
18
  type DividerSpacing = 'none' | 'sm' | 'md' | 'lg';
19
19
 
20
20
  export type DividerDynamicProps = {
21
21
  orientation?: DividerOrientation;
22
- thickness?: DividerThickness;
22
+ size?: DividerSize;
23
23
  type?: DividerType;
24
24
  intent?: DividerIntent;
25
25
  spacing?: DividerSpacing;
@@ -27,14 +27,20 @@ export type DividerDynamicProps = {
27
27
 
28
28
  export type LineDynamicProps = {
29
29
  orientation?: DividerOrientation;
30
- thickness?: DividerThickness;
30
+ size?: DividerSize;
31
31
  };
32
32
 
33
- function getThicknessValue(thickness: DividerThickness): number {
34
- switch (thickness) {
35
- case 'thin': return 1;
33
+ /**
34
+ * Maps Size to thickness value in pixels.
35
+ * xs=1, sm=1, md=2, lg=3, xl=4
36
+ */
37
+ function getSizeValue(size: DividerSize): number {
38
+ switch (size) {
39
+ case 'xs': return 1;
40
+ case 'sm': return 1;
36
41
  case 'md': return 2;
37
- case 'thick': return 4;
42
+ case 'lg': return 3;
43
+ case 'xl': return 4;
38
44
  default: return 1;
39
45
  }
40
46
  }
@@ -50,17 +56,17 @@ function getSpacingValue(spacing: DividerSpacing): number {
50
56
  }
51
57
 
52
58
  /**
53
- * Divider styles with dynamic functions for orientation/thickness/intent combinations.
59
+ * Divider styles with dynamic functions for orientation/size/intent combinations.
54
60
  */
55
61
  export const dividerStyles = defineStyle('Divider', (theme: Theme) => ({
56
62
  divider: ({
57
63
  orientation = 'horizontal',
58
- thickness = 'thin',
64
+ size = 'sm',
59
65
  type = 'solid',
60
66
  intent = 'neutral',
61
67
  spacing = 'md'
62
68
  }: DividerDynamicProps) => {
63
- const thicknessValue = getThicknessValue(thickness);
69
+ const sizeValue = getSizeValue(size);
64
70
  const spacingValue = getSpacingValue(spacing);
65
71
  const isHorizontal = orientation === 'horizontal';
66
72
  const isDashedOrDotted = type === 'dashed' || type === 'dotted';
@@ -72,11 +78,11 @@ export const dividerStyles = defineStyle('Divider', (theme: Theme) => ({
72
78
  ? theme.colors.border.secondary
73
79
  : intent === 'info'
74
80
  ? theme.intents.primary.primary
75
- : theme.intents[intent as Intent].primary;
81
+ : (theme.intents[intent as Intent]?.primary ?? theme.colors.border.primary);
76
82
 
77
83
  const dimensionStyles = isHorizontal
78
- ? { width: '100%', height: thicknessValue, flexDirection: 'row' as const }
79
- : { width: thicknessValue, height: '100%', flexDirection: 'column' as const };
84
+ ? { width: '100%', height: sizeValue, flexDirection: 'row' as const }
85
+ : { width: sizeValue, height: '100%', flexDirection: 'column' as const };
80
86
 
81
87
  const spacingStyles = isHorizontal
82
88
  ? { marginVertical: spacingValue }
@@ -86,8 +92,8 @@ export const dividerStyles = defineStyle('Divider', (theme: Theme) => ({
86
92
  border: 'none',
87
93
  backgroundColor: 'transparent',
88
94
  ...(isHorizontal
89
- ? { borderTop: `${thicknessValue}px ${type} ${color}` }
90
- : { borderLeft: `${thicknessValue}px ${type} ${color}` }
95
+ ? { borderTop: `${sizeValue}px ${type} ${color}` }
96
+ : { borderLeft: `${sizeValue}px ${type} ${color}` }
91
97
  ),
92
98
  } : {};
93
99
 
@@ -137,17 +143,33 @@ export const dividerStyles = defineStyle('Divider', (theme: Theme) => ({
137
143
  },
138
144
  }),
139
145
 
140
- line: ({ orientation = 'horizontal', thickness = 'thin' }: LineDynamicProps) => {
141
- const thicknessValue = getThicknessValue(thickness);
142
- const isHorizontal = orientation === 'horizontal';
143
-
144
- return {
145
- backgroundColor: theme.colors.border.secondary,
146
- flex: 1,
147
- ...(isHorizontal
148
- ? { height: thicknessValue }
149
- : { width: thicknessValue }
150
- ),
151
- } as const;
152
- },
146
+ line: (_props: LineDynamicProps) => ({
147
+ backgroundColor: theme.colors.border.secondary,
148
+ flex: 1,
149
+ variants: {
150
+ orientation: {
151
+ horizontal: {},
152
+ vertical: {},
153
+ },
154
+ size: {
155
+ xs: {},
156
+ sm: {},
157
+ md: {},
158
+ lg: {},
159
+ xl: {},
160
+ },
161
+ },
162
+ compoundVariants: [
163
+ { orientation: 'horizontal', size: 'xs', styles: { height: 1 } },
164
+ { orientation: 'horizontal', size: 'sm', styles: { height: 1 } },
165
+ { orientation: 'horizontal', size: 'md', styles: { height: 2 } },
166
+ { orientation: 'horizontal', size: 'lg', styles: { height: 3 } },
167
+ { orientation: 'horizontal', size: 'xl', styles: { height: 4 } },
168
+ { orientation: 'vertical', size: 'xs', styles: { width: 1 } },
169
+ { orientation: 'vertical', size: 'sm', styles: { width: 1 } },
170
+ { orientation: 'vertical', size: 'md', styles: { width: 2 } },
171
+ { orientation: 'vertical', size: 'lg', styles: { width: 3 } },
172
+ { orientation: 'vertical', size: 'xl', styles: { width: 4 } },
173
+ ],
174
+ }),
153
175
  }));
@@ -3,15 +3,16 @@ import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { DividerProps } from './types';
4
4
  import { dividerStyles } from './Divider.styles';
5
5
  import useMergeRefs from '../hooks/useMergeRefs';
6
+ import type { IdealystElement } from '../utils/refTypes';
6
7
 
7
8
  /**
8
9
  * Visual separator for dividing content sections horizontally or vertically.
9
10
  * Supports solid, dashed, and dotted styles with optional text content.
10
11
  */
11
- const Divider = forwardRef<HTMLDivElement, DividerProps>(({
12
+ const Divider = forwardRef<IdealystElement, DividerProps>(({
12
13
  orientation = 'horizontal',
13
14
  type = 'solid',
14
- thickness = 'thin',
15
+ size = 'sm',
15
16
  intent = 'neutral',
16
17
  length = 'full',
17
18
  spacing = 'md',
@@ -31,7 +32,7 @@ const Divider = forwardRef<HTMLDivElement, DividerProps>(({
31
32
  // Get dynamic divider style
32
33
  const dividerStyle = (dividerStyles.divider as any)({
33
34
  orientation,
34
- thickness,
35
+ size,
35
36
  type,
36
37
  intent,
37
38
  spacing,
@@ -40,7 +41,7 @@ const Divider = forwardRef<HTMLDivElement, DividerProps>(({
40
41
  // Get dynamic line style
41
42
  const lineStyle = (dividerStyles.line as any)({
42
43
  orientation,
43
- thickness,
44
+ size,
44
45
  });
45
46
 
46
47
  // Generate web props
@@ -5,9 +5,9 @@ import { BaseProps } from '../utils/viewStyleProps';
5
5
 
6
6
  // Component-specific type aliases for future extensibility
7
7
  export type DividerIntentVariant = Intent;
8
+ export type DividerSizeVariant = Size;
8
9
  export type DividerOrientationVariant = 'horizontal' | 'vertical';
9
10
  export type DividerType = 'solid' | 'dashed' | 'dotted';
10
- export type DividerThicknessVariant = 'thin' | 'md' | 'thick';
11
11
  export type DividerLengthVariant = 'full' | 'auto' | number;
12
12
  export type DividerSpacingVariant = 'none' | Size;
13
13
 
@@ -27,9 +27,9 @@ export interface DividerProps extends BaseProps {
27
27
  type?: DividerType;
28
28
 
29
29
  /**
30
- * The thickness of the divider
30
+ * The size (thickness) of the divider
31
31
  */
32
- thickness?: DividerThicknessVariant;
32
+ size?: DividerSizeVariant;
33
33
 
34
34
  /**
35
35
  * The color intent of the divider
@@ -2,8 +2,9 @@ import { forwardRef, useMemo } from 'react';
2
2
  import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons';
3
3
  import { IconProps } from './types';
4
4
  import { iconStyles } from './Icon.styles';
5
+ import type { IdealystElement } from '../utils/refTypes';
5
6
 
6
- const Icon = forwardRef<any, IconProps>(({
7
+ const Icon = forwardRef<IdealystElement, IconProps>(({
7
8
  name,
8
9
  size = 'md',
9
10
  color,
@@ -26,7 +27,7 @@ const Icon = forwardRef<any, IconProps>(({
26
27
 
27
28
  return (
28
29
  <MaterialDesignIcons
29
- ref={ref}
30
+ ref={ref as any}
30
31
  nativeID={id}
31
32
  size={iconSize}
32
33
  name={name}
@@ -7,12 +7,13 @@ import { useUnistyles } from 'react-native-unistyles';
7
7
  import useMergeRefs from '../hooks/useMergeRefs';
8
8
  import { getColorFromString, Intent, Color, Text } from '@idealyst/theme';
9
9
  import { IconRegistry } from './IconRegistry';
10
+ import type { IdealystElement } from '../utils/refTypes';
10
11
 
11
12
  /**
12
13
  * Vector icon display from the Material Design Icons library.
13
14
  * Supports intent-based coloring and theme-aware sizing.
14
15
  */
15
- const Icon = forwardRef<HTMLSpanElement, IconProps>((props, ref) => {
16
+ const Icon = forwardRef<IdealystElement, IconProps>((props, ref) => {
16
17
  const {
17
18
  name,
18
19
  size = 'md',
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { View } from 'react-native';
3
3
  import Svg, { Path } from 'react-native-svg';
4
+ import type { IdealystElement } from '../../utils/refTypes';
4
5
 
5
6
  /**
6
7
  * Internal component for rendering SVG icons directly from MDI paths.
@@ -18,7 +19,7 @@ interface IconSvgProps {
18
19
  'data-testid'?: string;
19
20
  }
20
21
 
21
- export const IconSvg = React.forwardRef<View, IconSvgProps>(({
22
+ export const IconSvg = React.forwardRef<IdealystElement, IconSvgProps>(({
22
23
  path,
23
24
  size = 24,
24
25
  color = 'currentColor',
@@ -29,7 +30,7 @@ export const IconSvg = React.forwardRef<View, IconSvgProps>(({
29
30
  const sizeNum = typeof size === 'string' ? parseFloat(size) : size;
30
31
 
31
32
  return (
32
- <View ref={ref} style={[{ width: sizeNum, height: sizeNum }, style]} testID={testID}>
33
+ <View ref={ref as any} style={[{ width: sizeNum, height: sizeNum }, style]} testID={testID}>
33
34
  <Svg viewBox="0 0 24 24" width={sizeNum} height={sizeNum}>
34
35
  <Path d={path} fill={color} />
35
36
  </Svg>
@@ -0,0 +1,219 @@
1
+ import { forwardRef, useMemo } from 'react';
2
+ import { ActivityIndicator, StyleSheet as RNStyleSheet, TouchableOpacity, View } from 'react-native';
3
+ import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons';
4
+ import Svg, { Defs, LinearGradient, Stop, Rect } from 'react-native-svg';
5
+ import { iconButtonStyles } from './IconButton.styles';
6
+ import { IconButtonProps } from './types';
7
+ import { getNativeInteractiveAccessibilityProps } from '../utils/accessibility';
8
+ import type { IdealystElement } from '../utils/refTypes';
9
+
10
+ const IconButton = forwardRef<IdealystElement, IconButtonProps>((props, ref) => {
11
+ const {
12
+ icon,
13
+ onPress,
14
+ onClick,
15
+ disabled = false,
16
+ loading = false,
17
+ type = 'contained',
18
+ intent = 'primary',
19
+ size = 'md',
20
+ gradient,
21
+ style,
22
+ testID,
23
+ id,
24
+ // Accessibility props
25
+ accessibilityLabel,
26
+ accessibilityHint,
27
+ accessibilityDisabled,
28
+ accessibilityHidden,
29
+ accessibilityRole,
30
+ accessibilityLabelledBy,
31
+ accessibilityDescribedBy,
32
+ accessibilityControls,
33
+ accessibilityExpanded,
34
+ accessibilityPressed,
35
+ } = props;
36
+
37
+ // Button is effectively disabled when loading
38
+ const isDisabled = disabled || loading;
39
+
40
+ // Determine the handler to use - onPress takes precedence
41
+ const pressHandler = onPress ?? onClick;
42
+
43
+ // Warn about deprecated onClick usage in development
44
+ if (__DEV__ && onClick && !onPress) {
45
+ console.warn(
46
+ 'IconButton: onClick prop is deprecated. Use onPress instead for cross-platform compatibility.'
47
+ );
48
+ }
49
+
50
+ // Apply variants for size, disabled, gradient
51
+ iconButtonStyles.useVariants({
52
+ size,
53
+ disabled: isDisabled,
54
+ gradient,
55
+ });
56
+
57
+ // Compute dynamic styles with all props for full flexibility
58
+ const dynamicProps = { intent, type, size, disabled: isDisabled, gradient };
59
+ const buttonStyle = (iconButtonStyles.button as any)(dynamicProps);
60
+ const iconStyle = (iconButtonStyles.icon as any)(dynamicProps);
61
+ const spinnerStyle = (iconButtonStyles.spinner as any)(dynamicProps);
62
+
63
+ // Gradient is only applicable to contained buttons
64
+ const showGradient = gradient && type === 'contained';
65
+
66
+ // Map button size to icon size
67
+ const iconSizeMap: Record<string, number> = {
68
+ xs: 12,
69
+ sm: 14,
70
+ md: 16,
71
+ lg: 18,
72
+ xl: 20,
73
+ };
74
+ const iconSize = iconSizeMap[size] ?? 16;
75
+
76
+ // Generate native accessibility props
77
+ const nativeA11yProps = useMemo(() => {
78
+ const computedLabel = accessibilityLabel ?? (typeof icon === 'string' ? icon : undefined);
79
+
80
+ return getNativeInteractiveAccessibilityProps({
81
+ accessibilityLabel: computedLabel,
82
+ accessibilityHint,
83
+ accessibilityDisabled: accessibilityDisabled ?? isDisabled,
84
+ accessibilityHidden,
85
+ accessibilityRole: accessibilityRole ?? 'button',
86
+ accessibilityLabelledBy,
87
+ accessibilityDescribedBy,
88
+ accessibilityControls,
89
+ accessibilityExpanded,
90
+ accessibilityPressed,
91
+ });
92
+ }, [
93
+ accessibilityLabel,
94
+ icon,
95
+ accessibilityHint,
96
+ accessibilityDisabled,
97
+ isDisabled,
98
+ accessibilityHidden,
99
+ accessibilityRole,
100
+ accessibilityLabelledBy,
101
+ accessibilityDescribedBy,
102
+ accessibilityControls,
103
+ accessibilityExpanded,
104
+ accessibilityPressed,
105
+ ]);
106
+
107
+ // Render gradient background layer
108
+ const renderGradientLayer = () => {
109
+ if (!showGradient) return null;
110
+
111
+ const [startColor, endColor] = useMemo(() => {
112
+ switch (gradient) {
113
+ case 'darken': return [{
114
+ stopColor: 'black',
115
+ stopOpacity: 0,
116
+ }, {
117
+ stopColor: 'black',
118
+ stopOpacity: 0.15,
119
+ }];
120
+ case 'lighten': return [{
121
+ stopColor: 'white',
122
+ stopOpacity: 0,
123
+ }, {
124
+ stopColor: 'white',
125
+ stopOpacity: 0.2,
126
+ }];
127
+ default: return [{
128
+ stopColor: 'black',
129
+ stopOpacity: 0,
130
+ }, {
131
+ stopColor: 'black',
132
+ stopOpacity: 0,
133
+ }];
134
+ }
135
+ }, [gradient]);
136
+
137
+ return (
138
+ <Svg style={RNStyleSheet.absoluteFill}>
139
+ <Defs>
140
+ <LinearGradient id="iconButtonGradient" x1="0%" y1="0%" x2="100%" y2="100%">
141
+ <Stop offset="0%" {...startColor} />
142
+ <Stop offset="100%" {...endColor} />
143
+ </LinearGradient>
144
+ </Defs>
145
+ <Rect
146
+ width="100%"
147
+ height="100%"
148
+ fill="url(#iconButtonGradient)"
149
+ rx={9999}
150
+ ry={9999}
151
+ />
152
+ </Svg>
153
+ );
154
+ };
155
+
156
+ // TouchableOpacity types don't include nativeID but it's a valid RN prop
157
+ const touchableProps = {
158
+ ref,
159
+ onPress: pressHandler,
160
+ disabled: isDisabled,
161
+ testID,
162
+ nativeID: id,
163
+ activeOpacity: 0.7,
164
+ style: [
165
+ buttonStyle,
166
+ showGradient && { overflow: 'hidden' },
167
+ style,
168
+ ],
169
+ accessibilityState: loading ? { busy: true } : undefined,
170
+ ...nativeA11yProps,
171
+ };
172
+
173
+ // Get spinner color from the spinner style (matches icon color)
174
+ const spinnerColor = spinnerStyle?.color || (type === 'contained' ? '#fff' : undefined);
175
+
176
+ // Content opacity - hide when loading but keep for sizing
177
+ const contentOpacity = loading ? 0 : 1;
178
+
179
+ // Render icon
180
+ const renderIcon = () => {
181
+ if (typeof icon === 'string') {
182
+ return (
183
+ <MaterialDesignIcons
184
+ name={icon}
185
+ size={iconSize}
186
+ style={[iconStyle, { opacity: contentOpacity }]}
187
+ />
188
+ );
189
+ }
190
+ // Custom ReactNode icon
191
+ return (
192
+ <View style={{ opacity: contentOpacity }}>
193
+ {icon}
194
+ </View>
195
+ );
196
+ };
197
+
198
+ return (
199
+ <TouchableOpacity {...touchableProps as any}>
200
+ {renderGradientLayer()}
201
+ {/* Centered spinner overlay */}
202
+ {loading && (
203
+ <View style={RNStyleSheet.absoluteFill}>
204
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
205
+ <ActivityIndicator
206
+ size="small"
207
+ color={spinnerColor}
208
+ />
209
+ </View>
210
+ </View>
211
+ )}
212
+ {renderIcon()}
213
+ </TouchableOpacity>
214
+ );
215
+ });
216
+
217
+ IconButton.displayName = 'IconButton';
218
+
219
+ export default IconButton;