@idealyst/components 1.1.7 → 1.1.9

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 (129) hide show
  1. package/package.json +3 -3
  2. package/plugin/web.js +280 -532
  3. package/src/Accordion/Accordion.native.tsx +8 -6
  4. package/src/Accordion/Accordion.styles.old.tsx +298 -0
  5. package/src/Accordion/Accordion.styles.tsx +102 -236
  6. package/src/Accordion/Accordion.web.tsx +1 -3
  7. package/src/ActivityIndicator/ActivityIndicator.styles.old.tsx +94 -0
  8. package/src/ActivityIndicator/ActivityIndicator.styles.tsx +44 -74
  9. package/src/Alert/Alert.native.tsx +16 -6
  10. package/src/Alert/Alert.styles.old.tsx +209 -0
  11. package/src/Alert/Alert.styles.tsx +67 -149
  12. package/src/Alert/Alert.web.tsx +3 -4
  13. package/src/Avatar/Avatar.styles.old.tsx +99 -0
  14. package/src/Avatar/Avatar.styles.tsx +35 -80
  15. package/src/Badge/Badge.styles.old.tsx +157 -0
  16. package/src/Badge/Badge.styles.tsx +61 -121
  17. package/src/Badge/Badge.web.tsx +8 -15
  18. package/src/Breadcrumb/Breadcrumb.styles.old.tsx +231 -0
  19. package/src/Breadcrumb/Breadcrumb.styles.tsx +83 -200
  20. package/src/Breadcrumb/Breadcrumb.web.tsx +31 -30
  21. package/src/Button/Button.native.tsx +14 -21
  22. package/src/Button/Button.styles.tsx +103 -140
  23. package/src/Button/Button.web.tsx +9 -19
  24. package/src/Card/Card.native.tsx +7 -11
  25. package/src/Card/Card.styles.old.tsx +160 -0
  26. package/src/Card/Card.styles.tsx +105 -142
  27. package/src/Card/Card.web.tsx +5 -4
  28. package/src/Checkbox/Checkbox.native.tsx +9 -5
  29. package/src/Checkbox/Checkbox.styles.old.tsx +271 -0
  30. package/src/Checkbox/Checkbox.styles.tsx +104 -216
  31. package/src/Checkbox/Checkbox.web.tsx +7 -8
  32. package/src/Chip/Chip.styles.old.tsx +184 -0
  33. package/src/Chip/Chip.styles.tsx +34 -72
  34. package/src/Chip/Chip.web.tsx +3 -5
  35. package/src/Dialog/Dialog.native.tsx +7 -4
  36. package/src/Dialog/Dialog.styles.old.tsx +202 -0
  37. package/src/Dialog/Dialog.styles.tsx +69 -133
  38. package/src/Dialog/Dialog.web.tsx +3 -3
  39. package/src/Dialog/types.ts +1 -1
  40. package/src/Divider/Divider.styles.old.tsx +172 -0
  41. package/src/Divider/Divider.styles.tsx +62 -84
  42. package/src/Icon/Icon.native.tsx +8 -8
  43. package/src/Icon/Icon.styles.old.tsx +81 -0
  44. package/src/Icon/Icon.styles.tsx +52 -66
  45. package/src/Icon/Icon.web.tsx +62 -21
  46. package/src/Icon/IconRegistry.native.ts +41 -0
  47. package/src/Icon/IconRegistry.ts +107 -0
  48. package/src/Icon/IconSvg/IconSvg.web.tsx +28 -5
  49. package/src/Icon/icon-resolver.ts +12 -43
  50. package/src/Icon/index.native.ts +2 -1
  51. package/src/Icon/index.ts +1 -0
  52. package/src/Icon/index.web.ts +1 -0
  53. package/src/Image/Image.styles.old.tsx +69 -0
  54. package/src/Image/Image.styles.tsx +46 -60
  55. package/src/Input/Input.native.tsx +138 -53
  56. package/src/Input/Input.styles.old.tsx +289 -0
  57. package/src/Input/Input.styles.tsx +134 -232
  58. package/src/Input/Input.web.tsx +5 -8
  59. package/src/List/List.native.tsx +5 -2
  60. package/src/List/List.styles.old.tsx +242 -0
  61. package/src/List/List.styles.tsx +179 -215
  62. package/src/List/ListItem.native.tsx +16 -11
  63. package/src/List/ListItem.web.tsx +26 -16
  64. package/src/Menu/Menu.styles.old.tsx +197 -0
  65. package/src/Menu/Menu.styles.tsx +68 -150
  66. package/src/Menu/MenuItem.native.tsx +5 -3
  67. package/src/Menu/MenuItem.styles.old.tsx +114 -0
  68. package/src/Menu/MenuItem.styles.tsx +57 -89
  69. package/src/Menu/MenuItem.web.tsx +10 -7
  70. package/src/Popover/Popover.native.tsx +10 -4
  71. package/src/Popover/Popover.styles.old.tsx +135 -0
  72. package/src/Popover/Popover.styles.tsx +51 -112
  73. package/src/Pressable/Pressable.styles.old.tsx +27 -0
  74. package/src/Pressable/Pressable.styles.tsx +35 -27
  75. package/src/Progress/Progress.styles.old.tsx +200 -0
  76. package/src/Progress/Progress.styles.tsx +75 -164
  77. package/src/RadioButton/RadioButton.native.tsx +4 -3
  78. package/src/RadioButton/RadioButton.styles.old.tsx +175 -0
  79. package/src/RadioButton/RadioButton.styles.tsx +83 -154
  80. package/src/RadioButton/RadioButton.web.tsx +2 -2
  81. package/src/SVGImage/SVGImage.styles.old.tsx +86 -0
  82. package/src/SVGImage/SVGImage.styles.tsx +35 -78
  83. package/src/Screen/Screen.native.tsx +19 -26
  84. package/src/Screen/Screen.styles.old.tsx +87 -0
  85. package/src/Screen/Screen.styles.tsx +103 -68
  86. package/src/Screen/Screen.web.tsx +2 -2
  87. package/src/Select/Select.native.tsx +42 -33
  88. package/src/Select/Select.styles.old.tsx +353 -0
  89. package/src/Select/Select.styles.tsx +214 -300
  90. package/src/Select/Select.web.tsx +45 -33
  91. package/src/Skeleton/Skeleton.styles.old.tsx +67 -0
  92. package/src/Skeleton/Skeleton.styles.tsx +29 -53
  93. package/src/Slider/Slider.styles.old.tsx +259 -0
  94. package/src/Slider/Slider.styles.tsx +153 -234
  95. package/src/Slider/Slider.web.tsx +2 -4
  96. package/src/Switch/Switch.native.tsx +9 -7
  97. package/src/Switch/Switch.styles.old.tsx +203 -0
  98. package/src/Switch/Switch.styles.tsx +101 -174
  99. package/src/Switch/Switch.web.tsx +7 -8
  100. package/src/TabBar/TabBar.native.tsx +3 -2
  101. package/src/TabBar/TabBar.styles.old.tsx +343 -0
  102. package/src/TabBar/TabBar.styles.tsx +145 -279
  103. package/src/Table/Table.native.tsx +180 -68
  104. package/src/Table/Table.styles.old.tsx +311 -0
  105. package/src/Table/Table.styles.tsx +140 -281
  106. package/src/Table/Table.web.tsx +169 -70
  107. package/src/Text/Text.native.tsx +1 -3
  108. package/src/Text/Text.style.demo.tsx +16 -0
  109. package/src/Text/Text.styles.old.tsx +219 -0
  110. package/src/Text/Text.styles.tsx +94 -84
  111. package/src/Text/Text.web.tsx +3 -2
  112. package/src/Text/index.ts +1 -0
  113. package/src/TextArea/TextArea.native.tsx +21 -8
  114. package/src/TextArea/TextArea.styles.old.tsx +213 -0
  115. package/src/TextArea/TextArea.styles.tsx +87 -187
  116. package/src/TextArea/TextArea.web.tsx +17 -6
  117. package/src/Tooltip/Tooltip.styles.old.tsx +82 -0
  118. package/src/Tooltip/Tooltip.styles.tsx +32 -56
  119. package/src/Video/Video.styles.old.tsx +51 -0
  120. package/src/Video/Video.styles.tsx +32 -44
  121. package/src/View/View.native.tsx +41 -13
  122. package/src/View/View.styles.old.tsx +125 -0
  123. package/src/View/View.styles.tsx +76 -106
  124. package/src/View/View.web.tsx +5 -21
  125. package/src/View/types.ts +31 -3
  126. package/src/examples/ButtonExamples.tsx +20 -0
  127. package/src/examples/CardExamples.tsx +0 -6
  128. package/src/extensions/extendComponent.ts +61 -0
  129. package/src/index.ts +1 -1
