@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.
Files changed (87) hide show
  1. package/README.md +99 -0
  2. package/dist/appbar/index.d.ts +71 -0
  3. package/dist/appbar/index.js +952 -0
  4. package/dist/button/index.d.ts +41 -0
  5. package/dist/button/index.js +454 -0
  6. package/dist/card/index.d.ts +31 -0
  7. package/dist/card/index.js +264 -0
  8. package/dist/checkbox/index.d.ts +25 -0
  9. package/dist/checkbox/index.js +291 -0
  10. package/dist/chip/index.d.ts +62 -0
  11. package/dist/chip/index.js +452 -0
  12. package/dist/icon-button/index.d.ts +10 -0
  13. package/dist/icon-button/index.js +575 -0
  14. package/dist/index.d.ts +19 -0
  15. package/dist/index.js +3374 -0
  16. package/dist/layout/index.d.ts +98 -0
  17. package/dist/layout/index.js +282 -0
  18. package/dist/list/index.d.ts +60 -0
  19. package/dist/list/index.js +300 -0
  20. package/dist/radio/index.d.ts +25 -0
  21. package/dist/radio/index.js +250 -0
  22. package/dist/switch/index.d.ts +37 -0
  23. package/dist/switch/index.js +315 -0
  24. package/dist/text-field/index.d.ts +52 -0
  25. package/dist/text-field/index.js +496 -0
  26. package/dist/types-D3hlyvz-.d.ts +51 -0
  27. package/dist/typography/index.d.ts +28 -0
  28. package/dist/typography/index.js +69 -0
  29. package/package.json +166 -0
  30. package/src/appbar/AppBar.tsx +302 -0
  31. package/src/appbar/index.ts +2 -0
  32. package/src/appbar/styles.ts +92 -0
  33. package/src/appbar/types.ts +67 -0
  34. package/src/button/Button.tsx +130 -0
  35. package/src/button/index.ts +2 -0
  36. package/src/button/styles.ts +288 -0
  37. package/src/button/types.ts +42 -0
  38. package/src/card/Card.tsx +69 -0
  39. package/src/card/index.ts +2 -0
  40. package/src/card/styles.ts +151 -0
  41. package/src/card/types.ts +27 -0
  42. package/src/checkbox/Checkbox.tsx +109 -0
  43. package/src/checkbox/index.ts +2 -0
  44. package/src/checkbox/styles.ts +155 -0
  45. package/src/checkbox/types.ts +20 -0
  46. package/src/chip/Chip.tsx +182 -0
  47. package/src/chip/index.ts +2 -0
  48. package/src/chip/styles.ts +240 -0
  49. package/src/chip/types.ts +58 -0
  50. package/src/icon-button/IconButton.tsx +358 -0
  51. package/src/icon-button/index.ts +6 -0
  52. package/src/icon-button/styles.ts +259 -0
  53. package/src/icon-button/types.ts +55 -0
  54. package/src/index.ts +51 -0
  55. package/src/layout/Box.tsx +99 -0
  56. package/src/layout/Column.tsx +16 -0
  57. package/src/layout/Grid.tsx +49 -0
  58. package/src/layout/Layout.tsx +81 -0
  59. package/src/layout/Row.tsx +22 -0
  60. package/src/layout/index.ts +13 -0
  61. package/src/layout/resolveSpacing.ts +11 -0
  62. package/src/layout/types.ts +82 -0
  63. package/src/list/List.tsx +17 -0
  64. package/src/list/ListDivider.tsx +20 -0
  65. package/src/list/ListItem.tsx +128 -0
  66. package/src/list/index.ts +9 -0
  67. package/src/list/styles.ts +132 -0
  68. package/src/list/types.ts +54 -0
  69. package/src/radio/Radio.tsx +103 -0
  70. package/src/radio/index.ts +2 -0
  71. package/src/radio/styles.ts +139 -0
  72. package/src/radio/types.ts +20 -0
  73. package/src/switch/Switch.tsx +118 -0
  74. package/src/switch/index.ts +2 -0
  75. package/src/switch/styles.ts +172 -0
  76. package/src/switch/types.ts +32 -0
  77. package/src/test-utils/render-with-theme.tsx +13 -0
  78. package/src/text-field/TextField.tsx +298 -0
  79. package/src/text-field/index.ts +2 -0
  80. package/src/text-field/styles.ts +240 -0
  81. package/src/text-field/types.ts +49 -0
  82. package/src/typography/Typography.tsx +65 -0
  83. package/src/typography/index.ts +3 -0
  84. package/src/typography/types.ts +17 -0
  85. package/src/utils/color.ts +64 -0
  86. package/src/utils/elevation.ts +33 -0
  87. 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,2 @@
1
+ export { Button } from './Button'
2
+ export type { ButtonProps, ButtonVariant } from './types'
@@ -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,2 @@
1
+ export { Card } from './Card'
2
+ export type { CardProps, CardVariant } from './types'
@@ -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
+ }