@urbint/cl 1.0.1

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 (206) hide show
  1. package/.cursor/rules +313 -0
  2. package/.rnstorybook/index.ts +11 -0
  3. package/.rnstorybook/main.ts +8 -0
  4. package/.rnstorybook/preview.tsx +14 -0
  5. package/.rnstorybook/storybook.requires.ts +49 -0
  6. package/.storybook/main.ts +16 -0
  7. package/.storybook/preview.ts +32 -0
  8. package/.storybook/vitest.setup.ts +7 -0
  9. package/App.tsx +422 -0
  10. package/README.md +229 -0
  11. package/app.json +33 -0
  12. package/assets/adaptive-icon.png +0 -0
  13. package/assets/favicon.png +0 -0
  14. package/assets/icon.png +0 -0
  15. package/assets/splash-icon.png +0 -0
  16. package/babel.config.js +16 -0
  17. package/docs/components/CodeBlock.tsx +80 -0
  18. package/docs/components/PropTable.tsx +93 -0
  19. package/docs/components/Sidebar.tsx +199 -0
  20. package/docs/components/index.ts +8 -0
  21. package/docs/data/colorTokens.ts +70 -0
  22. package/docs/data/componentData.tsx +1685 -0
  23. package/docs/data/index.ts +7 -0
  24. package/docs/index.ts +19 -0
  25. package/docs/navigation.ts +94 -0
  26. package/docs/pages/ColorsPage.tsx +226 -0
  27. package/docs/pages/ComponentPage.tsx +235 -0
  28. package/docs/pages/InstallationPage.tsx +232 -0
  29. package/docs/pages/IntroductionPage.tsx +163 -0
  30. package/docs/pages/ThemingPage.tsx +251 -0
  31. package/docs/pages/index.ts +10 -0
  32. package/docs/theme.ts +64 -0
  33. package/docs/types.ts +54 -0
  34. package/index.ts +8 -0
  35. package/llms.txt +1893 -0
  36. package/mcp-config.example.json +10 -0
  37. package/mcp-server/README.md +192 -0
  38. package/mcp-server/package-lock.json +1707 -0
  39. package/mcp-server/package.json +38 -0
  40. package/mcp-server/src/index.ts +1136 -0
  41. package/mcp-server/src/registry/components.ts +1446 -0
  42. package/mcp-server/src/registry/index.ts +3 -0
  43. package/mcp-server/src/registry/tokens.ts +256 -0
  44. package/mcp-server/tsconfig.json +19 -0
  45. package/package.json +92 -0
  46. package/src/components/Accordion/Accordion.stories.tsx +226 -0
  47. package/src/components/Accordion/Accordion.tsx +255 -0
  48. package/src/components/Accordion/index.ts +12 -0
  49. package/src/components/ActionSheet/ActionSheet.stories.tsx +393 -0
  50. package/src/components/ActionSheet/ActionSheet.tsx +258 -0
  51. package/src/components/ActionSheet/index.ts +2 -0
  52. package/src/components/Alert/Alert.stories.tsx +165 -0
  53. package/src/components/Alert/Alert.tsx +164 -0
  54. package/src/components/Alert/index.ts +2 -0
  55. package/src/components/AlertDialog/AlertDialog.stories.tsx +330 -0
  56. package/src/components/AlertDialog/AlertDialog.tsx +234 -0
  57. package/src/components/AlertDialog/index.ts +2 -0
  58. package/src/components/Avatar/Avatar.stories.tsx +154 -0
  59. package/src/components/Avatar/Avatar.tsx +219 -0
  60. package/src/components/Avatar/index.ts +2 -0
  61. package/src/components/Badge/Badge.stories.tsx +146 -0
  62. package/src/components/Badge/Badge.tsx +125 -0
  63. package/src/components/Badge/index.ts +2 -0
  64. package/src/components/Box/Box.stories.tsx +192 -0
  65. package/src/components/Box/Box.tsx +184 -0
  66. package/src/components/Box/index.ts +2 -0
  67. package/src/components/Button/Button.stories.tsx +157 -0
  68. package/src/components/Button/Button.tsx +180 -0
  69. package/src/components/Button/index.ts +2 -0
  70. package/src/components/Card/Card.stories.tsx +145 -0
  71. package/src/components/Card/Card.tsx +169 -0
  72. package/src/components/Card/index.ts +11 -0
  73. package/src/components/Center/Center.stories.tsx +215 -0
  74. package/src/components/Center/Center.tsx +29 -0
  75. package/src/components/Center/index.ts +2 -0
  76. package/src/components/Checkbox/Checkbox.stories.tsx +94 -0
  77. package/src/components/Checkbox/Checkbox.tsx +242 -0
  78. package/src/components/Checkbox/index.ts +2 -0
  79. package/src/components/DatePicker/DatePicker.stories.tsx +623 -0
  80. package/src/components/DatePicker/DatePicker.tsx +1228 -0
  81. package/src/components/DatePicker/index.ts +8 -0
  82. package/src/components/Divider/Divider.stories.tsx +224 -0
  83. package/src/components/Divider/Divider.tsx +73 -0
  84. package/src/components/Divider/index.ts +2 -0
  85. package/src/components/Drawer/Drawer.stories.tsx +414 -0
  86. package/src/components/Drawer/Drawer.tsx +342 -0
  87. package/src/components/Drawer/index.ts +11 -0
  88. package/src/components/Fab/Fab.stories.tsx +360 -0
  89. package/src/components/Fab/Fab.tsx +185 -0
  90. package/src/components/Fab/index.ts +2 -0
  91. package/src/components/FormControl/FormControl.stories.tsx +276 -0
  92. package/src/components/FormControl/FormControl.tsx +185 -0
  93. package/src/components/FormControl/index.ts +12 -0
  94. package/src/components/Grid/Grid.stories.tsx +244 -0
  95. package/src/components/Grid/Grid.tsx +93 -0
  96. package/src/components/Grid/index.ts +2 -0
  97. package/src/components/HStack/HStack.stories.tsx +230 -0
  98. package/src/components/HStack/HStack.tsx +80 -0
  99. package/src/components/HStack/index.ts +2 -0
  100. package/src/components/Heading/Heading.stories.tsx +111 -0
  101. package/src/components/Heading/Heading.tsx +85 -0
  102. package/src/components/Heading/index.ts +2 -0
  103. package/src/components/Icon/Icon.stories.tsx +320 -0
  104. package/src/components/Icon/Icon.tsx +117 -0
  105. package/src/components/Icon/index.ts +2 -0
  106. package/src/components/Image/Image.stories.tsx +357 -0
  107. package/src/components/Image/Image.tsx +168 -0
  108. package/src/components/Image/index.ts +2 -0
  109. package/src/components/Input/Input.stories.tsx +164 -0
  110. package/src/components/Input/Input.tsx +274 -0
  111. package/src/components/Input/index.ts +2 -0
  112. package/src/components/Link/Link.stories.tsx +187 -0
  113. package/src/components/Link/Link.tsx +104 -0
  114. package/src/components/Link/index.ts +2 -0
  115. package/src/components/Menu/Menu.stories.tsx +363 -0
  116. package/src/components/Menu/Menu.tsx +238 -0
  117. package/src/components/Menu/index.ts +2 -0
  118. package/src/components/Modal/Modal.stories.tsx +156 -0
  119. package/src/components/Modal/Modal.tsx +280 -0
  120. package/src/components/Modal/index.ts +11 -0
  121. package/src/components/Popover/Popover.stories.tsx +330 -0
  122. package/src/components/Popover/Popover.tsx +315 -0
  123. package/src/components/Popover/index.ts +11 -0
  124. package/src/components/Portal/Portal.stories.tsx +376 -0
  125. package/src/components/Portal/Portal.tsx +100 -0
  126. package/src/components/Portal/index.ts +2 -0
  127. package/src/components/Pressable/Pressable.stories.tsx +338 -0
  128. package/src/components/Pressable/Pressable.tsx +71 -0
  129. package/src/components/Pressable/index.ts +2 -0
  130. package/src/components/Progress/Progress.stories.tsx +131 -0
  131. package/src/components/Progress/Progress.tsx +219 -0
  132. package/src/components/Progress/index.ts +2 -0
  133. package/src/components/Radio/Radio.stories.tsx +101 -0
  134. package/src/components/Radio/Radio.tsx +234 -0
  135. package/src/components/Radio/index.ts +2 -0
  136. package/src/components/Select/Select.stories.tsx +908 -0
  137. package/src/components/Select/Select.tsx +659 -0
  138. package/src/components/Select/index.ts +8 -0
  139. package/src/components/Skeleton/Skeleton.stories.tsx +154 -0
  140. package/src/components/Skeleton/Skeleton.tsx +192 -0
  141. package/src/components/Skeleton/index.ts +8 -0
  142. package/src/components/Slider/Slider.stories.tsx +363 -0
  143. package/src/components/Slider/Slider.tsx +209 -0
  144. package/src/components/Slider/index.ts +2 -0
  145. package/src/components/Spinner/Spinner.stories.tsx +108 -0
  146. package/src/components/Spinner/Spinner.tsx +121 -0
  147. package/src/components/Spinner/index.ts +2 -0
  148. package/src/components/Switch/Switch.stories.tsx +116 -0
  149. package/src/components/Switch/Switch.tsx +172 -0
  150. package/src/components/Switch/index.ts +2 -0
  151. package/src/components/Table/Table.stories.tsx +417 -0
  152. package/src/components/Table/Table.tsx +233 -0
  153. package/src/components/Table/index.ts +2 -0
  154. package/src/components/Text/Text.stories.tsx +93 -0
  155. package/src/components/Text/Text.tsx +119 -0
  156. package/src/components/Text/index.ts +2 -0
  157. package/src/components/Textarea/Textarea.stories.tsx +280 -0
  158. package/src/components/Textarea/Textarea.tsx +212 -0
  159. package/src/components/Textarea/index.ts +2 -0
  160. package/src/components/Toast/Toast.stories.tsx +446 -0
  161. package/src/components/Toast/Toast.tsx +221 -0
  162. package/src/components/Toast/index.ts +2 -0
  163. package/src/components/Tooltip/Tooltip.stories.tsx +354 -0
  164. package/src/components/Tooltip/Tooltip.tsx +261 -0
  165. package/src/components/Tooltip/index.ts +2 -0
  166. package/src/components/VStack/VStack.stories.tsx +183 -0
  167. package/src/components/VStack/VStack.tsx +76 -0
  168. package/src/components/VStack/index.ts +2 -0
  169. package/src/components/index.ts +62 -0
  170. package/src/hooks/index.ts +7 -0
  171. package/src/hooks/useControllableState.ts +41 -0
  172. package/src/hooks/useDisclosure.ts +51 -0
  173. package/src/index.ts +22 -0
  174. package/src/stories/Button.stories.tsx +53 -0
  175. package/src/stories/Button.tsx +101 -0
  176. package/src/stories/Configure.mdx +364 -0
  177. package/src/stories/Header.stories.tsx +33 -0
  178. package/src/stories/Header.tsx +75 -0
  179. package/src/stories/Page.stories.tsx +25 -0
  180. package/src/stories/Page.tsx +154 -0
  181. package/src/stories/assets/accessibility.png +0 -0
  182. package/src/stories/assets/accessibility.svg +1 -0
  183. package/src/stories/assets/addon-library.png +0 -0
  184. package/src/stories/assets/assets.png +0 -0
  185. package/src/stories/assets/avif-test-image.avif +0 -0
  186. package/src/stories/assets/context.png +0 -0
  187. package/src/stories/assets/discord.svg +1 -0
  188. package/src/stories/assets/docs.png +0 -0
  189. package/src/stories/assets/figma-plugin.png +0 -0
  190. package/src/stories/assets/github.svg +1 -0
  191. package/src/stories/assets/share.png +0 -0
  192. package/src/stories/assets/styling.png +0 -0
  193. package/src/stories/assets/testing.png +0 -0
  194. package/src/stories/assets/theming.png +0 -0
  195. package/src/stories/assets/tutorials.svg +1 -0
  196. package/src/stories/assets/youtube.svg +1 -0
  197. package/src/styles/index.ts +7 -0
  198. package/src/styles/tokens.ts +318 -0
  199. package/src/styles/unistyles.ts +254 -0
  200. package/src/utils/createContext.tsx +25 -0
  201. package/src/utils/index.ts +7 -0
  202. package/src/utils/mergeRefs.ts +21 -0
  203. package/tsconfig.json +26 -0
  204. package/urbint-cl-1.0.0.tgz +0 -0
  205. package/vitest.config.ts +37 -0
  206. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Progress Component
