@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,240 @@
1
+ import { StyleSheet } from 'react-native'
2
+ import type { Theme } from '@onlynative/core'
3
+
4
+ import type { TextFieldVariant } from './types'
5
+ import { alphaColor } from '../utils/color'
6
+ import { transformOrigin } from '../utils/rtl'
7
+
8
+ const CONTAINER_HEIGHT = 56
9
+ const ICON_SIZE = 24
10
+ const LABEL_FLOATED_LINE_HEIGHT = 16
11
+
12
+ // Filled: label floated 8dp from top, input at 24dp (8 + 16 label height), 8dp bottom.
13
+ // Label resting = vertically centered = (56 - 24) / 2 = 16dp from top.
14
+ const FILLED_LABEL_RESTING_TOP = 16
15
+ const FILLED_LABEL_FLOATED_TOP = 8
16
+ const FILLED_INPUT_TOP = 24 // 8dp label top + 16dp label line-height
17
+ const FILLED_INPUT_BOTTOM = 8
18
+
19
+ // Outlined: input centered 16dp top/bottom. Label resting = same 16dp.
20
+ // Label floated = centered on the border = -(lineHeight / 2).
21
+ const OUTLINED_INPUT_VERTICAL = 16
22
+ const OUTLINED_LABEL_RESTING_TOP = 16
23
+ const OUTLINED_LABEL_FLOATED_TOP = -(LABEL_FLOATED_LINE_HEIGHT / 2) // -8
24
+
25
+ export const labelPositions = {
26
+ filledRestingTop: FILLED_LABEL_RESTING_TOP,
27
+ filledFloatedTop: FILLED_LABEL_FLOATED_TOP,
28
+ outlinedRestingTop: OUTLINED_LABEL_RESTING_TOP,
29
+ outlinedFloatedTop: OUTLINED_LABEL_FLOATED_TOP,
30
+ } as const
31
+
32
+ interface VariantColors {
33
+ backgroundColor: string
34
+ borderColor: string
35
+ focusedBorderColor: string
36
+ errorBorderColor: string
37
+ disabledBorderColor: string
38
+ disabledBackgroundColor: string
39
+ labelColor: string
40
+ focusedLabelColor: string
41
+ errorLabelColor: string
42
+ disabledLabelColor: string
43
+ textColor: string
44
+ disabledTextColor: string
45
+ placeholderColor: string
46
+ supportingTextColor: string
47
+ errorSupportingTextColor: string
48
+ iconColor: string
49
+ errorIconColor: string
50
+ disabledIconColor: string
51
+ }
52
+
53
+ function getVariantColors(
54
+ theme: Theme,
55
+ variant: TextFieldVariant,
56
+ ): VariantColors {
57
+ const disabledOpacity = theme.stateLayer.disabledOpacity
58
+
59
+ const common = {
60
+ focusedBorderColor: theme.colors.primary,
61
+ errorBorderColor: theme.colors.error,
62
+ focusedLabelColor: theme.colors.primary,
63
+ errorLabelColor: theme.colors.error,
64
+ textColor: theme.colors.onSurface,
65
+ disabledTextColor: alphaColor(theme.colors.onSurface, disabledOpacity),
66
+ disabledLabelColor: alphaColor(theme.colors.onSurface, disabledOpacity),
67
+ disabledBorderColor: alphaColor(theme.colors.onSurface, 0.12),
68
+ placeholderColor: theme.colors.onSurfaceVariant,
69
+ supportingTextColor: theme.colors.onSurfaceVariant,
70
+ errorSupportingTextColor: theme.colors.error,
71
+ iconColor: theme.colors.onSurfaceVariant,
72
+ errorIconColor: theme.colors.error,
73
+ disabledIconColor: alphaColor(theme.colors.onSurface, disabledOpacity),
74
+ }
75
+
76
+ if (variant === 'outlined') {
77
+ return {
78
+ ...common,
79
+ backgroundColor: 'transparent',
80
+ borderColor: theme.colors.outline,
81
+ disabledBackgroundColor: 'transparent',
82
+ labelColor: theme.colors.onSurfaceVariant,
83
+ }
84
+ }
85
+
86
+ return {
87
+ ...common,
88
+ backgroundColor: theme.colors.surfaceContainerHighest,
89
+ borderColor: theme.colors.onSurfaceVariant,
90
+ disabledBackgroundColor: alphaColor(theme.colors.onSurface, 0.04),
91
+ labelColor: theme.colors.onSurfaceVariant,
92
+ }
93
+ }
94
+
95
+ export function createStyles(theme: Theme, variant: TextFieldVariant) {
96
+ const colors = getVariantColors(theme, variant)
97
+ const bodyLarge = theme.typography.bodyLarge
98
+ const bodySmall = theme.typography.bodySmall
99
+ const isFilled = variant === 'filled'
100
+
101
+ return {
102
+ colors,
103
+ styles: StyleSheet.create({
104
+ root: {
105
+ alignSelf: 'stretch',
106
+ },
107
+ container: {
108
+ minHeight: CONTAINER_HEIGHT,
109
+ flexDirection: 'row',
110
+ alignItems: 'stretch',
111
+ backgroundColor: colors.backgroundColor,
112
+ paddingHorizontal: theme.spacing.md,
113
+ ...(isFilled
114
+ ? {
115
+ borderTopStartRadius: theme.shape.cornerExtraSmall,
116
+ borderTopEndRadius: theme.shape.cornerExtraSmall,
117
+ }
118
+ : {
119
+ borderRadius: theme.shape.cornerExtraSmall,
120
+ borderWidth: 1,
121
+ borderColor: colors.borderColor,
122
+ }),
123
+ },
124
+ containerFocused: isFilled
125
+ ? {}
126
+ : {
127
+ borderWidth: 2,
128
+ borderColor: colors.focusedBorderColor,
129
+ paddingHorizontal: theme.spacing.md - 1,
130
+ },
131
+ containerError: isFilled
132
+ ? {}
133
+ : {
134
+ borderWidth: 2,
135
+ borderColor: colors.errorBorderColor,
136
+ paddingHorizontal: theme.spacing.md - 1,
137
+ },
138
+ containerDisabled: isFilled
139
+ ? { backgroundColor: colors.disabledBackgroundColor }
140
+ : {
141
+ borderColor: colors.disabledBorderColor,
142
+ },
143
+ indicator: {
144
+ position: 'absolute',
145
+ start: 0,
146
+ end: 0,
147
+ bottom: 0,
148
+ height: 1,
149
+ backgroundColor: colors.borderColor,
150
+ },
151
+ indicatorFocused: {
152
+ height: 2,
153
+ backgroundColor: colors.focusedBorderColor,
154
+ },
155
+ indicatorError: {
156
+ height: 2,
157
+ backgroundColor: colors.errorBorderColor,
158
+ },
159
+ indicatorDisabled: {
160
+ backgroundColor: colors.disabledBorderColor,
161
+ },
162
+ inputWrapper: {
163
+ flex: 1,
164
+ justifyContent: 'center',
165
+ },
166
+ // When label is present, use explicit padding so the input position
167
+ // matches the label resting top exactly.
168
+ inputWrapperWithLabel: {
169
+ justifyContent: 'flex-start',
170
+ paddingTop: isFilled ? FILLED_INPUT_TOP : OUTLINED_INPUT_VERTICAL,
171
+ paddingBottom: isFilled ? FILLED_INPUT_BOTTOM : OUTLINED_INPUT_VERTICAL,
172
+ },
173
+ label: {
174
+ position: 'absolute',
175
+ zIndex: 1,
176
+ fontFamily: bodySmall.fontFamily,
177
+ fontSize: bodySmall.fontSize,
178
+ lineHeight: bodySmall.lineHeight,
179
+ fontWeight: bodySmall.fontWeight,
180
+ letterSpacing: bodySmall.letterSpacing,
181
+ color: colors.labelColor,
182
+ transformOrigin: transformOrigin('top'),
183
+ },
184
+ labelNotch: {
185
+ paddingHorizontal: 4,
186
+ },
187
+ input: {
188
+ fontFamily: bodyLarge.fontFamily,
189
+ fontSize: bodyLarge.fontSize,
190
+ lineHeight: bodyLarge.lineHeight,
191
+ fontWeight: bodyLarge.fontWeight,
192
+ letterSpacing: bodyLarge.letterSpacing,
193
+ color: colors.textColor,
194
+ paddingVertical: 0,
195
+ paddingHorizontal: 0,
196
+ margin: 0,
197
+ includeFontPadding: false,
198
+ },
199
+ inputDisabled: {
200
+ color: colors.disabledTextColor,
201
+ },
202
+ leadingIcon: {
203
+ alignSelf: 'center',
204
+ marginStart: -4, // 16dp container padding → 12dp icon inset per M3
205
+ marginEnd: theme.spacing.md,
206
+ width: ICON_SIZE,
207
+ height: ICON_SIZE,
208
+ alignItems: 'center',
209
+ justifyContent: 'center',
210
+ },
211
+ trailingIcon: {
212
+ alignSelf: 'center',
213
+ marginStart: theme.spacing.md,
214
+ marginEnd: -4, // 16dp container padding → 12dp icon inset per M3
215
+ width: ICON_SIZE,
216
+ height: ICON_SIZE,
217
+ alignItems: 'center',
218
+ justifyContent: 'center',
219
+ },
220
+ trailingIconPressable: {
221
+ alignSelf: 'center',
222
+ },
223
+ supportingTextRow: {
224
+ paddingHorizontal: theme.spacing.md,
225
+ paddingTop: theme.spacing.xs,
226
+ },
227
+ supportingText: {
228
+ fontFamily: bodySmall.fontFamily,
229
+ fontSize: bodySmall.fontSize,
230
+ lineHeight: bodySmall.lineHeight,
231
+ fontWeight: bodySmall.fontWeight,
232
+ letterSpacing: bodySmall.letterSpacing,
233
+ color: colors.supportingTextColor,
234
+ },
235
+ errorSupportingText: {
236
+ color: colors.errorSupportingTextColor,
237
+ },
238
+ }),
239
+ }
240
+ }
@@ -0,0 +1,49 @@
1
+ import type MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'
2
+ import type { ComponentProps } from 'react'
3
+ import type { StyleProp, TextInputProps, TextStyle } from 'react-native'
4
+
5
+ /** Visual container style for the text field. */
6
+ export type TextFieldVariant = 'filled' | 'outlined'
7
+
8
+ export interface TextFieldProps
9
+ extends Omit<TextInputProps, 'placeholderTextColor' | 'editable'> {
10
+ /** Floating label text. Animates above the input when focused or filled. */
11
+ label?: string
12
+ /**
13
+ * Container style.
14
+ * @default 'filled'
15
+ */
16
+ variant?: TextFieldVariant
17
+ /** Helper text displayed below the field. Hidden when `error` or `errorText` is active. */
18
+ supportingText?: string
19
+ /** Error message. When provided, implicitly sets `error` to `true` and replaces `supportingText`. */
20
+ errorText?: string
21
+ /**
22
+ * When `true`, renders the field in error state with error colors.
23
+ * @default false
24
+ */
25
+ error?: boolean
26
+ /**
27
+ * Disables text input and reduces opacity.
28
+ * @default false
29
+ */
30
+ disabled?: boolean
31
+ /** MaterialCommunityIcons icon name rendered at the start of the field. */
32
+ leadingIcon?: ComponentProps<typeof MaterialCommunityIcons>['name']
33
+ /** MaterialCommunityIcons icon name rendered at the end of the field. */
34
+ trailingIcon?: ComponentProps<typeof MaterialCommunityIcons>['name']
35
+ /** Called when the trailing icon is pressed. */
36
+ onTrailingIconPress?: () => void
37
+ /**
38
+ * Override the container (background) color.
39
+ * Disabled state still uses the standard disabled appearance.
40
+ */
41
+ containerColor?: string
42
+ /**
43
+ * Override the content (input text and icon) color.
44
+ * Error and disabled states take precedence.
45
+ */
46
+ contentColor?: string
47
+ /** Additional style applied to the text input element. */
48
+ inputStyle?: StyleProp<TextStyle>
49
+ }
@@ -0,0 +1,65 @@
1
+ import { useMemo } from 'react'
2
+ import type { ComponentType, ReactNode } from 'react'
3
+ import type { StyleProp, TextProps, TextStyle } from 'react-native'
4
+ import { Text } from 'react-native'
5
+ import { useTheme } from '@onlynative/core'
6
+ import type { Theme } from '@onlynative/core'
7
+
8
+ import type { TypographyVariant } from './types'
9
+
10
+ const HEADING_VARIANTS: ReadonlySet<TypographyVariant> = new Set([
11
+ 'displayLarge',
12
+ 'displayMedium',
13
+ 'displaySmall',
14
+ 'headlineLarge',
15
+ 'headlineMedium',
16
+ 'headlineSmall',
17
+ ])
18
+
19
+ export interface TypographyProps extends Omit<TextProps, 'children' | 'style'> {
20
+ /** Content to display. Accepts strings, numbers, or nested elements. */
21
+ children: ReactNode
22
+ /**
23
+ * MD3 type scale role. Controls font size, weight, line height, and letter spacing.
24
+ * @default 'bodyMedium'
25
+ */
26
+ variant?: TypographyVariant
27
+ /** Override the text color. Defaults to the theme's `onSurface` color. */
28
+ color?: string
29
+ /** Additional text styles merged after the theme typography styles. */
30
+ style?: StyleProp<TextStyle>
31
+ /**
32
+ * Override the underlying text component (e.g. Animated.Text).
33
+ * @default Text
34
+ */
35
+ as?: ComponentType<TextProps>
36
+ }
37
+
38
+ export function Typography({
39
+ children,
40
+ variant = 'bodyMedium',
41
+ color,
42
+ style,
43
+ as: Component = Text,
44
+ accessibilityRole,
45
+ ...textProps
46
+ }: TypographyProps) {
47
+ const theme = useTheme() as Theme
48
+ const typographyStyle = theme.typography[variant]
49
+ const colorStyle = useMemo(
50
+ () => ({ color: color ?? theme.colors.onSurface }),
51
+ [color, theme.colors.onSurface],
52
+ )
53
+ const resolvedRole =
54
+ accessibilityRole ?? (HEADING_VARIANTS.has(variant) ? 'header' : undefined)
55
+
56
+ return (
57
+ <Component
58
+ {...textProps}
59
+ accessibilityRole={resolvedRole}
60
+ style={[typographyStyle, colorStyle, style]}
61
+ >
62
+ {children}
63
+ </Component>
64
+ )
65
+ }
@@ -0,0 +1,3 @@
1
+ export { Typography } from './Typography'
2
+ export type { TypographyProps } from './Typography'
3
+ export type { TypographyVariant } from './types'
@@ -0,0 +1,17 @@
1
+ /** Material Design 3 type scale role. */
2
+ export type TypographyVariant =
3
+ | 'displayLarge'
4
+ | 'displayMedium'
5
+ | 'displaySmall'
6
+ | 'headlineLarge'
7
+ | 'headlineMedium'
8
+ | 'headlineSmall'
9
+ | 'titleLarge'
10
+ | 'titleMedium'
11
+ | 'titleSmall'
12
+ | 'bodyLarge'
13
+ | 'bodyMedium'
14
+ | 'bodySmall'
15
+ | 'labelLarge'
16
+ | 'labelMedium'
17
+ | 'labelSmall'
@@ -0,0 +1,64 @@
1
+ interface RgbChannels {
2
+ r: number
3
+ g: number
4
+ b: number
5
+ }
6
+
7
+ function parseHexColor(color: string): RgbChannels | null {
8
+ const normalized = color.replace('#', '')
9
+
10
+ if (normalized.length !== 6 && normalized.length !== 8) {
11
+ return null
12
+ }
13
+
14
+ const r = Number.parseInt(normalized.slice(0, 2), 16)
15
+ const g = Number.parseInt(normalized.slice(2, 4), 16)
16
+ const b = Number.parseInt(normalized.slice(4, 6), 16)
17
+
18
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
19
+ return null
20
+ }
21
+
22
+ return { r, g, b }
23
+ }
24
+
25
+ function clampAlpha(alpha: number): number {
26
+ return Math.max(0, Math.min(1, alpha))
27
+ }
28
+
29
+ export function alphaColor(color: string, alpha: number): string {
30
+ const channels = parseHexColor(color)
31
+ const boundedAlpha = clampAlpha(alpha)
32
+
33
+ if (!channels) {
34
+ return color
35
+ }
36
+
37
+ return `rgba(${channels.r}, ${channels.g}, ${channels.b}, ${boundedAlpha})`
38
+ }
39
+
40
+ export function blendColor(
41
+ base: string,
42
+ overlay: string,
43
+ overlayAlpha: number,
44
+ ): string {
45
+ const baseChannels = parseHexColor(base)
46
+ const overlayChannels = parseHexColor(overlay)
47
+ const boundedAlpha = clampAlpha(overlayAlpha)
48
+
49
+ if (!baseChannels || !overlayChannels) {
50
+ return alphaColor(overlay, boundedAlpha)
51
+ }
52
+
53
+ const r = Math.round(
54
+ (1 - boundedAlpha) * baseChannels.r + boundedAlpha * overlayChannels.r,
55
+ )
56
+ const g = Math.round(
57
+ (1 - boundedAlpha) * baseChannels.g + boundedAlpha * overlayChannels.g,
58
+ )
59
+ const b = Math.round(
60
+ (1 - boundedAlpha) * baseChannels.b + boundedAlpha * overlayChannels.b,
61
+ )
62
+
63
+ return `rgb(${r}, ${g}, ${b})`
64
+ }
@@ -0,0 +1,33 @@
1
+ import { Platform } from 'react-native'
2
+ import type { ViewStyle } from 'react-native'
3
+ import type { ElevationLevel } from '@onlynative/core'
4
+
5
+ /**
6
+ * Converts a theme elevation level into platform-appropriate shadow styles.
7
+ * - Native: uses shadow* props + elevation
8
+ * - Web: uses boxShadow string
9
+ */
10
+ export function elevationStyle(level: ElevationLevel): ViewStyle {
11
+ if (Platform.OS === 'web') {
12
+ const { shadowOffset, shadowOpacity, shadowRadius } = level
13
+
14
+ if (shadowOpacity === 0) {
15
+ return { boxShadow: 'none' } as ViewStyle
16
+ }
17
+
18
+ return {
19
+ boxShadow: `${shadowOffset.width}px ${shadowOffset.height}px ${shadowRadius}px rgba(0, 0, 0, ${shadowOpacity})`,
20
+ } as ViewStyle
21
+ }
22
+
23
+ return {
24
+ shadowColor: level.shadowColor,
25
+ shadowOffset: {
26
+ width: level.shadowOffset.width,
27
+ height: level.shadowOffset.height,
28
+ },
29
+ shadowOpacity: level.shadowOpacity,
30
+ shadowRadius: level.shadowRadius,
31
+ elevation: level.elevation,
32
+ }
33
+ }
@@ -0,0 +1,19 @@
1
+ import { I18nManager } from 'react-native'
2
+
3
+ /**
4
+ * Returns the appropriate transform origin for animations that scale
5
+ * from a horizontal edge (e.g. label shrink in TextField).
6
+ */
7
+ export function transformOrigin(
8
+ vertical: 'top' | 'center' | 'bottom' = 'top',
9
+ ): string {
10
+ return I18nManager.isRTL ? `right ${vertical}` : `left ${vertical}`
11
+ }
12
+
13
+ /**
14
+ * Picks a value based on layout direction.
15
+ * Useful for selecting mirrored icons or other direction-dependent values.
16
+ */
17
+ export function selectRTL<T>(ltr: T, rtl: T): T {
18
+ return I18nManager.isRTL ? rtl : ltr
19
+ }