@idealyst/components 1.1.6 → 1.1.7

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 (104) hide show
  1. package/package.json +8 -3
  2. package/src/Accordion/Accordion.native.tsx +15 -9
  3. package/src/Accordion/Accordion.styles.tsx +193 -168
  4. package/src/Accordion/Accordion.web.tsx +12 -7
  5. package/src/ActivityIndicator/ActivityIndicator.native.tsx +3 -2
  6. package/src/ActivityIndicator/ActivityIndicator.styles.tsx +22 -11
  7. package/src/ActivityIndicator/ActivityIndicator.web.tsx +2 -2
  8. package/src/Alert/Alert.native.tsx +11 -10
  9. package/src/Alert/Alert.styles.tsx +162 -253
  10. package/src/Alert/Alert.web.tsx +6 -10
  11. package/src/Avatar/Avatar.native.tsx +5 -2
  12. package/src/Avatar/Avatar.styles.tsx +48 -18
  13. package/src/Avatar/Avatar.web.tsx +2 -2
  14. package/src/Badge/Badge.native.tsx +2 -2
  15. package/src/Badge/Badge.styles.tsx +37 -16
  16. package/src/Badge/Badge.web.tsx +6 -6
  17. package/src/Breadcrumb/Breadcrumb.native.tsx +12 -5
  18. package/src/Breadcrumb/Breadcrumb.styles.tsx +59 -58
  19. package/src/Breadcrumb/Breadcrumb.web.tsx +13 -6
  20. package/src/Button/Button.native.tsx +39 -14
  21. package/src/Button/Button.styles.tsx +106 -208
  22. package/src/Button/Button.web.tsx +10 -8
  23. package/src/Card/Card.native.tsx +14 -6
  24. package/src/Card/Card.styles.tsx +64 -62
  25. package/src/Card/Card.web.tsx +5 -4
  26. package/src/Checkbox/Checkbox.native.tsx +7 -3
  27. package/src/Checkbox/Checkbox.styles.tsx +49 -25
  28. package/src/Checkbox/Checkbox.web.tsx +3 -3
  29. package/src/Chip/Chip.native.tsx +5 -5
  30. package/src/Chip/Chip.styles.tsx +71 -21
  31. package/src/Chip/Chip.web.tsx +5 -5
  32. package/src/Dialog/Dialog.native.tsx +10 -4
  33. package/src/Dialog/Dialog.styles.tsx +130 -90
  34. package/src/Dialog/Dialog.web.tsx +4 -4
  35. package/src/Divider/Divider.native.tsx +29 -42
  36. package/src/Divider/Divider.styles.tsx +138 -242
  37. package/src/Divider/Divider.web.tsx +17 -14
  38. package/src/Icon/Icon.native.tsx +11 -3
  39. package/src/Icon/Icon.styles.tsx +10 -4
  40. package/src/Image/Image.styles.tsx +53 -37
  41. package/src/Input/Input.native.tsx +6 -7
  42. package/src/Input/Input.styles.tsx +194 -174
  43. package/src/Input/Input.web.tsx +5 -8
  44. package/src/Link/Link.native.tsx +4 -1
  45. package/src/List/List.styles.tsx +79 -105
  46. package/src/List/ListItem.native.tsx +5 -3
  47. package/src/List/ListItem.web.tsx +4 -3
  48. package/src/Menu/Menu.native.tsx +1 -1
  49. package/src/Menu/Menu.styles.tsx +53 -37
  50. package/src/Menu/Menu.web.tsx +2 -2
  51. package/src/Menu/MenuItem.native.tsx +5 -3
  52. package/src/Menu/MenuItem.styles.tsx +68 -69
  53. package/src/Menu/MenuItem.web.tsx +16 -3
  54. package/src/Popover/Popover.native.tsx +1 -1
  55. package/src/Popover/Popover.styles.tsx +40 -29
  56. package/src/Popover/Popover.web.tsx +1 -1
  57. package/src/Pressable/Pressable.native.tsx +3 -1
  58. package/src/Pressable/Pressable.styles.tsx +20 -13
  59. package/src/Pressable/Pressable.web.tsx +1 -1
  60. package/src/Progress/Progress.native.tsx +15 -6
  61. package/src/Progress/Progress.styles.tsx +125 -85
  62. package/src/Progress/Progress.web.tsx +10 -9
  63. package/src/RadioButton/RadioButton.native.tsx +8 -3
  64. package/src/RadioButton/RadioButton.styles.tsx +44 -37
  65. package/src/RadioButton/RadioButton.web.tsx +3 -3
  66. package/src/SVGImage/SVGImage.styles.tsx +28 -16
  67. package/src/Screen/Screen.native.tsx +23 -13
  68. package/src/Screen/Screen.styles.tsx +57 -46
  69. package/src/Screen/Screen.web.tsx +1 -1
  70. package/src/Select/Select.native.tsx +11 -5
  71. package/src/Select/Select.styles.tsx +72 -52
  72. package/src/Select/Select.web.tsx +5 -5
  73. package/src/Skeleton/Skeleton.styles.tsx +26 -14
  74. package/src/Slider/Slider.native.tsx +9 -5
  75. package/src/Slider/Slider.styles.tsx +59 -48
  76. package/src/Slider/Slider.web.tsx +5 -5
  77. package/src/Switch/Switch.native.tsx +6 -2
  78. package/src/Switch/Switch.styles.tsx +46 -19
  79. package/src/Switch/Switch.web.tsx +4 -4
  80. package/src/TabBar/TabBar.native.tsx +23 -31
  81. package/src/TabBar/TabBar.styles.tsx +215 -371
  82. package/src/TabBar/TabBar.web.tsx +21 -33
  83. package/src/Table/Table.native.tsx +1 -1
  84. package/src/Table/Table.styles.tsx +11 -4
  85. package/src/Table/Table.web.tsx +1 -1
  86. package/src/Text/Text.native.tsx +3 -4
  87. package/src/Text/Text.styles.tsx +7 -1
  88. package/src/Text/Text.web.tsx +1 -1
  89. package/src/TextArea/TextArea.styles.tsx +90 -58
  90. package/src/Tooltip/Tooltip.native.tsx +2 -2
  91. package/src/Tooltip/Tooltip.styles.tsx +21 -12
  92. package/src/Tooltip/Tooltip.web.tsx +2 -2
  93. package/src/Video/Video.styles.tsx +39 -23
  94. package/src/View/View.native.tsx +4 -2
  95. package/src/View/View.styles.tsx +33 -22
  96. package/src/View/View.web.tsx +13 -2
  97. package/src/extensions/applyExtension.ts +210 -0
  98. package/src/extensions/extendComponent.ts +377 -0
  99. package/src/extensions/index.ts +102 -0
  100. package/src/extensions/types.ts +497 -0
  101. package/src/globals.ts +16 -0
  102. package/src/index.native.ts +4 -0
  103. package/src/index.ts +28 -0
  104. package/src/utils/deepMerge.ts +54 -2