3
+ * Progress bar and circular progress indicators
4
+ */
5
+
6
+ import React, { forwardRef, useEffect, useRef } from 'react';
7
+ import { View, ViewProps, Text, Animated, StyleSheet } from 'react-native';
8
+ import Svg, { Circle } from 'react-native-svg';
9
+ import { colors, spacing, borderRadius, typography } from '../../styles/tokens';
10
+
11
+ export interface ProgressProps extends ViewProps {
12
+ /** Progress value (0-100) */
13
+ value?: number;
14
+ /** Indeterminate state */
15
+ isIndeterminate?: boolean;
16
+ /** Progress size */
17
+ size?: 'sm' | 'md' | 'lg';
18
+ /** Color scheme */
19
+ colorScheme?: 'primary' | 'success' | 'warning' | 'danger';
20
+ /** Show label */
21
+ showLabel?: boolean;
22
+ /** Label position */
23
+ labelPosition?: 'inside' | 'outside';
24
+ /** Progress variant */
25
+ variant?: 'linear' | 'circular';
26
+ }
27
+
28
+ const sizeConfig = {
29
+ sm: { height: 4, circleSize: 32, strokeWidth: 3 },
30
+ md: { height: 8, circleSize: 48, strokeWidth: 4 },
31
+ lg: { height: 12, circleSize: 64, strokeWidth: 5 },
32
+ } as const;
33
+
34
+ const colorMap = {
35
+ primary: colors.brand.blue,
36
+ success: colors.feedback.success.content,
37
+ warning: colors.feedback.warning.content,
38
+ danger: colors.feedback.error.content,
39
+ };
40
+
41
+ export const Progress = forwardRef<View, ProgressProps>(
42
+ (
43
+ {
44
+ style,
45
+ value = 0,
46
+ isIndeterminate = false,
47
+ size = 'md',
48
+ colorScheme = 'primary',
49
+ showLabel = false,
50
+ labelPosition = 'outside',
51
+ variant = 'linear',
52
+ ...props
53
+ },
54
+ ref
55
+ ) => {
56
+ const config = sizeConfig[size];
57
+ const clampedValue = Math.max(0, Math.min(100, value));
58
+ const progressColor = colorMap[colorScheme];
59
+
60
+ const indeterminateAnim = useRef(new Animated.Value(0)).current;
61
+
62
+ useEffect(() => {
63
+ if (isIndeterminate) {
64
+ Animated.loop(
65
+ Animated.sequence([
66
+ Animated.timing(indeterminateAnim, {
67
+ toValue: 1,
68
+ duration: 1000,
69
+ useNativeDriver: true,
70
+ }),
71
+ Animated.timing(indeterminateAnim, {
72
+ toValue: 0,
73
+ duration: 1000,
74
+ useNativeDriver: true,
75
+ }),
76
+ ])
77
+ ).start();
78
+ }
79
+ }, [isIndeterminate]);
80
+
81
+ if (variant === 'circular') {
82
+ const circumference = 2 * Math.PI * ((config.circleSize - config.strokeWidth) / 2);
83
+ const strokeDashoffset = circumference - (clampedValue / 100) * circumference;
84
+
85
+ return (
86
+ <View ref={ref} style={[styles.circularContainer, style]} {...props}>
87
+ <Svg width={config.circleSize} height={config.circleSize}>
88
+ <Circle
89
+ cx={config.circleSize / 2}
90
+ cy={config.circleSize / 2}
91
+ r={(config.circleSize - config.strokeWidth) / 2}
92
+ stroke={colors.border.disabled}
93
+ strokeWidth={config.strokeWidth}
94
+ fill="none"
95
+ />
96
+ <Circle
97
+ cx={config.circleSize / 2}
98
+ cy={config.circleSize / 2}
99
+ r={(config.circleSize - config.strokeWidth) / 2}
100
+ stroke={progressColor}
101
+ strokeWidth={config.strokeWidth}
102
+ fill="none"
103
+ strokeDasharray={`${circumference} ${circumference}`}
104
+ strokeDashoffset={strokeDashoffset}
105
+ strokeLinecap="round"
106
+ transform={`rotate(-90 ${config.circleSize / 2} ${config.circleSize / 2})`}
107
+ />
108
+ </Svg>
109
+ {showLabel && (
110
+ <Text style={[styles.circularLabel, styles[`${size}Label` as keyof typeof styles]]}>{clampedValue}%</Text>
111
+ )}
112
+ </View>
113
+ );
114
+ }
115
+
116
+ const translateX = isIndeterminate
117
+ ? indeterminateAnim.interpolate({
118
+ inputRange: [0, 1],
119
+ outputRange: ['-100%', '100%'],
120
+ })
121
+ : undefined;
122
+
123
+ return (
124
+ <View ref={ref} style={[styles.container, style]} {...props}>
125
+ {showLabel && labelPosition === 'outside' && (
126
+ <View style={styles.labelContainer}>
127
+ <Text style={styles.label}>{clampedValue}%</Text>
128
+ </View>
129
+ )}
130
+ <View style={[styles.track, { height: config.height }]}>
131
+ {isIndeterminate ? (
132
+ <Animated.View
133
+ style={[
134
+ styles.indeterminateBar,
135
+ {
136
+ height: config.height,
137
+ backgroundColor: progressColor,
138
+ transform: [{ translateX: translateX as any }],
139
+ },
140
+ ]}
141
+ />
142
+ ) : (
143
+ <View
144
+ style={[
145
+ styles.filledTrack,
146
+ {
147
+ width: `${clampedValue}%`,
148
+ height: config.height,
149
+ backgroundColor: progressColor,
150
+ },
151
+ ]}
152
+ >
153
+ {showLabel && labelPosition === 'inside' && size === 'lg' && (
154
+ <Text style={styles.insideLabel}>{clampedValue}%</Text>
155
+ )}
156
+ </View>
157
+ )}
158
+ </View>
159
+ </View>
160
+ );
161
+ }
162
+ );
163
+
164
+ Progress.displayName = 'Progress';
165
+
166
+ const styles = StyleSheet.create({
167
+ container: {
168
+ width: '100%',
169
+ },
170
+ track: {
171
+ width: '100%',
172
+ backgroundColor: colors.border.disabled,
173
+ borderRadius: borderRadius.full,
174
+ overflow: 'hidden',
175
+ },
176
+ filledTrack: {
177
+ borderRadius: borderRadius.full,
178
+ justifyContent: 'center',
179
+ alignItems: 'flex-end',
180
+ },
181
+ indeterminateBar: {
182
+ width: '50%',
183
+ borderRadius: borderRadius.full,
184
+ },
185
+ labelContainer: {
186
+ marginBottom: spacing.base,
187
+ },
188
+ label: {
189
+ fontSize: typography.fontSize.caption,
190
+ fontWeight: typography.fontWeight.medium,
191
+ color: colors.text.default,
192
+ textAlign: 'right',
193
+ },
194
+ insideLabel: {
195
+ fontSize: 10,
196
+ fontWeight: typography.fontWeight.semiBold,
197
+ color: colors.white,
198
+ paddingRight: spacing.base,
199
+ },
200
+ circularContainer: {
201
+ position: 'relative',
202
+ alignItems: 'center',
203
+ justifyContent: 'center',
204
+ },
205
+ circularLabel: {
206
+ position: 'absolute',
207
+ fontWeight: typography.fontWeight.semiBold,
208
+ color: colors.text.default,
209
+ },
210
+ smLabel: {
211
+ fontSize: 8,
212
+ },
213
+ mdLabel: {
214
+ fontSize: 10,
215
+ },
216
+ lgLabel: {
217
+ fontSize: 12,
218
+ },
219
+ });
@@ -0,0 +1,2 @@
1
+ export { Progress, type ProgressProps } from './Progress';
2
+
@@ -0,0 +1,101 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import { Radio, RadioGroup } from './Radio';
4
+ import { VStack } from '../VStack';
5
+ import { colors, spacing, borderRadius } from '../../styles/tokens';
6
+
7
+ /**
8
+ * Story container with design system tokens
9
+ */
10
+ const StoryContainer: React.FC<{ children: React.ReactNode }> = ({ children }) => (
11
+ <View style={styles.container}>{children}</View>
12
+ );
13
+
14
+ const meta: Meta<typeof Radio> = {
15
+ title: 'Components/Radio',
16
+ component: Radio,
17
+ decorators: [
18
+ (Story) => (
19
+ <StoryContainer>
20
+ <Story />
21
+ </StoryContainer>
22
+ ),
23
+ ],
24
+ };
25
+
26
+ export default meta;
27
+
28
+ type Story = StoryObj<typeof Radio>;
29
+
30
+ export const Default: Story = {
31
+ render: () => (
32
+ <RadioGroup defaultValue="option1">
33
+ <Radio value="option1" label="Option 1" />
34
+ <Radio value="option2" label="Option 2" />
35
+ <Radio value="option3" label="Option 3" />
36
+ </RadioGroup>
37
+ ),
38
+ };
39
+
40
+ export const Sizes: Story = {
41
+ render: () => (
42
+ <VStack space={spacing.lg}>
43
+ <RadioGroup defaultValue="sm" size="sm" label="Small">
44
+ <Radio value="sm" label="Small option" />
45
+ </RadioGroup>
46
+ <RadioGroup defaultValue="md" size="md" label="Medium">
47
+ <Radio value="md" label="Medium option" />
48
+ </RadioGroup>
49
+ <RadioGroup defaultValue="lg" size="lg" label="Large">
50
+ <Radio value="lg" label="Large option" />
51
+ </RadioGroup>
52
+ </VStack>
53
+ ),
54
+ };
55
+
56
+ export const ColorSchemes: Story = {
57
+ render: () => (
58
+ <VStack space={spacing.lg}>
59
+ <RadioGroup defaultValue="primary" colorScheme="primary" label="Primary">
60
+ <Radio value="primary" label="Primary radio" />
61
+ </RadioGroup>
62
+ <RadioGroup defaultValue="success" colorScheme="success" label="Success">
63
+ <Radio value="success" label="Success radio" />
64
+ </RadioGroup>
65
+ <RadioGroup defaultValue="danger" colorScheme="danger" label="Danger">
66
+ <Radio value="danger" label="Danger radio" />
67
+ </RadioGroup>
68
+ </VStack>
69
+ ),
70
+ };
71
+
72
+ export const Horizontal: Story = {
73
+ render: () => (
74
+ <RadioGroup defaultValue="yes" direction="row" label="Subscribe to newsletter?">
75
+ <Radio value="yes" label="Yes" />
76
+ <Radio value="no" label="No" />
77
+ <Radio value="maybe" label="Maybe" />
78
+ </RadioGroup>
79
+ ),
80
+ };
81
+
82
+ export const Disabled: Story = {
83
+ render: () => (
84
+ <RadioGroup defaultValue="option1" isDisabled label="Disabled group">
85
+ <Radio value="option1" label="Option 1" />
86
+ <Radio value="option2" label="Option 2" />
87
+ </RadioGroup>
88
+ ),
89
+ };
90
+
91
+ /**
92
+ * Styles using design system tokens
93
+ */
94
+ const styles = StyleSheet.create({
95
+ container: {
96
+ padding: spacing.lg,
97
+ backgroundColor: colors.background.default,
98
+ borderRadius: borderRadius.lg,
99
+ },
100
+ });
101
+
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Radio Component
3
+ * Radio button for single selection
4
+ */
5
+
6
+ import React, { forwardRef, createContext, useContext } from 'react';
7
+ import { View, ViewProps, Pressable, Text, StyleSheet } from 'react-native';
8
+ import { colors, spacing, typography } from '../../styles/tokens';
9
+
10
+ interface RadioGroupContextValue {
11
+ value?: string;
12
+ onChange?: (value: string) => void;
13
+ isDisabled?: boolean;
14
+ size?: 'sm' | 'md' | 'lg';
15
+ colorScheme?: 'primary' | 'success' | 'danger';
16
+ }
17
+
18
+ const RadioGroupContext = createContext<RadioGroupContextValue>({});
19
+
20
+ export interface RadioProps extends Omit<ViewProps, 'children'> {
21
+ /** Radio value */
22
+ value: string;
23
+ /** Radio label */
24
+ label?: string;
25
+ /** Is disabled */
26
+ isDisabled?: boolean;
27
+ /** Is invalid */
28
+ isInvalid?: boolean;
29
+ /** Radio size */
30
+ size?: 'sm' | 'md' | 'lg';
31
+ /** Radio color */
32
+ colorScheme?: 'primary' | 'success' | 'danger';
33
+ }
34
+
35
+ const sizeMap = {
36
+ sm: 16,
37
+ md: 20,
38
+ lg: 24,
39
+ } as const;
40
+
41
+ const colorMap = {
42
+ primary: colors.brand.blue,
43
+ success: colors.feedback.success.content,
44
+ danger: colors.feedback.error.content,
45
+ };
46
+
47
+ export const Radio = forwardRef<View, RadioProps>(
48
+ (
49
+ {
50
+ style,
51
+ value,
52
+ label,
53
+ isDisabled: localDisabled,
54
+ isInvalid = false,
55
+ size: localSize,
56
+ colorScheme: localColorScheme,
57
+ ...props
58
+ },
59
+ ref
60
+ ) => {
61
+ const group = useContext(RadioGroupContext);
62
+
63
+ const isDisabled = localDisabled ?? group.isDisabled ?? false;
64
+ const size = localSize ?? group.size ?? 'md';
65
+ const colorScheme = localColorScheme ?? group.colorScheme ?? 'primary';
66
+ const isSelected = group.value === value;
67
+ const radioSize = sizeMap[size];
68
+ const activeColor = colorMap[colorScheme];
69
+
70
+ const handlePress = () => {
71
+ if (isDisabled) return;
72
+ group.onChange?.(value);
73
+ };
74
+
75
+ return (
76
+ <Pressable
77
+ ref={ref}
78
+ onPress={handlePress}
79
+ disabled={isDisabled}
80
+ style={[styles.container, style]}
81
+ accessibilityRole="radio"
82
+ accessibilityState={{ checked: isSelected, disabled: isDisabled }}
83
+ {...props}
84
+ >
85
+ <View
86
+ style={[
87
+ styles.radio,
88
+ { width: radioSize, height: radioSize },
89
+ isSelected && { borderColor: activeColor },
90
+ isInvalid && styles.invalid,
91
+ isDisabled && styles.disabled,
92
+ ]}
93
+ >
94
+ {isSelected && (
95
+ <View
96
+ style={[
97
+ styles.innerDot,
98
+ {
99
+ width: radioSize / 2,
100
+ height: radioSize / 2,
101
+ backgroundColor: isDisabled ? colors.text.disabled : activeColor,
102
+ },
103
+ ]}
104
+ />
105
+ )}
106
+ </View>
107
+ {label && (
108
+ <Text style={[styles.label, styles[`${size}Label` as keyof typeof styles], isDisabled && styles.labelDisabled]}>
109
+ {label}
110
+ </Text>
111
+ )}
112
+ </Pressable>
113
+ );
114
+ }
115
+ );
116
+
117
+ Radio.displayName = 'Radio';
118
+
119
+ export interface RadioGroupProps extends ViewProps {
120
+ /** Group label */
121
+ label?: string;
122
+ /** Selected value */
123
+ value?: string;
124
+ /** Default selected value */
125
+ defaultValue?: string;
126
+ /** On change handler */
127
+ onChange?: (value: string) => void;
128
+ /** Is disabled */
129
+ isDisabled?: boolean;
130
+ /** Direction */
131
+ direction?: 'row' | 'column';
132
+ /** Radio size */
133
+ size?: 'sm' | 'md' | 'lg';
134
+ /** Color scheme */
135
+ colorScheme?: 'primary' | 'success' | 'danger';
136
+ }
137
+
138
+ export const RadioGroup = forwardRef<View, RadioGroupProps>(
139
+ (
140
+ {
141
+ style,
142
+ label,
143
+ value: controlledValue,
144
+ defaultValue,
145
+ onChange,
146
+ isDisabled = false,
147
+ direction = 'column',
148
+ size = 'md',
149
+ colorScheme = 'primary',
150
+ children,
151
+ ...props
152
+ },
153
+ ref
154
+ ) => {
155
+ const [internalValue, setInternalValue] = React.useState(defaultValue);
156
+ const isControlled = controlledValue !== undefined;
157
+ const value = isControlled ? controlledValue : internalValue;
158
+
159
+ const handleChange = (newValue: string) => {
160
+ if (!isControlled) {
161
+ setInternalValue(newValue);
162
+ }
163
+ onChange?.(newValue);
164
+ };
165
+
166
+ return (
167
+ <RadioGroupContext.Provider
168
+ value={{ value, onChange: handleChange, isDisabled, size, colorScheme }}
169
+ >
170
+ <View ref={ref} style={[styles.group, style]} {...props}>
171
+ {label && <Text style={styles.groupLabel}>{label}</Text>}
172
+ <View
173
+ style={[styles.groupContent, { flexDirection: direction }]}
174
+ accessibilityRole="radiogroup"
175
+ >
176
+ {children}
177
+ </View>
178
+ </View>
179
+ </RadioGroupContext.Provider>
180
+ );
181
+ }
182
+ );
183
+
184
+ RadioGroup.displayName = 'RadioGroup';
185
+
186
+ const styles = StyleSheet.create({
187
+ container: {
188
+ flexDirection: 'row',
189
+ alignItems: 'center',
190
+ },
191
+ radio: {
192
+ borderWidth: 2,
193
+ borderColor: colors.border.default,
194
+ borderRadius: 9999,
195
+ alignItems: 'center',
196
+ justifyContent: 'center',
197
+ },
198
+ innerDot: {
199
+ borderRadius: 9999,
200
+ },
201
+ invalid: {
202
+ borderColor: colors.border.danger,
203
+ },
204
+ disabled: {
205
+ backgroundColor: colors.background.secondary,
206
+ borderColor: colors.border.disabled,
207
+ },
208
+ label: {
209
+ marginLeft: spacing['2x'],
210
+ color: colors.text.default,
211
+ },
212
+ smLabel: {
213
+ fontSize: typography.fontSize.small,
214
+ },
215
+ mdLabel: {
216
+ fontSize: typography.fontSize.body,
217
+ },
218
+ lgLabel: {
219
+ fontSize: typography.fontSize.body,
220
+ },
221
+ labelDisabled: {
222
+ color: colors.text.disabled,
223
+ },
224
+ group: {},
225
+ groupLabel: {
226
+ fontSize: typography.fontSize.componentLabel,
227
+ fontWeight: typography.fontWeight.semiBold,
228
+ color: colors.text.default,
229
+ marginBottom: spacing['2x'],
230
+ },
231
+ groupContent: {
232
+ gap: spacing['3x'],
233
+ },
234
+ });
@@ -0,0 +1,2 @@
1
+ export { Radio, RadioGroup, type RadioProps, type RadioGroupProps } from './Radio';
2
+