@robin-ux/native 0.1.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/.eslintrc.js +5 -0
- package/README.md +35 -0
- package/android/build.gradle +43 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/robinuxnative/RobinUxNativeModule.kt +50 -0
- package/android/src/main/java/expo/modules/robinuxnative/RobinUxNativeView.kt +30 -0
- package/build/RobinUxNative.types.d.ts +18 -0
- package/build/RobinUxNative.types.d.ts.map +1 -0
- package/build/RobinUxNative.types.js +2 -0
- package/build/RobinUxNative.types.js.map +1 -0
- package/build/RobinUxNativeModule.d.ts +10 -0
- package/build/RobinUxNativeModule.d.ts.map +1 -0
- package/build/RobinUxNativeModule.js +4 -0
- package/build/RobinUxNativeModule.js.map +1 -0
- package/build/RobinUxNativeModule.web.d.ts +10 -0
- package/build/RobinUxNativeModule.web.d.ts.map +1 -0
- package/build/RobinUxNativeModule.web.js +12 -0
- package/build/RobinUxNativeModule.web.js.map +1 -0
- package/build/RobinUxNativeView.d.ts +4 -0
- package/build/RobinUxNativeView.d.ts.map +1 -0
- package/build/RobinUxNativeView.js +7 -0
- package/build/RobinUxNativeView.js.map +1 -0
- package/build/RobinUxNativeView.web.d.ts +4 -0
- package/build/RobinUxNativeView.web.d.ts.map +1 -0
- package/build/RobinUxNativeView.web.js +7 -0
- package/build/RobinUxNativeView.web.js.map +1 -0
- package/build/components/Badge.d.ts +36 -0
- package/build/components/Badge.d.ts.map +1 -0
- package/build/components/Badge.js +78 -0
- package/build/components/Badge.js.map +1 -0
- package/build/components/Button.d.ts +43 -0
- package/build/components/Button.d.ts.map +1 -0
- package/build/components/Button.js +120 -0
- package/build/components/Button.js.map +1 -0
- package/build/components/DynamicStatusBar.d.ts +30 -0
- package/build/components/DynamicStatusBar.d.ts.map +1 -0
- package/build/components/DynamicStatusBar.js +70 -0
- package/build/components/DynamicStatusBar.js.map +1 -0
- package/build/components/Input.d.ts +73 -0
- package/build/components/Input.d.ts.map +1 -0
- package/build/components/Input.js +138 -0
- package/build/components/Input.js.map +1 -0
- package/build/components/SegmentedControl.d.ts +40 -0
- package/build/components/SegmentedControl.d.ts.map +1 -0
- package/build/components/SegmentedControl.js +73 -0
- package/build/components/SegmentedControl.js.map +1 -0
- package/build/components/Text.d.ts +32 -0
- package/build/components/Text.d.ts.map +1 -0
- package/build/components/Text.js +51 -0
- package/build/components/Text.js.map +1 -0
- package/build/components/index.d.ts +13 -0
- package/build/components/index.d.ts.map +1 -0
- package/build/components/index.js +13 -0
- package/build/components/index.js.map +1 -0
- package/build/index.d.ts +7 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +10 -0
- package/build/index.js.map +1 -0
- package/build/theme/ThemeContext.d.ts +22 -0
- package/build/theme/ThemeContext.d.ts.map +1 -0
- package/build/theme/ThemeContext.js +35 -0
- package/build/theme/ThemeContext.js.map +1 -0
- package/build/theme/ThemeProvider.d.ts +33 -0
- package/build/theme/ThemeProvider.d.ts.map +1 -0
- package/build/theme/ThemeProvider.js +31 -0
- package/build/theme/ThemeProvider.js.map +1 -0
- package/build/theme/defaultTheme.d.ts +18 -0
- package/build/theme/defaultTheme.d.ts.map +1 -0
- package/build/theme/defaultTheme.js +77 -0
- package/build/theme/defaultTheme.js.map +1 -0
- package/build/theme/index.d.ts +6 -0
- package/build/theme/index.d.ts.map +1 -0
- package/build/theme/index.js +7 -0
- package/build/theme/index.js.map +1 -0
- package/build/theme/types.d.ts +40 -0
- package/build/theme/types.d.ts.map +1 -0
- package/build/theme/types.js +2 -0
- package/build/theme/types.js.map +1 -0
- package/build/utils/index.d.ts +3 -0
- package/build/utils/index.d.ts.map +1 -0
- package/build/utils/index.js +3 -0
- package/build/utils/index.js.map +1 -0
- package/build/utils/styles.d.ts +88 -0
- package/build/utils/styles.d.ts.map +1 -0
- package/build/utils/styles.js +150 -0
- package/build/utils/styles.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/RobinUxNative.podspec +29 -0
- package/ios/RobinUxNativeModule.swift +48 -0
- package/ios/RobinUxNativeView.swift +38 -0
- package/package.json +69 -0
- package/src/RobinUxNative.types.ts +19 -0
- package/src/RobinUxNativeModule.ts +12 -0
- package/src/RobinUxNativeModule.web.ts +15 -0
- package/src/RobinUxNativeView.tsx +11 -0
- package/src/RobinUxNativeView.web.tsx +15 -0
- package/src/components/Badge.tsx +101 -0
- package/src/components/Button.tsx +176 -0
- package/src/components/DynamicStatusBar.tsx +93 -0
- package/src/components/Input.tsx +248 -0
- package/src/components/SegmentedControl.tsx +107 -0
- package/src/components/Text.tsx +107 -0
- package/src/components/index.ts +23 -0
- package/src/index.ts +70 -0
- package/src/theme/ThemeContext.ts +38 -0
- package/src/theme/ThemeProvider.tsx +45 -0
- package/src/theme/defaultTheme.ts +91 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/types.ts +52 -0
- package/src/utils/index.ts +19 -0
- package/src/utils/styles.ts +188 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Control, useController } from 'react-hook-form';
|
|
3
|
+
import { TextInput, TextInputProps, View, ViewStyle } from 'react-native';
|
|
4
|
+
import { useThemeSafe } from '../theme';
|
|
5
|
+
import { borderRadius, spacing, useThemedStyles } from '../utils/styles';
|
|
6
|
+
import { Button } from './Button';
|
|
7
|
+
import { Text } from './Text';
|
|
8
|
+
|
|
9
|
+
export interface InputProps extends Omit<TextInputProps, 'style'> {
|
|
10
|
+
/** Field label */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** Error message to display */
|
|
13
|
+
error?: string;
|
|
14
|
+
/** Helper text below input */
|
|
15
|
+
helperText?: string;
|
|
16
|
+
/** Custom input style */
|
|
17
|
+
style?: TextInputProps['style'];
|
|
18
|
+
/** Container style */
|
|
19
|
+
containerStyle?: ViewStyle;
|
|
20
|
+
/** Input ref */
|
|
21
|
+
ref?: React.RefObject<TextInput | null>;
|
|
22
|
+
/** Element to render at the end of the input */
|
|
23
|
+
endElement?: React.ReactNode;
|
|
24
|
+
/** Child elements */
|
|
25
|
+
children?: React.ReactNode;
|
|
26
|
+
/** React Hook Form control object (optional) */
|
|
27
|
+
control?: Control<any>;
|
|
28
|
+
/** Field name for React Hook Form (optional) */
|
|
29
|
+
name?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const inputStyles = {
|
|
33
|
+
paddingHorizontal: spacing.md,
|
|
34
|
+
paddingVertical: spacing.md,
|
|
35
|
+
borderRadius: borderRadius.lg,
|
|
36
|
+
fontSize: 16,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Base input wrapper providing label, error, and helper text layout.
|
|
41
|
+
*/
|
|
42
|
+
export function BaseInput({
|
|
43
|
+
label,
|
|
44
|
+
error,
|
|
45
|
+
helperText,
|
|
46
|
+
containerStyle,
|
|
47
|
+
children,
|
|
48
|
+
}: Pick<InputProps, 'label' | 'error' | 'helperText' | 'containerStyle' | 'children'>) {
|
|
49
|
+
const styles = useThemedStyles((colors) => ({
|
|
50
|
+
label: {
|
|
51
|
+
marginBottom: spacing.xs,
|
|
52
|
+
color: colors.foregroundSecondary,
|
|
53
|
+
fontSize: 14,
|
|
54
|
+
},
|
|
55
|
+
helperText: {
|
|
56
|
+
marginTop: spacing.xs,
|
|
57
|
+
},
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<View style={containerStyle}>
|
|
62
|
+
{label && (
|
|
63
|
+
<Text variant="body" weight="medium" style={styles.label}>
|
|
64
|
+
{label}
|
|
65
|
+
</Text>
|
|
66
|
+
)}
|
|
67
|
+
{children}
|
|
68
|
+
{error && (
|
|
69
|
+
<Text variant="caption" color="error" style={styles.helperText}>
|
|
70
|
+
{error}
|
|
71
|
+
</Text>
|
|
72
|
+
)}
|
|
73
|
+
{!error && helperText && (
|
|
74
|
+
<Text variant="caption" color="secondary" style={styles.helperText}>
|
|
75
|
+
{helperText}
|
|
76
|
+
</Text>
|
|
77
|
+
)}
|
|
78
|
+
</View>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Password input with visibility toggle.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```tsx
|
|
87
|
+
* <PasswordInput
|
|
88
|
+
* label="Password"
|
|
89
|
+
* placeholder="Enter your password"
|
|
90
|
+
* value={password}
|
|
91
|
+
* onChangeText={setPassword}
|
|
92
|
+
* />
|
|
93
|
+
*
|
|
94
|
+
* // With React Hook Form
|
|
95
|
+
* <PasswordInput
|
|
96
|
+
* label="Password"
|
|
97
|
+
* control={control}
|
|
98
|
+
* name="password"
|
|
99
|
+
* />
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export const PasswordInput: React.FC<InputProps> = ({
|
|
103
|
+
value,
|
|
104
|
+
onChangeText,
|
|
105
|
+
style,
|
|
106
|
+
onSubmitEditing,
|
|
107
|
+
ref,
|
|
108
|
+
error,
|
|
109
|
+
control,
|
|
110
|
+
name,
|
|
111
|
+
...props
|
|
112
|
+
}) => {
|
|
113
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
114
|
+
const { colors } = useThemeSafe();
|
|
115
|
+
|
|
116
|
+
// Use useController if control and name are provided
|
|
117
|
+
const controller = control && name ? useController({ control, name }) : null;
|
|
118
|
+
|
|
119
|
+
const inputValue = controller ? controller.field.value : value;
|
|
120
|
+
const inputOnChange = controller ? controller.field.onChange : onChangeText;
|
|
121
|
+
const inputOnBlur = controller ? controller.field.onBlur : props.onBlur;
|
|
122
|
+
const inputRef = controller ? controller.field.ref : ref;
|
|
123
|
+
|
|
124
|
+
const hasError = error || controller?.fieldState.error;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<BaseInput
|
|
128
|
+
label={props.label}
|
|
129
|
+
error={error || controller?.fieldState.error?.message}
|
|
130
|
+
helperText={props.helperText}
|
|
131
|
+
containerStyle={props.containerStyle}
|
|
132
|
+
>
|
|
133
|
+
<View
|
|
134
|
+
style={[
|
|
135
|
+
{
|
|
136
|
+
backgroundColor: colors.backgroundSecondary,
|
|
137
|
+
borderColor: hasError ? colors.error : colors.border,
|
|
138
|
+
borderWidth: hasError ? 1 : 0,
|
|
139
|
+
flexDirection: 'row',
|
|
140
|
+
alignItems: 'center',
|
|
141
|
+
borderRadius: borderRadius.lg,
|
|
142
|
+
},
|
|
143
|
+
style as ViewStyle,
|
|
144
|
+
]}
|
|
145
|
+
>
|
|
146
|
+
<TextInput
|
|
147
|
+
placeholderTextColor={colors.foregroundSecondary}
|
|
148
|
+
ref={inputRef as any}
|
|
149
|
+
{...props}
|
|
150
|
+
style={[
|
|
151
|
+
inputStyles,
|
|
152
|
+
{
|
|
153
|
+
flex: 1,
|
|
154
|
+
color: colors.foreground,
|
|
155
|
+
},
|
|
156
|
+
]}
|
|
157
|
+
value={inputValue}
|
|
158
|
+
onChangeText={inputOnChange}
|
|
159
|
+
autoCapitalize="none"
|
|
160
|
+
onBlur={inputOnBlur}
|
|
161
|
+
onSubmitEditing={onSubmitEditing}
|
|
162
|
+
secureTextEntry={!showPassword}
|
|
163
|
+
/>
|
|
164
|
+
<Button
|
|
165
|
+
variant="ghost"
|
|
166
|
+
icon={showPassword ? 'eye-off' : 'eye'}
|
|
167
|
+
size="sm"
|
|
168
|
+
onPress={() => setShowPassword(!showPassword)}
|
|
169
|
+
/>
|
|
170
|
+
</View>
|
|
171
|
+
</BaseInput>
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Text input component with label, error, and helper text support.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```tsx
|
|
180
|
+
* <Input
|
|
181
|
+
* label="Email"
|
|
182
|
+
* placeholder="Enter your email"
|
|
183
|
+
* value={email}
|
|
184
|
+
* onChangeText={setEmail}
|
|
185
|
+
* keyboardType="email-address"
|
|
186
|
+
* />
|
|
187
|
+
*
|
|
188
|
+
* // With React Hook Form
|
|
189
|
+
* <Input
|
|
190
|
+
* label="Email"
|
|
191
|
+
* control={control}
|
|
192
|
+
* name="email"
|
|
193
|
+
* />
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
export const Input: React.FC<InputProps> = ({
|
|
197
|
+
value,
|
|
198
|
+
error,
|
|
199
|
+
onChangeText,
|
|
200
|
+
onSubmitEditing,
|
|
201
|
+
style,
|
|
202
|
+
ref,
|
|
203
|
+
control,
|
|
204
|
+
name,
|
|
205
|
+
...props
|
|
206
|
+
}) => {
|
|
207
|
+
const { colors } = useThemeSafe();
|
|
208
|
+
|
|
209
|
+
// Use useController if control and name are provided
|
|
210
|
+
const controller = control && name ? useController({ control, name }) : null;
|
|
211
|
+
|
|
212
|
+
const inputValue = controller ? controller.field.value : value;
|
|
213
|
+
const inputOnChange = controller ? controller.field.onChange : onChangeText;
|
|
214
|
+
const inputOnBlur = controller ? controller.field.onBlur : props.onBlur;
|
|
215
|
+
const inputRef = controller ? controller.field.ref : ref;
|
|
216
|
+
|
|
217
|
+
const hasError = error || controller?.fieldState.error;
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<BaseInput
|
|
221
|
+
label={props.label}
|
|
222
|
+
error={error || controller?.fieldState.error?.message}
|
|
223
|
+
helperText={props.helperText}
|
|
224
|
+
containerStyle={props.containerStyle}
|
|
225
|
+
>
|
|
226
|
+
<TextInput
|
|
227
|
+
placeholderTextColor={colors.foregroundSecondary}
|
|
228
|
+
ref={inputRef as any}
|
|
229
|
+
value={inputValue}
|
|
230
|
+
onChangeText={inputOnChange}
|
|
231
|
+
onBlur={inputOnBlur}
|
|
232
|
+
onSubmitEditing={onSubmitEditing}
|
|
233
|
+
{...props}
|
|
234
|
+
style={[
|
|
235
|
+
inputStyles,
|
|
236
|
+
{
|
|
237
|
+
backgroundColor: colors.backgroundSecondary,
|
|
238
|
+
color: colors.foreground,
|
|
239
|
+
borderColor: hasError ? colors.error : colors.border,
|
|
240
|
+
borderWidth: hasError ? 1 : 0,
|
|
241
|
+
borderRadius: borderRadius.lg,
|
|
242
|
+
},
|
|
243
|
+
style,
|
|
244
|
+
]}
|
|
245
|
+
/>
|
|
246
|
+
</BaseInput>
|
|
247
|
+
);
|
|
248
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Pressable, StyleProp, View, ViewStyle } from "react-native";
|
|
4
|
+
import { useThemeSafe } from "../theme";
|
|
5
|
+
import { borderRadius, spacing, useThemedStyles } from "../utils/styles";
|
|
6
|
+
import { Text } from "./Text";
|
|
7
|
+
|
|
8
|
+
export interface Segment {
|
|
9
|
+
/** Unique value for this segment */
|
|
10
|
+
value: string;
|
|
11
|
+
/** Display label (optional if using icon only) */
|
|
12
|
+
label?: string;
|
|
13
|
+
/** Icon name from Ionicons (optional) */
|
|
14
|
+
icon?: keyof typeof Ionicons.glyphMap;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SegmentedControlProps {
|
|
18
|
+
/** Array of segments to display */
|
|
19
|
+
segments: Segment[];
|
|
20
|
+
/** Currently selected segment value */
|
|
21
|
+
value: string;
|
|
22
|
+
/** Callback when selection changes */
|
|
23
|
+
onChange: (value: string) => void;
|
|
24
|
+
/** Control size */
|
|
25
|
+
size?: "sm" | "md";
|
|
26
|
+
/** Custom container style */
|
|
27
|
+
style?: StyleProp<ViewStyle>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Segmented control component for switching between options.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* <SegmentedControl
|
|
36
|
+
* segments={[
|
|
37
|
+
* { value: 'list', label: 'List', icon: 'list' },
|
|
38
|
+
* { value: 'grid', label: 'Grid', icon: 'grid' },
|
|
39
|
+
* ]}
|
|
40
|
+
* value={view}
|
|
41
|
+
* onChange={setView}
|
|
42
|
+
* />
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function SegmentedControl({ segments, value, onChange, size = "sm", style }: SegmentedControlProps) {
|
|
46
|
+
const { isDark } = useThemeSafe();
|
|
47
|
+
|
|
48
|
+
const styles = useThemedStyles((colors) => ({
|
|
49
|
+
container: {
|
|
50
|
+
backgroundColor: colors.backgroundSecondary,
|
|
51
|
+
borderRadius: borderRadius.full,
|
|
52
|
+
padding: spacing.xs,
|
|
53
|
+
flexDirection: "row",
|
|
54
|
+
alignSelf: "flex-start",
|
|
55
|
+
},
|
|
56
|
+
segment: {
|
|
57
|
+
alignItems: "center",
|
|
58
|
+
justifyContent: "center",
|
|
59
|
+
paddingVertical: spacing[size],
|
|
60
|
+
paddingHorizontal: spacing[size],
|
|
61
|
+
borderRadius: borderRadius.full,
|
|
62
|
+
flexDirection: "row",
|
|
63
|
+
gap: spacing[size],
|
|
64
|
+
},
|
|
65
|
+
activeSegment: {
|
|
66
|
+
backgroundColor: isDark ? colors.primaryLight : colors.primaryDark,
|
|
67
|
+
},
|
|
68
|
+
segmentText: {
|
|
69
|
+
color: colors.foregroundSecondary,
|
|
70
|
+
fontSize: size === "sm" ? 12 : 14,
|
|
71
|
+
fontWeight: "600",
|
|
72
|
+
},
|
|
73
|
+
segmentWithText: {
|
|
74
|
+
paddingHorizontal: size == "md" ? spacing.xl : spacing.lg,
|
|
75
|
+
},
|
|
76
|
+
activeSegmentText: {
|
|
77
|
+
color: colors.background,
|
|
78
|
+
},
|
|
79
|
+
segmentIcon: {
|
|
80
|
+
color: colors.foregroundSecondary,
|
|
81
|
+
fontSize: size === "sm" ? 14 : 16,
|
|
82
|
+
},
|
|
83
|
+
activeSegmentIcon: {
|
|
84
|
+
color: colors.background,
|
|
85
|
+
},
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<View style={[styles.container, style]}>
|
|
90
|
+
{segments.map((segment) => {
|
|
91
|
+
const isActive = segment.value === value;
|
|
92
|
+
return (
|
|
93
|
+
<Pressable
|
|
94
|
+
key={segment.value}
|
|
95
|
+
style={[styles.segment, isActive && styles.activeSegment, segment.label && styles.segmentWithText]}
|
|
96
|
+
onPress={() => onChange(segment.value)}
|
|
97
|
+
accessibilityRole="radio"
|
|
98
|
+
accessibilityState={{ selected: isActive }}
|
|
99
|
+
>
|
|
100
|
+
{segment.icon && <Ionicons name={segment.icon} style={[styles.segmentIcon, isActive && styles.activeSegmentIcon]} />}
|
|
101
|
+
{segment.label && <Text style={[styles.segmentText, isActive && styles.activeSegmentText]}>{segment.label}</Text>}
|
|
102
|
+
</Pressable>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
</View>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text as RNText, TextProps as RNTextProps, TextStyle } from 'react-native';
|
|
3
|
+
import { useThemeSafe } from '../theme';
|
|
4
|
+
import { typography } from '../utils/styles';
|
|
5
|
+
|
|
6
|
+
export type TextVariant =
|
|
7
|
+
| 'heading'
|
|
8
|
+
| 'subheading'
|
|
9
|
+
| 'title'
|
|
10
|
+
| 'body'
|
|
11
|
+
| 'bodyMedium'
|
|
12
|
+
| 'bodySemibold'
|
|
13
|
+
| 'caption'
|
|
14
|
+
| 'label';
|
|
15
|
+
|
|
16
|
+
export type TextWeight = 'regular' | 'medium' | 'semibold' | 'bold' | 'extrabold';
|
|
17
|
+
|
|
18
|
+
export type TextColor =
|
|
19
|
+
| 'primary'
|
|
20
|
+
| 'secondary'
|
|
21
|
+
| 'tertiary'
|
|
22
|
+
| 'accent'
|
|
23
|
+
| 'error'
|
|
24
|
+
| 'success'
|
|
25
|
+
| 'warning'
|
|
26
|
+
| 'info'
|
|
27
|
+
| 'white';
|
|
28
|
+
|
|
29
|
+
export type TextAlign = 'left' | 'center' | 'right' | 'justify';
|
|
30
|
+
|
|
31
|
+
export interface TextProps extends Omit<RNTextProps, 'style'> {
|
|
32
|
+
/** Typography variant */
|
|
33
|
+
variant?: TextVariant;
|
|
34
|
+
/** Font weight override */
|
|
35
|
+
weight?: TextWeight;
|
|
36
|
+
/** Text color */
|
|
37
|
+
color?: TextColor;
|
|
38
|
+
/** Text alignment */
|
|
39
|
+
align?: TextAlign;
|
|
40
|
+
/** Custom style */
|
|
41
|
+
style?: RNTextProps['style'];
|
|
42
|
+
/** Text content */
|
|
43
|
+
children: React.ReactNode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Map weights to system font weights
|
|
47
|
+
const fontWeightMap: Record<TextWeight, TextStyle['fontWeight']> = {
|
|
48
|
+
regular: '400',
|
|
49
|
+
medium: '500',
|
|
50
|
+
semibold: '600',
|
|
51
|
+
bold: '700',
|
|
52
|
+
extrabold: '800',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Text component with semantic variants and theme support.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```tsx
|
|
60
|
+
* <Text variant="heading" color="primary">Hello World</Text>
|
|
61
|
+
* <Text variant="body" weight="semibold">Bold text</Text>
|
|
62
|
+
* <Text variant="caption" color="secondary">Small helper text</Text>
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function Text({
|
|
66
|
+
variant = 'body',
|
|
67
|
+
weight,
|
|
68
|
+
color = 'primary',
|
|
69
|
+
align = 'left',
|
|
70
|
+
style,
|
|
71
|
+
children,
|
|
72
|
+
...props
|
|
73
|
+
}: TextProps) {
|
|
74
|
+
const { colors } = useThemeSafe();
|
|
75
|
+
|
|
76
|
+
// Get base variant style
|
|
77
|
+
const baseStyle = typography[variant];
|
|
78
|
+
|
|
79
|
+
// Determine font weight
|
|
80
|
+
const fontWeight = weight ? fontWeightMap[weight] : baseStyle.fontWeight;
|
|
81
|
+
|
|
82
|
+
// Get text color from theme
|
|
83
|
+
const textColor: Record<TextColor, string> = {
|
|
84
|
+
primary: colors.foreground,
|
|
85
|
+
secondary: colors.foregroundSecondary,
|
|
86
|
+
tertiary: colors.foregroundTertiary,
|
|
87
|
+
accent: colors.accent,
|
|
88
|
+
error: colors.error,
|
|
89
|
+
success: colors.success,
|
|
90
|
+
warning: colors.warning,
|
|
91
|
+
info: colors.info,
|
|
92
|
+
white: '#ffffff',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const computedStyle: TextStyle[] = [
|
|
96
|
+
baseStyle,
|
|
97
|
+
{ color: textColor[color], textAlign: align },
|
|
98
|
+
fontWeight ? { fontWeight } : {},
|
|
99
|
+
style as TextStyle,
|
|
100
|
+
].filter(Boolean);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<RNText style={computedStyle} {...props}>
|
|
104
|
+
{children}
|
|
105
|
+
</RNText>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Button
|
|
2
|
+
export { Button } from './Button';
|
|
3
|
+
export type { ButtonProps, ButtonVariant, ButtonSize } from './Button';
|
|
4
|
+
|
|
5
|
+
// Text
|
|
6
|
+
export { Text } from './Text';
|
|
7
|
+
export type { TextProps, TextVariant, TextWeight, TextColor, TextAlign } from './Text';
|
|
8
|
+
|
|
9
|
+
// Input
|
|
10
|
+
export { Input, PasswordInput, BaseInput } from './Input';
|
|
11
|
+
export type { InputProps } from './Input';
|
|
12
|
+
|
|
13
|
+
// SegmentedControl
|
|
14
|
+
export { SegmentedControl } from './SegmentedControl';
|
|
15
|
+
export type { SegmentedControlProps, Segment } from './SegmentedControl';
|
|
16
|
+
|
|
17
|
+
// Badge
|
|
18
|
+
export { Badge } from './Badge';
|
|
19
|
+
export type { BadgeProps, BadgeStatus } from './Badge';
|
|
20
|
+
|
|
21
|
+
// DynamicStatusBar
|
|
22
|
+
export { DynamicStatusBar } from './DynamicStatusBar';
|
|
23
|
+
export type { DynamicStatusBarProps } from './DynamicStatusBar';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// @robin-ux/native - Themeable React Native UI Components
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
// Theme system
|
|
6
|
+
export {
|
|
7
|
+
ThemeProvider,
|
|
8
|
+
ThemeContext,
|
|
9
|
+
useTheme,
|
|
10
|
+
useThemeSafe,
|
|
11
|
+
defaultTheme,
|
|
12
|
+
defaultDarkTheme,
|
|
13
|
+
defaultLightColors,
|
|
14
|
+
defaultDarkColors,
|
|
15
|
+
} from './theme';
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
Theme,
|
|
19
|
+
ThemeColors,
|
|
20
|
+
ThemeContextValue,
|
|
21
|
+
ThemeProviderProps,
|
|
22
|
+
} from './theme';
|
|
23
|
+
|
|
24
|
+
// Components
|
|
25
|
+
export {
|
|
26
|
+
Button,
|
|
27
|
+
Text,
|
|
28
|
+
Input,
|
|
29
|
+
PasswordInput,
|
|
30
|
+
BaseInput,
|
|
31
|
+
SegmentedControl,
|
|
32
|
+
Badge,
|
|
33
|
+
DynamicStatusBar,
|
|
34
|
+
} from './components';
|
|
35
|
+
|
|
36
|
+
export type {
|
|
37
|
+
ButtonProps,
|
|
38
|
+
ButtonVariant,
|
|
39
|
+
ButtonSize,
|
|
40
|
+
TextProps,
|
|
41
|
+
TextVariant,
|
|
42
|
+
TextWeight,
|
|
43
|
+
TextColor,
|
|
44
|
+
TextAlign,
|
|
45
|
+
InputProps,
|
|
46
|
+
SegmentedControlProps,
|
|
47
|
+
Segment,
|
|
48
|
+
BadgeProps,
|
|
49
|
+
BadgeStatus,
|
|
50
|
+
DynamicStatusBarProps,
|
|
51
|
+
} from './components';
|
|
52
|
+
|
|
53
|
+
// Utilities
|
|
54
|
+
export {
|
|
55
|
+
spacing,
|
|
56
|
+
borderRadius,
|
|
57
|
+
typography,
|
|
58
|
+
shadows,
|
|
59
|
+
layouts,
|
|
60
|
+
useThemedStyles,
|
|
61
|
+
createThemedStyle,
|
|
62
|
+
} from './utils';
|
|
63
|
+
|
|
64
|
+
export type {
|
|
65
|
+
SpacingKey,
|
|
66
|
+
BorderRadiusKey,
|
|
67
|
+
TypographyVariant,
|
|
68
|
+
ShadowKey,
|
|
69
|
+
LayoutKey,
|
|
70
|
+
} from './utils';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { defaultTheme } from './defaultTheme';
|
|
3
|
+
import { Theme, ThemeContextValue } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Theme context for providing theme to components.
|
|
7
|
+
* Default value is undefined to allow detection of missing provider.
|
|
8
|
+
*/
|
|
9
|
+
export const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Hook to access the theme context.
|
|
13
|
+
* Throws an error if used outside of a ThemeProvider.
|
|
14
|
+
*
|
|
15
|
+
* @throws Error if no ThemeProvider is found in the component tree
|
|
16
|
+
*/
|
|
17
|
+
export function useTheme(): Theme {
|
|
18
|
+
const context = useContext(ThemeContext);
|
|
19
|
+
if (context === undefined) {
|
|
20
|
+
throw new Error('useTheme must be used within a ThemeProvider');
|
|
21
|
+
}
|
|
22
|
+
return context.theme;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Safe hook to access theme with fallback to default.
|
|
27
|
+
* Never throws - returns default theme if no provider is found.
|
|
28
|
+
*
|
|
29
|
+
* This is the recommended hook for component libraries to ensure
|
|
30
|
+
* components work even without an explicit ThemeProvider.
|
|
31
|
+
*/
|
|
32
|
+
export function useThemeSafe(): Theme {
|
|
33
|
+
const context = useContext(ThemeContext);
|
|
34
|
+
if (context === undefined) {
|
|
35
|
+
return defaultTheme;
|
|
36
|
+
}
|
|
37
|
+
return context.theme;
|
|
38
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { ThemeContext } from './ThemeContext';
|
|
3
|
+
import { defaultTheme } from './defaultTheme';
|
|
4
|
+
import { Theme, ThemeContextValue } from './types';
|
|
5
|
+
|
|
6
|
+
export interface ThemeProviderProps {
|
|
7
|
+
/**
|
|
8
|
+
* The theme to provide to child components.
|
|
9
|
+
* Falls back to default neutral theme if not provided.
|
|
10
|
+
*/
|
|
11
|
+
theme?: Theme;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Theme provider component that makes theme available to all child components.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* import { ThemeProvider } from '@robin-ux/native';
|
|
21
|
+
*
|
|
22
|
+
* const myTheme = {
|
|
23
|
+
* colors: { ... },
|
|
24
|
+
* isDark: false,
|
|
25
|
+
* };
|
|
26
|
+
*
|
|
27
|
+
* function App() {
|
|
28
|
+
* return (
|
|
29
|
+
* <ThemeProvider theme={myTheme}>
|
|
30
|
+
* <MyApp />
|
|
31
|
+
* </ThemeProvider>
|
|
32
|
+
* );
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function ThemeProvider({ theme = defaultTheme, children }: ThemeProviderProps) {
|
|
37
|
+
const value: ThemeContextValue = useMemo(
|
|
38
|
+
() => ({
|
|
39
|
+
theme,
|
|
40
|
+
}),
|
|
41
|
+
[theme]
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
45
|
+
}
|