@idealyst/components 1.2.30 → 1.2.32
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 +4 -4
- package/plugin/web.js +2 -0
- package/src/ActivityIndicator/ActivityIndicator.native.tsx +1 -0
- package/src/ActivityIndicator/ActivityIndicator.styles.tsx +1 -5
- package/src/ActivityIndicator/ActivityIndicator.web.tsx +15 -28
- package/src/Alert/Alert.native.tsx +2 -2
- package/src/Alert/Alert.styles.tsx +88 -63
- package/src/Alert/Alert.web.tsx +26 -27
- package/src/Chip/Chip.styles.tsx +124 -142
- package/src/IconButton/IconButton.native.tsx +219 -0
- package/src/IconButton/IconButton.styles.tsx +127 -0
- package/src/IconButton/IconButton.web.tsx +198 -0
- package/src/IconButton/index.native.ts +5 -0
- package/src/IconButton/index.ts +5 -0
- package/src/IconButton/index.web.ts +5 -0
- package/src/IconButton/types.ts +84 -0
- package/src/Skeleton/Skeleton.web.tsx +1 -1
- package/src/Switch/Switch.styles.tsx +23 -6
- package/src/Switch/Switch.web.tsx +7 -9
- package/src/View/View.styles.tsx +1 -0
- package/src/View/View.web.tsx +7 -1
- package/src/examples/ActivityIndicatorExamples.tsx +177 -0
- package/src/examples/SwitchExamples.tsx +24 -24
- package/src/examples/index.ts +1 -0
- package/src/extensions/types.ts +12 -0
- package/src/index.native.ts +4 -0
- package/src/index.ts +4 -0
package/src/Chip/Chip.styles.tsx
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Chip styles using defineStyle with
|
|
3
|
-
*
|
|
4
|
-
* Chip has compound logic between type+selected+intent that's handled via
|
|
5
|
-
* nested variants. The $intents iterator expands for all intent values.
|
|
2
|
+
* Chip styles using defineStyle with dynamic size/intent/type/selected handling.
|
|
6
3
|
*/
|
|
7
4
|
import { StyleSheet } from 'react-native-unistyles';
|
|
8
5
|
import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
|
|
@@ -25,144 +22,129 @@ export type ChipDynamicProps = {
|
|
|
25
22
|
};
|
|
26
23
|
|
|
27
24
|
/**
|
|
28
|
-
* Chip styles with
|
|
25
|
+
* Chip styles with size/intent/type/selected handling.
|
|
29
26
|
*/
|
|
30
27
|
export const chipStyles = defineStyle('Chip', (theme: Theme) => ({
|
|
31
|
-
container: (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
filled: { color: theme.$intents.contrast },
|
|
154
|
-
outlined: { color: theme.$intents.primary },
|
|
155
|
-
soft: { color: theme.$intents.dark },
|
|
156
|
-
},
|
|
157
|
-
selected: {
|
|
158
|
-
true: {},
|
|
159
|
-
false: {},
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
compoundVariants: [
|
|
163
|
-
{ type: 'filled', selected: true, styles: { color: theme.$intents.primary } },
|
|
164
|
-
{ type: 'outlined', selected: true, styles: { color: theme.colors.text.inverse } },
|
|
165
|
-
{ type: 'soft', selected: true, styles: { color: theme.colors.text.inverse } },
|
|
166
|
-
],
|
|
167
|
-
}),
|
|
28
|
+
container: ({ size = 'md', intent = 'primary', type = 'filled', selected = false, disabled = false }: ChipDynamicProps) => {
|
|
29
|
+
const intentValue = theme.intents[intent];
|
|
30
|
+
const sizeValue = theme.sizes.chip[size];
|
|
31
|
+
|
|
32
|
+
// Compute colors based on type and selected state
|
|
33
|
+
let backgroundColor: string;
|
|
34
|
+
let borderColor: string;
|
|
35
|
+
let borderWidth: number;
|
|
36
|
+
|
|
37
|
+
if (type === 'filled') {
|
|
38
|
+
borderWidth = 1;
|
|
39
|
+
backgroundColor = selected ? intentValue.contrast : intentValue.primary;
|
|
40
|
+
borderColor = selected ? intentValue.primary : 'transparent';
|
|
41
|
+
} else if (type === 'outlined') {
|
|
42
|
+
borderWidth = 1;
|
|
43
|
+
backgroundColor = selected ? intentValue.primary : 'transparent';
|
|
44
|
+
borderColor = intentValue.primary;
|
|
45
|
+
} else { // soft
|
|
46
|
+
borderWidth = 0;
|
|
47
|
+
backgroundColor = selected ? intentValue.primary : intentValue.light;
|
|
48
|
+
borderColor = 'transparent';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
display: 'flex' as const,
|
|
53
|
+
flexDirection: 'row' as const,
|
|
54
|
+
alignItems: 'center' as const,
|
|
55
|
+
justifyContent: 'center' as const,
|
|
56
|
+
gap: 4,
|
|
57
|
+
paddingHorizontal: sizeValue.paddingHorizontal as number,
|
|
58
|
+
paddingVertical: sizeValue.paddingVertical as number,
|
|
59
|
+
minHeight: sizeValue.minHeight as number,
|
|
60
|
+
borderRadius: sizeValue.borderRadius as number,
|
|
61
|
+
backgroundColor,
|
|
62
|
+
borderColor,
|
|
63
|
+
borderWidth,
|
|
64
|
+
borderStyle: borderWidth > 0 ? ('solid' as const) : undefined,
|
|
65
|
+
opacity: disabled ? 0.5 : 1,
|
|
66
|
+
} as const;
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
label: ({ size = 'md', intent = 'primary', type = 'filled', selected = false }: ChipDynamicProps) => {
|
|
70
|
+
const intentValue = theme.intents[intent];
|
|
71
|
+
const sizeValue = theme.sizes.chip[size];
|
|
72
|
+
|
|
73
|
+
// Compute color based on type and selected state
|
|
74
|
+
let color: string;
|
|
75
|
+
if (type === 'filled') {
|
|
76
|
+
color = selected ? intentValue.primary : intentValue.contrast;
|
|
77
|
+
} else if (type === 'outlined') {
|
|
78
|
+
color = selected ? theme.colors.text.inverse : intentValue.primary;
|
|
79
|
+
} else { // soft
|
|
80
|
+
color = selected ? theme.colors.text.inverse : intentValue.dark;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
fontFamily: 'inherit' as const,
|
|
85
|
+
fontWeight: '500' as const,
|
|
86
|
+
fontSize: sizeValue.fontSize as number,
|
|
87
|
+
lineHeight: sizeValue.lineHeight as number,
|
|
88
|
+
color,
|
|
89
|
+
} as const;
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
icon: ({ size = 'md', intent = 'primary', type = 'filled', selected = false }: ChipDynamicProps) => {
|
|
93
|
+
const intentValue = theme.intents[intent];
|
|
94
|
+
const sizeValue = theme.sizes.chip[size];
|
|
95
|
+
|
|
96
|
+
// Same color logic as label
|
|
97
|
+
let color: string;
|
|
98
|
+
if (type === 'filled') {
|
|
99
|
+
color = selected ? intentValue.primary : intentValue.contrast;
|
|
100
|
+
} else if (type === 'outlined') {
|
|
101
|
+
color = selected ? theme.colors.text.inverse : intentValue.primary;
|
|
102
|
+
} else {
|
|
103
|
+
color = selected ? theme.colors.text.inverse : intentValue.dark;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
display: 'flex' as const,
|
|
108
|
+
alignItems: 'center' as const,
|
|
109
|
+
justifyContent: 'center' as const,
|
|
110
|
+
width: sizeValue.iconSize as number,
|
|
111
|
+
height: sizeValue.iconSize as number,
|
|
112
|
+
color,
|
|
113
|
+
} as const;
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
deleteButton: ({ size = 'md' }: ChipDynamicProps) => {
|
|
117
|
+
const sizeValue = theme.sizes.chip[size];
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
display: 'flex' as const,
|
|
121
|
+
alignItems: 'center' as const,
|
|
122
|
+
justifyContent: 'center' as const,
|
|
123
|
+
padding: 0,
|
|
124
|
+
marginLeft: 4,
|
|
125
|
+
borderRadius: 12,
|
|
126
|
+
width: sizeValue.iconSize as number,
|
|
127
|
+
height: sizeValue.iconSize as number,
|
|
128
|
+
} as const;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
deleteIcon: ({ size = 'md', intent = 'primary', type = 'filled', selected = false }: ChipDynamicProps) => {
|
|
132
|
+
const intentValue = theme.intents[intent];
|
|
133
|
+
const sizeValue = theme.sizes.chip[size];
|
|
134
|
+
|
|
135
|
+
// Same color logic as label/icon
|
|
136
|
+
let color: string;
|
|
137
|
+
if (type === 'filled') {
|
|
138
|
+
color = selected ? intentValue.primary : intentValue.contrast;
|
|
139
|
+
} else if (type === 'outlined') {
|
|
140
|
+
color = selected ? theme.colors.text.inverse : intentValue.primary;
|
|
141
|
+
} else {
|
|
142
|
+
color = selected ? theme.colors.text.inverse : intentValue.dark;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
fontSize: sizeValue.iconSize as number,
|
|
147
|
+
color,
|
|
148
|
+
} as const;
|
|
149
|
+
},
|
|
168
150
|
}));
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { forwardRef, useMemo } from 'react';
|
|
2
|
+
import { ActivityIndicator, StyleSheet as RNStyleSheet, TouchableOpacity, View } from 'react-native';
|
|
3
|
+
import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons';
|
|
4
|
+
import Svg, { Defs, LinearGradient, Stop, Rect } from 'react-native-svg';
|
|
5
|
+
import { iconButtonStyles } from './IconButton.styles';
|
|
6
|
+
import { IconButtonProps } from './types';
|
|
7
|
+
import { getNativeInteractiveAccessibilityProps } from '../utils/accessibility';
|
|
8
|
+
import type { IdealystElement } from '../utils/refTypes';
|
|
9
|
+
|
|
10
|
+
const IconButton = forwardRef<IdealystElement, IconButtonProps>((props, ref) => {
|
|
11
|
+
const {
|
|
12
|
+
icon,
|
|
13
|
+
onPress,
|
|
14
|
+
onClick,
|
|
15
|
+
disabled = false,
|
|
16
|
+
loading = false,
|
|
17
|
+
type = 'contained',
|
|
18
|
+
intent = 'primary',
|
|
19
|
+
size = 'md',
|
|
20
|
+
gradient,
|
|
21
|
+
style,
|
|
22
|
+
testID,
|
|
23
|
+
id,
|
|
24
|
+
// Accessibility props
|
|
25
|
+
accessibilityLabel,
|
|
26
|
+
accessibilityHint,
|
|
27
|
+
accessibilityDisabled,
|
|
28
|
+
accessibilityHidden,
|
|
29
|
+
accessibilityRole,
|
|
30
|
+
accessibilityLabelledBy,
|
|
31
|
+
accessibilityDescribedBy,
|
|
32
|
+
accessibilityControls,
|
|
33
|
+
accessibilityExpanded,
|
|
34
|
+
accessibilityPressed,
|
|
35
|
+
} = props;
|
|
36
|
+
|
|
37
|
+
// Button is effectively disabled when loading
|
|
38
|
+
const isDisabled = disabled || loading;
|
|
39
|
+
|
|
40
|
+
// Determine the handler to use - onPress takes precedence
|
|
41
|
+
const pressHandler = onPress ?? onClick;
|
|
42
|
+
|
|
43
|
+
// Warn about deprecated onClick usage in development
|
|
44
|
+
if (__DEV__ && onClick && !onPress) {
|
|
45
|
+
console.warn(
|
|
46
|
+
'IconButton: onClick prop is deprecated. Use onPress instead for cross-platform compatibility.'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Apply variants for size, disabled, gradient
|
|
51
|
+
iconButtonStyles.useVariants({
|
|
52
|
+
size,
|
|
53
|
+
disabled: isDisabled,
|
|
54
|
+
gradient,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Compute dynamic styles with all props for full flexibility
|
|
58
|
+
const dynamicProps = { intent, type, size, disabled: isDisabled, gradient };
|
|
59
|
+
const buttonStyle = (iconButtonStyles.button as any)(dynamicProps);
|
|
60
|
+
const iconStyle = (iconButtonStyles.icon as any)(dynamicProps);
|
|
61
|
+
const spinnerStyle = (iconButtonStyles.spinner as any)(dynamicProps);
|
|
62
|
+
|
|
63
|
+
// Gradient is only applicable to contained buttons
|
|
64
|
+
const showGradient = gradient && type === 'contained';
|
|
65
|
+
|
|
66
|
+
// Map button size to icon size
|
|
67
|
+
const iconSizeMap: Record<string, number> = {
|
|
68
|
+
xs: 12,
|
|
69
|
+
sm: 14,
|
|
70
|
+
md: 16,
|
|
71
|
+
lg: 18,
|
|
72
|
+
xl: 20,
|
|
73
|
+
};
|
|
74
|
+
const iconSize = iconSizeMap[size] ?? 16;
|
|
75
|
+
|
|
76
|
+
// Generate native accessibility props
|
|
77
|
+
const nativeA11yProps = useMemo(() => {
|
|
78
|
+
const computedLabel = accessibilityLabel ?? (typeof icon === 'string' ? icon : undefined);
|
|
79
|
+
|
|
80
|
+
return getNativeInteractiveAccessibilityProps({
|
|
81
|
+
accessibilityLabel: computedLabel,
|
|
82
|
+
accessibilityHint,
|
|
83
|
+
accessibilityDisabled: accessibilityDisabled ?? isDisabled,
|
|
84
|
+
accessibilityHidden,
|
|
85
|
+
accessibilityRole: accessibilityRole ?? 'button',
|
|
86
|
+
accessibilityLabelledBy,
|
|
87
|
+
accessibilityDescribedBy,
|
|
88
|
+
accessibilityControls,
|
|
89
|
+
accessibilityExpanded,
|
|
90
|
+
accessibilityPressed,
|
|
91
|
+
});
|
|
92
|
+
}, [
|
|
93
|
+
accessibilityLabel,
|
|
94
|
+
icon,
|
|
95
|
+
accessibilityHint,
|
|
96
|
+
accessibilityDisabled,
|
|
97
|
+
isDisabled,
|
|
98
|
+
accessibilityHidden,
|
|
99
|
+
accessibilityRole,
|
|
100
|
+
accessibilityLabelledBy,
|
|
101
|
+
accessibilityDescribedBy,
|
|
102
|
+
accessibilityControls,
|
|
103
|
+
accessibilityExpanded,
|
|
104
|
+
accessibilityPressed,
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
// Render gradient background layer
|
|
108
|
+
const renderGradientLayer = () => {
|
|
109
|
+
if (!showGradient) return null;
|
|
110
|
+
|
|
111
|
+
const [startColor, endColor] = useMemo(() => {
|
|
112
|
+
switch (gradient) {
|
|
113
|
+
case 'darken': return [{
|
|
114
|
+
stopColor: 'black',
|
|
115
|
+
stopOpacity: 0,
|
|
116
|
+
}, {
|
|
117
|
+
stopColor: 'black',
|
|
118
|
+
stopOpacity: 0.15,
|
|
119
|
+
}];
|
|
120
|
+
case 'lighten': return [{
|
|
121
|
+
stopColor: 'white',
|
|
122
|
+
stopOpacity: 0,
|
|
123
|
+
}, {
|
|
124
|
+
stopColor: 'white',
|
|
125
|
+
stopOpacity: 0.2,
|
|
126
|
+
}];
|
|
127
|
+
default: return [{
|
|
128
|
+
stopColor: 'black',
|
|
129
|
+
stopOpacity: 0,
|
|
130
|
+
}, {
|
|
131
|
+
stopColor: 'black',
|
|
132
|
+
stopOpacity: 0,
|
|
133
|
+
}];
|
|
134
|
+
}
|
|
135
|
+
}, [gradient]);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<Svg style={RNStyleSheet.absoluteFill}>
|
|
139
|
+
<Defs>
|
|
140
|
+
<LinearGradient id="iconButtonGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
141
|
+
<Stop offset="0%" {...startColor} />
|
|
142
|
+
<Stop offset="100%" {...endColor} />
|
|
143
|
+
</LinearGradient>
|
|
144
|
+
</Defs>
|
|
145
|
+
<Rect
|
|
146
|
+
width="100%"
|
|
147
|
+
height="100%"
|
|
148
|
+
fill="url(#iconButtonGradient)"
|
|
149
|
+
rx={9999}
|
|
150
|
+
ry={9999}
|
|
151
|
+
/>
|
|
152
|
+
</Svg>
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// TouchableOpacity types don't include nativeID but it's a valid RN prop
|
|
157
|
+
const touchableProps = {
|
|
158
|
+
ref,
|
|
159
|
+
onPress: pressHandler,
|
|
160
|
+
disabled: isDisabled,
|
|
161
|
+
testID,
|
|
162
|
+
nativeID: id,
|
|
163
|
+
activeOpacity: 0.7,
|
|
164
|
+
style: [
|
|
165
|
+
buttonStyle,
|
|
166
|
+
showGradient && { overflow: 'hidden' },
|
|
167
|
+
style,
|
|
168
|
+
],
|
|
169
|
+
accessibilityState: loading ? { busy: true } : undefined,
|
|
170
|
+
...nativeA11yProps,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Get spinner color from the spinner style (matches icon color)
|
|
174
|
+
const spinnerColor = spinnerStyle?.color || (type === 'contained' ? '#fff' : undefined);
|
|
175
|
+
|
|
176
|
+
// Content opacity - hide when loading but keep for sizing
|
|
177
|
+
const contentOpacity = loading ? 0 : 1;
|
|
178
|
+
|
|
179
|
+
// Render icon
|
|
180
|
+
const renderIcon = () => {
|
|
181
|
+
if (typeof icon === 'string') {
|
|
182
|
+
return (
|
|
183
|
+
<MaterialDesignIcons
|
|
184
|
+
name={icon}
|
|
185
|
+
size={iconSize}
|
|
186
|
+
style={[iconStyle, { opacity: contentOpacity }]}
|
|
187
|
+
/>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
// Custom ReactNode icon
|
|
191
|
+
return (
|
|
192
|
+
<View style={{ opacity: contentOpacity }}>
|
|
193
|
+
{icon}
|
|
194
|
+
</View>
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<TouchableOpacity {...touchableProps as any}>
|
|
200
|
+
{renderGradientLayer()}
|
|
201
|
+
{/* Centered spinner overlay */}
|
|
202
|
+
{loading && (
|
|
203
|
+
<View style={RNStyleSheet.absoluteFill}>
|
|
204
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
205
|
+
<ActivityIndicator
|
|
206
|
+
size="small"
|
|
207
|
+
color={spinnerColor}
|
|
208
|
+
/>
|
|
209
|
+
</View>
|
|
210
|
+
</View>
|
|
211
|
+
)}
|
|
212
|
+
{renderIcon()}
|
|
213
|
+
</TouchableOpacity>
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
IconButton.displayName = 'IconButton';
|
|
218
|
+
|
|
219
|
+
export default IconButton;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IconButton styles using defineStyle with $iterator expansion.
|
|
3
|
+
*
|
|
4
|
+
* Dynamic style functions are used for intent/type combinations since
|
|
5
|
+
* the color depends on both values (compound logic).
|
|
6
|
+
*/
|
|
7
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
8
|
+
import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
|
|
9
|
+
import type { Theme as BaseTheme, Intent, Size } from '@idealyst/theme';
|
|
10
|
+
import { IconButtonGradient } from './types';
|
|
11
|
+
|
|
12
|
+
// Required: Unistyles must see StyleSheet usage in original source to process this file
|
|
13
|
+
void StyleSheet;
|
|
14
|
+
|
|
15
|
+
// Wrap theme for $iterator support
|
|
16
|
+
type Theme = ThemeStyleWrapper<BaseTheme>;
|
|
17
|
+
|
|
18
|
+
type IconButtonSize = Size;
|
|
19
|
+
type IconButtonType = 'contained' | 'outlined' | 'text';
|
|
20
|
+
|
|
21
|
+
export type IconButtonVariants = {
|
|
22
|
+
size: IconButtonSize;
|
|
23
|
+
intent: Intent;
|
|
24
|
+
type: IconButtonType;
|
|
25
|
+
disabled: boolean;
|
|
26
|
+
gradient?: IconButtonGradient;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* All dynamic props passed to icon button style functions.
|
|
31
|
+
*/
|
|
32
|
+
export type IconButtonDynamicProps = {
|
|
33
|
+
intent?: Intent;
|
|
34
|
+
type?: IconButtonType;
|
|
35
|
+
size?: Size;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
gradient?: IconButtonGradient;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* IconButton styles with $iterator expansion for size variants.
|
|
42
|
+
* Circular button that only contains an icon.
|
|
43
|
+
*/
|
|
44
|
+
export const iconButtonStyles = defineStyle('IconButton', (theme: Theme) => ({
|
|
45
|
+
button: ({ intent = 'primary', type = 'contained' }: IconButtonDynamicProps) => ({
|
|
46
|
+
boxSizing: 'border-box',
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
justifyContent: 'center',
|
|
49
|
+
borderRadius: 9999, // Fully circular
|
|
50
|
+
// Inline theme accesses so Unistyles can trace them
|
|
51
|
+
backgroundColor: type === 'contained'
|
|
52
|
+
? theme.intents[intent].primary
|
|
53
|
+
: type === 'outlined'
|
|
54
|
+
? theme.colors.surface.primary
|
|
55
|
+
: 'transparent',
|
|
56
|
+
borderColor: type === 'outlined'
|
|
57
|
+
? theme.intents[intent].primary
|
|
58
|
+
: 'transparent',
|
|
59
|
+
borderWidth: type === 'outlined' ? 1 : 0,
|
|
60
|
+
borderStyle: type === 'outlined' ? 'solid' as const : undefined,
|
|
61
|
+
_web: {
|
|
62
|
+
display: 'flex',
|
|
63
|
+
transition: 'all 0.1s ease',
|
|
64
|
+
},
|
|
65
|
+
variants: {
|
|
66
|
+
type: {
|
|
67
|
+
contained: {
|
|
68
|
+
backgroundColor: theme.$intents.primary,
|
|
69
|
+
borderColor: 'transparent',
|
|
70
|
+
},
|
|
71
|
+
outlined: {
|
|
72
|
+
backgroundColor: 'transparent',
|
|
73
|
+
borderColor: theme.$intents.primary,
|
|
74
|
+
},
|
|
75
|
+
text: {
|
|
76
|
+
backgroundColor: 'transparent',
|
|
77
|
+
borderColor: 'transparent',
|
|
78
|
+
borderWidth: 0,
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
// Size variants - circular so width equals height
|
|
82
|
+
size: {
|
|
83
|
+
width: theme.sizes.$iconButton.size,
|
|
84
|
+
height: theme.sizes.$iconButton.size,
|
|
85
|
+
minWidth: theme.sizes.$iconButton.size,
|
|
86
|
+
minHeight: theme.sizes.$iconButton.size,
|
|
87
|
+
},
|
|
88
|
+
disabled: {
|
|
89
|
+
true: { opacity: 0.6 },
|
|
90
|
+
false: { opacity: 1, _web: { cursor: 'pointer', _hover: { opacity: 0.90 }, _active: { opacity: 0.75 } } },
|
|
91
|
+
},
|
|
92
|
+
gradient: {
|
|
93
|
+
darken: { _web: { backgroundImage: 'linear-gradient(135deg, transparent 0%, rgba(0, 0, 0, 0.15) 100%)' } },
|
|
94
|
+
lighten: { _web: { backgroundImage: 'linear-gradient(135deg, transparent 0%, rgba(255, 255, 255, 0.2) 100%)' } },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
icon: ({ intent = 'primary', type = 'contained' }: IconButtonDynamicProps) => ({
|
|
99
|
+
display: 'flex',
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
justifyContent: 'center',
|
|
102
|
+
color: type === 'contained'
|
|
103
|
+
? theme.intents[intent].contrast
|
|
104
|
+
: theme.intents[intent].primary,
|
|
105
|
+
variants: {
|
|
106
|
+
size: {
|
|
107
|
+
width: theme.sizes.$iconButton.iconSize,
|
|
108
|
+
height: theme.sizes.$iconButton.iconSize,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
spinner: ({ intent = 'primary', type = 'contained' }: IconButtonDynamicProps) => ({
|
|
113
|
+
display: 'flex',
|
|
114
|
+
alignItems: 'center',
|
|
115
|
+
justifyContent: 'center',
|
|
116
|
+
// Match the icon color based on button type
|
|
117
|
+
color: type === 'contained'
|
|
118
|
+
? theme.intents[intent].contrast
|
|
119
|
+
: theme.intents[intent].primary,
|
|
120
|
+
variants: {
|
|
121
|
+
size: {
|
|
122
|
+
width: theme.sizes.$iconButton.iconSize,
|
|
123
|
+
height: theme.sizes.$iconButton.iconSize,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
}));
|