@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.
Files changed (112) hide show
  1. package/.eslintrc.js +5 -0
  2. package/README.md +35 -0
  3. package/android/build.gradle +43 -0
  4. package/android/src/main/AndroidManifest.xml +2 -0
  5. package/android/src/main/java/expo/modules/robinuxnative/RobinUxNativeModule.kt +50 -0
  6. package/android/src/main/java/expo/modules/robinuxnative/RobinUxNativeView.kt +30 -0
  7. package/build/RobinUxNative.types.d.ts +18 -0
  8. package/build/RobinUxNative.types.d.ts.map +1 -0
  9. package/build/RobinUxNative.types.js +2 -0
  10. package/build/RobinUxNative.types.js.map +1 -0
  11. package/build/RobinUxNativeModule.d.ts +10 -0
  12. package/build/RobinUxNativeModule.d.ts.map +1 -0
  13. package/build/RobinUxNativeModule.js +4 -0
  14. package/build/RobinUxNativeModule.js.map +1 -0
  15. package/build/RobinUxNativeModule.web.d.ts +10 -0
  16. package/build/RobinUxNativeModule.web.d.ts.map +1 -0
  17. package/build/RobinUxNativeModule.web.js +12 -0
  18. package/build/RobinUxNativeModule.web.js.map +1 -0
  19. package/build/RobinUxNativeView.d.ts +4 -0
  20. package/build/RobinUxNativeView.d.ts.map +1 -0
  21. package/build/RobinUxNativeView.js +7 -0
  22. package/build/RobinUxNativeView.js.map +1 -0
  23. package/build/RobinUxNativeView.web.d.ts +4 -0
  24. package/build/RobinUxNativeView.web.d.ts.map +1 -0
  25. package/build/RobinUxNativeView.web.js +7 -0
  26. package/build/RobinUxNativeView.web.js.map +1 -0
  27. package/build/components/Badge.d.ts +36 -0
  28. package/build/components/Badge.d.ts.map +1 -0
  29. package/build/components/Badge.js +78 -0
  30. package/build/components/Badge.js.map +1 -0
  31. package/build/components/Button.d.ts +43 -0
  32. package/build/components/Button.d.ts.map +1 -0
  33. package/build/components/Button.js +120 -0
  34. package/build/components/Button.js.map +1 -0
  35. package/build/components/DynamicStatusBar.d.ts +30 -0
  36. package/build/components/DynamicStatusBar.d.ts.map +1 -0
  37. package/build/components/DynamicStatusBar.js +70 -0
  38. package/build/components/DynamicStatusBar.js.map +1 -0
  39. package/build/components/Input.d.ts +73 -0
  40. package/build/components/Input.d.ts.map +1 -0
  41. package/build/components/Input.js +138 -0
  42. package/build/components/Input.js.map +1 -0
  43. package/build/components/SegmentedControl.d.ts +40 -0
  44. package/build/components/SegmentedControl.d.ts.map +1 -0
  45. package/build/components/SegmentedControl.js +73 -0
  46. package/build/components/SegmentedControl.js.map +1 -0
  47. package/build/components/Text.d.ts +32 -0
  48. package/build/components/Text.d.ts.map +1 -0
  49. package/build/components/Text.js +51 -0
  50. package/build/components/Text.js.map +1 -0
  51. package/build/components/index.d.ts +13 -0
  52. package/build/components/index.d.ts.map +1 -0
  53. package/build/components/index.js +13 -0
  54. package/build/components/index.js.map +1 -0
  55. package/build/index.d.ts +7 -0
  56. package/build/index.d.ts.map +1 -0
  57. package/build/index.js +10 -0
  58. package/build/index.js.map +1 -0
  59. package/build/theme/ThemeContext.d.ts +22 -0
  60. package/build/theme/ThemeContext.d.ts.map +1 -0
  61. package/build/theme/ThemeContext.js +35 -0
  62. package/build/theme/ThemeContext.js.map +1 -0
  63. package/build/theme/ThemeProvider.d.ts +33 -0
  64. package/build/theme/ThemeProvider.d.ts.map +1 -0
  65. package/build/theme/ThemeProvider.js +31 -0
  66. package/build/theme/ThemeProvider.js.map +1 -0
  67. package/build/theme/defaultTheme.d.ts +18 -0
  68. package/build/theme/defaultTheme.d.ts.map +1 -0
  69. package/build/theme/defaultTheme.js +77 -0
  70. package/build/theme/defaultTheme.js.map +1 -0
  71. package/build/theme/index.d.ts +6 -0
  72. package/build/theme/index.d.ts.map +1 -0
  73. package/build/theme/index.js +7 -0
  74. package/build/theme/index.js.map +1 -0
  75. package/build/theme/types.d.ts +40 -0
  76. package/build/theme/types.d.ts.map +1 -0
  77. package/build/theme/types.js +2 -0
  78. package/build/theme/types.js.map +1 -0
  79. package/build/utils/index.d.ts +3 -0
  80. package/build/utils/index.d.ts.map +1 -0
  81. package/build/utils/index.js +3 -0
  82. package/build/utils/index.js.map +1 -0
  83. package/build/utils/styles.d.ts +88 -0
  84. package/build/utils/styles.d.ts.map +1 -0
  85. package/build/utils/styles.js +150 -0
  86. package/build/utils/styles.js.map +1 -0
  87. package/expo-module.config.json +9 -0
  88. package/ios/RobinUxNative.podspec +29 -0
  89. package/ios/RobinUxNativeModule.swift +48 -0
  90. package/ios/RobinUxNativeView.swift +38 -0
  91. package/package.json +69 -0
  92. package/src/RobinUxNative.types.ts +19 -0
  93. package/src/RobinUxNativeModule.ts +12 -0
  94. package/src/RobinUxNativeModule.web.ts +15 -0
  95. package/src/RobinUxNativeView.tsx +11 -0
  96. package/src/RobinUxNativeView.web.tsx +15 -0
  97. package/src/components/Badge.tsx +101 -0
  98. package/src/components/Button.tsx +176 -0
  99. package/src/components/DynamicStatusBar.tsx +93 -0
  100. package/src/components/Input.tsx +248 -0
  101. package/src/components/SegmentedControl.tsx +107 -0
  102. package/src/components/Text.tsx +107 -0
  103. package/src/components/index.ts +23 -0
  104. package/src/index.ts +70 -0
  105. package/src/theme/ThemeContext.ts +38 -0
  106. package/src/theme/ThemeProvider.tsx +45 -0
  107. package/src/theme/defaultTheme.ts +91 -0
  108. package/src/theme/index.ts +12 -0
  109. package/src/theme/types.ts +52 -0
  110. package/src/utils/index.ts +19 -0
  111. package/src/utils/styles.ts +188 -0
  112. 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
+ }