@@ -1,198 +1,123 @@
1
1
  import { StyleSheet } from 'react-native-unistyles';
2
- import { Theme, Intent, Size, CompoundVariants} from '@idealyst/theme';
2
+ import { Theme, Intent, Size } from '@idealyst/theme';
3
3
  import { buildSizeVariants } from '../utils/buildSizeVariants';
4
4
  import { ButtonGradient } from './types';
5
+ import { applyExtensions } from '../extensions/applyExtension';
5
6
 
6
7
  type ButtonSize = Size;
7
- type ButtonIntent = Intent;
8
8
  type ButtonType = 'contained' | 'outlined' | 'text';
9
9
 
10
10
  export type ButtonVariants = {
11
11
  size: ButtonSize;
12
- intent: ButtonIntent;
12
+ intent: Intent;
13
13
  type: ButtonType;
14
14
  disabled: boolean;
15
15
  gradient?: ButtonGradient;
16
16
  }
17
17
 
18
18
  /**
19
- * Create intent variants (placeholder, colors handled by compound variants)
19
+ * All dynamic props passed to button style functions.
20
+ * Every style function receives all props for maximum flexibility
21
+ * when using extensions or replacements.
20
22
  */
21
- function createIntentVariants(theme: Theme) {
22
- const variants: any = {};
23
- for (const intent in theme.intents) {
24
- variants[intent] = {};
25
- }
26
- return variants;
27
- }
28
-
29
- /**
30
- * Create type variants (structure only, colors handled by compound variants)
31
- */
32
- function createButtonTypeVariants(theme: Theme) {
33
- return {
34
- contained: {
35
- borderWidth: 0,
36
- },
37
- outlined: {
38
- borderWidth: 1,
39
- borderStyle: 'solid' ,
40
- backgroundColor: theme.colors.surface.primary,
41
- },
42
- text: {
43
- borderWidth: 0,
44
- backgroundColor: 'transparent',
45
- },
46
- } as const;
47
- }
48
-
49
- /**
50
- * Create compound variants for intent+type combinations
51
- */
52
- function createButtonCompoundVariants(theme: Theme): CompoundVariants<keyof ButtonVariants> {
53
- const compoundVariants: CompoundVariants<keyof ButtonVariants> = [];
54
-
55
- for (const intent in theme.intents) {
56
- const intentValue = theme.intents[intent];
57
-
58
- // Contained + intent
59
- compoundVariants.push({
60
- intent,
61
- type: 'contained',
62
- styles: {
63
- backgroundColor: intentValue.primary,
64
- color: intentValue.contrast,
65
- },
66
- });
67
-
68
- // Outlined + intent
69
- compoundVariants.push({
70
- intent,
71
- type: 'outlined',
72
- styles: {
73
- color: intentValue.primary,
74
- borderColor: intentValue.primary,
75
- },
76
- });
77
-
78
- // Text + intent
79
- compoundVariants.push({
80
- intent,
81
- type: 'text',
82
- styles: {
83
- color: intentValue.primary,
84
- },
85
- });
86
- };
87
-
88
- return compoundVariants;
89
- }
23
+ export type ButtonDynamicProps = {
24
+ intent?: Intent;
25
+ type?: ButtonType;
26
+ size?: Size;
27
+ disabled?: boolean;
28
+ gradient?: ButtonGradient;
29
+ };
90
30
 
