@onlynative/components 0.1.0-alpha.0
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/README.md +99 -0
- package/dist/appbar/index.d.ts +71 -0
- package/dist/appbar/index.js +952 -0
- package/dist/button/index.d.ts +41 -0
- package/dist/button/index.js +454 -0
- package/dist/card/index.d.ts +31 -0
- package/dist/card/index.js +264 -0
- package/dist/checkbox/index.d.ts +25 -0
- package/dist/checkbox/index.js +291 -0
- package/dist/chip/index.d.ts +62 -0
- package/dist/chip/index.js +452 -0
- package/dist/icon-button/index.d.ts +10 -0
- package/dist/icon-button/index.js +575 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +3374 -0
- package/dist/layout/index.d.ts +98 -0
- package/dist/layout/index.js +282 -0
- package/dist/list/index.d.ts +60 -0
- package/dist/list/index.js +300 -0
- package/dist/radio/index.d.ts +25 -0
- package/dist/radio/index.js +250 -0
- package/dist/switch/index.d.ts +37 -0
- package/dist/switch/index.js +315 -0
- package/dist/text-field/index.d.ts +52 -0
- package/dist/text-field/index.js +496 -0
- package/dist/types-D3hlyvz-.d.ts +51 -0
- package/dist/typography/index.d.ts +28 -0
- package/dist/typography/index.js +69 -0
- package/package.json +166 -0
- package/src/appbar/AppBar.tsx +302 -0
- package/src/appbar/index.ts +2 -0
- package/src/appbar/styles.ts +92 -0
- package/src/appbar/types.ts +67 -0
- package/src/button/Button.tsx +130 -0
- package/src/button/index.ts +2 -0
- package/src/button/styles.ts +288 -0
- package/src/button/types.ts +42 -0
- package/src/card/Card.tsx +69 -0
- package/src/card/index.ts +2 -0
- package/src/card/styles.ts +151 -0
- package/src/card/types.ts +27 -0
- package/src/checkbox/Checkbox.tsx +109 -0
- package/src/checkbox/index.ts +2 -0
- package/src/checkbox/styles.ts +155 -0
- package/src/checkbox/types.ts +20 -0
- package/src/chip/Chip.tsx +182 -0
- package/src/chip/index.ts +2 -0
- package/src/chip/styles.ts +240 -0
- package/src/chip/types.ts +58 -0
- package/src/icon-button/IconButton.tsx +358 -0
- package/src/icon-button/index.ts +6 -0
- package/src/icon-button/styles.ts +259 -0
- package/src/icon-button/types.ts +55 -0
- package/src/index.ts +51 -0
- package/src/layout/Box.tsx +99 -0
- package/src/layout/Column.tsx +16 -0
- package/src/layout/Grid.tsx +49 -0
- package/src/layout/Layout.tsx +81 -0
- package/src/layout/Row.tsx +22 -0
- package/src/layout/index.ts +13 -0
- package/src/layout/resolveSpacing.ts +11 -0
- package/src/layout/types.ts +82 -0
- package/src/list/List.tsx +17 -0
- package/src/list/ListDivider.tsx +20 -0
- package/src/list/ListItem.tsx +128 -0
- package/src/list/index.ts +9 -0
- package/src/list/styles.ts +132 -0
- package/src/list/types.ts +54 -0
- package/src/radio/Radio.tsx +103 -0
- package/src/radio/index.ts +2 -0
- package/src/radio/styles.ts +139 -0
- package/src/radio/types.ts +20 -0
- package/src/switch/Switch.tsx +118 -0
- package/src/switch/index.ts +2 -0
- package/src/switch/styles.ts +172 -0
- package/src/switch/types.ts +32 -0
- package/src/test-utils/render-with-theme.tsx +13 -0
- package/src/text-field/TextField.tsx +298 -0
- package/src/text-field/index.ts +2 -0
- package/src/text-field/styles.ts +240 -0
- package/src/text-field/types.ts +49 -0
- package/src/typography/Typography.tsx +65 -0
- package/src/typography/index.ts +3 -0
- package/src/typography/types.ts +17 -0
- package/src/utils/color.ts +64 -0
- package/src/utils/elevation.ts +33 -0
- package/src/utils/rtl.ts +19 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'
|
|
2
|
+
import { useMemo } from 'react'
|
|
3
|
+
import { Platform, Pressable } from 'react-native'
|
|
4
|
+
import { StyleSheet } from 'react-native'
|
|
5
|
+
import { Text } from 'react-native'
|
|
6
|
+
import type { StyleProp, ViewStyle } from 'react-native'
|
|
7
|
+
import { useTheme } from '@onlynative/core'
|
|
8
|
+
|
|
9
|
+
import { createStyles } from './styles'
|
|
10
|
+
import type { ButtonProps } from './types'
|
|
11
|
+
|
|
12
|
+
interface PressableState {
|
|
13
|
+
pressed: boolean
|
|
14
|
+
hovered?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveStyle(
|
|
18
|
+
containerStyle: StyleProp<ViewStyle>,
|
|
19
|
+
hoveredContainerStyle: StyleProp<ViewStyle>,
|
|
20
|
+
pressedContainerStyle: StyleProp<ViewStyle>,
|
|
21
|
+
disabledContainerStyle: StyleProp<ViewStyle>,
|
|
22
|
+
disabled: boolean,
|
|
23
|
+
style: ButtonProps['style'],
|
|
24
|
+
): (state: PressableState) => StyleProp<ViewStyle> {
|
|
25
|
+
if (typeof style === 'function') {
|
|
26
|
+
return (state) => [
|
|
27
|
+
containerStyle,
|
|
28
|
+
state.hovered && !state.pressed && !disabled
|
|
29
|
+
? hoveredContainerStyle
|
|
30
|
+
: undefined,
|
|
31
|
+
state.pressed && !disabled ? pressedContainerStyle : undefined,
|
|
32
|
+
disabled ? disabledContainerStyle : undefined,
|
|
33
|
+
style(state),
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (state) => [
|
|
38
|
+
containerStyle,
|
|
39
|
+
state.hovered && !state.pressed && !disabled
|
|
40
|
+
? hoveredContainerStyle
|
|
41
|
+
: undefined,
|
|
42
|
+
state.pressed && !disabled ? pressedContainerStyle : undefined,
|
|
43
|
+
disabled ? disabledContainerStyle : undefined,
|
|
44
|
+
style,
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function Button({
|
|
49
|
+
children,
|
|
50
|
+
style,
|
|
51
|
+
variant = 'filled',
|
|
52
|
+
leadingIcon,
|
|
53
|
+
trailingIcon,
|
|
54
|
+
iconSize = 18,
|
|
55
|
+
containerColor,
|
|
56
|
+
contentColor,
|
|
57
|
+
labelStyle: labelStyleOverride,
|
|
58
|
+
disabled = false,
|
|
59
|
+
...props
|
|
60
|
+
}: ButtonProps) {
|
|
61
|
+
const isDisabled = Boolean(disabled)
|
|
62
|
+
const hasLeading = Boolean(leadingIcon)
|
|
63
|
+
const hasTrailing = Boolean(trailingIcon)
|
|
64
|
+
const theme = useTheme()
|
|
65
|
+
const styles = useMemo(
|
|
66
|
+
() =>
|
|
67
|
+
createStyles(
|
|
68
|
+
theme,
|
|
69
|
+
variant,
|
|
70
|
+
hasLeading,
|
|
71
|
+
hasTrailing,
|
|
72
|
+
containerColor,
|
|
73
|
+
contentColor,
|
|
74
|
+
),
|
|
75
|
+
[theme, variant, hasLeading, hasTrailing, containerColor, contentColor],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const resolvedIconColor = useMemo(() => {
|
|
79
|
+
const base = StyleSheet.flatten([
|
|
80
|
+
styles.label,
|
|
81
|
+
isDisabled ? styles.disabledLabel : undefined,
|
|
82
|
+
])
|
|
83
|
+
return typeof base?.color === 'string' ? base.color : undefined
|
|
84
|
+
}, [styles.label, styles.disabledLabel, isDisabled])
|
|
85
|
+
|
|
86
|
+
const computedLabelStyle = useMemo(
|
|
87
|
+
() => [
|
|
88
|
+
styles.label,
|
|
89
|
+
isDisabled ? styles.disabledLabel : undefined,
|
|
90
|
+
labelStyleOverride,
|
|
91
|
+
],
|
|
92
|
+
[isDisabled, styles.disabledLabel, styles.label, labelStyleOverride],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<Pressable
|
|
97
|
+
{...props}
|
|
98
|
+
accessibilityRole="button"
|
|
99
|
+
accessibilityState={{ disabled: isDisabled }}
|
|
100
|
+
hitSlop={Platform.OS === 'web' ? undefined : 4}
|
|
101
|
+
disabled={isDisabled}
|
|
102
|
+
style={resolveStyle(
|
|
103
|
+
styles.container,
|
|
104
|
+
styles.hoveredContainer,
|
|
105
|
+
styles.pressedContainer,
|
|
106
|
+
styles.disabledContainer,
|
|
107
|
+
isDisabled,
|
|
108
|
+
style,
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
{leadingIcon ? (
|
|
112
|
+
<MaterialCommunityIcons
|
|
113
|
+
name={leadingIcon}
|
|
114
|
+
size={iconSize}
|
|
115
|
+
color={resolvedIconColor}
|
|
116
|
+
style={styles.leadingIcon}
|
|
117
|
+
/>
|
|
118
|
+
) : null}
|
|
119
|
+
<Text style={computedLabelStyle}>{children}</Text>
|
|
120
|
+
{trailingIcon ? (
|
|
121
|
+
<MaterialCommunityIcons
|
|
122
|
+
name={trailingIcon}
|
|
123
|
+
size={iconSize}
|
|
124
|
+
color={resolvedIconColor}
|
|
125
|
+
style={styles.trailingIcon}
|
|
126
|
+
/>
|
|
127
|
+
) : null}
|
|
128
|
+
</Pressable>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native'
|
|
2
|
+
import type { Theme } from '@onlynative/core'
|
|
3
|
+
|
|
4
|
+
import type { ButtonVariant } from './types'
|
|
5
|
+
import { alphaColor, blendColor } from '../utils/color'
|
|
6
|
+
import { elevationStyle } from '../utils/elevation'
|
|
7
|
+
|
|
8
|
+
interface VariantColors {
|
|
9
|
+
backgroundColor: string
|
|
10
|
+
textColor: string
|
|
11
|
+
borderColor: string
|
|
12
|
+
borderWidth: number
|
|
13
|
+
hoveredBackgroundColor: string
|
|
14
|
+
pressedBackgroundColor: string
|
|
15
|
+
disabledBackgroundColor: string
|
|
16
|
+
disabledTextColor: string
|
|
17
|
+
disabledBorderColor: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getVariantColors(theme: Theme, variant: ButtonVariant): VariantColors {
|
|
21
|
+
const disabledContainerColor = alphaColor(theme.colors.onSurface, 0.12)
|
|
22
|
+
const disabledLabelColor = alphaColor(theme.colors.onSurface, 0.38)
|
|
23
|
+
const disabledOutlineColor = alphaColor(theme.colors.onSurface, 0.12)
|
|
24
|
+
|
|
25
|
+
if (variant === 'outlined') {
|
|
26
|
+
return {
|
|
27
|
+
backgroundColor: 'transparent',
|
|
28
|
+
textColor: theme.colors.primary,
|
|
29
|
+
borderColor: theme.colors.outline,
|
|
30
|
+
borderWidth: 1,
|
|
31
|
+
hoveredBackgroundColor: alphaColor(
|
|
32
|
+
theme.colors.primary,
|
|
33
|
+
theme.stateLayer.hoveredOpacity,
|
|
34
|
+
),
|
|
35
|
+
pressedBackgroundColor: alphaColor(
|
|
36
|
+
theme.colors.primary,
|
|
37
|
+
theme.stateLayer.pressedOpacity,
|
|
38
|
+
),
|
|
39
|
+
disabledBackgroundColor: 'transparent',
|
|
40
|
+
disabledTextColor: disabledLabelColor,
|
|
41
|
+
disabledBorderColor: disabledOutlineColor,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (variant === 'text') {
|
|
46
|
+
return {
|
|
47
|
+
backgroundColor: 'transparent',
|
|
48
|
+
textColor: theme.colors.primary,
|
|
49
|
+
borderColor: 'transparent',
|
|
50
|
+
borderWidth: 0,
|
|
51
|
+
hoveredBackgroundColor: alphaColor(
|
|
52
|
+
theme.colors.primary,
|
|
53
|
+
theme.stateLayer.hoveredOpacity,
|
|
54
|
+
),
|
|
55
|
+
pressedBackgroundColor: alphaColor(
|
|
56
|
+
theme.colors.primary,
|
|
57
|
+
theme.stateLayer.pressedOpacity,
|
|
58
|
+
),
|
|
59
|
+
disabledBackgroundColor: 'transparent',
|
|
60
|
+
disabledTextColor: disabledLabelColor,
|
|
61
|
+
disabledBorderColor: 'transparent',
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (variant === 'elevated') {
|
|
66
|
+
return {
|
|
67
|
+
backgroundColor: theme.colors.surfaceContainerLow,
|
|
68
|
+
textColor: theme.colors.primary,
|
|
69
|
+
borderColor: theme.colors.surfaceContainerLow,
|
|
70
|
+
borderWidth: 0,
|
|
71
|
+
hoveredBackgroundColor: blendColor(
|
|
72
|
+
theme.colors.surfaceContainerLow,
|
|
73
|
+
theme.colors.primary,
|
|
74
|
+
theme.stateLayer.hoveredOpacity,
|
|
75
|
+
),
|
|
76
|
+
pressedBackgroundColor: blendColor(
|
|
77
|
+
theme.colors.surfaceContainerLow,
|
|
78
|
+
theme.colors.primary,
|
|
79
|
+
theme.stateLayer.pressedOpacity,
|
|
80
|
+
),
|
|
81
|
+
disabledBackgroundColor: disabledContainerColor,
|
|
82
|
+
disabledTextColor: disabledLabelColor,
|
|
83
|
+
disabledBorderColor: disabledContainerColor,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (variant === 'tonal') {
|
|
88
|
+
return {
|
|
89
|
+
backgroundColor: theme.colors.secondaryContainer,
|
|
90
|
+
textColor: theme.colors.onSecondaryContainer,
|
|
91
|
+
borderColor: theme.colors.secondaryContainer,
|
|
92
|
+
borderWidth: 0,
|
|
93
|
+
hoveredBackgroundColor: blendColor(
|
|
94
|
+
theme.colors.secondaryContainer,
|
|
95
|
+
theme.colors.onSecondaryContainer,
|
|
96
|
+
theme.stateLayer.hoveredOpacity,
|
|
97
|
+
),
|
|
98
|
+
pressedBackgroundColor: blendColor(
|
|
99
|
+
theme.colors.secondaryContainer,
|
|
100
|
+
theme.colors.onSecondaryContainer,
|
|
101
|
+
theme.stateLayer.pressedOpacity,
|
|
102
|
+
),
|
|
103
|
+
disabledBackgroundColor: disabledContainerColor,
|
|
104
|
+
disabledTextColor: disabledLabelColor,
|
|
105
|
+
disabledBorderColor: disabledContainerColor,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// filled (default)
|
|
110
|
+
return {
|
|
111
|
+
backgroundColor: theme.colors.primary,
|
|
112
|
+
textColor: theme.colors.onPrimary,
|
|
113
|
+
borderColor: theme.colors.primary,
|
|
114
|
+
borderWidth: 0,
|
|
115
|
+
hoveredBackgroundColor: blendColor(
|
|
116
|
+
theme.colors.primary,
|
|
117
|
+
theme.colors.onPrimary,
|
|
118
|
+
theme.stateLayer.hoveredOpacity,
|
|
119
|
+
),
|
|
120
|
+
pressedBackgroundColor: blendColor(
|
|
121
|
+
theme.colors.primary,
|
|
122
|
+
theme.colors.onPrimary,
|
|
123
|
+
theme.stateLayer.pressedOpacity,
|
|
124
|
+
),
|
|
125
|
+
disabledBackgroundColor: disabledContainerColor,
|
|
126
|
+
disabledTextColor: disabledLabelColor,
|
|
127
|
+
disabledBorderColor: disabledContainerColor,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getHorizontalPadding(
|
|
132
|
+
theme: Theme,
|
|
133
|
+
variant: ButtonVariant,
|
|
134
|
+
hasLeadingIcon: boolean,
|
|
135
|
+
hasTrailingIcon: boolean,
|
|
136
|
+
): { paddingStart: number; paddingEnd: number } {
|
|
137
|
+
if (variant === 'text') {
|
|
138
|
+
// M3: text button uses 12dp base, opposite side of icon gets 16dp
|
|
139
|
+
return {
|
|
140
|
+
paddingStart: hasLeadingIcon
|
|
141
|
+
? 12
|
|
142
|
+
: hasTrailingIcon
|
|
143
|
+
? theme.spacing.md
|
|
144
|
+
: 12,
|
|
145
|
+
paddingEnd: hasTrailingIcon ? 12 : hasLeadingIcon ? theme.spacing.md : 12,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// M3: filled/elevated/tonal/outlined use 24dp base, icon side gets 16dp
|
|
150
|
+
return {
|
|
151
|
+
paddingStart: hasLeadingIcon ? theme.spacing.md : theme.spacing.lg,
|
|
152
|
+
paddingEnd: hasTrailingIcon ? theme.spacing.md : theme.spacing.lg,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function applyColorOverrides(
|
|
157
|
+
theme: Theme,
|
|
158
|
+
colors: VariantColors,
|
|
159
|
+
containerColor?: string,
|
|
160
|
+
contentColor?: string,
|
|
161
|
+
): VariantColors {
|
|
162
|
+
if (!containerColor && !contentColor) return colors
|
|
163
|
+
|
|
164
|
+
const result = { ...colors }
|
|
165
|
+
|
|
166
|
+
if (contentColor) {
|
|
167
|
+
result.textColor = contentColor
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (containerColor) {
|
|
171
|
+
const overlay = contentColor ?? colors.textColor
|
|
172
|
+
result.backgroundColor = containerColor
|
|
173
|
+
result.borderColor = containerColor
|
|
174
|
+
result.hoveredBackgroundColor = blendColor(
|
|
175
|
+
containerColor,
|
|
176
|
+
overlay,
|
|
177
|
+
theme.stateLayer.hoveredOpacity,
|
|
178
|
+
)
|
|
179
|
+
result.pressedBackgroundColor = blendColor(
|
|
180
|
+
containerColor,
|
|
181
|
+
overlay,
|
|
182
|
+
theme.stateLayer.pressedOpacity,
|
|
183
|
+
)
|
|
184
|
+
} else if (contentColor) {
|
|
185
|
+
if (colors.backgroundColor === 'transparent') {
|
|
186
|
+
result.hoveredBackgroundColor = alphaColor(
|
|
187
|
+
contentColor,
|
|
188
|
+
theme.stateLayer.hoveredOpacity,
|
|
189
|
+
)
|
|
190
|
+
result.pressedBackgroundColor = alphaColor(
|
|
191
|
+
contentColor,
|
|
192
|
+
theme.stateLayer.pressedOpacity,
|
|
193
|
+
)
|
|
194
|
+
} else {
|
|
195
|
+
result.hoveredBackgroundColor = blendColor(
|
|
196
|
+
colors.backgroundColor,
|
|
197
|
+
contentColor,
|
|
198
|
+
theme.stateLayer.hoveredOpacity,
|
|
199
|
+
)
|
|
200
|
+
result.pressedBackgroundColor = blendColor(
|
|
201
|
+
colors.backgroundColor,
|
|
202
|
+
contentColor,
|
|
203
|
+
theme.stateLayer.pressedOpacity,
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return result
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function createStyles(
|
|
212
|
+
theme: Theme,
|
|
213
|
+
variant: ButtonVariant,
|
|
214
|
+
hasLeadingIcon: boolean,
|
|
215
|
+
hasTrailingIcon: boolean,
|
|
216
|
+
containerColor?: string,
|
|
217
|
+
contentColor?: string,
|
|
218
|
+
) {
|
|
219
|
+
const baseColors = getVariantColors(theme, variant)
|
|
220
|
+
const colors = applyColorOverrides(
|
|
221
|
+
theme,
|
|
222
|
+
baseColors,
|
|
223
|
+
containerColor,
|
|
224
|
+
contentColor,
|
|
225
|
+
)
|
|
226
|
+
const labelStyle = theme.typography.labelLarge
|
|
227
|
+
const padding = getHorizontalPadding(
|
|
228
|
+
theme,
|
|
229
|
+
variant,
|
|
230
|
+
hasLeadingIcon,
|
|
231
|
+
hasTrailingIcon,
|
|
232
|
+
)
|
|
233
|
+
const elevationLevel0 = elevationStyle(theme.elevation.level0)
|
|
234
|
+
const elevationLevel1 = elevationStyle(theme.elevation.level1)
|
|
235
|
+
const elevationLevel2 = elevationStyle(theme.elevation.level2)
|
|
236
|
+
const baseElevation =
|
|
237
|
+
variant === 'elevated' ? elevationLevel1 : elevationLevel0
|
|
238
|
+
|
|
239
|
+
return StyleSheet.create({
|
|
240
|
+
container: {
|
|
241
|
+
alignSelf: 'flex-start',
|
|
242
|
+
alignItems: 'center',
|
|
243
|
+
flexDirection: 'row',
|
|
244
|
+
justifyContent: 'center',
|
|
245
|
+
minWidth: 58,
|
|
246
|
+
minHeight: 40,
|
|
247
|
+
paddingStart: padding.paddingStart,
|
|
248
|
+
paddingEnd: padding.paddingEnd,
|
|
249
|
+
paddingVertical: 10,
|
|
250
|
+
borderRadius: theme.shape.cornerFull,
|
|
251
|
+
backgroundColor: colors.backgroundColor,
|
|
252
|
+
borderColor: colors.borderColor,
|
|
253
|
+
borderWidth: colors.borderWidth,
|
|
254
|
+
cursor: 'pointer',
|
|
255
|
+
...baseElevation,
|
|
256
|
+
},
|
|
257
|
+
hoveredContainer: {
|
|
258
|
+
backgroundColor: colors.hoveredBackgroundColor,
|
|
259
|
+
...(variant === 'elevated' ? elevationLevel2 : undefined),
|
|
260
|
+
},
|
|
261
|
+
pressedContainer: {
|
|
262
|
+
backgroundColor: colors.pressedBackgroundColor,
|
|
263
|
+
},
|
|
264
|
+
disabledContainer: {
|
|
265
|
+
backgroundColor: colors.disabledBackgroundColor,
|
|
266
|
+
borderColor: colors.disabledBorderColor,
|
|
267
|
+
cursor: 'auto',
|
|
268
|
+
...elevationLevel0,
|
|
269
|
+
},
|
|
270
|
+
label: {
|
|
271
|
+
fontFamily: labelStyle.fontFamily,
|
|
272
|
+
fontSize: labelStyle.fontSize,
|
|
273
|
+
lineHeight: labelStyle.lineHeight,
|
|
274
|
+
fontWeight: labelStyle.fontWeight,
|
|
275
|
+
letterSpacing: labelStyle.letterSpacing,
|
|
276
|
+
color: colors.textColor,
|
|
277
|
+
},
|
|
278
|
+
leadingIcon: {
|
|
279
|
+
marginEnd: theme.spacing.sm,
|
|
280
|
+
},
|
|
281
|
+
trailingIcon: {
|
|
282
|
+
marginStart: theme.spacing.sm,
|
|
283
|
+
},
|
|
284
|
+
disabledLabel: {
|
|
285
|
+
color: colors.disabledTextColor,
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'
|
|
2
|
+
import type { ComponentProps } from 'react'
|
|
3
|
+
import type { PressableProps, StyleProp, TextStyle } from 'react-native'
|
|
4
|
+
|
|
5
|
+
/** Visual style variant of the button following Material Design 3 roles. */
|
|
6
|
+
export type ButtonVariant =
|
|
7
|
+
| 'filled'
|
|
8
|
+
| 'elevated'
|
|
9
|
+
| 'outlined'
|
|
10
|
+
| 'text'
|
|
11
|
+
| 'tonal'
|
|
12
|
+
|
|
13
|
+
export interface ButtonProps extends Omit<PressableProps, 'children'> {
|
|
14
|
+
/** Text label rendered inside the button. */
|
|
15
|
+
children: string
|
|
16
|
+
/**
|
|
17
|
+
* Visual variant. Controls background, border, and text color.
|
|
18
|
+
* @default 'filled'
|
|
19
|
+
*/
|
|
20
|
+
variant?: ButtonVariant
|
|
21
|
+
/** Name of a MaterialCommunityIcons icon to show before the label. */
|
|
22
|
+
leadingIcon?: ComponentProps<typeof MaterialCommunityIcons>['name']
|
|
23
|
+
/** Name of a MaterialCommunityIcons icon to show after the label. */
|
|
24
|
+
trailingIcon?: ComponentProps<typeof MaterialCommunityIcons>['name']
|
|
25
|
+
/**
|
|
26
|
+
* Size of leading and trailing icons in dp.
|
|
27
|
+
* @default 18
|
|
28
|
+
*/
|
|
29
|
+
iconSize?: number
|
|
30
|
+
/**
|
|
31
|
+
* Override the container (background) color.
|
|
32
|
+
* State-layer colors (hover, press) are derived automatically.
|
|
33
|
+
*/
|
|
34
|
+
containerColor?: string
|
|
35
|
+
/**
|
|
36
|
+
* Override the content (label and icon) color.
|
|
37
|
+
* State-layer colors are derived automatically when no containerColor is set.
|
|
38
|
+
*/
|
|
39
|
+
contentColor?: string
|
|
40
|
+
/** Additional style applied to the label text. */
|
|
41
|
+
labelStyle?: StyleProp<TextStyle>
|
|
42
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { Platform, Pressable, View } from 'react-native'
|
|
3
|
+
import type { StyleProp, ViewStyle } from 'react-native'
|
|
4
|
+
import { useTheme } from '@onlynative/core'
|
|
5
|
+
|
|
6
|
+
import { createStyles } from './styles'
|
|
7
|
+
import type { CardProps } from './types'
|
|
8
|
+
|
|
9
|
+
interface PressableState {
|
|
10
|
+
pressed: boolean
|
|
11
|
+
hovered?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Card({
|
|
15
|
+
children,
|
|
16
|
+
style,
|
|
17
|
+
variant = 'elevated',
|
|
18
|
+
onPress,
|
|
19
|
+
disabled = false,
|
|
20
|
+
containerColor,
|
|
21
|
+
...props
|
|
22
|
+
}: CardProps) {
|
|
23
|
+
const isDisabled = Boolean(disabled)
|
|
24
|
+
const isInteractive = onPress !== undefined
|
|
25
|
+
const theme = useTheme()
|
|
26
|
+
const styles = useMemo(
|
|
27
|
+
() => createStyles(theme, variant, containerColor),
|
|
28
|
+
[theme, variant, containerColor],
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if (!isInteractive) {
|
|
32
|
+
return (
|
|
33
|
+
<View {...props} style={[styles.container, style]}>
|
|
34
|
+
{children}
|
|
35
|
+
</View>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const resolvedStyle = (state: PressableState): StyleProp<ViewStyle> => [
|
|
40
|
+
styles.container,
|
|
41
|
+
styles.interactiveContainer,
|
|
42
|
+
state.hovered && !state.pressed && !isDisabled
|
|
43
|
+
? styles.hoveredContainer
|
|
44
|
+
: undefined,
|
|
45
|
+
state.pressed && !isDisabled ? styles.pressedContainer : undefined,
|
|
46
|
+
isDisabled ? styles.disabledContainer : undefined,
|
|
47
|
+
typeof style === 'function'
|
|
48
|
+
? (style as (state: PressableState) => ViewStyle)(state)
|
|
49
|
+
: style,
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Pressable
|
|
54
|
+
{...props}
|
|
55
|
+
role="button"
|
|
56
|
+
accessibilityState={{ disabled: isDisabled }}
|
|
57
|
+
hitSlop={Platform.OS === 'web' ? undefined : 4}
|
|
58
|
+
disabled={isDisabled}
|
|
59
|
+
onPress={onPress}
|
|
60
|
+
style={resolvedStyle}
|
|
61
|
+
>
|
|
62
|
+
{isDisabled ? (
|
|
63
|
+
<View style={styles.disabledContent}>{children}</View>
|
|
64
|
+
) : (
|
|
65
|
+
children
|
|
66
|
+
)}
|
|
67
|
+
</Pressable>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native'
|
|
2
|
+
import type { Theme } from '@onlynative/core'
|
|
3
|
+
|
|
4
|
+
import type { CardVariant } from './types'
|
|
5
|
+
import { alphaColor, blendColor } from '../utils/color'
|
|
6
|
+
import { elevationStyle } from '../utils/elevation'
|
|
7
|
+
|
|
8
|
+
interface VariantColors {
|
|
9
|
+
backgroundColor: string
|
|
10
|
+
borderColor: string
|
|
11
|
+
borderWidth: number
|
|
12
|
+
hoveredBackgroundColor: string
|
|
13
|
+
pressedBackgroundColor: string
|
|
14
|
+
disabledBackgroundColor: string
|
|
15
|
+
disabledBorderColor: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getVariantColors(theme: Theme, variant: CardVariant): VariantColors {
|
|
19
|
+
const disabledContainerColor = alphaColor(theme.colors.onSurface, 0.12)
|
|
20
|
+
const disabledOutlineColor = alphaColor(theme.colors.onSurface, 0.12)
|
|
21
|
+
|
|
22
|
+
if (variant === 'outlined') {
|
|
23
|
+
return {
|
|
24
|
+
backgroundColor: theme.colors.surface,
|
|
25
|
+
borderColor: theme.colors.outline,
|
|
26
|
+
borderWidth: 1,
|
|
27
|
+
hoveredBackgroundColor: alphaColor(
|
|
28
|
+
theme.colors.onSurface,
|
|
29
|
+
theme.stateLayer.hoveredOpacity,
|
|
30
|
+
),
|
|
31
|
+
pressedBackgroundColor: alphaColor(
|
|
32
|
+
theme.colors.onSurface,
|
|
33
|
+
theme.stateLayer.pressedOpacity,
|
|
34
|
+
),
|
|
35
|
+
disabledBackgroundColor: theme.colors.surface,
|
|
36
|
+
disabledBorderColor: disabledOutlineColor,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (variant === 'filled') {
|
|
41
|
+
return {
|
|
42
|
+
backgroundColor: theme.colors.surfaceContainerHighest,
|
|
43
|
+
borderColor: 'transparent',
|
|
44
|
+
borderWidth: 0,
|
|
45
|
+
hoveredBackgroundColor: blendColor(
|
|
46
|
+
theme.colors.surfaceContainerHighest,
|
|
47
|
+
theme.colors.onSurface,
|
|
48
|
+
theme.stateLayer.hoveredOpacity,
|
|
49
|
+
),
|
|
50
|
+
pressedBackgroundColor: blendColor(
|
|
51
|
+
theme.colors.surfaceContainerHighest,
|
|
52
|
+
theme.colors.onSurface,
|
|
53
|
+
theme.stateLayer.pressedOpacity,
|
|
54
|
+
),
|
|
55
|
+
disabledBackgroundColor: disabledContainerColor,
|
|
56
|
+
disabledBorderColor: 'transparent',
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// elevated (default)
|
|
61
|
+
return {
|
|
62
|
+
backgroundColor: theme.colors.surface,
|
|
63
|
+
borderColor: 'transparent',
|
|
64
|
+
borderWidth: 0,
|
|
65
|
+
hoveredBackgroundColor: blendColor(
|
|
66
|
+
theme.colors.surface,
|
|
67
|
+
theme.colors.onSurface,
|
|
68
|
+
theme.stateLayer.hoveredOpacity,
|
|
69
|
+
),
|
|
70
|
+
pressedBackgroundColor: blendColor(
|
|
71
|
+
theme.colors.surface,
|
|
72
|
+
theme.colors.onSurface,
|
|
73
|
+
theme.stateLayer.pressedOpacity,
|
|
74
|
+
),
|
|
75
|
+
disabledBackgroundColor: disabledContainerColor,
|
|
76
|
+
disabledBorderColor: 'transparent',
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function applyColorOverrides(
|
|
81
|
+
theme: Theme,
|
|
82
|
+
colors: VariantColors,
|
|
83
|
+
containerColor?: string,
|
|
84
|
+
): VariantColors {
|
|
85
|
+
if (!containerColor) return colors
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
...colors,
|
|
89
|
+
backgroundColor: containerColor,
|
|
90
|
+
borderColor: containerColor,
|
|
91
|
+
borderWidth: 0,
|
|
92
|
+
hoveredBackgroundColor: blendColor(
|
|
93
|
+
containerColor,
|
|
94
|
+
theme.colors.onSurface,
|
|
95
|
+
theme.stateLayer.hoveredOpacity,
|
|
96
|
+
),
|
|
97
|
+
pressedBackgroundColor: blendColor(
|
|
98
|
+
containerColor,
|
|
99
|
+
theme.colors.onSurface,
|
|
100
|
+
theme.stateLayer.pressedOpacity,
|
|
101
|
+
),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createStyles(
|
|
106
|
+
theme: Theme,
|
|
107
|
+
variant: CardVariant,
|
|
108
|
+
containerColor?: string,
|
|
109
|
+
) {
|
|
110
|
+
const baseColors = getVariantColors(theme, variant)
|
|
111
|
+
const colors = applyColorOverrides(theme, baseColors, containerColor)
|
|
112
|
+
const elevationLevel0 = elevationStyle(theme.elevation.level0)
|
|
113
|
+
const elevationLevel1 = elevationStyle(theme.elevation.level1)
|
|
114
|
+
const elevationLevel2 = elevationStyle(theme.elevation.level2)
|
|
115
|
+
const baseElevation =
|
|
116
|
+
variant === 'elevated' ? elevationLevel1 : elevationLevel0
|
|
117
|
+
|
|
118
|
+
return StyleSheet.create({
|
|
119
|
+
container: {
|
|
120
|
+
borderRadius: theme.shape.cornerMedium,
|
|
121
|
+
backgroundColor: colors.backgroundColor,
|
|
122
|
+
borderColor: colors.borderColor,
|
|
123
|
+
borderWidth: colors.borderWidth,
|
|
124
|
+
overflow: 'hidden',
|
|
125
|
+
...baseElevation,
|
|
126
|
+
},
|
|
127
|
+
interactiveContainer: {
|
|
128
|
+
cursor: 'pointer',
|
|
129
|
+
},
|
|
130
|
+
hoveredContainer: {
|
|
131
|
+
backgroundColor: colors.hoveredBackgroundColor,
|
|
132
|
+
...(variant === 'elevated'
|
|
133
|
+
? elevationLevel2
|
|
134
|
+
: variant === 'filled'
|
|
135
|
+
? elevationLevel1
|
|
136
|
+
: undefined),
|
|
137
|
+
},
|
|
138
|
+
pressedContainer: {
|
|
139
|
+
backgroundColor: colors.pressedBackgroundColor,
|
|
140
|
+
},
|
|
141
|
+
disabledContainer: {
|
|
142
|
+
backgroundColor: colors.disabledBackgroundColor,
|
|
143
|
+
borderColor: colors.disabledBorderColor,
|
|
144
|
+
cursor: 'auto',
|
|
145
|
+
...elevationLevel0,
|
|
146
|
+
},
|
|
147
|
+
disabledContent: {
|
|
148
|
+
opacity: theme.stateLayer.disabledOpacity,
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
}
|