@idealyst/components 1.1.1 → 1.1.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.
Files changed (102) hide show
  1. package/package.json +8 -3
  2. package/src/Accordion/Accordion.native.tsx +2 -1
  3. package/src/Accordion/Accordion.web.tsx +2 -1
  4. package/src/ActivityIndicator/ActivityIndicator.native.tsx +2 -0
  5. package/src/ActivityIndicator/ActivityIndicator.web.tsx +2 -1
  6. package/src/ActivityIndicator/types.ts +2 -1
  7. package/src/Alert/Alert.native.tsx +2 -0
  8. package/src/Alert/Alert.web.tsx +2 -0
  9. package/src/Alert/types.ts +2 -1
  10. package/src/Avatar/Avatar.native.tsx +2 -1
  11. package/src/Avatar/Avatar.web.tsx +2 -1
  12. package/src/Avatar/types.ts +2 -1
  13. package/src/Badge/Badge.native.tsx +3 -0
  14. package/src/Badge/Badge.web.tsx +3 -0
  15. package/src/Badge/types.ts +2 -1
  16. package/src/Breadcrumb/Breadcrumb.native.tsx +2 -0
  17. package/src/Breadcrumb/Breadcrumb.web.tsx +2 -1
  18. package/src/Breadcrumb/types.ts +2 -1
  19. package/src/Button/Button.native.tsx +17 -12
  20. package/src/Button/Button.styles.tsx +3 -2
  21. package/src/Button/Button.web.tsx +2 -0
  22. package/src/Button/types.ts +2 -1
  23. package/src/Card/Card.native.tsx +2 -0
  24. package/src/Card/Card.web.tsx +2 -0
  25. package/src/Checkbox/Checkbox.native.tsx +2 -1
  26. package/src/Checkbox/Checkbox.web.tsx +2 -1
  27. package/src/Chip/Chip.native.tsx +3 -1
  28. package/src/Chip/Chip.web.tsx +2 -0
  29. package/src/Chip/types.ts +2 -1
  30. package/src/Dialog/Dialog.native.tsx +2 -0
  31. package/src/Dialog/Dialog.web.tsx +2 -0
  32. package/src/Dialog/types.ts +2 -1
  33. package/src/Divider/Divider.native.tsx +4 -0
  34. package/src/Divider/Divider.web.tsx +3 -0
  35. package/src/Divider/types.ts +2 -1
  36. package/src/Icon/Icon.native.tsx +2 -0
  37. package/src/Icon/Icon.web.tsx +3 -1
  38. package/src/Icon/types.ts +2 -1
  39. package/src/Image/Image.native.tsx +2 -1
  40. package/src/Image/Image.web.tsx +2 -0
  41. package/src/Image/types.ts +2 -1
  42. package/src/Input/Input.native.tsx +2 -1
  43. package/src/Input/Input.styles.tsx +1 -0
  44. package/src/Input/Input.web.tsx +2 -1
  45. package/src/Link/Link.native.tsx +2 -0
  46. package/src/Link/Link.web.tsx +2 -0
  47. package/src/Link/types.ts +2 -1
  48. package/src/List/List.native.tsx +3 -1
  49. package/src/List/List.web.tsx +2 -0
  50. package/src/Menu/Menu.native.tsx +2 -1
  51. package/src/Menu/Menu.web.tsx +2 -0
  52. package/src/Menu/types.ts +2 -1
  53. package/src/Popover/Popover.native.tsx +2 -0
  54. package/src/Popover/Popover.web.tsx +2 -1
  55. package/src/Popover/types.ts +2 -1
  56. package/src/Pressable/Pressable.native.tsx +2 -1
  57. package/src/Pressable/Pressable.web.tsx +2 -0
  58. package/src/Progress/Progress.native.tsx +3 -2
  59. package/src/Progress/Progress.web.tsx +3 -2
  60. package/src/Progress/types.ts +2 -1
  61. package/src/RadioButton/RadioButton.native.tsx +2 -0
  62. package/src/RadioButton/RadioButton.web.tsx +2 -0
  63. package/src/RadioButton/RadioGroup.native.tsx +2 -0
  64. package/src/RadioButton/RadioGroup.web.tsx +2 -0
  65. package/src/RadioButton/types.ts +2 -2
  66. package/src/SVGImage/SVGImage.native.tsx +3 -2
  67. package/src/SVGImage/SVGImage.web.tsx +3 -2
  68. package/src/SVGImage/types.ts +2 -1
  69. package/src/Screen/Screen.native.tsx +3 -1
  70. package/src/Screen/Screen.web.tsx +2 -0
  71. package/src/Select/Select.native.tsx +2 -1
  72. package/src/Select/Select.web.tsx +2 -1
  73. package/src/Skeleton/Skeleton.native.tsx +4 -0
  74. package/src/Skeleton/Skeleton.web.tsx +4 -0
  75. package/src/Skeleton/types.ts +3 -2
  76. package/src/Slider/Slider.native.tsx +5 -4
  77. package/src/Slider/Slider.web.tsx +2 -1
  78. package/src/Switch/Switch.native.tsx +3 -0
  79. package/src/Switch/Switch.web.tsx +2 -0
  80. package/src/TabBar/TabBar.native.tsx +48 -6
  81. package/src/TabBar/TabBar.styles.tsx +88 -1
  82. package/src/TabBar/TabBar.web.tsx +50 -2
  83. package/src/TabBar/types.ts +10 -0
  84. package/src/Table/Table.native.tsx +2 -0
  85. package/src/Table/Table.web.tsx +2 -1
  86. package/src/Text/Text.native.tsx +7 -7
  87. package/src/Text/Text.styles.tsx +23 -30
  88. package/src/Text/Text.web.tsx +8 -8
  89. package/src/Text/types.ts +1 -9
  90. package/src/TextArea/TextArea.native.tsx +2 -1
  91. package/src/TextArea/TextArea.web.tsx +2 -1
  92. package/src/Tooltip/Tooltip.native.tsx +2 -1
  93. package/src/Tooltip/Tooltip.web.tsx +2 -0
  94. package/src/Tooltip/types.ts +2 -1
  95. package/src/Video/Video.native.tsx +3 -2
  96. package/src/Video/Video.web.tsx +2 -0
  97. package/src/Video/types.ts +2 -1
  98. package/src/View/View.native.tsx +3 -1
  99. package/src/View/View.web.tsx +2 -0
  100. package/src/examples/TabBarExamples.tsx +122 -1
  101. package/src/utils/index.ts +20 -0
  102. package/src/utils/viewStyleProps.ts +12 -4