91
31
  /**
92
- * Create gradient variant styles for web
93
- * Applies a transparent overlay gradient over the intent background color
32
+ * Get button background color based on intent and type
94
33
  */
95
- function createGradientVariants() {
96
- return {
97
- 'darken': {
98
- _web: {
99
- backgroundImage: 'linear-gradient(135deg, transparent 0%, rgba(0, 0, 0, 0.15) 100%)',
100
- },
101
- },
102
- 'lighten': {
103
- _web: {
104
- backgroundImage: 'linear-gradient(135deg, transparent 0%, rgba(255, 255, 255, 0.2) 100%)',
105
- },
106
- },
107
- } as const;
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';
108
42
  }
109
43
 
110
44
  /**
111
- * Create icon compound variants for intent+type combinations
45
+ * Get button border color based on intent and type
112
46
  */
113
- function createIconCompoundVariants(theme: Theme): CompoundVariants<keyof ButtonVariants> {
114
- const compoundVariants: CompoundVariants<keyof ButtonVariants> = [];
115
-
116
- for (const intent in theme.intents) {
117
- const intentValue = theme.intents[intent as Intent];
118
-
119
- // Contained + intent
120
- compoundVariants.push({
121
- intent,
122
- type: 'contained',
123
- styles: { color: intentValue.contrast },
124
- });
125
-
126
- // Outlined + intent
127
- compoundVariants.push({
128
- intent,
129
- type: 'outlined',
130
- styles: { color: intentValue.primary },
131
- });
132
-
133
- // Text + intent
134
- compoundVariants.push({
135
- intent,
136
- type: 'text',
137
- styles: { color: intentValue.primary },
138
- });
47
+ function getButtonBorderColor(theme: Theme, intent: Intent, type: ButtonType): string {
48
+ if (type === 'outlined') {
49
+ return theme.intents[intent].primary;
139
50
  }
140
-
141
- return compoundVariants;
51
+ return 'transparent';
142
52
  }
143
53
 
144
54
  /**
145
- * Create icon color variants dynamically based on theme, intent, and type
55
+ * Get text/icon color based on intent and type
146
56
  */
147
- function createIconColorVariants(theme: Theme, intent: Intent) {
148
- const intentValue = theme.intents[intent];
149
-
150
- return {
151
- contained: {
152
- color: intentValue.contrast,
153
- },
154
- outlined: {
155
- color: intentValue.primary,
156
- },
157
- text: {
158
- color: intentValue.primary,
159
- },
160
- } as const;
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;
161
62
  }
162
63
 
163
64
  /**
164
- * Generate button icon styles
65
+ * Create dynamic button styles
66
+ * Receives all ButtonDynamicProps for flexibility in extensions/replacements
165
67
  */
166
- const createButtonIconStyles = (theme: Theme) => {
167
- return ({ intent }: Partial<ButtonVariants>) => {
68
+ function createButtonStyles(theme: Theme) {
69
+ return (props: ButtonDynamicProps) => {
70
+ const { intent = 'primary', type = 'contained' } = props;
168
71
  return {
169
- display: 'flex',
72
+ boxSizing: 'border-box',
170
73
  alignItems: 'center',
171
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
+ },
172
86
  variants: {
173
87
  size: buildSizeVariants(theme, 'button', size => ({
174
- width: size.iconSize,
175
- height: size.iconSize,
88
+ paddingVertical: size.paddingVertical,
89
+ paddingHorizontal: size.paddingHorizontal,
90
+ minHeight: size.minHeight,
176
91
  })),
177
- type: createIconColorVariants(theme, intent),
92
+ disabled: {
93
+ true: { opacity: 0.6 },
94
+ false: { opacity: 1, _web: { cursor: 'pointer', _hover: { opacity: 0.90 }, _active: { opacity: 0.75 } } },
95
+ },
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%)' } },
99
+ },
178
100
  },
179
101
  } as const;
180
102
  };
181
103
  }
182
104
 
183
105
  /**
184
- * Generate button text styles
106
+ * Create dynamic text styles
107
+ * Receives all ButtonDynamicProps for flexibility in extensions/replacements
185
108
  */
186
- const createButtonTextStyles = (theme: Theme) => {
187
- return ({ intent }: Partial<ButtonVariants>) => {
109
+ function createTextStyles(theme: Theme) {
110
+ return (props: ButtonDynamicProps) => {
111
+ const { intent = 'primary', type = 'contained' } = props;
188
112
  return {
189
113
  fontWeight: '600',
190
114
  textAlign: 'center',
115
+ color: getTextColor(theme, intent, type),
191
116
  variants: {
192
117
  size: buildSizeVariants(theme, 'button', size => ({
193
118
  fontSize: size.fontSize,
119
+ lineHeight: size.fontSize,
194
120
  })),
195
- type: createIconColorVariants(theme, intent), // Text uses same colors as icons
196
121
  disabled: {
197
122
  true: { opacity: 0.6 },
198
123
  false: { opacity: 1 },
@@ -202,78 +127,51 @@ const createButtonTextStyles = (theme: Theme) => {
202
127
  };
203
128
  }
204
129
 
205
- // Styles are inlined here instead of in @idealyst/theme because Unistyles' Babel
206
- // transform on native cannot resolve function calls to extract variant structures.
207
- export const buttonStyles = StyleSheet.create((theme: Theme) => {
208
- return {
209
- button: {
210
- boxSizing: 'border-box',
211
- alignItems: 'center',
212
- justifyContent: 'center',
213
- borderRadius: 8,
214
- fontWeight: '600',
215
- textAlign: 'center',
216
- _web: {
217
- display: 'flex',
218
- transition: 'all 0.1s ease',
219
- },
220
- variants: {
221
- size: buildSizeVariants(theme, 'button', size => ({
222
- paddingVertical: size.paddingVertical,
223
- paddingHorizontal: size.paddingHorizontal,
224
- minHeight: size.minHeight,
225
- })),
226
- type: createButtonTypeVariants(theme),
227
- disabled: {
228
- true: { opacity: 0.6 },
229
- false: { opacity: 1, _web: {
230
- cursor: 'pointer',
231
- _hover: {
232
- opacity: 0.90,
233
- },
234
- _active: {
235
- opacity: 0.75,
236
- },
237
- } },
238
- } as const,
239
- gradient: createGradientVariants(),
240
- } as const,
241
- compoundVariants: createButtonCompoundVariants(theme),
242
- } as const,
243
- icon: {
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 {
244
138
  display: 'flex',
245
139
  alignItems: 'center',
246
140
  justifyContent: 'center',
141
+ color: getTextColor(theme, intent, type),
247
142
  variants: {
248
143
  size: buildSizeVariants(theme, 'button', size => ({
249
144
  width: size.iconSize,
250
145
  height: size.iconSize,
251
146
  })),
252
- intent: createIntentVariants(theme),
253
- } as const,
254
- compoundVariants: createIconCompoundVariants(theme),
255
- } as const,
256
- iconContainer: {
257
- display: 'flex',
258
- flexDirection: 'row',
259
- alignItems: 'center',
260
- justifyContent: 'center',
261
- gap: 4,
262
- } as const,
263
- text: {
264
- fontWeight: '600',
265
- textAlign: 'center',
266
- variants: {
267
- size: buildSizeVariants(theme, 'button', size => ({
268
- fontSize: size.fontSize,
269
- })),
270
- intent: createIntentVariants(theme),
271
- disabled: {
272
- true: { opacity: 0.6 },
273
- false: { opacity: 1 },
274
- },
275
147
  },
276
- compoundVariants: createIconCompoundVariants(theme), // Text uses same colors as icons
277
- } as const,
148
+ } as const;
278
149
  };
279
- });
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) => ({
160
+ display: 'flex' as const,
161
+ flexDirection: 'row' as const,
162
+ alignItems: 'center' as const,
163
+ justifyContent: 'center' as const,
164
+ 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
+ });
@@ -45,9 +45,8 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButton
45
45
  accessibilityHasPopup,
46
46
  } = props;
47
47
 
48
+ // Apply variants for size, disabled, gradient
48
49
  buttonStyles.useVariants({
49
- type,
50
- intent,
51
50
  size,
52
51
  disabled,
53
52
  gradient,
@@ -55,8 +54,10 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButton
55
54
 
56
55
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
57
56
  e.preventDefault();
58
- e.stopPropagation();
57
+ // Only stop propagation if we have an onPress handler
58
+ // Otherwise, let clicks bubble up to parent handlers (e.g., Menu triggers)
59
59
  if (!disabled && onPress) {
60
+ e.stopPropagation();
60
61
  onPress();
61
62
  }
62
63
  };
@@ -105,10 +106,11 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButton
105
106
  accessibilityHasPopup,
106
107
  ]);
107
108
 
108
- // Compute dynamic styles
109
+ // Compute dynamic styles with all props for full flexibility
110
+ const dynamicProps = { intent, type, size, disabled, gradient };
109
111
  const buttonStyleArray = [
110
- buttonStyles.button,
111
- buttonStyles.text,
112
+ (buttonStyles.button as any)(dynamicProps),
113
+ (buttonStyles.text as any)(dynamicProps),
112
114
  style as any,
113
115
  ];
114
116
 
@@ -116,10 +118,10 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButton
116
118
  const webProps = getWebProps(buttonStyleArray);
117
119
 
118
120
  // Icon container styles
119
- const iconContainerProps = getWebProps([buttonStyles.iconContainer]);
121
+ const iconContainerProps = getWebProps([(buttonStyles.iconContainer as any)(dynamicProps)]);
120
122
 
121
123
  // Icon styles with dynamic function
122
- const iconStyleArray = [buttonStyles.icon];
124
+ const iconStyleArray = [(buttonStyles.icon as any)(dynamicProps)];
123
125
  const iconProps = getWebProps(iconStyleArray);
124
126
 
125
127
  // Helper to render icon
@@ -1,7 +1,8 @@
1
1
  import React, { forwardRef, ComponentRef, useMemo } from 'react';
2
2
  import { View, Pressable } from 'react-native';
3
+ import { useUnistyles } from 'react-native-unistyles';
3
4
  import { CardProps } from './types';
4
- import { cardStyles } from './Card.styles';
5
+ import { cardStyles, getCardBorderRadius } from './Card.styles';
5
6
  import { getNativeInteractiveAccessibilityProps } from '../utils/accessibility';
6
7
 
7
8
  const Card = forwardRef<ComponentRef<typeof View> | ComponentRef<typeof Pressable>, CardProps>(({
@@ -42,12 +43,13 @@ const Card = forwardRef<ComponentRef<typeof View> | ComponentRef<typeof Pressabl
42
43
  accessibilityPressed,
43
44
  });
44
45
  }, [accessibilityLabel, accessibilityHint, accessibilityDisabled, disabled, accessibilityHidden, accessibilityRole, clickable, accessibilityPressed]);
45
- // Apply variants
46
+
47
+ // Get theme for radii values
48
+ const { theme } = useUnistyles();
49
+
50
+ // Apply variants (for spacing only - radius is applied directly below)
46
51
  cardStyles.useVariants({
47
52
  clickable,
48
- radius,
49
- type,
50
- intent,
51
53
  disabled,
52
54
  gap,
53
55
  padding,
@@ -58,13 +60,19 @@ const Card = forwardRef<ComponentRef<typeof View> | ComponentRef<typeof Pressabl
58
60
  marginHorizontal,
59
61
  });
60
62
 
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);
68
+
61
69
  // Use appropriate component based on clickable state
62
70
  const Component = clickable ? Pressable : View;
63
71
 
64
72
  const componentProps = {
65
73
  ref,
66
74
  nativeID: id,
67
- style: [cardStyles.card, style],
75
+ style: [cardStyle, { borderRadius }, style],
68
76
  testID,
69
77
  ...nativeA11yProps,
70
78
  ...(clickable && {
@@ -1,5 +1,5 @@
1
1
  import { StyleSheet } from 'react-native-unistyles';
2
- import { Theme, Intent, CompoundVariants } from '@idealyst/theme';
2
+ import { Theme, Intent, Radius } from '@idealyst/theme';
3
3
  import {
4
4
  buildGapVariants,
5
5
  buildPaddingVariants,
@@ -10,11 +10,19 @@ import {
10
10
  buildMarginHorizontalVariants,
11
11
  } from '../utils/buildViewStyleVariants';
12
12
  import { ViewStyleSize } from '../utils/viewStyleProps';
13
+ import { applyExtensions } from '../extensions/applyExtension';
13
14
 
14
15
  type CardType = 'outlined' | 'elevated' | 'filled';
15
- type CardRadius = 'none' | 'sm' | 'md' | 'lg' | 'xs' | 'xl';
16
+ type CardRadius = Radius;
16
17
  type CardIntent = Intent | 'info' | 'neutral';
17
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
+
18
26
  export type CardVariants = {
19
27
  type: CardType;
20
28
  radius: CardRadius;
@@ -31,76 +39,63 @@ export type CardVariants = {
31
39
  marginHorizontal: ViewStyleSize;
32
40
  };
33
41
 
42
+ type CardDynamicProps = {
43
+ intent?: CardIntent;
44
+ type?: CardType;
45
+ };
46
+
34
47
  /**
35
- * Create type variants (structure only, colors handled by compound variants)
48
+ * Get the border color based on intent (only used for outlined type)
36
49
  */
37
- function createTypeVariants(theme: Theme) {
38
- return {
39
- outlined: {
40
- backgroundColor: 'transparent',
41
- borderWidth: 1,
42
- borderStyle: 'solid' as const,
43
- },
44
- elevated: {
45
- backgroundColor: theme.colors.surface.primary,
46
- borderWidth: 0,
47
- ...theme.shadows.md,
48
- },
49
- filled: {
50
- backgroundColor: theme.colors.surface.secondary,
51
- borderWidth: 0,
52
- },
53
- } as const;
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;
54
58
  }
55
59
 
56
60
  /**
57
- * Create compound variants for type + intent combinations
61
+ * Get type-specific styles
58
62
  */
59
- function createCardCompoundVariants(theme: Theme) {
60
- const compoundVariants: CompoundVariants<keyof CardVariants> = [];
61
-
62
- // Add intent-based border colors for outlined type
63
- for (const intent in theme.intents) {
64
- const intentValue = theme.intents[intent as Intent];
65
-
66
- compoundVariants.push({
67
- intent,
68
- type: 'outlined',
69
- styles: {
70
- borderColor: intentValue.primary,
71
- },
72
- });
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 {};
73
85
  }
74
-
75
- // Add special intents (info, neutral) for outlined type
76
- compoundVariants.push({
77
- intent: 'info',
78
- type: 'outlined',
79
- styles: {
80
- borderColor: theme.colors.border.secondary,
81
- },
82
- });
83
- compoundVariants.push({
84
- intent: 'neutral',
85
- type: 'outlined',
86
- styles: {
87
- borderColor: theme.colors.border.secondary,
88
- },
89
- });
90
-
91
- return compoundVariants;
92
86
  }
93
87
 
94
- // Styles are inlined here instead of in @idealyst/theme because Unistyles' Babel
95
- // transform on native cannot resolve function calls to extract variant structures.
96
- export const cardStyles = StyleSheet.create((theme: Theme) => {
97
- return {
98
- card: {
99
- backgroundColor: theme.colors.surface.primary,
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,
100
96
  position: 'relative',
101
97
  overflow: 'hidden',
102
98
  variants: {
103
- type: createTypeVariants(theme),
104
99
  radius: {
105
100
  none: { borderRadius: 0 },
106
101
  xs: { borderRadius: 2 },
@@ -147,12 +142,19 @@ export const cardStyles = StyleSheet.create((theme: Theme) => {
147
142
  marginVertical: buildMarginVerticalVariants(theme),
148
143
  marginHorizontal: buildMarginHorizontalVariants(theme),
149
144
  },
150
- compoundVariants: createCardCompoundVariants(theme),
151
145
  _web: {
152
146
  display: 'flex',
153
147
  flexDirection: 'column',
154
148
  boxSizing: 'border-box',
155
149
  },
156
- } as const,
150
+ } as const;
157
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
+ });
158
160
  });
@@ -49,12 +49,10 @@ const Card = forwardRef<HTMLDivElement | HTMLButtonElement, CardProps>(({
49
49
  }
50
50
  };
51
51
 
52
- // Apply variants
52
+ // Apply variants (for radius, clickable, disabled, and spacing)
53
53
  cardStyles.useVariants({
54
54
  clickable,
55
55
  radius,
56
- type,
57
- intent,
58
56
  disabled,
59
57
  gap,
60
58
  padding,
@@ -65,8 +63,11 @@ const Card = forwardRef<HTMLDivElement | HTMLButtonElement, CardProps>(({
65
63
  marginHorizontal,
66
64
  });
67
65
 
66
+ // Get dynamic card style with type and intent props
67
+ const cardStyle = (cardStyles.card as any)({ type, intent });
68
+
68
69
  // Generate web props
69
- const webProps = getWebProps([cardStyles.card, style as any]);
70
+ const webProps = getWebProps([cardStyle, style as any]);
70
71
 
71
72
  const mergedRef = useMergeRefs(ref, webProps.ref);
72
73