@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.
- package/package.json +8 -3
- package/src/Accordion/Accordion.native.tsx +2 -1
- package/src/Accordion/Accordion.web.tsx +2 -1
- package/src/ActivityIndicator/ActivityIndicator.native.tsx +2 -0
- package/src/ActivityIndicator/ActivityIndicator.web.tsx +2 -1
- package/src/ActivityIndicator/types.ts +2 -1
- package/src/Alert/Alert.native.tsx +2 -0
- package/src/Alert/Alert.web.tsx +2 -0
- package/src/Alert/types.ts +2 -1
- package/src/Avatar/Avatar.native.tsx +2 -1
- package/src/Avatar/Avatar.web.tsx +2 -1
- package/src/Avatar/types.ts +2 -1
- package/src/Badge/Badge.native.tsx +3 -0
- package/src/Badge/Badge.web.tsx +3 -0
- package/src/Badge/types.ts +2 -1
- package/src/Breadcrumb/Breadcrumb.native.tsx +2 -0
- package/src/Breadcrumb/Breadcrumb.web.tsx +2 -1
- package/src/Breadcrumb/types.ts +2 -1
- package/src/Button/Button.native.tsx +17 -12
- package/src/Button/Button.styles.tsx +3 -2
- package/src/Button/Button.web.tsx +2 -0
- package/src/Button/types.ts +2 -1
- package/src/Card/Card.native.tsx +2 -0
- package/src/Card/Card.web.tsx +2 -0
- package/src/Checkbox/Checkbox.native.tsx +2 -1
- package/src/Checkbox/Checkbox.web.tsx +2 -1
- package/src/Chip/Chip.native.tsx +3 -1
- package/src/Chip/Chip.web.tsx +2 -0
- package/src/Chip/types.ts +2 -1
- package/src/Dialog/Dialog.native.tsx +2 -0
- package/src/Dialog/Dialog.web.tsx +2 -0
- package/src/Dialog/types.ts +2 -1
- package/src/Divider/Divider.native.tsx +4 -0
- package/src/Divider/Divider.web.tsx +3 -0
- package/src/Divider/types.ts +2 -1
- package/src/Icon/Icon.native.tsx +2 -0
- package/src/Icon/Icon.web.tsx +3 -1
- package/src/Icon/types.ts +2 -1
- package/src/Image/Image.native.tsx +2 -1
- package/src/Image/Image.web.tsx +2 -0
- package/src/Image/types.ts +2 -1
- package/src/Input/Input.native.tsx +2 -1
- package/src/Input/Input.styles.tsx +1 -0
- package/src/Input/Input.web.tsx +2 -1
- package/src/Link/Link.native.tsx +2 -0
- package/src/Link/Link.web.tsx +2 -0
- package/src/Link/types.ts +2 -1
- package/src/List/List.native.tsx +3 -1
- package/src/List/List.web.tsx +2 -0
- package/src/Menu/Menu.native.tsx +2 -1
- package/src/Menu/Menu.web.tsx +2 -0
- package/src/Menu/types.ts +2 -1
- package/src/Popover/Popover.native.tsx +2 -0
- package/src/Popover/Popover.web.tsx +2 -1
- package/src/Popover/types.ts +2 -1
- package/src/Pressable/Pressable.native.tsx +2 -1
- package/src/Pressable/Pressable.web.tsx +2 -0
- package/src/Progress/Progress.native.tsx +3 -2
- package/src/Progress/Progress.web.tsx +3 -2
- package/src/Progress/types.ts +2 -1
- package/src/RadioButton/RadioButton.native.tsx +2 -0
- package/src/RadioButton/RadioButton.web.tsx +2 -0
- package/src/RadioButton/RadioGroup.native.tsx +2 -0
- package/src/RadioButton/RadioGroup.web.tsx +2 -0
- package/src/RadioButton/types.ts +2 -2
- package/src/SVGImage/SVGImage.native.tsx +3 -2
- package/src/SVGImage/SVGImage.web.tsx +3 -2
- package/src/SVGImage/types.ts +2 -1
- package/src/Screen/Screen.native.tsx +3 -1
- package/src/Screen/Screen.web.tsx +2 -0
- package/src/Select/Select.native.tsx +2 -1
- package/src/Select/Select.web.tsx +2 -1
- package/src/Skeleton/Skeleton.native.tsx +4 -0
- package/src/Skeleton/Skeleton.web.tsx +4 -0
- package/src/Skeleton/types.ts +3 -2
- package/src/Slider/Slider.native.tsx +5 -4
- package/src/Slider/Slider.web.tsx +2 -1
- package/src/Switch/Switch.native.tsx +3 -0
- package/src/Switch/Switch.web.tsx +2 -0
- package/src/TabBar/TabBar.native.tsx +48 -6
- package/src/TabBar/TabBar.styles.tsx +88 -1
- package/src/TabBar/TabBar.web.tsx +50 -2
- package/src/TabBar/types.ts +10 -0
- package/src/Table/Table.native.tsx +2 -0
- package/src/Table/Table.web.tsx +2 -1
- package/src/Text/Text.native.tsx +7 -7
- package/src/Text/Text.styles.tsx +23 -30
- package/src/Text/Text.web.tsx +8 -8
- package/src/Text/types.ts +1 -9
- package/src/TextArea/TextArea.native.tsx +2 -1
- package/src/TextArea/TextArea.web.tsx +2 -1
- package/src/Tooltip/Tooltip.native.tsx +2 -1
- package/src/Tooltip/Tooltip.web.tsx +2 -0
- package/src/Tooltip/types.ts +2 -1
- package/src/Video/Video.native.tsx +3 -2
- package/src/Video/Video.web.tsx +2 -0
- package/src/Video/types.ts +2 -1
- package/src/View/View.native.tsx +3 -1
- package/src/View/View.web.tsx +2 -0
- package/src/examples/TabBarExamples.tsx +122 -1
- package/src/utils/index.ts +20 -0
- 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) => (
|
package/src/Skeleton/types.ts
CHANGED
|
@@ -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
|
|
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}
|
|
246
|
-
<Text style={sliderStyles.minMaxLabel}
|
|
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 = '
|
|
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;
|
package/src/TabBar/types.ts
CHANGED
|
@@ -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}
|
package/src/Table/Table.web.tsx
CHANGED
|
@@ -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>
|