@@ -5,6 +5,7 @@ import Svg, { Defs, LinearGradient, Stop, Rect } from 'react-native-svg';
5
5
  import { buttonStyles } from './Button.styles';
6
6
  import { ButtonProps } from './types';
7
7
  import { getNativeInteractiveAccessibilityProps } from '../utils/accessibility';
8
+ import { useUnistyles } from 'react-native-unistyles';
8
9
 
9
10
  const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>((props, ref) => {
10
11
  const {
@@ -71,24 +72,6 @@ const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>((p
71
72
  } as const;
72
73
  const iconSize = iconSizeMap[size];
73
74
 
74
- // Helper to render icon - uses the icon styles from buttonStyles
75
- const renderIcon = (icon: string | React.ReactNode) => {
76
- if (typeof icon === 'string') {
77
- // Render MaterialCommunityIcons with explicit size prop
78
- // The icon styles provide the correct color based on dynamic styles
79
- return (
80
- <MaterialCommunityIcons
81
- name={icon}
82
- size={iconSize}
83
- style={iconStyle}
84
- />
85
- );
86
- } else if (isValidElement(icon)) {
87
- // Render custom component as-is
88
- return icon;
89
- }
90
- return null;
91
- };
92
75
 
93
76
  // Use children if available, otherwise use title
94
77
  const buttonContent = children || title;
@@ -199,12 +182,22 @@ const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>((p
199
182
  <TouchableOpacity {...touchableProps as any}>
200
183
  {renderGradientLayer()}
201
184
  {hasIcons ? (
202
- <View style={iconContainerStyle}>
203
- {leftIcon && renderIcon(leftIcon)}
185
+ <View style={iconContainerStyle}>
186
+ {leftIcon &&
187
+ <MaterialCommunityIcons
188
+ name={leftIcon}
189
+ size={iconSize}
190
+ style={iconStyle}
191
+ />}
204
192
  <Text style={textStyle}>
205
193
  {buttonContent}
206
194
  </Text>
207
- {rightIcon && renderIcon(rightIcon)}
195
+ {rightIcon &&
196
+ <MaterialCommunityIcons
197
+ name={rightIcon}
198
+ size={iconSize}
199
+ style={iconStyle}
200
+ />}
208
201
  </View>
209
202
  ) : (
210
203
  <Text style={textStyle}>
@@ -1,8 +1,19 @@
1
+ /**
2
+ * Button styles using defineStyle with $iterator expansion.
3
+ *
4
+ * Dynamic style functions are used for intent/type combinations since
5
+ * the color depends on both values (compound logic).
6
+ */
1
7
  import { StyleSheet } from 'react-native-unistyles';
2
- import { Theme, Intent, Size } from '@idealyst/theme';
3
- import { buildSizeVariants } from '../utils/buildSizeVariants';
8
+ import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
9
+ import type { Theme as BaseTheme, Intent, Size } from '@idealyst/theme';
4
10
  import { ButtonGradient } from './types';
5
- import { applyExtensions } from '../extensions/applyExtension';
11
+
12
+ // Required: Unistyles must see StyleSheet usage in original source to process this file
13
+ void StyleSheet;
14
+
15
+ // Wrap theme for $iterator support
16
+ type Theme = ThemeStyleWrapper<BaseTheme>;
6
17
 
7
18
  type ButtonSize = Size;
8
19
  type ButtonType = 'contained' | 'outlined' | 'text';
@@ -17,8 +28,6 @@ export type ButtonVariants = {
17
28
 
18
29
  /**
19
30
  * All dynamic props passed to button style functions.
20
- * Every style function receives all props for maximum flexibility
21
- * when using extensions or replacements.
22
31
  */
23
32
  export type ButtonDynamicProps = {
24
33
  intent?: Intent;
@@ -29,149 +38,103 @@ export type ButtonDynamicProps = {
29
38
  };
30
39
 
31
40
  /**
32
- * Get button background color based on intent and type
33
- */
34
- function getButtonBackgroundColor(theme: Theme, intent: Intent, type: ButtonType): string {
35
- if (type === 'contained') {
36
- return theme.intents[intent].primary;
37
- }
38
- if (type === 'outlined') {
39
- return theme.colors.surface.primary;
40
- }
41
- return 'transparent';
42
- }
43
-
44
- /**
45
- * Get button border color based on intent and type
41
+ * Button styles with $iterator expansion for size variants.
42
+ *
43
+ * Intent/type combinations use dynamic functions with inlined theme accesses
44
+ * so Unistyles can trace all possible theme paths.
46
45
  */
47
- function getButtonBorderColor(theme: Theme, intent: Intent, type: ButtonType): string {
48
- if (type === 'outlined') {
49
- return theme.intents[intent].primary;
50
- }
51
- return 'transparent';
52
- }
53
-
54
- /**
55
- * Get text/icon color based on intent and type
56
- */
57
- function getTextColor(theme: Theme, intent: Intent, type: ButtonType): string {
58
- if (type === 'contained') {
59
- return theme.intents[intent].contrast;
60
- }
61
- return theme.intents[intent].primary;
62
- }
63
-
64
- /**
65
- * Create dynamic button styles
66
- * Receives all ButtonDynamicProps for flexibility in extensions/replacements
67
- */
68
- function createButtonStyles(theme: Theme) {
69
- return (props: ButtonDynamicProps) => {
70
- const { intent = 'primary', type = 'contained' } = props;
71
- return {
72
- boxSizing: 'border-box',
73
- alignItems: 'center',
74
- justifyContent: 'center',
75
- borderRadius: 8,
76
- fontWeight: '600',
77
- textAlign: 'center',
78
- backgroundColor: getButtonBackgroundColor(theme, intent, type),
79
- borderColor: getButtonBorderColor(theme, intent, type),
80
- borderWidth: type === 'outlined' ? 1 : 0,
81
- borderStyle: type === 'outlined' ? 'solid' as const : undefined,
82
- _web: {
83
- display: 'flex',
84
- transition: 'all 0.1s ease',
85
- },
86
- variants: {
87
- size: buildSizeVariants(theme, 'button', size => ({
88
- paddingVertical: size.paddingVertical,
89
- paddingHorizontal: size.paddingHorizontal,
90
- minHeight: size.minHeight,
91
- })),
92
- disabled: {
93
- true: { opacity: 0.6 },
94
- false: { opacity: 1, _web: { cursor: 'pointer', _hover: { opacity: 0.90 }, _active: { opacity: 0.75 } } },
46
+ export const buttonStyles = defineStyle('Button', (theme: Theme) => ({
47
+ button: ({ intent = 'primary', type = 'contained' }: ButtonDynamicProps) => ({
48
+ boxSizing: 'border-box',
49
+ alignItems: 'center',
50
+ justifyContent: 'center',
51
+ borderRadius: 8,
52
+ fontWeight: '600',
53
+ textAlign: 'center',
54
+ // Inline theme accesses so Unistyles can trace them
55
+ backgroundColor: type === 'contained'
56
+ ? theme.intents[intent].primary
57
+ : type === 'outlined'
58
+ ? theme.colors.surface.primary
59
+ : 'transparent',
60
+ borderColor: type === 'outlined'
61
+ ? theme.intents[intent].primary
62
+ : 'transparent',
63
+ borderWidth: type === 'outlined' ? 1 : 0,
64
+ borderStyle: type === 'outlined' ? 'solid' as const : undefined,
65
+ _web: {
66
+ display: 'flex',
67
+ transition: 'all 0.1s ease',
68
+ },
69
+ variants: {
70
+ // $iterator expands for each button size
71
+ type: {
72
+ contained: {
73
+ backgroundColor: theme.intents[intent].primary,
74
+ borderColor: 'transparent',
95
75
  },
96
- gradient: {
97
- darken: { _web: { backgroundImage: 'linear-gradient(135deg, transparent 0%, rgba(0, 0, 0, 0.15) 100%)' } },
98
- lighten: { _web: { backgroundImage: 'linear-gradient(135deg, transparent 0%, rgba(255, 255, 255, 0.2) 100%)' } },
76
+ outlined: {
77
+ backgroundColor: 'transparent',
78
+ borderColor: theme.intents[intent].primary,
99
79
  },
80
+ text: {
81
+ backgroundColor: 'transparent',
82
+ borderColor: 'transparent',
83
+ borderWidth: 0,
84
+ }
100
85
  },
101
- } as const;
102
- };
103
- }
104
-
105
- /**
106
- * Create dynamic text styles
107
- * Receives all ButtonDynamicProps for flexibility in extensions/replacements
108
- */
109
- function createTextStyles(theme: Theme) {
110
- return (props: ButtonDynamicProps) => {
111
- const { intent = 'primary', type = 'contained' } = props;
112
- return {
113
- fontWeight: '600',
114
- textAlign: 'center',
115
- color: getTextColor(theme, intent, type),
116
- variants: {
117
- size: buildSizeVariants(theme, 'button', size => ({
118
- fontSize: size.fontSize,
119
- lineHeight: size.fontSize,
120
- })),
121
- disabled: {
122
- true: { opacity: 0.6 },
123
- false: { opacity: 1 },
124
- },
86
+ size: {
87
+ paddingVertical: theme.sizes.$button.paddingVertical,
88
+ paddingHorizontal: theme.sizes.$button.paddingHorizontal,
89
+ minHeight: theme.sizes.$button.minHeight,
125
90
  },
126
- } as const;
127
- };
128
- }
129
-
130
- /**
131
- * Create dynamic icon styles
132
- * Receives all ButtonDynamicProps for flexibility in extensions/replacements
133
- */
134
- function createIconStyles(theme: Theme) {
135
- return (props: ButtonDynamicProps) => {
136
- const { intent = 'primary', type = 'contained' } = props;
137
- return {
138
- display: 'flex',
139
- alignItems: 'center',
140
- justifyContent: 'center',
141
- color: getTextColor(theme, intent, type),
142
- variants: {
143
- size: buildSizeVariants(theme, 'button', size => ({
144
- width: size.iconSize,
145
- height: size.iconSize,
146
- })),
91
+ disabled: {
92
+ true: { opacity: 0.6 },
93
+ false: { opacity: 1, _web: { cursor: 'pointer', _hover: { opacity: 0.90 }, _active: { opacity: 0.75 } } },
147
94
  },
148
- } as const;
149
- };
150
- }
151
-
152
- /**
153
- * Create icon container styles.
154
- * Receives all ButtonDynamicProps for flexibility in extensions/replacements.
155
- * NOTE: All styles must be dynamic functions (not static objects) to avoid
156
- * Babel transform issues with Unistyles on native.
157
- */
158
- function createIconContainerStyles() {
159
- return (_props: ButtonDynamicProps) => ({
95
+ gradient: {
96
+ darken: { _web: { backgroundImage: 'linear-gradient(135deg, transparent 0%, rgba(0, 0, 0, 0.15) 100%)' } },
97
+ lighten: { _web: { backgroundImage: 'linear-gradient(135deg, transparent 0%, rgba(255, 255, 255, 0.2) 100%)' } },
98
+ },
99
+ },
100
+ }),
101
+ text: ({ intent = 'primary', type = 'contained' }: ButtonDynamicProps) => ({
102
+ fontWeight: '600',
103
+ textAlign: 'center',
104
+ // Inline: contained uses contrast, others use primary
105
+ color: type === 'contained'
106
+ ? theme.intents[intent].contrast
107
+ : theme.intents[intent].primary,
108
+ variants: {
109
+ size: {
110
+ fontSize: theme.sizes.$button.fontSize,
111
+ lineHeight: theme.sizes.$button.fontSize,
112
+ },
113
+ disabled: {
114
+ true: { opacity: 0.6 },
115
+ false: { opacity: 1 },
116
+ },
117
+ },
118
+ }),
119
+ icon: ({ intent = 'primary', type = 'contained' }: ButtonDynamicProps) => ({
120
+ display: 'flex',
121
+ alignItems: 'center',
122
+ justifyContent: 'center',
123
+ color: type === 'contained'
124
+ ? theme.intents[intent].contrast
125
+ : theme.intents[intent].primary,
126
+ variants: {
127
+ size: {
128
+ width: theme.sizes.$button.iconSize,
129
+ height: theme.sizes.$button.iconSize,
130
+ },
131
+ },
132
+ }),
133
+ iconContainer: (_props: ButtonDynamicProps) => ({
160
134
  display: 'flex' as const,
161
135
  flexDirection: 'row' as const,
162
136
  alignItems: 'center' as const,
163
137
  justifyContent: 'center' as const,
164
138
  gap: 4,
165
- });
166
- }
167
-
168
- // Styles use dynamic functions for intent/type to support theme extensions
169
- // applyExtensions handles both replacements and extensions automatically
170
- export const buttonStyles = StyleSheet.create((theme: Theme) => {
171
- return applyExtensions('Button', theme, {
172
- button: createButtonStyles(theme),
173
- text: createTextStyles(theme),
174
- icon: createIconStyles(theme),
175
- iconContainer: createIconContainerStyles(),
176
- });
177
- });
139
+ }),
140
+ }));
@@ -6,14 +6,7 @@ import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
6
6
  import useMergeRefs from '../hooks/useMergeRefs';
7
7
  import { getWebInteractiveAriaProps, generateAccessibilityId } from '../utils/accessibility';
8
8
 
9
- // Extended props to include path props added by Babel plugin
10
- interface InternalButtonProps extends ButtonProps {
11
- leftIconPath?: string;
12
- rightIconPath?: string;
13
- }
14
-
15
- const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButtonProps, ref) => {
16
-
9
+ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
17
10
  const {
18
11
  title,
19
12
  children,
@@ -25,8 +18,6 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButton
25
18
  gradient,
26
19
  leftIcon,
27
20
  rightIcon,
28
- leftIconPath,
29
- rightIconPath,
30
21
  style,
31
22
  testID,
32
23
  id,
@@ -124,14 +115,13 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButton
124
115
  const iconStyleArray = [(buttonStyles.icon as any)(dynamicProps)];
125
116
  const iconProps = getWebProps(iconStyleArray);
126
117
 
127
- // Helper to render icon
128
- const renderIcon = (icon: string | React.ReactNode, iconPath?: string) => {
129
- if (typeof icon === 'string' && iconPath) {
130
- // Render IconSvg directly with the path from Babel plugin
131
- // Don't pass size - let the style control the dimensions
118
+ // Helper to render icon - now uses icon name directly
119
+ const renderIcon = (icon: string | React.ReactNode) => {
120
+ if (typeof icon === 'string') {
121
+ // Render IconSvg with the icon name - registry lookup happens inside
132
122
  return (
133
123
  <IconSvg
134
- path={iconPath}
124
+ name={icon}
135
125
  {...iconProps}
136
126
  aria-label={icon}
137
127
  />
@@ -164,9 +154,9 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButton
164
154
  >
165
155
  {hasIcons ? (
166
156
  <div {...iconContainerProps}>
167
- {leftIcon && renderIcon(leftIcon, leftIconPath)}
157
+ {leftIcon && renderIcon(leftIcon)}
168
158
  {buttonContent}
169
- {rightIcon && renderIcon(rightIcon, rightIconPath)}
159
+ {rightIcon && renderIcon(rightIcon)}
170
160
  </div>
171
161
  ) : (
172
162
  buttonContent
@@ -177,4 +167,4 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButton
177
167
 
178
168
  Button.displayName = 'Button';
179
169
 
180
- export default Button;
170
+ export default Button;
@@ -2,7 +2,7 @@ import React, { forwardRef, ComponentRef, useMemo } from 'react';
2
2
  import { View, Pressable } from 'react-native';
3
3
  import { useUnistyles } from 'react-native-unistyles';
4
4
  import { CardProps } from './types';
5
- import { cardStyles, getCardBorderRadius } from './Card.styles';
5
+ import { cardStyles } from './Card.styles';
6
6
  import { getNativeInteractiveAccessibilityProps } from '../utils/accessibility';
7
7
 
8
8
  const Card = forwardRef<ComponentRef<typeof View> | ComponentRef<typeof Pressable>, CardProps>(({
@@ -44,11 +44,10 @@ const Card = forwardRef<ComponentRef<typeof View> | ComponentRef<typeof Pressabl
44
44
  });
45
45
  }, [accessibilityLabel, accessibilityHint, accessibilityDisabled, disabled, accessibilityHidden, accessibilityRole, clickable, accessibilityPressed]);
46
46
 
47
- // Get theme for radii values
48
- const { theme } = useUnistyles();
49
-
50
- // Apply variants (for spacing only - radius is applied directly below)
47
+ // Apply variants
51
48
  cardStyles.useVariants({
49
+ type,
50
+ radius,
52
51
  clickable,
53
52
  disabled,
54
53
  gap,
@@ -60,11 +59,8 @@ const Card = forwardRef<ComponentRef<typeof View> | ComponentRef<typeof Pressabl
60
59
  marginHorizontal,
61
60
  });
62
61
 
63
- // Get dynamic card style with type and intent props
64
- const cardStyle = (cardStyles.card as any)({ type, intent });
65
-
66
- // Get border radius from theme - variants don't work with dynamic styles on iOS
67
- const borderRadius = getCardBorderRadius(theme, radius);
62
+ // Get card style
63
+ const cardStyle = (cardStyles.card as any)({});
68
64
 
69
65
  // Use appropriate component based on clickable state
70
66
  const Component = clickable ? Pressable : View;
@@ -72,7 +68,7 @@ const Card = forwardRef<ComponentRef<typeof View> | ComponentRef<typeof Pressabl
72
68
  const componentProps = {
73
69
  ref,
74
70
  nativeID: id,
75
- style: [cardStyle, { borderRadius }, style],
71
+ style: [cardStyle, style],
76
72
  testID,
77
73
  ...nativeA11yProps,
78
74
  ...(clickable && {
@@ -0,0 +1,160 @@
1
+ import { StyleSheet } from 'react-native-unistyles';
2
+ import { Theme, Intent, Radius } from '@idealyst/theme';
3
+ import {
4
+ buildGapVariants,
5
+ buildPaddingVariants,
6
+ buildPaddingVerticalVariants,
7
+ buildPaddingHorizontalVariants,
8
+ buildMarginVariants,
9
+ buildMarginVerticalVariants,
10
+ buildMarginHorizontalVariants,
11
+ } from '../utils/buildViewStyleVariants';
12
+ import { ViewStyleSize } from '../utils/viewStyleProps';
13
+ import { applyExtensions } from '../extensions/applyExtension';
14
+
15
+ type CardType = 'outlined' | 'elevated' | 'filled';
16
+ type CardRadius = Radius;
17
+ type CardIntent = Intent | 'info' | 'neutral';
18
+
19
+ /**
20
+ * Get border radius value from theme
21
+ */
22
+ export function getCardBorderRadius(theme: Theme, radius: CardRadius): number {
23
+ return theme.radii[radius];
24
+ }
25
+
26
+ export type CardVariants = {
27
+ type: CardType;
28
+ radius: CardRadius;
29
+ intent: CardIntent;
30
+ clickable: boolean;
31
+ disabled: boolean;
32
+ // Spacing variants from ContainerStyleProps
33
+ gap: ViewStyleSize;
34
+ padding: ViewStyleSize;
35
+ paddingVertical: ViewStyleSize;
36
+ paddingHorizontal: ViewStyleSize;
37
+ margin: ViewStyleSize;
38
+ marginVertical: ViewStyleSize;
39
+ marginHorizontal: ViewStyleSize;
40
+ };
41
+
42
+ type CardDynamicProps = {
43
+ intent?: CardIntent;
44
+ type?: CardType;
45
+ };
46
+
47
+ /**
48
+ * Get the border color based on intent (only used for outlined type)
49
+ */
50
+ function getBorderColor(theme: Theme, intent: CardIntent): string {
51
+ if (intent === 'info' || intent === 'neutral') {
52
+ return theme.colors.border.secondary;
53
+ }
54
+ if (intent in theme.intents) {
55
+ return theme.intents[intent as Intent].primary;
56
+ }
57
+ return theme.colors.border.secondary;
58
+ }
59
+
60
+ /**
61
+ * Get type-specific styles
62
+ */
63
+ function getTypeStyles(theme: Theme, type: CardType, intent: CardIntent) {
64
+ switch (type) {
65
+ case 'outlined':
66
+ return {
67
+ backgroundColor: 'transparent',
68
+ borderWidth: 1,
69
+ borderStyle: 'solid' as const,
70
+ borderColor: getBorderColor(theme, intent),
71
+ };
72
+ case 'elevated':
73
+ return {
74
+ backgroundColor: theme.colors.surface.primary,
75
+ borderWidth: 0,
76
+ ...theme.shadows.md,
77
+ };
78
+ case 'filled':
79
+ return {
80
+ backgroundColor: theme.colors.surface.secondary,
81
+ borderWidth: 0,
82
+ };
83
+ default:
84
+ return {};
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Create dynamic card styles
90
+ */
91
+ function createCardStyles(theme: Theme) {
92
+ return ({ intent = 'neutral', type = 'elevated' }: CardDynamicProps) => {
93
+ const typeStyles = getTypeStyles(theme, type, intent);
94
+ return {
95
+ ...typeStyles,
96
+ position: 'relative',
97
+ overflow: 'hidden',
98
+ variants: {
99
+ radius: {
100
+ none: { borderRadius: 0 },
101
+ xs: { borderRadius: 2 },
102
+ sm: { borderRadius: 4 },
103
+ md: { borderRadius: 8 },
104
+ lg: { borderRadius: 12 },
105
+ xl: { borderRadius: 16 },
106
+ },
107
+ clickable: {
108
+ true: {
109
+ _web: {
110
+ cursor: 'pointer',
111
+ transition: 'all 0.2s ease',
112
+ _hover: {
113
+ transform: 'translateY(-2px)',
114
+ boxShadow:
115
+ '0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.06)',
116
+ },
117
+ },
118
+ },
119
+ false: {
120
+ _web: {
121
+ cursor: 'default',
122
+ },
123
+ },
124
+ },
125
+ disabled: {
126
+ true: {
127
+ opacity: 0.6,
128
+ _web: {
129
+ cursor: 'not-allowed',
130
+ },
131
+ },
132
+ false: {
133
+ opacity: 1,
134
+ },
135
+ },
136
+ // Spacing variants from ContainerStyleProps
137
+ gap: buildGapVariants(theme),
138
+ padding: buildPaddingVariants(theme),
139
+ paddingVertical: buildPaddingVerticalVariants(theme),
140
+ paddingHorizontal: buildPaddingHorizontalVariants(theme),
141
+ margin: buildMarginVariants(theme),
142
+ marginVertical: buildMarginVerticalVariants(theme),
143
+ marginHorizontal: buildMarginHorizontalVariants(theme),
144
+ },
145
+ _web: {
146
+ display: 'flex',
147
+ flexDirection: 'column',
148
+ boxSizing: 'border-box',
149
+ },
150
+ } as const;
151
+ };
152
+ }
153
+
154
+ // Styles are inlined here instead of in @idealyst/theme because Unistyles' Babel
155
+ // transform on native cannot resolve function calls to extract variant structures.
156
+ export const cardStyles = StyleSheet.create((theme: Theme) => {
157
+ return applyExtensions('Card', theme, {
158
+ card: createCardStyles(theme),
159
+ });
160
+ });