@idealyst/components 1.3.2 → 1.3.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/components",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "Shared component library for React and React Native",
5
5
  "documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/components#readme",
6
6
  "readme": "README.md",
@@ -56,7 +56,7 @@
56
56
  "publish:npm": "npm publish"
57
57
  },
58
58
  "peerDependencies": {
59
- "@idealyst/theme": "^1.3.2",
59
+ "@idealyst/theme": "^1.3.3",
60
60
  "@mdi/js": ">=7.0.0",
61
61
  "@mdi/react": ">=1.0.0",
62
62
  "@react-native-vector-icons/common": ">=12.0.0",
@@ -111,8 +111,8 @@
111
111
  },
112
112
  "devDependencies": {
113
113
  "@idealyst/blur": "^1.2.40",
114
- "@idealyst/theme": "^1.3.2",
115
- "@idealyst/tooling": "^1.3.2",
114
+ "@idealyst/theme": "^1.3.3",
115
+ "@idealyst/tooling": "^1.3.3",
116
116
  "@mdi/react": "^1.6.1",
117
117
  "@types/react": "^19.1.0",
118
118
  "react": "^19.1.0",
@@ -89,12 +89,14 @@ interface BreadcrumbSeparatorProps {
89
89
 
90
90
  const BreadcrumbSeparator: React.FC<BreadcrumbSeparatorProps> = ({ separator, size, separatorStyle }) => {
91
91
  breadcrumbStyles.useVariants({ size });
92
- const sepStyle = (breadcrumbStyles.separator as any)({});
93
92
 
94
93
  if (typeof separator === 'string') {
94
+ const sepStyle = (breadcrumbStyles.separator as any)({});
95
95
  return <Text style={[sepStyle, separatorStyle]}>{separator}</Text>;
96
96
  }
97
- return <View style={[sepStyle, separatorStyle]}>{separator}</View>;
97
+
98
+ const sepIconStyle = (breadcrumbStyles.separatorIcon as any)({});
99
+ return <View style={[sepIconStyle, separatorStyle]}>{separator}</View>;
98
100
  };
99
101
 
100
102
  interface BreadcrumbEllipsisProps {
@@ -30,7 +30,7 @@ export const breadcrumbStyles = defineStyle('Breadcrumb', (theme: Theme) => ({
30
30
  flexDirection: 'row' as const,
31
31
  alignItems: 'center' as const,
32
32
  flexWrap: 'wrap' as const,
33
- gap: 8,
33
+ gap: 4,
34
34
  }),
35
35
 
36
36
  item: (_props: BreadcrumbDynamicProps) => ({
@@ -60,6 +60,12 @@ export const breadcrumbStyles = defineStyle('Breadcrumb', (theme: Theme) => ({
60
60
  lineHeight: theme.sizes.$breadcrumb.lineHeight,
61
61
  },
62
62
  },
63
+ _web: clickable && !isLast && !disabled ? {
64
+ _hover: {
65
+ textDecoration: 'underline',
66
+ opacity: 0.8,
67
+ },
68
+ } : {},
63
69
  } as const;
64
70
  },
65
71
 
@@ -83,6 +89,20 @@ export const breadcrumbStyles = defineStyle('Breadcrumb', (theme: Theme) => ({
83
89
  },
84
90
  }),
85
91
 
92
+ separatorIcon: (_props: BreadcrumbDynamicProps) => ({
93
+ display: 'flex' as const,
94
+ alignItems: 'center' as const,
95
+ justifyContent: 'center' as const,
96
+ color: theme.colors.text.tertiary,
97
+ variants: {
98
+ size: {
99
+ width: theme.sizes.$breadcrumb.iconSize,
100
+ height: theme.sizes.$breadcrumb.iconSize,
101
+ fontSize: theme.sizes.$breadcrumb.iconSize,
102
+ },
103
+ },
104
+ }),
105
+
86
106
  ellipsis: (_props: BreadcrumbDynamicProps) => ({
87
107
  display: 'flex' as const,
88
108
  alignItems: 'center' as const,
@@ -89,8 +89,11 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ item, isLast, size, int
89
89
  background: 'none',
90
90
  border: 'none',
91
91
  padding: 0,
92
+ margin: 0,
92
93
  cursor: 'pointer',
93
94
  textDecoration: 'none',
95
+ font: 'inherit',
96
+ color: 'inherit',
94
97
  }}
95
98
  disabled={isDisabled}
96
99
  aria-current={isLast ? 'page' : undefined}
@@ -115,11 +118,22 @@ interface BreadcrumbSeparatorProps {
115
118
 
116
119
  const BreadcrumbSeparator: React.FC<BreadcrumbSeparatorProps> = ({ separator, size, separatorStyle }) => {
117
120
  breadcrumbStyles.useVariants({ size });
118
- const separatorStyle_ = (breadcrumbStyles.separator as any)({});
119
- const separatorProps = getWebProps([separatorStyle_, separatorStyle]);
121
+ const isTextSeparator = typeof separator === 'string';
120
122
 
123
+ if (isTextSeparator) {
124
+ const separatorStyle_ = (breadcrumbStyles.separator as any)({});
125
+ const separatorProps = getWebProps([separatorStyle_, separatorStyle]);
126
+ return (
127
+ <span {...separatorProps} aria-hidden="true">
128
+ {separator}
129
+ </span>
130
+ );
131
+ }
132
+
133
+ const separatorIconStyle = (breadcrumbStyles.separatorIcon as any)({});
134
+ const separatorIconProps = getWebProps([separatorIconStyle, separatorStyle]);
121
135
  return (
122
- <span {...separatorProps} aria-hidden="true">
136
+ <span {...separatorIconProps} aria-hidden="true">
123
137
  {separator}
124
138
  </span>
125
139
  );
@@ -81,6 +81,14 @@ export const buttonStyles = defineStyle('Button', (theme: Theme) => ({
81
81
  backgroundColor: 'transparent',
82
82
  borderColor: 'transparent',
83
83
  borderWidth: 0,
84
+ _web: {
85
+ _hover: {
86
+ backgroundColor: theme.colors.surface.secondary,
87
+ },
88
+ _active: {
89
+ backgroundColor: theme.colors.surface.tertiary,
90
+ },
91
+ },
84
92
  }
85
93
  },
86
94
  size: {
@@ -76,6 +76,14 @@ export const iconButtonStyles = defineStyle('IconButton', (theme: Theme) => ({
76
76
  backgroundColor: 'transparent',
77
77
  borderColor: 'transparent',
78
78
  borderWidth: 0,
79
+ _web: {
80
+ _hover: {
81
+ backgroundColor: theme.colors.surface.secondary,
82
+ },
83
+ _active: {
84
+ backgroundColor: theme.colors.surface.tertiary,
85
+ },
86
+ },
79
87
  }
80
88
  },
81
89
  // Size variants - circular so width equals height
@@ -0,0 +1,11 @@
1
+ /**
2
+ * IconButton Documentation Sample Props
3
+ */
4
+
5
+ import type { SampleProps } from '@idealyst/tooling';
6
+
7
+ export const sampleProps: SampleProps = {
8
+ props: {
9
+ icon: 'heart',
10
+ },
11
+ };
@@ -34,6 +34,77 @@ function renderIcon(
34
34
  return icon;
35
35
  }
36
36
 
37
+ /**
38
+ * Individual tab component to isolate useVariants calls per tab.
39
+ */
40
+ const TabItem = ({
41
+ item,
42
+ isActive,
43
+ size,
44
+ type,
45
+ iconPosition,
46
+ justify,
47
+ onPress,
48
+ onLayout,
49
+ testID,
50
+ }: {
51
+ item: TabBarItem;
52
+ isActive: boolean;
53
+ size: TabBarProps['size'];
54
+ type: TabBarProps['type'];
55
+ iconPosition: TabBarProps['iconPosition'];
56
+ justify: TabBarProps['justify'];
57
+ onPress: () => void;
58
+ onLayout: (e: LayoutChangeEvent) => void;
59
+ testID?: string;
60
+ }) => {
61
+ const iconSize = ICON_SIZES[size || 'md'] || 18;
62
+
63
+ // Apply tab variants per tab (active/disabled differ per item)
64
+ tabBarTabStyles.useVariants({
65
+ size,
66
+ type,
67
+ active: isActive,
68
+ disabled: Boolean(item.disabled),
69
+ iconPosition,
70
+ justify,
71
+ });
72
+
73
+ // Apply label variants
74
+ tabBarLabelStyles.useVariants({
75
+ size,
76
+ type,
77
+ active: isActive,
78
+ disabled: Boolean(item.disabled),
79
+ });
80
+
81
+ // Apply icon variants
82
+ tabBarIconStyles.useVariants({
83
+ size,
84
+ disabled: Boolean(item.disabled),
85
+ iconPosition,
86
+ });
87
+
88
+ const icon = renderIcon(item.icon, isActive, iconSize);
89
+
90
+ return (
91
+ <TouchableOpacity
92
+ onLayout={onLayout}
93
+ style={tabBarTabStyles.tab as any}
94
+ onPress={onPress}
95
+ disabled={item.disabled}
96
+ activeOpacity={0.7}
97
+ testID={`${testID}-tab-${item.value}`}
98
+ accessibilityRole="tab"
99
+ accessibilityLabel={item.label}
100
+ accessibilityState={{ selected: isActive, disabled: item.disabled }}
101
+ >
102
+ {icon && <View style={tabBarIconStyles.tabIcon as any}>{icon}</View>}
103
+ <Text style={tabBarLabelStyles.tabLabel as any}>{item.label}</Text>
104
+ </TouchableOpacity>
105
+ );
106
+ };
107
+
37
108
  const TabBar = forwardRef<IdealystElement, TabBarProps>(({
38
109
  items,
39
110
  value: controlledValue,
@@ -129,8 +200,10 @@ const TabBar = forwardRef<IdealystElement, TabBarProps>(({
129
200
  };
130
201
  });
131
202
 
132
- // Apply container variants (for spacing only)
203
+ // Apply container variants
133
204
  tabBarContainerStyles.useVariants({
205
+ type,
206
+ pillMode,
134
207
  justify,
135
208
  gap,
136
209
  padding,
@@ -141,9 +214,11 @@ const TabBar = forwardRef<IdealystElement, TabBarProps>(({
141
214
  marginHorizontal,
142
215
  });
143
216
 
144
- // Compute dynamic container and indicator styles
145
- const containerStyle = (tabBarContainerStyles.container as any)({ type, pillMode });
146
- const indicatorStyle = (tabBarIndicatorStyles.indicator as any)({ type, pillMode });
217
+ // Apply indicator variants
218
+ tabBarIndicatorStyles.useVariants({
219
+ type,
220
+ pillMode,
221
+ });
147
222
 
148
223
  return (
149
224
  <ScrollView
@@ -155,11 +230,11 @@ const TabBar = forwardRef<IdealystElement, TabBarProps>(({
155
230
  }}
156
231
  style={{ width: '100%' }}
157
232
  >
158
- <View ref={ref as any} nativeID={id} style={[containerStyle, style]} testID={testID} {...nativeA11yProps}>
233
+ <View ref={ref as any} nativeID={id} style={[tabBarContainerStyles.container as any, style]} testID={testID} {...nativeA11yProps}>
159
234
  {/* Animated indicator - render first so it's behind */}
160
235
  <Animated.View
161
236
  style={[
162
- indicatorStyle,
237
+ tabBarIndicatorStyles.indicator as any,
163
238
  indicatorAnimatedStyle,
164
239
  ]}
165
240
  />
@@ -168,40 +243,23 @@ const TabBar = forwardRef<IdealystElement, TabBarProps>(({
168
243
  <View style={{ flexDirection: 'row', flex: 1 }}>
169
244
  {items.map((item) => {
170
245
  const isActive = value === item.value;
171
- const iconSize = ICON_SIZES[size] || 18;
172
-
173
- // Apply icon variants (size, disabled, iconPosition)
174
- tabBarIconStyles.useVariants({
175
- size,
176
- disabled: Boolean(item.disabled),
177
- iconPosition,
178
- });
179
-
180
- // Compute dynamic styles for this tab - call as functions for theme reactivity
181
- const tabStyle = (tabBarTabStyles.tab as any)({ type, size, active: isActive, pillMode, justify });
182
- const labelStyle = (tabBarLabelStyles.tabLabel as any)({ type, active: isActive, pillMode });
183
-
184
- const icon = renderIcon(item.icon, isActive, iconSize);
185
246
 
186
247
  return (
187
- <TouchableOpacity
248
+ <TabItem
188
249
  key={item.value}
250
+ item={item}
251
+ isActive={isActive}
252
+ size={size}
253
+ type={type}
254
+ iconPosition={iconPosition}
255
+ justify={justify}
256
+ onPress={() => handleTabClick(item.value, item.disabled)}
189
257
  onLayout={(event: LayoutChangeEvent) => {
190
258
  const { x, width } = event.nativeEvent.layout;
191
259
  handleTabLayout(item.value, x, width);
192
260
  }}
193
- style={tabStyle}
194
- onPress={() => handleTabClick(item.value, item.disabled)}
195
- disabled={item.disabled}
196
- activeOpacity={0.7}
197
- testID={`${testID}-tab-${item.value}`}
198
- accessibilityRole="tab"
199
- accessibilityLabel={item.label}
200
- accessibilityState={{ selected: isActive, disabled: item.disabled }}
201
- >
202
- {icon && <View style={tabBarIconStyles.tabIcon as any}>{icon}</View>}
203
- <Text style={labelStyle}>{item.label}</Text>
204
- </TouchableOpacity>
261
+ testID={testID}
262
+ />
205
263
  );
206
264
  })}
207
265
  </View>
@@ -1,9 +1,9 @@
1
1
  /**
2
- * TabBar styles using defineStyle with dynamic props.
2
+ * TabBar styles using defineStyle with static variants + compoundVariants.
3
3
  */
4
4
  import { StyleSheet } from 'react-native-unistyles';
5
5
  import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
6
- import type { Theme as BaseTheme, Size } from '@idealyst/theme';
6
+ import type { Theme as BaseTheme } from '@idealyst/theme';
7
7
  import { ViewStyleSize } from '../utils/viewStyleProps';
8
8
 
9
9
  // Required: Unistyles must see StyleSheet usage in original source to process this file
@@ -12,19 +12,14 @@ void StyleSheet;
12
12
  // Wrap theme for $iterator support
13
13
  type Theme = ThemeStyleWrapper<BaseTheme>;
14
14
 
15
- type TabBarType = 'standard' | 'underline' | 'pills';
16
- type TabBarPillMode = 'light' | 'dark';
17
- type TabBarIconPosition = 'left' | 'top';
18
- type TabBarJustify = 'start' | 'center' | 'equal' | 'space-between';
19
-
20
- export type TabBarDynamicProps = {
21
- size?: Size;
22
- type?: TabBarType;
23
- pillMode?: TabBarPillMode;
15
+ export type TabBarVariants = {
16
+ size?: ViewStyleSize;
17
+ type?: 'standard' | 'underline' | 'pills';
18
+ pillMode?: 'light' | 'dark';
24
19
  active?: boolean;
25
20
  disabled?: boolean;
26
- iconPosition?: TabBarIconPosition;
27
- justify?: TabBarJustify;
21
+ iconPosition?: 'left' | 'top';
22
+ justify?: 'start' | 'center' | 'equal' | 'space-between';
28
23
  gap?: ViewStyleSize;
29
24
  padding?: ViewStyleSize;
30
25
  paddingVertical?: ViewStyleSize;
@@ -35,140 +30,158 @@ export type TabBarDynamicProps = {
35
30
  };
36
31
 
37
32
  /**
38
- * TabBar styles with type/pillMode/active handling.
33
+ * TabBar styles with static variants and compoundVariants.
39
34
  */
40
35
  export const tabBarStyles = defineStyle('TabBar', (theme: Theme) => ({
41
- container: ({ type = 'standard', pillMode = 'light', justify = 'start' }: TabBarDynamicProps) => {
42
- const backgroundColor = type === 'pills'
43
- ? (pillMode === 'dark' ? theme.colors.surface.inverse : theme.colors.surface.secondary)
44
- : undefined;
45
-
46
- const justifyContent = {
47
- start: 'flex-start',
48
- center: 'center',
49
- equal: 'stretch',
50
- 'space-between': 'space-between',
51
- }[justify];
52
-
53
- return {
54
- display: 'flex' as const,
55
- flexDirection: 'row' as const,
56
- gap: type === 'pills' ? 4 : 0,
57
- position: 'relative' as const,
58
- borderBottomWidth: type === 'pills' ? 0 : 1,
59
- borderBottomStyle: 'solid' as const,
60
- borderBottomColor: theme.colors.border.primary,
61
- padding: type === 'pills' ? 4 : undefined,
62
- backgroundColor: backgroundColor || (type === 'pills' ? theme.colors.surface.secondary : undefined),
63
- overflow: type === 'pills' ? ('hidden' as const) : undefined,
64
- alignSelf: type === 'pills' ? ('flex-start' as const) : undefined,
65
- width: type === 'pills' ? undefined : '100%',
66
- borderRadius: type === 'pills' ? 9999 : undefined,
67
- justifyContent: justifyContent as any,
68
- variants: {
69
- gap: {
70
- gap: theme.sizes.$view.padding,
71
- },
72
- padding: {
73
- padding: theme.sizes.$view.padding,
74
- },
75
- paddingVertical: {
76
- paddingVertical: theme.sizes.$view.padding,
77
- },
78
- paddingHorizontal: {
79
- paddingHorizontal: theme.sizes.$view.padding,
80
- },
81
- margin: {
82
- margin: theme.sizes.$view.padding,
83
- },
84
- marginVertical: {
85
- marginVertical: theme.sizes.$view.padding,
86
- },
87
- marginHorizontal: {
88
- marginHorizontal: theme.sizes.$view.padding,
36
+ container: {
37
+ display: 'flex' as const,
38
+ flexDirection: 'row' as const,
39
+ position: 'relative' as const,
40
+ borderBottomWidth: 1,
41
+ borderBottomStyle: 'solid' as const,
42
+ borderBottomColor: theme.colors.border.primary,
43
+ width: '100%',
44
+ variants: {
45
+ type: {
46
+ standard: {},
47
+ underline: {},
48
+ pills: {
49
+ gap: 4,
50
+ borderBottomWidth: 0,
51
+ padding: 4,
52
+ backgroundColor: theme.colors.surface.secondary,
53
+ overflow: 'hidden' as const,
54
+ alignSelf: 'flex-start' as const,
55
+ width: undefined,
56
+ borderRadius: 9999,
89
57
  },
90
58
  },
91
- } as const;
59
+ justify: {
60
+ start: { justifyContent: 'flex-start' as const },
61
+ center: { justifyContent: 'center' as const },
62
+ equal: { justifyContent: 'stretch' as const },
63
+ 'space-between': { justifyContent: 'space-between' as const },
64
+ },
65
+ gap: {
66
+ gap: theme.sizes.$view.padding,
67
+ },
68
+ padding: {
69
+ padding: theme.sizes.$view.padding,
70
+ },
71
+ paddingVertical: {
72
+ paddingVertical: theme.sizes.$view.padding,
73
+ },
74
+ paddingHorizontal: {
75
+ paddingHorizontal: theme.sizes.$view.padding,
76
+ },
77
+ margin: {
78
+ margin: theme.sizes.$view.padding,
79
+ },
80
+ marginVertical: {
81
+ marginVertical: theme.sizes.$view.padding,
82
+ },
83
+ marginHorizontal: {
84
+ marginHorizontal: theme.sizes.$view.padding,
85
+ },
86
+ },
87
+ compoundVariants: [
88
+ { type: 'pills', pillMode: 'dark', styles: { backgroundColor: theme.colors.surface.inverse } },
89
+ ],
92
90
  },
93
91
 
94
- tab: ({ type = 'standard', size = 'md', active = false, pillMode: _pillMode = 'light', disabled = false, iconPosition = 'left', justify = 'start' }: TabBarDynamicProps) => {
95
- // Resolve padding at runtime — can't use $iterator with runtime `type` check
96
- // Use explicit top/bottom/left/right for cross-platform compatibility
97
- const pillsPaddingMap: Record<Size, { paddingTop: number; paddingBottom: number; paddingLeft: number; paddingRight: number }> = {
98
- xs: { paddingTop: 2, paddingBottom: 2, paddingLeft: 8, paddingRight: 8 },
99
- sm: { paddingTop: 3, paddingBottom: 3, paddingLeft: 10, paddingRight: 10 },
100
- md: { paddingTop: 4, paddingBottom: 4, paddingLeft: 12, paddingRight: 12 },
101
- lg: { paddingTop: 6, paddingBottom: 6, paddingLeft: 16, paddingRight: 16 },
102
- xl: { paddingTop: 8, paddingBottom: 8, paddingLeft: 20, paddingRight: 20 },
103
- };
104
- const sizeValues = theme.sizes.tabBar[size];
105
- const tabPadding = type === 'pills'
106
- ? pillsPaddingMap[size]
107
- : {
108
- paddingTop: sizeValues.padding,
109
- paddingBottom: sizeValues.padding,
110
- paddingLeft: sizeValues.padding,
111
- paddingRight: sizeValues.padding,
112
- };
113
-
114
- // Color based on type and active state
115
- let color = active ? theme.colors.text.primary : theme.colors.text.secondary;
116
- if (active) {
117
- if (type === 'pills') color = theme.intents.primary.contrast;
118
- else if (type === 'underline') color = theme.intents.primary.primary;
119
- }
120
-
121
- return {
122
- display: 'flex' as const,
123
- flexDirection: iconPosition === 'top' ? ('column' as const) : ('row' as const),
124
- alignItems: 'center' as const,
125
- justifyContent: 'center' as const,
126
- fontWeight: '500' as const,
127
- flex: justify === 'equal' ? 1 : undefined,
128
- color,
129
- position: 'relative' as const,
130
- zIndex: 2,
131
- backgroundColor: 'transparent' as const,
132
- gap: 6,
133
- borderRadius: type === 'pills' ? 9999 : undefined,
134
- opacity: disabled ? 0.5 : 1,
135
- ...tabPadding,
136
- variants: {
137
- size: {
138
- fontSize: theme.sizes.$tabBar.fontSize,
139
- lineHeight: theme.sizes.$tabBar.lineHeight,
92
+ tab: {
93
+ display: 'flex' as const,
94
+ flexDirection: 'row' as const,
95
+ alignItems: 'center' as const,
96
+ justifyContent: 'center' as const,
97
+ fontWeight: '500' as const,
98
+ color: theme.colors.text.secondary,
99
+ position: 'relative' as const,
100
+ zIndex: 2,
101
+ backgroundColor: 'transparent' as const,
102
+ gap: 6,
103
+ opacity: 0.9,
104
+ variants: {
105
+ size: {
106
+ fontSize: theme.sizes.$tabBar.fontSize,
107
+ lineHeight: theme.sizes.$tabBar.lineHeight,
108
+ paddingTop: theme.sizes.$tabBar.padding,
109
+ paddingBottom: theme.sizes.$tabBar.padding,
110
+ paddingLeft: theme.sizes.$tabBar.padding,
111
+ paddingRight: theme.sizes.$tabBar.padding,
112
+ },
113
+ type: {
114
+ standard: {},
115
+ underline: {},
116
+ pills: {
117
+ borderRadius: 9999,
118
+ },
119
+ },
120
+ active: {
121
+ true: {
122
+ color: theme.colors.text.primary,
123
+ opacity: 1,
140
124
  },
125
+ false: {},
126
+ },
127
+ disabled: {
128
+ true: { opacity: 0.5 },
129
+ false: {},
130
+ },
131
+ iconPosition: {
132
+ top: { flexDirection: 'column' as const },
133
+ left: { flexDirection: 'row' as const },
141
134
  },
142
- _web: {
143
- border: 'none',
144
- cursor: disabled ? 'not-allowed' : 'pointer',
145
- outline: 'none',
146
- transition: 'color 0.2s ease',
147
- _hover: disabled ? {} : { color: theme.colors.text.primary },
135
+ justify: {
136
+ start: {},
137
+ center: {},
138
+ equal: { flex: 1 },
139
+ 'space-between': {},
148
140
  },
149
- } as const;
141
+ },
142
+ compoundVariants: [
143
+ // Active + underline: use primary intent color
144
+ { type: 'underline', active: true, styles: { color: theme.intents.primary.primary } },
145
+ // Active + pills: use contrast color
146
+ { type: 'pills', active: true, styles: { color: theme.intents.primary.contrast } },
147
+ // Pills type: tighter padding per size (from theme)
148
+ { type: 'pills', size: 'xs', styles: { paddingTop: theme.sizes.tabBar.xs.pillPaddingVertical, paddingBottom: theme.sizes.tabBar.xs.pillPaddingVertical, paddingLeft: theme.sizes.tabBar.xs.pillPaddingHorizontal, paddingRight: theme.sizes.tabBar.xs.pillPaddingHorizontal } },
149
+ { type: 'pills', size: 'sm', styles: { paddingTop: theme.sizes.tabBar.sm.pillPaddingVertical, paddingBottom: theme.sizes.tabBar.sm.pillPaddingVertical, paddingLeft: theme.sizes.tabBar.sm.pillPaddingHorizontal, paddingRight: theme.sizes.tabBar.sm.pillPaddingHorizontal } },
150
+ { type: 'pills', size: 'md', styles: { paddingTop: theme.sizes.tabBar.md.pillPaddingVertical, paddingBottom: theme.sizes.tabBar.md.pillPaddingVertical, paddingLeft: theme.sizes.tabBar.md.pillPaddingHorizontal, paddingRight: theme.sizes.tabBar.md.pillPaddingHorizontal } },
151
+ { type: 'pills', size: 'lg', styles: { paddingTop: theme.sizes.tabBar.lg.pillPaddingVertical, paddingBottom: theme.sizes.tabBar.lg.pillPaddingVertical, paddingLeft: theme.sizes.tabBar.lg.pillPaddingHorizontal, paddingRight: theme.sizes.tabBar.lg.pillPaddingHorizontal } },
152
+ { type: 'pills', size: 'xl', styles: { paddingTop: theme.sizes.tabBar.xl.pillPaddingVertical, paddingBottom: theme.sizes.tabBar.xl.pillPaddingVertical, paddingLeft: theme.sizes.tabBar.xl.pillPaddingHorizontal, paddingRight: theme.sizes.tabBar.xl.pillPaddingHorizontal } },
153
+ ],
154
+ _web: {
155
+ border: 'none',
156
+ cursor: 'pointer',
157
+ outline: 'none',
158
+ transition: 'color 0.2s ease, opacity 0.2s ease',
159
+ },
150
160
  },
151
161
 
152
- tabLabel: ({ type = 'standard', active = false, pillMode: _pillMode = 'light', disabled = false }: TabBarDynamicProps) => {
153
- let color = active ? theme.colors.text.primary : theme.colors.text.secondary;
154
- if (active) {
155
- if (type === 'pills') color = theme.colors.text.primary;
156
- else if (type === 'underline') color = theme.intents.primary.primary;
157
- }
158
-
159
- return {
160
- position: 'relative' as const,
161
- zIndex: 3,
162
- fontWeight: '500' as const,
163
- color,
164
- opacity: disabled ? 0.5 : 1,
165
- variants: {
166
- size: {
167
- fontSize: theme.sizes.$tabBar.fontSize,
168
- lineHeight: theme.sizes.$tabBar.lineHeight,
169
- },
162
+ tabLabel: {
163
+ position: 'relative' as const,
164
+ zIndex: 3,
165
+ fontWeight: '500' as const,
166
+ color: theme.colors.text.secondary,
167
+ variants: {
168
+ size: {
169
+ fontSize: theme.sizes.$tabBar.fontSize,
170
+ lineHeight: theme.sizes.$tabBar.lineHeight,
170
171
  },
171
- } as const;
172
+ active: {
173
+ true: { color: theme.colors.text.primary },
174
+ false: {},
175
+ },
176
+ disabled: {
177
+ true: { opacity: 0.5 },
178
+ false: { opacity: 1 },
179
+ },
180
+ },
181
+ compoundVariants: [
182
+ { type: 'underline', active: true, styles: { color: theme.intents.primary.primary } },
183
+ { type: 'pills', active: true, styles: { color: theme.colors.text.primary } },
184
+ ],
172
185
  },
173
186
 
174
187
  tabIcon: {
@@ -191,31 +204,33 @@ export const tabBarStyles = defineStyle('TabBar', (theme: Theme) => ({
191
204
  },
192
205
  },
193
206
 
194
- indicator: ({ type = 'standard', pillMode = 'light' }: TabBarDynamicProps) => {
195
- const backgroundColor = type === 'pills'
196
- ? (pillMode === 'dark' ? theme.colors.surface.secondary : theme.colors.surface.tertiary)
197
- : theme.intents.primary.primary;
198
-
199
- const typeStyles = type === 'pills' ? {
200
- borderRadius: 9999,
201
- top: 4,
202
- bottom: 4,
203
- left: 0,
204
- } : {
205
- bottom: -1,
206
- height: 2,
207
- };
208
-
209
- return {
210
- position: 'absolute' as const,
211
- pointerEvents: 'none' as const,
212
- zIndex: 1,
213
- backgroundColor,
214
- ...typeStyles,
215
- _web: {
216
- transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
217
- },
218
- } as const;
207
+ indicator: {
208
+ position: 'absolute' as const,
209
+ pointerEvents: 'none' as const,
210
+ zIndex: 1,
211
+ backgroundColor: theme.intents.primary.primary,
212
+ bottom: -1,
213
+ height: 2,
214
+ variants: {
215
+ type: {
216
+ standard: {},
217
+ underline: {},
218
+ pills: {
219
+ borderRadius: 9999,
220
+ top: 4,
221
+ bottom: 4,
222
+ height: undefined,
223
+ left: 0,
224
+ backgroundColor: theme.colors.surface.tertiary,
225
+ },
226
+ },
227
+ },
228
+ compoundVariants: [
229
+ { type: 'pills', pillMode: 'dark', styles: { backgroundColor: theme.colors.surface.secondary } },
230
+ ],
231
+ _web: {
232
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
233
+ },
219
234
  },
220
235
  }));
221
236
 
@@ -66,29 +66,33 @@ const Tab: React.FC<TabProps> = ({
66
66
  }) => {
67
67
  const iconSize = ICON_SIZES[size || 'md'] || 18;
68
68
 
69
- // Apply tab variants (size) for variant expansion (padding, fontSize, lineHeight)
69
+ // Apply tab variants
70
70
  tabBarTabStyles.useVariants({
71
71
  size,
72
+ type,
73
+ active: isActive,
74
+ disabled: Boolean(item.disabled),
75
+ iconPosition,
76
+ justify,
72
77
  });
73
78
 
74
- // Apply label variants (size) for variant expansion
79
+ // Apply label variants
75
80
  tabBarLabelStyles.useVariants({
76
81
  size,
82
+ type,
83
+ active: isActive,
84
+ disabled: Boolean(item.disabled),
77
85
  });
78
86
 
79
- // Apply icon variants (size, disabled, iconPosition)
87
+ // Apply icon variants
80
88
  tabBarIconStyles.useVariants({
81
89
  size,
82
90
  disabled: Boolean(item.disabled),
83
91
  iconPosition,
84
92
  });
85
93
 
86
- // Compute dynamic styles for this tab
87
- const tabStyle = (tabBarTabStyles.tab as any)({ type, size, active: isActive, pillMode, justify });
88
- const labelStyle = (tabBarLabelStyles.tabLabel as any)({ type, active: isActive, pillMode });
89
-
90
- const tabProps = getWebProps([tabStyle]);
91
- const labelProps = getWebProps([labelStyle]);
94
+ const tabProps = getWebProps([tabBarTabStyles.tab as any]);
95
+ const labelProps = getWebProps([tabBarLabelStyles.tabLabel as any]);
92
96
  const iconProps = getWebProps([tabBarIconStyles.tabIcon as any]);
93
97
 
94
98
  // Merge refs from getWebProps with our tracking ref
@@ -262,8 +266,10 @@ const TabBar: React.FC<TabBarProps> = ({
262
266
  onChange?.(itemValue);
263
267
  };
264
268
 
265
- // Apply container variants (for spacing only)
266
- (tabBarContainerStyles.useVariants as any)({
269
+ // Apply container variants
270
+ tabBarContainerStyles.useVariants({
271
+ type,
272
+ pillMode,
267
273
  justify,
268
274
  gap,
269
275
  padding,
@@ -274,12 +280,14 @@ const TabBar: React.FC<TabBarProps> = ({
274
280
  marginHorizontal,
275
281
  });
276
282
 
277
- // Compute dynamic container and indicator styles
278
- const containerStyle = (tabBarContainerStyles.container as any)({ type, pillMode });
279
- const containerProps = getWebProps([containerStyle, style as any]);
283
+ // Apply indicator variants
284
+ tabBarIndicatorStyles.useVariants({
285
+ type,
286
+ pillMode,
287
+ });
280
288
 
281
- const indicatorVisualStyle = (tabBarIndicatorStyles.indicator as any)({ type, pillMode });
282
- const indicatorProps = getWebProps([indicatorVisualStyle]);
289
+ const containerProps = getWebProps([tabBarContainerStyles.container as any, style as any]);
290
+ const indicatorProps = getWebProps([tabBarIndicatorStyles.indicator as any]);
283
291
 
284
292
  // Merge container ref with getWebProps ref
285
293
  const mergedContainerRef = useMergeRefs<HTMLDivElement>(
@@ -0,0 +1,177 @@
1
+ import React from 'react';
2
+ import { Screen, View, IconButton, Text } from '@idealyst/components';
3
+ import { useUnistyles } from 'react-native-unistyles';
4
+
5
+ export const IconButtonExamples = () => {
6
+ const handlePress = (label: string) => {
7
+ console.log(`IconButton pressed: ${label}`);
8
+ };
9
+
10
+ const { theme } = useUnistyles();
11
+
12
+ return (
13
+ <Screen background="primary">
14
+ <View gap="xl">
15
+ <Text typography="h4" align="center">
16
+ IconButton Examples
17
+ </Text>
18
+
19
+ {/* Type Variants */}
20
+ <View gap="md">
21
+ <Text typography="subtitle1">Variants</Text>
22
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
23
+ <IconButton
24
+ icon="heart"
25
+ type="contained"
26
+ intent="primary"
27
+ onPress={() => handlePress('contained')}
28
+ />
29
+ <IconButton
30
+ icon="heart"
31
+ type="outlined"
32
+ intent="primary"
33
+ onPress={() => handlePress('outlined')}
34
+ />
35
+ <IconButton
36
+ icon="heart"
37
+ type="text"
38
+ intent="primary"
39
+ onPress={() => handlePress('text')}
40
+ />
41
+ </View>
42
+ </View>
43
+
44
+ {/* All Intents */}
45
+ <View gap="md">
46
+ <Text typography="subtitle1">All Intents</Text>
47
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
48
+ {(Object.keys(theme.intents) as Array<keyof typeof theme.intents>).map((intent) => (
49
+ <IconButton
50
+ key={intent}
51
+ icon="star"
52
+ type="contained"
53
+ intent={intent}
54
+ onPress={() => handlePress(`intent-${intent}`)}
55
+ />
56
+ ))}
57
+ </View>
58
+ </View>
59
+
60
+ {/* Outlined Intents */}
61
+ <View gap="md">
62
+ <Text typography="subtitle1">Outlined Intents</Text>
63
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
64
+ <IconButton icon="information" type="outlined" intent="primary" onPress={() => handlePress('outlined-primary')} />
65
+ <IconButton icon="check-circle" type="outlined" intent="success" onPress={() => handlePress('outlined-success')} />
66
+ <IconButton icon="alert" type="outlined" intent="warning" onPress={() => handlePress('outlined-warning')} />
67
+ <IconButton icon="alert-circle" type="outlined" intent="danger" onPress={() => handlePress('outlined-danger')} />
68
+ <IconButton icon="cog" type="outlined" intent="neutral" onPress={() => handlePress('outlined-neutral')} />
69
+ </View>
70
+ </View>
71
+
72
+ {/* Sizes */}
73
+ <View gap="md">
74
+ <Text typography="subtitle1">Sizes</Text>
75
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
76
+ <IconButton icon="plus" size="xs" type="contained" intent="primary" onPress={() => handlePress('xs')} />
77
+ <IconButton icon="plus" size="sm" type="contained" intent="primary" onPress={() => handlePress('sm')} />
78
+ <IconButton icon="plus" size="md" type="contained" intent="primary" onPress={() => handlePress('md')} />
79
+ <IconButton icon="plus" size="lg" type="contained" intent="primary" onPress={() => handlePress('lg')} />
80
+ <IconButton icon="plus" size="xl" type="contained" intent="primary" onPress={() => handlePress('xl')} />
81
+ </View>
82
+ </View>
83
+
84
+ {/* Disabled States */}
85
+ <View gap="md">
86
+ <Text typography="subtitle1">Disabled States</Text>
87
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
88
+ <IconButton icon="pencil" type="contained" intent="primary" disabled onPress={() => handlePress('disabled-contained')} />
89
+ <IconButton icon="pencil" type="outlined" intent="primary" disabled onPress={() => handlePress('disabled-outlined')} />
90
+ <IconButton icon="pencil" type="text" intent="primary" disabled onPress={() => handlePress('disabled-text')} />
91
+ </View>
92
+ </View>
93
+
94
+ {/* Gradient Overlay */}
95
+ <View gap="md">
96
+ <Text typography="subtitle1">Gradient Overlay</Text>
97
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
98
+ <IconButton icon="rocket-launch" type="contained" intent="primary" onPress={() => handlePress('no-gradient')} />
99
+ <IconButton icon="rocket-launch" type="contained" intent="primary" gradient="darken" onPress={() => handlePress('gradient-darken')} />
100
+ <IconButton icon="rocket-launch" type="contained" intent="primary" gradient="lighten" onPress={() => handlePress('gradient-lighten')} />
101
+ </View>
102
+ </View>
103
+
104
+ {/* Gradient with Different Intents */}
105
+ <View gap="md">
106
+ <Text typography="subtitle1">Gradient Intents (Darken)</Text>
107
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
108
+ <IconButton icon="star" type="contained" intent="primary" gradient="darken" onPress={() => handlePress('gradient-primary')} />
109
+ <IconButton icon="check" type="contained" intent="success" gradient="darken" onPress={() => handlePress('gradient-success')} />
110
+ <IconButton icon="alert" type="contained" intent="warning" gradient="darken" onPress={() => handlePress('gradient-warning')} />
111
+ <IconButton icon="close" type="contained" intent="danger" gradient="darken" onPress={() => handlePress('gradient-danger')} />
112
+ <IconButton icon="cog" type="contained" intent="neutral" gradient="darken" onPress={() => handlePress('gradient-neutral')} />
113
+ </View>
114
+ </View>
115
+
116
+ {/* Loading State */}
117
+ <View gap="md">
118
+ <Text typography="subtitle1">Loading State</Text>
119
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
120
+ <IconButton icon="refresh" type="contained" intent="primary" loading onPress={() => handlePress('loading-contained')} />
121
+ <IconButton icon="refresh" type="outlined" intent="primary" loading onPress={() => handlePress('loading-outlined')} />
122
+ <IconButton icon="refresh" type="text" intent="primary" loading onPress={() => handlePress('loading-text')} />
123
+ </View>
124
+ </View>
125
+
126
+ {/* Interactive Loading */}
127
+ <View gap="md">
128
+ <Text typography="subtitle1">Interactive Loading</Text>
129
+ <InteractiveLoadingIconButton />
130
+ </View>
131
+
132
+ {/* Common Use Cases */}
133
+ <View gap="md">
134
+ <Text typography="subtitle1">Common Use Cases</Text>
135
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
136
+ <IconButton icon="magnify" type="text" intent="neutral" onPress={() => handlePress('search')} />
137
+ <IconButton icon="bell" type="text" intent="neutral" onPress={() => handlePress('notifications')} />
138
+ <IconButton icon="cog" type="text" intent="neutral" onPress={() => handlePress('settings')} />
139
+ <IconButton icon="dots-vertical" type="text" intent="neutral" onPress={() => handlePress('more')} />
140
+ <IconButton icon="close" type="text" intent="neutral" onPress={() => handlePress('close')} />
141
+ <IconButton icon="plus" type="contained" intent="primary" onPress={() => handlePress('add')} />
142
+ <IconButton icon="delete" type="contained" intent="danger" onPress={() => handlePress('delete')} />
143
+ </View>
144
+ </View>
145
+ </View>
146
+ </Screen>
147
+ );
148
+ };
149
+
150
+ const InteractiveLoadingIconButton = () => {
151
+ const [loading, setLoading] = React.useState(false);
152
+
153
+ const handlePress = async () => {
154
+ setLoading(true);
155
+ await new Promise(resolve => setTimeout(resolve, 2000));
156
+ setLoading(false);
157
+ };
158
+
159
+ return (
160
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
161
+ <IconButton
162
+ icon="refresh"
163
+ type="contained"
164
+ intent="primary"
165
+ loading={loading}
166
+ onPress={handlePress}
167
+ />
168
+ <IconButton
169
+ icon="download"
170
+ type="outlined"
171
+ intent="success"
172
+ loading={loading}
173
+ onPress={handlePress}
174
+ />
175
+ </View>
176
+ );
177
+ };
@@ -1,5 +1,6 @@
1
1
  export { ActivityIndicatorExamples } from './ActivityIndicatorExamples';
2
2
  export { ButtonExamples } from './ButtonExamples';
3
+ export { IconButtonExamples } from './IconButtonExamples';
3
4
  export { TextExamples } from './TextExamples';
4
5
  export { ViewExamples } from './ViewExamples';
5
6
  export { LinkExamples } from './LinkExamples';