@@ -30,6 +30,7 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(({
30
30
  style,
31
31
  testID,
32
32
  accessibilityLabel,
33
+ id,
33
34
  }, ref) => {
34
35
  const [isOpen, setIsOpen] = useState(false);
35
36
  const [searchTerm, setSearchTerm] = useState('');
@@ -191,7 +192,7 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(({
191
192
  const mergedRef = useMergeRefs(ref, containerWebProps.ref);
192
193
 
193
194
  return (
194
- <div {...containerWebProps} ref={mergedRef} data-testid={testID}>
195
+ <div {...containerWebProps} ref={mergedRef} id={id} data-testid={testID}>
195
196
  {label && (
196
197
  <label {...getWebProps([selectStyles.label])}>
197
198
  {label}
@@ -19,6 +19,7 @@ const Skeleton = forwardRef<View, SkeletonProps>(({
19
19
  animation = 'pulse',
20
20
  style,
21
21
  testID,
22
+ id,
22
23
  }, ref) => {
23
24
  skeletonStyles.useVariants({
24
25
  shape,
@@ -78,6 +79,7 @@ const Skeleton = forwardRef<View, SkeletonProps>(({
78
79
  return (
79
80
  <Animated.View
80
81
  ref={ref as any}
82
+ nativeID={id}
81
83
  style={[
82
84
  skeletonStyles.skeleton,
83
85
  customStyles,
@@ -113,11 +115,13 @@ export const SkeletonGroup: React.FC<SkeletonGroupProps> = ({
113
115
  skeletonProps,
114
116
  style,
115
117
  testID,
118
+ id,
116
119
  }) => {
117
120
  skeletonStyles.useVariants({});
118
121
 
119
122
  return (
120
123
  <View
124
+ nativeID={id}
121
125
  style={[
122
126
  skeletonStyles.group,
123
127
  { gap: spacing },
@@ -11,6 +11,7 @@ const Skeleton: React.FC<SkeletonProps> = ({
11
11
  animation = 'pulse',
12
12
  style,
13
13
  testID,
14
+ id,
14
15
  }) => {
15
16
  skeletonStyles.useVariants({
16
17
  shape,
@@ -60,6 +61,7 @@ const Skeleton: React.FC<SkeletonProps> = ({
60
61
  ...customStyles,
61
62
  ...animationStyles,
62
63
  }}
64
+ id={id}
63
65
  data-testid={testID}
64
66
  >
65
67
  {animation === 'wave' && (
@@ -86,6 +88,7 @@ export const SkeletonGroup: React.FC<SkeletonGroupProps> = ({
86
88
  skeletonProps,
87
89
  style,
88
90
  testID,
91
+ id,
89
92
  }) => {
90
93
  skeletonStyles.useVariants({});
91
94
  const groupProps = getWebProps([skeletonStyles.group, style as any]);
@@ -96,6 +99,7 @@ export const SkeletonGroup: React.FC<SkeletonGroupProps> = ({
96
99
  style={{
97
100
  gap: `${spacing}px`,
98
101
  }}
102
+ id={id}
99
103
  data-testid={testID}
100
104
  >
101
105
  {Array.from({ length: count }).map((_, index) => (
@@ -1,9 +1,10 @@
1
1
  import type { StyleProp, ViewStyle } from 'react-native';
2
+ import { BaseProps } from '../utils/viewStyleProps';
2
3
 
3
4
  export type SkeletonShape = 'rectangle' | 'circle' | 'rounded';
4
5
  export type SkeletonAnimation = 'pulse' | 'wave' | 'none';
5
6
 
6
- export interface SkeletonProps {
7
+ export interface SkeletonProps extends BaseProps {
7
8
  /**
8
9
  * Width of the skeleton (number in pixels or string with units)
9
10
  * @default '100%'
@@ -45,7 +46,7 @@ export interface SkeletonProps {
45
46
  testID?: string;
46
47
  }
47
48
 
48
- export interface SkeletonGroupProps {
49
+ export interface SkeletonGroupProps extends BaseProps {
49
50
  /**
50
51
  * Number of skeleton items to render
51
52
  * @default 3
@@ -29,6 +29,7 @@ const Slider = forwardRef<View, SliderProps>(({
29
29
  onValueCommit,
30
30
  style,
31
31
  testID,
32
+ id,
32
33
  }, ref) => {
33
34
  const [internalValue, setInternalValue] = useState(defaultValue);
34
35
  const [trackWidthState, setTrackWidthState] = useState(0);
@@ -166,7 +167,7 @@ const Slider = forwardRef<View, SliderProps>(({
166
167
  };
167
168
 
168
169
  return (
169
- <View ref={ref} style={[sliderStyles.container, style]} testID={testID}>
170
+ <View ref={ref} nativeID={id} style={[sliderStyles.container, style]} testID={testID}>
170
171
  {showValue && (
171
172
  <View style={sliderStyles.valueLabel as any}>
172
173
  <Text>{value}</Text>
@@ -209,7 +210,7 @@ const Slider = forwardRef<View, SliderProps>(({
209
210
  { left: markPosition },
210
211
  ]}
211
212
  >
212
- <Text size="sm">{mark.label}</Text>
213
+ <Text typography="caption">{mark.label}</Text>
213
214
  </View>
214
215
  )}
215
216
  </View>
@@ -242,8 +243,8 @@ const Slider = forwardRef<View, SliderProps>(({
242
243
 
243
244
  {showMinMax && (
244
245
  <View style={sliderStyles.minMaxLabels}>
245
- <Text style={sliderStyles.minMaxLabel} size="sm">{min}</Text>
246
- <Text style={sliderStyles.minMaxLabel} size="sm">{max}</Text>
246
+ <Text style={sliderStyles.minMaxLabel} typography="caption">{min}</Text>
247
+ <Text style={sliderStyles.minMaxLabel} typography="caption">{max}</Text>
247
248
  </View>
248
249
  )}
249
250
  </View>
@@ -27,6 +27,7 @@ const Slider = forwardRef<HTMLDivElement, SliderProps>(({
27
27
  onValueCommit,
28
28
  style,
29
29
  testID,
30
+ id,
30
31
  }, ref) => {
31
32
  const [internalValue, setInternalValue] = useState(defaultValue);
32
33
  const [isDragging, setIsDragging] = useState(false);
@@ -165,7 +166,7 @@ const Slider = forwardRef<HTMLDivElement, SliderProps>(({
165
166
  const mergedRef = useMergeRefs(ref, containerProps.ref);
166
167
 
167
168
  return (
168
- <div {...containerProps} ref={mergedRef} data-testid={testID}>
169
+ <div {...containerProps} ref={mergedRef} id={id} data-testid={testID}>
169
170
  {showValue && (
170
171
  <div {...valueLabelProps}>
171
172
  {value}
@@ -19,6 +19,7 @@ const Switch = forwardRef<ComponentRef<typeof Pressable>, SwitchProps>(({
19
19
  marginHorizontal,
20
20
  style,
21
21
  testID,
22
+ id,
22
23
  }, ref) => {
23
24
  switchStyles.useVariants({
24
25
  size,
@@ -81,6 +82,7 @@ const Switch = forwardRef<ComponentRef<typeof Pressable>, SwitchProps>(({
81
82
  const switchElement = (
82
83
  <Pressable
83
84
  ref={!label ? ref : undefined}
85
+ nativeID={!label ? id : undefined}
84
86
  onPress={handlePress}
85
87
  disabled={disabled}
86
88
  style={switchStyles.switchContainer}
@@ -115,6 +117,7 @@ const Switch = forwardRef<ComponentRef<typeof Pressable>, SwitchProps>(({
115
117
  return (
116
118
  <Pressable
117
119
  ref={ref}
120
+ nativeID={id}
118
121
  onPress={handlePress}
119
122
  disabled={disabled}
120
123
  style={[switchStyles.container, style]}
@@ -22,6 +22,7 @@ const Switch = forwardRef<HTMLDivElement | HTMLButtonElement, SwitchProps>(({
22
22
  marginHorizontal,
23
23
  style,
24
24
  testID,
25
+ id,
25
26
  }, ref) => {
26
27
  const handleClick = () => {
27
28
  if (!disabled && onCheckedChange) {
@@ -90,6 +91,7 @@ const Switch = forwardRef<HTMLDivElement | HTMLButtonElement, SwitchProps>(({
90
91
  ref={mergedButtonRef}
91
92
  onClick={handleClick}
92
93
  disabled={disabled}
94
+ id={id}
93
95
  data-testid={testID}
94
96
  role="switch"
95
97
  aria-checked={checked}
@@ -1,22 +1,47 @@
1
- import React, { useState, useRef, useEffect, forwardRef } from 'react';
1
+ import React, { useState, useRef, useEffect, forwardRef, ReactNode } from 'react';
2
2
  import { View, TouchableOpacity, Text, ScrollView } from 'react-native';
3
3
  import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
4
4
  import {
5
5
  tabBarContainerStyles,
6
6
  tabBarTabStyles,
7
7
  tabBarLabelStyles,
8
- tabBarIndicatorStyles
8
+ tabBarIndicatorStyles,
9
+ tabBarIconStyles
9
10
  } from './TabBar.styles';
10
- import type { TabBarProps } from './types';
11
+ import type { TabBarProps, TabBarItem } from './types';
12
+
13
+ // Icon size mapping based on size variant
14
+ const ICON_SIZES: Record<string, number> = {
15
+ xs: 14,
16
+ sm: 16,
17
+ md: 18,
18
+ lg: 20,
19
+ xl: 24,
20
+ };
21
+
22
+ // Helper to render icon
23
+ function renderIcon(
24
+ icon: TabBarItem['icon'],
25
+ active: boolean,
26
+ size: number
27
+ ): ReactNode {
28
+ if (!icon) return null;
29
+ if (typeof icon === 'function') {
30
+ return icon({ active, size });
31
+ }
32
+ return icon;
33
+ }
11
34
 
12
35
  const TabBar = forwardRef<View, TabBarProps>(({
13
36
  items,
14
37
  value: controlledValue,
15
38
  defaultValue,
16
39
  onChange,
17
- type = 'default',
40
+ type = 'standard',
18
41
  size = 'md',
19
42
  pillMode = 'light',
43
+ iconPosition = 'left',
44
+ justify = 'start',
20
45
  // Spacing variants from ContainerStyleProps
21
46
  gap,
22
47
  padding,
@@ -27,6 +52,7 @@ const TabBar = forwardRef<View, TabBarProps>(({
27
52
  marginHorizontal,
28
53
  style,
29
54
  testID,
55
+ id,
30
56
  }, ref) => {
31
57
  const firstItemValue = items[0]?.value || '';
32
58
  const [internalValue, setInternalValue] = useState(defaultValue || firstItemValue);
@@ -86,8 +112,10 @@ const TabBar = forwardRef<View, TabBarProps>(({
86
112
 
87
113
  // Apply container and indicator types right before rendering
88
114
  tabBarContainerStyles.useVariants({
115
+ type,
89
116
  size,
90
117
  pillMode,
118
+ justify,
91
119
  gap,
92
120
  padding,
93
121
  paddingVertical,
@@ -96,7 +124,7 @@ const TabBar = forwardRef<View, TabBarProps>(({
96
124
  marginVertical,
97
125
  marginHorizontal,
98
126
  });
99
- tabBarIndicatorStyles.useVariants({ pillMode });
127
+ tabBarIndicatorStyles.useVariants({ type, pillMode });
100
128
 
101
129
  return (
102
130
  <ScrollView
@@ -104,7 +132,7 @@ const TabBar = forwardRef<View, TabBarProps>(({
104
132
  showsHorizontalScrollIndicator={false}
105
133
  contentContainerStyle={{ position: 'relative' }}
106
134
  >
107
- <View ref={ref} style={[tabBarContainerStyles.container, style]} testID={testID}>
135
+ <View ref={ref} nativeID={id} style={[tabBarContainerStyles.container, style]} testID={testID}>
108
136
  {/* Animated indicator - render first so it's behind */}
109
137
  <Animated.View
110
138
  style={[
@@ -117,20 +145,33 @@ const TabBar = forwardRef<View, TabBarProps>(({
117
145
  <View style={{ flexDirection: 'row' }}>
118
146
  {items.map((item) => {
119
147
  const isActive = value === item.value;
148
+ const iconSize = ICON_SIZES[size] || 18;
120
149
 
121
150
  // Apply tab and label types for this specific tab
122
151
  tabBarTabStyles.useVariants({
123
152
  size,
153
+ type,
124
154
  active: isActive,
125
155
  disabled: Boolean(item.disabled),
126
156
  pillMode,
157
+ iconPosition,
158
+ justify,
127
159
  });
128
160
  tabBarLabelStyles.useVariants({
129
161
  size,
162
+ type,
130
163
  pillMode,
131
164
  active: isActive,
132
165
  disabled: Boolean(item.disabled),
133
166
  });
167
+ tabBarIconStyles.useVariants({
168
+ size,
169
+ active: isActive,
170
+ disabled: Boolean(item.disabled),
171
+ iconPosition,
172
+ });
173
+
174
+ const icon = renderIcon(item.icon, isActive, iconSize);
134
175
 
135
176
  return (
136
177
  <TouchableOpacity
@@ -145,6 +186,7 @@ const TabBar = forwardRef<View, TabBarProps>(({
145
186
  activeOpacity={0.7}
146
187
  testID={`${testID}-tab-${item.value}`}
147
188
  >
189
+ {icon && <View style={tabBarIconStyles.tabIcon}>{icon}</View>}
148
190
  <Text style={tabBarLabelStyles.tabLabel}>{item.label}</Text>
149
191
  </TouchableOpacity>
150
192
  );
@@ -10,12 +10,13 @@ import {
10
10
  buildMarginVerticalVariants,
11
11
  buildMarginHorizontalVariants,
12
12
  } from '../utils/buildViewStyleVariants';
13
- import { TabBarPillMode, TabBarSizeVariant, TabBarType } from './types';
13
+ import { TabBarPillMode, TabBarSizeVariant, TabBarType, TabBarIconPosition, TabBarJustify } from './types';
14
14
 
15
15
  type TabBarContainerVariants = {
16
16
  type: TabBarType;
17
17
  size: TabBarSizeVariant;
18
18
  pillMode: TabBarPillMode;
19
+ justify: TabBarJustify;
19
20
  }
20
21
 
21
22
  type TabBarTabVariants = {
@@ -24,6 +25,15 @@ type TabBarTabVariants = {
24
25
  active: boolean;
25
26
  disabled: boolean;
26
27
  pillMode: TabBarPillMode;
28
+ iconPosition: TabBarIconPosition;
29
+ justify: TabBarJustify;
30
+ }
31
+
32
+ type TabBarIconVariants = {
33
+ size: TabBarSizeVariant;
34
+ active: boolean;
35
+ disabled: boolean;
36
+ iconPosition: TabBarIconPosition;
27
37
  }
28
38
 
29
39
  type TabBarLabelVariants = {
@@ -238,6 +248,22 @@ const createContainerStyles = (theme: Theme) => {
238
248
  light: {},
239
249
  dark: {},
240
250
  },
251
+ justify: {
252
+ start: {
253
+ justifyContent: 'flex-start',
254
+ },
255
+ center: {
256
+ justifyContent: 'center',
257
+ },
258
+ equal: {
259
+ justifyContent: 'stretch',
260
+ width: '100%',
261
+ },
262
+ 'space-between': {
263
+ justifyContent: 'space-between',
264
+ width: '100%',
265
+ },
266
+ },
241
267
  // Spacing variants from ContainerStyleProps
242
268
  gap: buildGapVariants(theme),
243
269
  padding: buildPaddingVariants(theme),
@@ -262,6 +288,7 @@ const createTabStyles = (theme: Theme) => {
262
288
  position: 'relative',
263
289
  zIndex: 2,
264
290
  backgroundColor: 'transparent',
291
+ gap: 6,
265
292
  variants: {
266
293
  size: createTabSizeVariants(theme),
267
294
  type: {
@@ -298,6 +325,22 @@ const createTabStyles = (theme: Theme) => {
298
325
  light: {},
299
326
  dark: {},
300
327
  },
328
+ iconPosition: {
329
+ left: {
330
+ flexDirection: 'row',
331
+ },
332
+ top: {
333
+ flexDirection: 'column',
334
+ },
335
+ },
336
+ justify: {
337
+ start: {},
338
+ center: {},
339
+ equal: {
340
+ flex: 1,
341
+ },
342
+ 'space-between': {},
343
+ },
301
344
  } as const,
302
345
  compoundVariants: createTabCompoundVariants(theme),
303
346
  _web: {
@@ -379,6 +422,43 @@ const createIndicatorStyles = (theme: Theme) => {
379
422
  } as const;
380
423
  }
381
424
 
425
+ /**
426
+ * Create size variants for icon
427
+ */
428
+ function createIconSizeVariants(theme: Theme) {
429
+ return buildSizeVariants(theme, 'tabBar', (size) => ({
430
+ width: size.iconSize || size.fontSize,
431
+ height: size.iconSize || size.fontSize,
432
+ }));
433
+ }
434
+
435
+ const createIconStyles = (theme: Theme) => {
436
+ return {
437
+ display: 'flex',
438
+ alignItems: 'center',
439
+ justifyContent: 'center',
440
+ variants: {
441
+ size: createIconSizeVariants(theme),
442
+ active: {
443
+ true: {},
444
+ false: {},
445
+ },
446
+ disabled: {
447
+ true: {
448
+ opacity: 0.5,
449
+ },
450
+ false: {},
451
+ },
452
+ iconPosition: {
453
+ left: {},
454
+ top: {
455
+ marginBottom: 2,
456
+ },
457
+ },
458
+ } as const,
459
+ } as const;
460
+ }
461
+
382
462
  // Styles are inlined here instead of in @idealyst/theme because Unistyles' Babel transform on native cannot resolve function calls to extract variant structures.
383
463
  // @ts-ignore - TS language server needs restart to pick up theme structure changes
384
464
  export const tabBarStyles = StyleSheet.create((theme: Theme) => {
@@ -386,6 +466,7 @@ export const tabBarStyles = StyleSheet.create((theme: Theme) => {
386
466
  container: createContainerStyles(theme),
387
467
  tab: createTabStyles(theme),
388
468
  tabLabel: createTabLabelStyles(theme),
469
+ tabIcon: createIconStyles(theme),
389
470
  indicator: createIndicatorStyles(theme),
390
471
  };
391
472
  });
@@ -414,3 +495,9 @@ export const tabBarIndicatorStyles = StyleSheet.create((theme: Theme) => {
414
495
  indicator: createIndicatorStyles(theme),
415
496
  } as const;
416
497
  });
498
+
499
+ export const tabBarIconStyles = StyleSheet.create((theme: Theme) => {
500
+ return {
501
+ tabIcon: createIconStyles(theme),
502
+ } as const;
503
+ });
@@ -1,14 +1,37 @@
1
- import React, { useState, useRef, useEffect } from 'react';
1
+ import React, { useState, useRef, useEffect, ReactNode } from 'react';
2
2
  import { getWebProps } from 'react-native-unistyles/web';
3
3
  import {
4
4
  tabBarContainerStyles,
5
5
  tabBarTabStyles,
6
6
  tabBarLabelStyles,
7
- tabBarIndicatorStyles
7
+ tabBarIndicatorStyles,
8
+ tabBarIconStyles
8
9
  } from './TabBar.styles';
9
10
  import type { TabBarProps, TabBarItem } from './types';
10
11
  import useMergeRefs from '../hooks/useMergeRefs';
11
12
 
13
+ // Icon size mapping based on size variant
14
+ const ICON_SIZES: Record<string, number> = {
15
+ xs: 14,
16
+ sm: 16,
17
+ md: 18,
18
+ lg: 20,
19
+ xl: 24,
20
+ };
21
+
22
+ // Helper to render icon
23
+ function renderIcon(
24
+ icon: TabBarItem['icon'],
25
+ active: boolean,
26
+ size: number
27
+ ): ReactNode {
28
+ if (!icon) return null;
29
+ if (typeof icon === 'function') {
30
+ return icon({ active, size });
31
+ }
32
+ return icon;
33
+ }
34
+
12
35
  interface TabProps {
13
36
  item: TabBarItem;
14
37
  isActive: boolean;
@@ -16,6 +39,8 @@ interface TabProps {
16
39
  size: TabBarProps['size'];
17
40
  type: TabBarProps['type'];
18
41
  pillMode: TabBarProps['pillMode'];
42
+ iconPosition: TabBarProps['iconPosition'];
43
+ justify: TabBarProps['justify'];
19
44
  testID?: string;
20
45
  tabRef: (el: HTMLButtonElement | null) => void;
21
46
  }
@@ -27,9 +52,13 @@ const Tab: React.FC<TabProps> = ({
27
52
  size,
28
53
  type,
29
54
  pillMode,
55
+ iconPosition,
56
+ justify,
30
57
  testID,
31
58
  tabRef,
32
59
  }) => {
60
+ const iconSize = ICON_SIZES[size || 'md'] || 18;
61
+
33
62
  // Apply tab and label types for this specific tab
34
63
  tabBarTabStyles.useVariants({
35
64
  size,
@@ -37,6 +66,8 @@ const Tab: React.FC<TabProps> = ({
37
66
  active: isActive,
38
67
  disabled: Boolean(item.disabled),
39
68
  pillMode,
69
+ iconPosition,
70
+ justify,
40
71
  });
41
72
  tabBarLabelStyles.useVariants({
42
73
  size,
@@ -45,9 +76,16 @@ const Tab: React.FC<TabProps> = ({
45
76
  active: isActive,
46
77
  disabled: Boolean(item.disabled),
47
78
  });
79
+ tabBarIconStyles.useVariants({
80
+ size,
81
+ active: isActive,
82
+ disabled: Boolean(item.disabled),
83
+ iconPosition,
84
+ });
48
85
 
49
86
  const tabProps = getWebProps([tabBarTabStyles.tab]);
50
87
  const labelProps = getWebProps([tabBarLabelStyles.tabLabel]);
88
+ const iconProps = getWebProps([tabBarIconStyles.tabIcon]);
51
89
 
52
90
  // Merge refs from getWebProps with our tracking ref
53
91
  const mergedRef = useMergeRefs<HTMLButtonElement>(
@@ -55,6 +93,8 @@ const Tab: React.FC<TabProps> = ({
55
93
  tabRef
56
94
  );
57
95
 
96
+ const icon = renderIcon(item.icon, isActive, iconSize);
97
+
58
98
  return (
59
99
  <button
60
100
  {...tabProps}
@@ -66,6 +106,7 @@ const Tab: React.FC<TabProps> = ({
66
106
  aria-disabled={item.disabled}
67
107
  data-testid={`${testID}-tab-${item.value}`}
68
108
  >
109
+ {icon && <span {...iconProps}>{icon}</span>}
69
110
  <span {...labelProps}>{item.label}</span>
70
111
  </button>
71
112
  );
@@ -79,6 +120,8 @@ const TabBar: React.FC<TabBarProps> = ({
79
120
  type = 'standard',
80
121
  size = 'md',
81
122
  pillMode = 'light',
123
+ iconPosition = 'left',
124
+ justify = 'start',
82
125
  // Spacing variants from ContainerStyleProps
83
126
  gap,
84
127
  padding,
@@ -89,6 +132,7 @@ const TabBar: React.FC<TabBarProps> = ({
89
132
  marginHorizontal,
90
133
  style,
91
134
  testID,
135
+ id,
92
136
  }) => {
93
137
  const firstItemValue = items[0]?.value || '';
94
138
  const [internalValue, setInternalValue] = useState(defaultValue || firstItemValue);
@@ -151,6 +195,7 @@ const TabBar: React.FC<TabBarProps> = ({
151
195
  type,
152
196
  size,
153
197
  pillMode,
198
+ justify,
154
199
  gap,
155
200
  padding,
156
201
  paddingVertical,
@@ -186,6 +231,7 @@ const TabBar: React.FC<TabBarProps> = ({
186
231
  {...containerProps}
187
232
  ref={mergedContainerRef}
188
233
  role="tablist"
234
+ id={id}
189
235
  data-testid={testID}
190
236
  >
191
237
  {/* Sliding indicator */}
@@ -206,6 +252,8 @@ const TabBar: React.FC<TabBarProps> = ({
206
252
  size={size}
207
253
  type={type}
208
254
  pillMode={pillMode}
255
+ iconPosition={iconPosition}
256
+ justify={justify}
209
257
  testID={testID}
210
258
  tabRef={(el) => {
211
259
  tabRefs.current[item.value] = el;
@@ -1,4 +1,5 @@
1
1
  import { Size } from '@idealyst/theme';
2
+ import type { ReactNode } from 'react';
2
3
  import type { StyleProp, ViewStyle } from 'react-native';
3
4
  import { ContainerStyleProps } from '../utils/viewStyleProps';
4
5
 
@@ -6,10 +7,15 @@ import { ContainerStyleProps } from '../utils/viewStyleProps';
6
7
  export type TabBarSizeVariant = Size;
7
8
  export type TabBarType = 'standard' | 'pills' | 'underline';
8
9
  export type TabBarPillMode = 'light' | 'dark';
10
+ export type TabBarIconPosition = 'left' | 'top';
11
+ /** Layout justification for tabs */
12
+ export type TabBarJustify = 'start' | 'center' | 'equal' | 'space-between';
9
13
 
10
14
  export interface TabBarItem {
11
15
  value: string;
12
16
  label: string;
17
+ /** Icon to display - can be a React node or a render function receiving active state */
18
+ icon?: ReactNode | ((props: { active: boolean; size: number }) => ReactNode);
13
19
  disabled?: boolean;
14
20
  }
15
21
 
@@ -22,6 +28,10 @@ export interface TabBarProps extends ContainerStyleProps {
22
28
  size?: TabBarSizeVariant;
23
29
  /** Mode for pills variant: 'light' for light backgrounds (dark pill), 'dark' for dark backgrounds (light pill) */
24
30
  pillMode?: TabBarPillMode;
31
+ /** Position of icon relative to label: 'left' (horizontal) or 'top' (stacked) */
32
+ iconPosition?: TabBarIconPosition;
33
+ /** Layout justification: 'start' (left), 'center', 'equal' (full width equal tabs), 'space-between' */
34
+ justify?: TabBarJustify;
25
35
  style?: StyleProp<ViewStyle>;
26
36
  testID?: string;
27
37
  }
@@ -20,6 +20,7 @@ function TableInner<T = any>({
20
20
  marginHorizontal,
21
21
  style,
22
22
  testID,
23
+ id,
23
24
  }: TableProps<T>, ref: React.Ref<ScrollView>) {
24
25
  // Apply variants
25
26
  tableStyles.useVariants({
@@ -49,6 +50,7 @@ function TableInner<T = any>({
49
50
  return (
50
51
  <ScrollView
51
52
  ref={ref}
53
+ nativeID={id}
52
54
  horizontal
53
55
  style={[tableStyles.container, style]}
54
56
  testID={testID}
@@ -20,6 +20,7 @@ function Table<T = any>({
20
20
  marginHorizontal,
21
21
  style,
22
22
  testID,
23
+ id,
23
24
  }: TableProps<T>) {
24
25
  // Apply variants
25
26
  tableStyles.useVariants({
@@ -49,7 +50,7 @@ function Table<T = any>({
49
50
  const isClickable = !!onRowPress;
50
51
 
51
52
  return (
52
- <div {...containerProps} data-testid={testID}>
53
+ <div {...containerProps} id={id} data-testid={testID}>
53
54
  <table {...tableProps}>
54
55
  <thead {...getWebProps([tableStyles.thead])}>
55
56
  <tr>