@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.
- package/.cursor/rules +313 -0
- package/.rnstorybook/index.ts +11 -0
- package/.rnstorybook/main.ts +8 -0
- package/.rnstorybook/preview.tsx +14 -0
- package/.rnstorybook/storybook.requires.ts +49 -0
- package/.storybook/main.ts +16 -0
- package/.storybook/preview.ts +32 -0
- package/.storybook/vitest.setup.ts +7 -0
- package/App.tsx +422 -0
- package/README.md +229 -0
- package/app.json +33 -0
- package/assets/adaptive-icon.png +0 -0
- package/assets/favicon.png +0 -0
- package/assets/icon.png +0 -0
- package/assets/splash-icon.png +0 -0
- package/babel.config.js +16 -0
- package/docs/components/CodeBlock.tsx +80 -0
- package/docs/components/PropTable.tsx +93 -0
- package/docs/components/Sidebar.tsx +199 -0
- package/docs/components/index.ts +8 -0
- package/docs/data/colorTokens.ts +70 -0
- package/docs/data/componentData.tsx +1685 -0
- package/docs/data/index.ts +7 -0
- package/docs/index.ts +19 -0
- package/docs/navigation.ts +94 -0
- package/docs/pages/ColorsPage.tsx +226 -0
- package/docs/pages/ComponentPage.tsx +235 -0
- package/docs/pages/InstallationPage.tsx +232 -0
- package/docs/pages/IntroductionPage.tsx +163 -0
- package/docs/pages/ThemingPage.tsx +251 -0
- package/docs/pages/index.ts +10 -0
- package/docs/theme.ts +64 -0
- package/docs/types.ts +54 -0
- package/index.ts +8 -0
- package/llms.txt +1893 -0
- package/mcp-config.example.json +10 -0
- package/mcp-server/README.md +192 -0
- package/mcp-server/package-lock.json +1707 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/src/index.ts +1136 -0
- package/mcp-server/src/registry/components.ts +1446 -0
- package/mcp-server/src/registry/index.ts +3 -0
- package/mcp-server/src/registry/tokens.ts +256 -0
- package/mcp-server/tsconfig.json +19 -0
- package/package.json +92 -0
- package/src/components/Accordion/Accordion.stories.tsx +226 -0
- package/src/components/Accordion/Accordion.tsx +255 -0
- package/src/components/Accordion/index.ts +12 -0
- package/src/components/ActionSheet/ActionSheet.stories.tsx +393 -0
- package/src/components/ActionSheet/ActionSheet.tsx +258 -0
- package/src/components/ActionSheet/index.ts +2 -0
- package/src/components/Alert/Alert.stories.tsx +165 -0
- package/src/components/Alert/Alert.tsx +164 -0
- package/src/components/Alert/index.ts +2 -0
- package/src/components/AlertDialog/AlertDialog.stories.tsx +330 -0
- package/src/components/AlertDialog/AlertDialog.tsx +234 -0
- package/src/components/AlertDialog/index.ts +2 -0
- package/src/components/Avatar/Avatar.stories.tsx +154 -0
- package/src/components/Avatar/Avatar.tsx +219 -0
- package/src/components/Avatar/index.ts +2 -0
- package/src/components/Badge/Badge.stories.tsx +146 -0
- package/src/components/Badge/Badge.tsx +125 -0
- package/src/components/Badge/index.ts +2 -0
- package/src/components/Box/Box.stories.tsx +192 -0
- package/src/components/Box/Box.tsx +184 -0
- package/src/components/Box/index.ts +2 -0
- package/src/components/Button/Button.stories.tsx +157 -0
- package/src/components/Button/Button.tsx +180 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Card/Card.stories.tsx +145 -0
- package/src/components/Card/Card.tsx +169 -0
- package/src/components/Card/index.ts +11 -0
- package/src/components/Center/Center.stories.tsx +215 -0
- package/src/components/Center/Center.tsx +29 -0
- package/src/components/Center/index.ts +2 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +94 -0
- package/src/components/Checkbox/Checkbox.tsx +242 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/DatePicker/DatePicker.stories.tsx +623 -0
- package/src/components/DatePicker/DatePicker.tsx +1228 -0
- package/src/components/DatePicker/index.ts +8 -0
- package/src/components/Divider/Divider.stories.tsx +224 -0
- package/src/components/Divider/Divider.tsx +73 -0
- package/src/components/Divider/index.ts +2 -0
- package/src/components/Drawer/Drawer.stories.tsx +414 -0
- package/src/components/Drawer/Drawer.tsx +342 -0
- package/src/components/Drawer/index.ts +11 -0
- package/src/components/Fab/Fab.stories.tsx +360 -0
- package/src/components/Fab/Fab.tsx +185 -0
- package/src/components/Fab/index.ts +2 -0
- package/src/components/FormControl/FormControl.stories.tsx +276 -0
- package/src/components/FormControl/FormControl.tsx +185 -0
- package/src/components/FormControl/index.ts +12 -0
- package/src/components/Grid/Grid.stories.tsx +244 -0
- package/src/components/Grid/Grid.tsx +93 -0
- package/src/components/Grid/index.ts +2 -0
- package/src/components/HStack/HStack.stories.tsx +230 -0
- package/src/components/HStack/HStack.tsx +80 -0
- package/src/components/HStack/index.ts +2 -0
- package/src/components/Heading/Heading.stories.tsx +111 -0
- package/src/components/Heading/Heading.tsx +85 -0
- package/src/components/Heading/index.ts +2 -0
- package/src/components/Icon/Icon.stories.tsx +320 -0
- package/src/components/Icon/Icon.tsx +117 -0
- package/src/components/Icon/index.ts +2 -0
- package/src/components/Image/Image.stories.tsx +357 -0
- package/src/components/Image/Image.tsx +168 -0
- package/src/components/Image/index.ts +2 -0
- package/src/components/Input/Input.stories.tsx +164 -0
- package/src/components/Input/Input.tsx +274 -0
- package/src/components/Input/index.ts +2 -0
- package/src/components/Link/Link.stories.tsx +187 -0
- package/src/components/Link/Link.tsx +104 -0
- package/src/components/Link/index.ts +2 -0
- package/src/components/Menu/Menu.stories.tsx +363 -0
- package/src/components/Menu/Menu.tsx +238 -0
- package/src/components/Menu/index.ts +2 -0
- package/src/components/Modal/Modal.stories.tsx +156 -0
- package/src/components/Modal/Modal.tsx +280 -0
- package/src/components/Modal/index.ts +11 -0
- package/src/components/Popover/Popover.stories.tsx +330 -0
- package/src/components/Popover/Popover.tsx +315 -0
- package/src/components/Popover/index.ts +11 -0
- package/src/components/Portal/Portal.stories.tsx +376 -0
- package/src/components/Portal/Portal.tsx +100 -0
- package/src/components/Portal/index.ts +2 -0
- package/src/components/Pressable/Pressable.stories.tsx +338 -0
- package/src/components/Pressable/Pressable.tsx +71 -0
- package/src/components/Pressable/index.ts +2 -0
- package/src/components/Progress/Progress.stories.tsx +131 -0
- package/src/components/Progress/Progress.tsx +219 -0
- package/src/components/Progress/index.ts +2 -0
- package/src/components/Radio/Radio.stories.tsx +101 -0
- package/src/components/Radio/Radio.tsx +234 -0
- package/src/components/Radio/index.ts +2 -0
- package/src/components/Select/Select.stories.tsx +908 -0
- package/src/components/Select/Select.tsx +659 -0
- package/src/components/Select/index.ts +8 -0
- package/src/components/Skeleton/Skeleton.stories.tsx +154 -0
- package/src/components/Skeleton/Skeleton.tsx +192 -0
- package/src/components/Skeleton/index.ts +8 -0
- package/src/components/Slider/Slider.stories.tsx +363 -0
- package/src/components/Slider/Slider.tsx +209 -0
- package/src/components/Slider/index.ts +2 -0
- package/src/components/Spinner/Spinner.stories.tsx +108 -0
- package/src/components/Spinner/Spinner.tsx +121 -0
- package/src/components/Spinner/index.ts +2 -0
- package/src/components/Switch/Switch.stories.tsx +116 -0
- package/src/components/Switch/Switch.tsx +172 -0
- package/src/components/Switch/index.ts +2 -0
- package/src/components/Table/Table.stories.tsx +417 -0
- package/src/components/Table/Table.tsx +233 -0
- package/src/components/Table/index.ts +2 -0
- package/src/components/Text/Text.stories.tsx +93 -0
- package/src/components/Text/Text.tsx +119 -0
- package/src/components/Text/index.ts +2 -0
- package/src/components/Textarea/Textarea.stories.tsx +280 -0
- package/src/components/Textarea/Textarea.tsx +212 -0
- package/src/components/Textarea/index.ts +2 -0
- package/src/components/Toast/Toast.stories.tsx +446 -0
- package/src/components/Toast/Toast.tsx +221 -0
- package/src/components/Toast/index.ts +2 -0
- package/src/components/Tooltip/Tooltip.stories.tsx +354 -0
- package/src/components/Tooltip/Tooltip.tsx +261 -0
- package/src/components/Tooltip/index.ts +2 -0
- package/src/components/VStack/VStack.stories.tsx +183 -0
- package/src/components/VStack/VStack.tsx +76 -0
- package/src/components/VStack/index.ts +2 -0
- package/src/components/index.ts +62 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useControllableState.ts +41 -0
- package/src/hooks/useDisclosure.ts +51 -0
- package/src/index.ts +22 -0
- package/src/stories/Button.stories.tsx +53 -0
- package/src/stories/Button.tsx +101 -0
- package/src/stories/Configure.mdx +364 -0
- package/src/stories/Header.stories.tsx +33 -0
- package/src/stories/Header.tsx +75 -0
- package/src/stories/Page.stories.tsx +25 -0
- package/src/stories/Page.tsx +154 -0
- package/src/stories/assets/accessibility.png +0 -0
- package/src/stories/assets/accessibility.svg +1 -0
- package/src/stories/assets/addon-library.png +0 -0
- package/src/stories/assets/assets.png +0 -0
- package/src/stories/assets/avif-test-image.avif +0 -0
- package/src/stories/assets/context.png +0 -0
- package/src/stories/assets/discord.svg +1 -0
- package/src/stories/assets/docs.png +0 -0
- package/src/stories/assets/figma-plugin.png +0 -0
- package/src/stories/assets/github.svg +1 -0
- package/src/stories/assets/share.png +0 -0
- package/src/stories/assets/styling.png +0 -0
- package/src/stories/assets/testing.png +0 -0
- package/src/stories/assets/theming.png +0 -0
- package/src/stories/assets/tutorials.svg +1 -0
- package/src/stories/assets/youtube.svg +1 -0
- package/src/styles/index.ts +7 -0
- package/src/styles/tokens.ts +318 -0
- package/src/styles/unistyles.ts +254 -0
- package/src/utils/createContext.tsx +25 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/mergeRefs.ts +21 -0
- package/tsconfig.json +26 -0
- package/urbint-cl-1.0.0.tgz +0 -0
- package/vitest.config.ts +37 -0
- package/vitest.shims.d.ts +1 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fab Component
|
|
3
|
+
* Floating Action Button
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { forwardRef, useState } from 'react';
|
|
7
|
+
import { View, Pressable, PressableProps, Text, StyleSheet } from 'react-native';
|
|
8
|
+
import Svg, { Path } from 'react-native-svg';
|
|
9
|
+
import { colors, spacing, typography, elevation } from '../../styles/tokens';
|
|
10
|
+
|
|
11
|
+
export interface FabProps extends Omit<PressableProps, 'style'> {
|
|
12
|
+
/** Icon element */
|
|
13
|
+
icon?: React.ReactNode;
|
|
14
|
+
/** Label (for extended FAB) */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** FAB size */
|
|
17
|
+
size?: 'sm' | 'md' | 'lg';
|
|
18
|
+
/** FAB placement */
|
|
19
|
+
placement?: 'bottom-right' | 'bottom-left' | 'bottom-center' | 'top-right' | 'top-left';
|
|
20
|
+
/** Color scheme */
|
|
21
|
+
colorScheme?: 'primary' | 'secondary' | 'danger';
|
|
22
|
+
/** Is extended */
|
|
23
|
+
isExtended?: boolean;
|
|
24
|
+
/** Is loading */
|
|
25
|
+
isLoading?: boolean;
|
|
26
|
+
/** Is disabled */
|
|
27
|
+
isDisabled?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const sizeConfig = {
|
|
31
|
+
sm: { size: 40, iconSize: 18 },
|
|
32
|
+
md: { size: 56, iconSize: 24 },
|
|
33
|
+
lg: { size: 72, iconSize: 28 },
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export const Fab = forwardRef<View, FabProps>(
|
|
37
|
+
(
|
|
38
|
+
{
|
|
39
|
+
icon,
|
|
40
|
+
label,
|
|
41
|
+
size = 'md',
|
|
42
|
+
placement = 'bottom-right',
|
|
43
|
+
colorScheme = 'primary',
|
|
44
|
+
isExtended = false,
|
|
45
|
+
isLoading = false,
|
|
46
|
+
isDisabled = false,
|
|
47
|
+
onPressIn,
|
|
48
|
+
onPressOut,
|
|
49
|
+
...props
|
|
50
|
+
},
|
|
51
|
+
ref
|
|
52
|
+
) => {
|
|
53
|
+
const [isPressed, setIsPressed] = useState(false);
|
|
54
|
+
const config = sizeConfig[size];
|
|
55
|
+
|
|
56
|
+
const handlePressIn = (e: any) => {
|
|
57
|
+
setIsPressed(true);
|
|
58
|
+
onPressIn?.(e);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handlePressOut = (e: any) => {
|
|
62
|
+
setIsPressed(false);
|
|
63
|
+
onPressOut?.(e);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const getColorSchemeStyles = () => {
|
|
67
|
+
switch (colorScheme) {
|
|
68
|
+
case 'secondary':
|
|
69
|
+
return {
|
|
70
|
+
backgroundColor: isPressed
|
|
71
|
+
? colors.secondary.active
|
|
72
|
+
: colors.secondary.default,
|
|
73
|
+
iconColor: colors.text.default,
|
|
74
|
+
};
|
|
75
|
+
case 'danger':
|
|
76
|
+
return {
|
|
77
|
+
backgroundColor: isPressed
|
|
78
|
+
? colors.danger.active
|
|
79
|
+
: colors.danger.default,
|
|
80
|
+
iconColor: colors.white,
|
|
81
|
+
};
|
|
82
|
+
default:
|
|
83
|
+
return {
|
|
84
|
+
backgroundColor: isPressed
|
|
85
|
+
? colors.brand.navy
|
|
86
|
+
: colors.brand.blue,
|
|
87
|
+
iconColor: colors.white,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const colorStyles = getColorSchemeStyles();
|
|
93
|
+
|
|
94
|
+
const getPlacementStyle = () => {
|
|
95
|
+
const base = { position: 'absolute' as const };
|
|
96
|
+
const offset = spacing['4x'];
|
|
97
|
+
|
|
98
|
+
switch (placement) {
|
|
99
|
+
case 'bottom-right':
|
|
100
|
+
return { ...base, bottom: offset, right: offset };
|
|
101
|
+
case 'bottom-left':
|
|
102
|
+
return { ...base, bottom: offset, left: offset };
|
|
103
|
+
case 'bottom-center':
|
|
104
|
+
return { ...base, bottom: offset, alignSelf: 'center' as const };
|
|
105
|
+
case 'top-right':
|
|
106
|
+
return { ...base, top: offset, right: offset };
|
|
107
|
+
case 'top-left':
|
|
108
|
+
return { ...base, top: offset, left: offset };
|
|
109
|
+
default:
|
|
110
|
+
return base;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const defaultIcon = (
|
|
115
|
+
<Svg width={config.iconSize} height={config.iconSize} viewBox="0 0 24 24" fill="none">
|
|
116
|
+
<Path
|
|
117
|
+
d="M12 5v14M5 12h14"
|
|
118
|
+
stroke={colorStyles.iconColor}
|
|
119
|
+
strokeWidth={2}
|
|
120
|
+
strokeLinecap="round"
|
|
121
|
+
strokeLinejoin="round"
|
|
122
|
+
/>
|
|
123
|
+
</Svg>
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<Pressable
|
|
128
|
+
ref={ref}
|
|
129
|
+
onPressIn={handlePressIn}
|
|
130
|
+
onPressOut={handlePressOut}
|
|
131
|
+
disabled={isDisabled || isLoading}
|
|
132
|
+
style={[
|
|
133
|
+
styles.fab,
|
|
134
|
+
getPlacementStyle(),
|
|
135
|
+
{
|
|
136
|
+
backgroundColor: colorStyles.backgroundColor,
|
|
137
|
+
width: isExtended ? 'auto' : config.size,
|
|
138
|
+
height: config.size,
|
|
139
|
+
borderRadius: config.size / 2,
|
|
140
|
+
paddingHorizontal: isExtended ? spacing['4x'] : 0,
|
|
141
|
+
},
|
|
142
|
+
isDisabled && styles.disabled,
|
|
143
|
+
]}
|
|
144
|
+
accessibilityRole="button"
|
|
145
|
+
{...props}
|
|
146
|
+
>
|
|
147
|
+
{isLoading ? (
|
|
148
|
+
<Text style={{ color: colorStyles.iconColor }}>...</Text>
|
|
149
|
+
) : (
|
|
150
|
+
<>
|
|
151
|
+
{icon || defaultIcon}
|
|
152
|
+
{isExtended && label && (
|
|
153
|
+
<Text
|
|
154
|
+
style={[
|
|
155
|
+
styles.label,
|
|
156
|
+
{ color: colorStyles.iconColor, marginLeft: spacing['2x'] },
|
|
157
|
+
]}
|
|
158
|
+
>
|
|
159
|
+
{label}
|
|
160
|
+
</Text>
|
|
161
|
+
)}
|
|
162
|
+
</>
|
|
163
|
+
)}
|
|
164
|
+
</Pressable>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
Fab.displayName = 'Fab';
|
|
170
|
+
|
|
171
|
+
const styles = StyleSheet.create({
|
|
172
|
+
fab: {
|
|
173
|
+
flexDirection: 'row',
|
|
174
|
+
alignItems: 'center',
|
|
175
|
+
justifyContent: 'center',
|
|
176
|
+
...elevation['20'],
|
|
177
|
+
},
|
|
178
|
+
disabled: {
|
|
179
|
+
opacity: 0.5,
|
|
180
|
+
},
|
|
181
|
+
label: {
|
|
182
|
+
fontSize: typography.fontSize.componentLabel,
|
|
183
|
+
fontWeight: typography.fontWeight.semiBold,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { FormControl, FormLabel, FormHelperText, FormErrorMessage } from './FormControl';
|
|
3
|
+
import { Input } from '../Input';
|
|
4
|
+
import { Textarea } from '../Textarea';
|
|
5
|
+
import { Select } from '../Select';
|
|
6
|
+
import { Checkbox } from '../Checkbox';
|
|
7
|
+
import { VStack } from '../VStack';
|
|
8
|
+
import { Text } from '../Text';
|
|
9
|
+
import { Box } from '../Box';
|
|
10
|
+
import { colors, spacing, borderRadius, elevation } from '../../styles/tokens';
|
|
11
|
+
|
|
12
|
+
const meta: Meta<typeof FormControl> = {
|
|
13
|
+
title: 'Forms/FormControl',
|
|
14
|
+
component: FormControl,
|
|
15
|
+
argTypes: {
|
|
16
|
+
isInvalid: { control: 'boolean' },
|
|
17
|
+
isDisabled: { control: 'boolean' },
|
|
18
|
+
isRequired: { control: 'boolean' },
|
|
19
|
+
isReadOnly: { control: 'boolean' },
|
|
20
|
+
},
|
|
21
|
+
args: {
|
|
22
|
+
isInvalid: false,
|
|
23
|
+
isDisabled: false,
|
|
24
|
+
isRequired: false,
|
|
25
|
+
isReadOnly: false,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default meta;
|
|
30
|
+
|
|
31
|
+
type Story = StoryObj<typeof FormControl>;
|
|
32
|
+
|
|
33
|
+
export const Default: Story = {
|
|
34
|
+
render: (args) => (
|
|
35
|
+
<FormControl {...args}>
|
|
36
|
+
<FormLabel>Email Address</FormLabel>
|
|
37
|
+
<Input placeholder="Enter your email" />
|
|
38
|
+
<FormHelperText>We'll never share your email.</FormHelperText>
|
|
39
|
+
</FormControl>
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const Required: Story = {
|
|
44
|
+
render: () => (
|
|
45
|
+
<FormControl isRequired>
|
|
46
|
+
<FormLabel>Full Name</FormLabel>
|
|
47
|
+
<Input placeholder="Enter your full name" />
|
|
48
|
+
<FormHelperText>Required field</FormHelperText>
|
|
49
|
+
</FormControl>
|
|
50
|
+
),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const WithError: Story = {
|
|
54
|
+
render: () => (
|
|
55
|
+
<FormControl isInvalid>
|
|
56
|
+
<FormLabel>Password</FormLabel>
|
|
57
|
+
<Input placeholder="Enter password" secureTextEntry />
|
|
58
|
+
<FormErrorMessage>Password must be at least 8 characters</FormErrorMessage>
|
|
59
|
+
</FormControl>
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const Disabled: Story = {
|
|
64
|
+
render: () => (
|
|
65
|
+
<FormControl isDisabled>
|
|
66
|
+
<FormLabel>Username</FormLabel>
|
|
67
|
+
<Input placeholder="Username" value="john_doe" />
|
|
68
|
+
<FormHelperText>You cannot change your username</FormHelperText>
|
|
69
|
+
</FormControl>
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const WithTextarea: Story = {
|
|
74
|
+
render: () => (
|
|
75
|
+
<VStack space={spacing.lg}>
|
|
76
|
+
<Text weight="semiBold">FormControl with Textarea</Text>
|
|
77
|
+
<FormControl>
|
|
78
|
+
<FormLabel>Bio</FormLabel>
|
|
79
|
+
<Textarea placeholder="Tell us about yourself" rows={4} />
|
|
80
|
+
<FormHelperText>Maximum 500 characters</FormHelperText>
|
|
81
|
+
</FormControl>
|
|
82
|
+
<FormControl isRequired isInvalid>
|
|
83
|
+
<FormLabel>Description</FormLabel>
|
|
84
|
+
<Textarea placeholder="Describe the issue" rows={3} />
|
|
85
|
+
<FormErrorMessage>Description is required</FormErrorMessage>
|
|
86
|
+
</FormControl>
|
|
87
|
+
</VStack>
|
|
88
|
+
),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const WithSelect: Story = {
|
|
92
|
+
render: () => (
|
|
93
|
+
<VStack space={spacing.lg}>
|
|
94
|
+
<Text weight="semiBold">FormControl with Select</Text>
|
|
95
|
+
<FormControl isRequired>
|
|
96
|
+
<FormLabel>Country</FormLabel>
|
|
97
|
+
<Select
|
|
98
|
+
placeholder="Select country"
|
|
99
|
+
options={[
|
|
100
|
+
{ label: 'United States', value: 'us' },
|
|
101
|
+
{ label: 'Canada', value: 'ca' },
|
|
102
|
+
{ label: 'United Kingdom', value: 'uk' },
|
|
103
|
+
]}
|
|
104
|
+
/>
|
|
105
|
+
<FormHelperText>Select your country of residence</FormHelperText>
|
|
106
|
+
</FormControl>
|
|
107
|
+
</VStack>
|
|
108
|
+
),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const WithCheckbox: Story = {
|
|
112
|
+
render: () => (
|
|
113
|
+
<VStack space={spacing.lg}>
|
|
114
|
+
<Text weight="semiBold">FormControl with Checkbox</Text>
|
|
115
|
+
<FormControl isRequired>
|
|
116
|
+
<Checkbox>I agree to the Terms of Service</Checkbox>
|
|
117
|
+
<FormHelperText>You must agree to continue</FormHelperText>
|
|
118
|
+
</FormControl>
|
|
119
|
+
<FormControl isRequired isInvalid>
|
|
120
|
+
<Checkbox>I accept the Privacy Policy</Checkbox>
|
|
121
|
+
<FormErrorMessage>You must accept the privacy policy</FormErrorMessage>
|
|
122
|
+
</FormControl>
|
|
123
|
+
</VStack>
|
|
124
|
+
),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const CompleteForm: Story = {
|
|
128
|
+
render: () => (
|
|
129
|
+
<VStack space={spacing.lg}>
|
|
130
|
+
<Text weight="semiBold">Registration Form</Text>
|
|
131
|
+
<Box p={20} bg={colors.white} rounded="lg" shadow="10">
|
|
132
|
+
<VStack space={0}>
|
|
133
|
+
<FormControl isRequired>
|
|
134
|
+
<FormLabel>Full Name</FormLabel>
|
|
135
|
+
<Input placeholder="John Doe" />
|
|
136
|
+
</FormControl>
|
|
137
|
+
|
|
138
|
+
<FormControl isRequired>
|
|
139
|
+
<FormLabel>Email Address</FormLabel>
|
|
140
|
+
<Input placeholder="john@example.com" keyboardType="email-address" />
|
|
141
|
+
<FormHelperText>We'll send a confirmation email</FormHelperText>
|
|
142
|
+
</FormControl>
|
|
143
|
+
|
|
144
|
+
<FormControl isRequired isInvalid>
|
|
145
|
+
<FormLabel>Password</FormLabel>
|
|
146
|
+
<Input placeholder="Create a password" secureTextEntry />
|
|
147
|
+
<FormErrorMessage>Password must contain at least one number</FormErrorMessage>
|
|
148
|
+
</FormControl>
|
|
149
|
+
|
|
150
|
+
<FormControl>
|
|
151
|
+
<FormLabel>Phone Number</FormLabel>
|
|
152
|
+
<Input placeholder="+1 (555) 000-0000" keyboardType="phone-pad" />
|
|
153
|
+
<FormHelperText>Optional</FormHelperText>
|
|
154
|
+
</FormControl>
|
|
155
|
+
|
|
156
|
+
<FormControl isRequired>
|
|
157
|
+
<FormLabel>Country</FormLabel>
|
|
158
|
+
<Select
|
|
159
|
+
placeholder="Select country"
|
|
160
|
+
options={[
|
|
161
|
+
{ label: 'United States', value: 'us' },
|
|
162
|
+
{ label: 'Canada', value: 'ca' },
|
|
163
|
+
{ label: 'United Kingdom', value: 'uk' },
|
|
164
|
+
]}
|
|
165
|
+
/>
|
|
166
|
+
</FormControl>
|
|
167
|
+
|
|
168
|
+
<FormControl isRequired>
|
|
169
|
+
<Checkbox>I agree to the Terms of Service and Privacy Policy</Checkbox>
|
|
170
|
+
</FormControl>
|
|
171
|
+
</VStack>
|
|
172
|
+
</Box>
|
|
173
|
+
</VStack>
|
|
174
|
+
),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export const ContactForm: Story = {
|
|
178
|
+
render: () => (
|
|
179
|
+
<VStack space={spacing.lg}>
|
|
180
|
+
<Text weight="semiBold">Contact Form</Text>
|
|
181
|
+
<Box p={20} bg={colors.background.secondary} rounded="lg">
|
|
182
|
+
<VStack space={0}>
|
|
183
|
+
<FormControl isRequired>
|
|
184
|
+
<FormLabel>Your Name</FormLabel>
|
|
185
|
+
<Input placeholder="Enter your name" />
|
|
186
|
+
</FormControl>
|
|
187
|
+
|
|
188
|
+
<FormControl isRequired>
|
|
189
|
+
<FormLabel>Email</FormLabel>
|
|
190
|
+
<Input placeholder="your@email.com" keyboardType="email-address" />
|
|
191
|
+
</FormControl>
|
|
192
|
+
|
|
193
|
+
<FormControl isRequired>
|
|
194
|
+
<FormLabel>Subject</FormLabel>
|
|
195
|
+
<Select
|
|
196
|
+
placeholder="Select a subject"
|
|
197
|
+
options={[
|
|
198
|
+
{ label: 'General Inquiry', value: 'general' },
|
|
199
|
+
{ label: 'Technical Support', value: 'support' },
|
|
200
|
+
{ label: 'Sales', value: 'sales' },
|
|
201
|
+
{ label: 'Feedback', value: 'feedback' },
|
|
202
|
+
]}
|
|
203
|
+
/>
|
|
204
|
+
</FormControl>
|
|
205
|
+
|
|
206
|
+
<FormControl isRequired>
|
|
207
|
+
<FormLabel>Message</FormLabel>
|
|
208
|
+
<Textarea
|
|
209
|
+
placeholder="How can we help you?"
|
|
210
|
+
rows={5}
|
|
211
|
+
/>
|
|
212
|
+
<FormHelperText>Please be as detailed as possible</FormHelperText>
|
|
213
|
+
</FormControl>
|
|
214
|
+
</VStack>
|
|
215
|
+
</Box>
|
|
216
|
+
</VStack>
|
|
217
|
+
),
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export const ReadOnly: Story = {
|
|
221
|
+
render: () => (
|
|
222
|
+
<VStack space={spacing.lg}>
|
|
223
|
+
<Text weight="semiBold">Read Only Form</Text>
|
|
224
|
+
<Box p={20} bg={colors.background.secondary} rounded="lg">
|
|
225
|
+
<VStack space={0}>
|
|
226
|
+
<FormControl isReadOnly>
|
|
227
|
+
<FormLabel>Account ID</FormLabel>
|
|
228
|
+
<Input value="ACC-123456" />
|
|
229
|
+
</FormControl>
|
|
230
|
+
|
|
231
|
+
<FormControl isReadOnly>
|
|
232
|
+
<FormLabel>Created Date</FormLabel>
|
|
233
|
+
<Input value="January 15, 2024" />
|
|
234
|
+
</FormControl>
|
|
235
|
+
|
|
236
|
+
<FormControl isReadOnly>
|
|
237
|
+
<FormLabel>Status</FormLabel>
|
|
238
|
+
<Input value="Active" />
|
|
239
|
+
</FormControl>
|
|
240
|
+
</VStack>
|
|
241
|
+
</Box>
|
|
242
|
+
</VStack>
|
|
243
|
+
),
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export const ValidationStates: Story = {
|
|
247
|
+
render: () => (
|
|
248
|
+
<VStack space={spacing.lg}>
|
|
249
|
+
<Text weight="semiBold">Various Validation States</Text>
|
|
250
|
+
<FormControl>
|
|
251
|
+
<FormLabel>Normal State</FormLabel>
|
|
252
|
+
<Input placeholder="Enter value" />
|
|
253
|
+
<FormHelperText>This is a normal field</FormHelperText>
|
|
254
|
+
</FormControl>
|
|
255
|
+
|
|
256
|
+
<FormControl isInvalid>
|
|
257
|
+
<FormLabel>Error State</FormLabel>
|
|
258
|
+
<Input placeholder="Enter value" />
|
|
259
|
+
<FormErrorMessage>This field has an error</FormErrorMessage>
|
|
260
|
+
</FormControl>
|
|
261
|
+
|
|
262
|
+
<FormControl isDisabled>
|
|
263
|
+
<FormLabel>Disabled State</FormLabel>
|
|
264
|
+
<Input placeholder="Enter value" value="Disabled value" />
|
|
265
|
+
<FormHelperText>This field is disabled</FormHelperText>
|
|
266
|
+
</FormControl>
|
|
267
|
+
|
|
268
|
+
<FormControl isRequired>
|
|
269
|
+
<FormLabel>Required State</FormLabel>
|
|
270
|
+
<Input placeholder="Enter value" />
|
|
271
|
+
<FormHelperText>This field is required</FormHelperText>
|
|
272
|
+
</FormControl>
|
|
273
|
+
</VStack>
|
|
274
|
+
),
|
|
275
|
+
};
|
|
276
|
+
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormControl Component
|
|
3
|
+
* Wrapper for form fields with label, helper, and error states
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { forwardRef, createContext, useContext } from 'react';
|
|
7
|
+
import { View, ViewProps, Text, StyleSheet } from 'react-native';
|
|
8
|
+
import { colors, spacing, typography } from '../../styles/tokens';
|
|
9
|
+
|
|
10
|
+
interface FormControlContextValue {
|
|
11
|
+
isInvalid?: boolean;
|
|
12
|
+
isDisabled?: boolean;
|
|
13
|
+
isRequired?: boolean;
|
|
14
|
+
isReadOnly?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const FormControlContext = createContext<FormControlContextValue>({});
|
|
18
|
+
|
|
19
|
+
export const useFormControl = () => useContext(FormControlContext);
|
|
20
|
+
|
|
21
|
+
export interface FormControlProps extends ViewProps {
|
|
22
|
+
/** Label text */
|
|
23
|
+
label?: string;
|
|
24
|
+
/** Helper text shown below the input */
|
|
25
|
+
helperText?: string;
|
|
26
|
+
/** Error message shown when invalid */
|
|
27
|
+
errorMessage?: string;
|
|
28
|
+
/** Is invalid */
|
|
29
|
+
isInvalid?: boolean;
|
|
30
|
+
/** Is disabled */
|
|
31
|
+
isDisabled?: boolean;
|
|
32
|
+
/** Is required */
|
|
33
|
+
isRequired?: boolean;
|
|
34
|
+
/** Is read only */
|
|
35
|
+
isReadOnly?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const FormControl = forwardRef<View, FormControlProps>(
|
|
39
|
+
(
|
|
40
|
+
{
|
|
41
|
+
style,
|
|
42
|
+
label,
|
|
43
|
+
helperText,
|
|
44
|
+
errorMessage,
|
|
45
|
+
isInvalid = false,
|
|
46
|
+
isDisabled = false,
|
|
47
|
+
isRequired = false,
|
|
48
|
+
isReadOnly = false,
|
|
49
|
+
children,
|
|
50
|
+
...props
|
|
51
|
+
},
|
|
52
|
+
ref
|
|
53
|
+
) => {
|
|
54
|
+
const hasError = isInvalid || !!errorMessage;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<FormControlContext.Provider
|
|
58
|
+
value={{ isInvalid: hasError, isDisabled, isRequired, isReadOnly }}
|
|
59
|
+
>
|
|
60
|
+
<View ref={ref} style={[styles.formControl, style]} {...props}>
|
|
61
|
+
{label && (
|
|
62
|
+
<View style={styles.labelContainer}>
|
|
63
|
+
<Text style={[styles.label, isDisabled && styles.labelDisabled]}>
|
|
64
|
+
{label}
|
|
65
|
+
{isRequired && <Text style={styles.required}> *</Text>}
|
|
66
|
+
</Text>
|
|
67
|
+
</View>
|
|
68
|
+
)}
|
|
69
|
+
{children}
|
|
70
|
+
{helperText && !hasError && (
|
|
71
|
+
<View style={styles.helperContainer}>
|
|
72
|
+
<Text style={styles.helperText}>{helperText}</Text>
|
|
73
|
+
</View>
|
|
74
|
+
)}
|
|
75
|
+
{hasError && errorMessage && (
|
|
76
|
+
<View style={styles.errorContainer}>
|
|
77
|
+
<Text style={styles.errorText}>{errorMessage}</Text>
|
|
78
|
+
</View>
|
|
79
|
+
)}
|
|
80
|
+
</View>
|
|
81
|
+
</FormControlContext.Provider>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
FormControl.displayName = 'FormControl';
|
|
87
|
+
|
|
88
|
+
export interface FormLabelProps extends ViewProps {
|
|
89
|
+
/** Label text */
|
|
90
|
+
children: React.ReactNode;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const FormLabel = forwardRef<View, FormLabelProps>(
|
|
94
|
+
({ style, children, ...props }, ref) => {
|
|
95
|
+
const { isRequired, isDisabled } = useFormControl();
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<View ref={ref} style={[styles.labelContainer, style]} {...props}>
|
|
99
|
+
<Text style={[styles.label, isDisabled && styles.labelDisabled]}>
|
|
100
|
+
{children}
|
|
101
|
+
{isRequired && <Text style={styles.required}> *</Text>}
|
|
102
|
+
</Text>
|
|
103
|
+
</View>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
FormLabel.displayName = 'FormLabel';
|
|
109
|
+
|
|
110
|
+
export interface FormHelperTextProps extends ViewProps {
|
|
111
|
+
/** Helper text */
|
|
112
|
+
children: React.ReactNode;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const FormHelperText = forwardRef<View, FormHelperTextProps>(
|
|
116
|
+
({ style, children, ...props }, ref) => {
|
|
117
|
+
const { isInvalid } = useFormControl();
|
|
118
|
+
|
|
119
|
+
if (isInvalid) return null;
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<View ref={ref} style={[styles.helperContainer, style]} {...props}>
|
|
123
|
+
<Text style={styles.helperText}>{children}</Text>
|
|
124
|
+
</View>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
FormHelperText.displayName = 'FormHelperText';
|
|
130
|
+
|
|
131
|
+
export interface FormErrorMessageProps extends ViewProps {
|
|
132
|
+
/** Error message */
|
|
133
|
+
children: React.ReactNode;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const FormErrorMessage = forwardRef<View, FormErrorMessageProps>(
|
|
137
|
+
({ style, children, ...props }, ref) => {
|
|
138
|
+
const { isInvalid } = useFormControl();
|
|
139
|
+
|
|
140
|
+
if (!isInvalid) return null;
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<View ref={ref} style={[styles.errorContainer, style]} {...props}>
|
|
144
|
+
<Text style={styles.errorText}>{children}</Text>
|
|
145
|
+
</View>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
FormErrorMessage.displayName = 'FormErrorMessage';
|
|
151
|
+
|
|
152
|
+
const styles = StyleSheet.create({
|
|
153
|
+
formControl: {
|
|
154
|
+
width: '100%',
|
|
155
|
+
marginBottom: spacing['4x'],
|
|
156
|
+
},
|
|
157
|
+
labelContainer: {
|
|
158
|
+
marginBottom: spacing.base,
|
|
159
|
+
},
|
|
160
|
+
label: {
|
|
161
|
+
fontSize: typography.fontSize.componentLabel,
|
|
162
|
+
fontWeight: typography.fontWeight.semiBold,
|
|
163
|
+
color: colors.text.default,
|
|
164
|
+
},
|
|
165
|
+
labelDisabled: {
|
|
166
|
+
color: colors.text.disabled,
|
|
167
|
+
},
|
|
168
|
+
required: {
|
|
169
|
+
color: colors.feedback.error.content,
|
|
170
|
+
},
|
|
171
|
+
helperContainer: {
|
|
172
|
+
marginTop: spacing.base,
|
|
173
|
+
},
|
|
174
|
+
helperText: {
|
|
175
|
+
fontSize: typography.fontSize.caption,
|
|
176
|
+
color: colors.text.secondary,
|
|
177
|
+
},
|
|
178
|
+
errorContainer: {
|
|
179
|
+
marginTop: spacing.base,
|
|
180
|
+
},
|
|
181
|
+
errorText: {
|
|
182
|
+
fontSize: typography.fontSize.caption,
|
|
183
|
+
color: colors.feedback.error.content,
|
|
184
|
+
},
|
|
185
|
+
});
|