@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,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Component
|
|
3
|
+
* Text input with variants and states
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { forwardRef, useState } from 'react';
|
|
7
|
+
import {
|
|
8
|
+
View,
|
|
9
|
+
TextInput,
|
|
10
|
+
TextInputProps,
|
|
11
|
+
Pressable,
|
|
12
|
+
Text,
|
|
13
|
+
StyleSheet,
|
|
14
|
+
} from 'react-native';
|
|
15
|
+
import { colors, spacing, typography, borderRadius } from '../../styles/tokens';
|
|
16
|
+
|
|
17
|
+
export interface InputProps extends Omit<TextInputProps, 'style'> {
|
|
18
|
+
/** Input variant */
|
|
19
|
+
variant?: 'outline' | 'filled' | 'flushed';
|
|
20
|
+
/** Input size */
|
|
21
|
+
size?: 'sm' | 'md' | 'lg';
|
|
22
|
+
/** Label text */
|
|
23
|
+
label?: string;
|
|
24
|
+
/** Helper text */
|
|
25
|
+
helperText?: string;
|
|
26
|
+
/** Error message */
|
|
27
|
+
errorMessage?: string;
|
|
28
|
+
/** Is invalid */
|
|
29
|
+
isInvalid?: boolean;
|
|
30
|
+
/** Is disabled */
|
|
31
|
+
isDisabled?: boolean;
|
|
32
|
+
/** Is read only */
|
|
33
|
+
isReadOnly?: boolean;
|
|
34
|
+
/** Is required */
|
|
35
|
+
isRequired?: boolean;
|
|
36
|
+
/** Left element */
|
|
37
|
+
leftElement?: React.ReactNode;
|
|
38
|
+
/** Right element */
|
|
39
|
+
rightElement?: React.ReactNode;
|
|
40
|
+
/** Password visibility toggle */
|
|
41
|
+
isPassword?: boolean;
|
|
42
|
+
/** Custom border color */
|
|
43
|
+
borderColor?: string;
|
|
44
|
+
/** Custom focus border color */
|
|
45
|
+
focusBorderColor?: string;
|
|
46
|
+
/** Custom error border color */
|
|
47
|
+
errorBorderColor?: string;
|
|
48
|
+
/** Custom background color */
|
|
49
|
+
backgroundColor?: string;
|
|
50
|
+
/** Custom border radius */
|
|
51
|
+
borderRadius?: number;
|
|
52
|
+
/** Custom border width */
|
|
53
|
+
borderWidth?: number;
|
|
54
|
+
/** Custom input wrapper style */
|
|
55
|
+
inputWrapperStyle?: object;
|
|
56
|
+
/** Custom input text style */
|
|
57
|
+
inputStyle?: object;
|
|
58
|
+
/** Show focus border highlight (default: false) */
|
|
59
|
+
showFocusBorder?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const Input = forwardRef<TextInput, InputProps>(
|
|
63
|
+
(
|
|
64
|
+
{
|
|
65
|
+
variant = 'outline',
|
|
66
|
+
size = 'md',
|
|
67
|
+
label,
|
|
68
|
+
helperText,
|
|
69
|
+
errorMessage,
|
|
70
|
+
isInvalid = false,
|
|
71
|
+
isDisabled = false,
|
|
72
|
+
isReadOnly = false,
|
|
73
|
+
isRequired = false,
|
|
74
|
+
leftElement,
|
|
75
|
+
rightElement,
|
|
76
|
+
isPassword = false,
|
|
77
|
+
secureTextEntry,
|
|
78
|
+
onFocus,
|
|
79
|
+
onBlur,
|
|
80
|
+
borderColor: customBorderColor,
|
|
81
|
+
focusBorderColor,
|
|
82
|
+
errorBorderColor,
|
|
83
|
+
backgroundColor: customBackgroundColor,
|
|
84
|
+
borderRadius: customBorderRadius,
|
|
85
|
+
borderWidth: customBorderWidth,
|
|
86
|
+
inputWrapperStyle,
|
|
87
|
+
inputStyle,
|
|
88
|
+
showFocusBorder = false,
|
|
89
|
+
...props
|
|
90
|
+
},
|
|
91
|
+
ref
|
|
92
|
+
) => {
|
|
93
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
94
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
95
|
+
|
|
96
|
+
const handleFocus = (e: any) => {
|
|
97
|
+
setIsFocused(true);
|
|
98
|
+
onFocus?.(e);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const handleBlur = (e: any) => {
|
|
102
|
+
setIsFocused(false);
|
|
103
|
+
onBlur?.(e);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const hasError = isInvalid || !!errorMessage;
|
|
107
|
+
|
|
108
|
+
// Determine border color based on state
|
|
109
|
+
const getBorderColor = () => {
|
|
110
|
+
if (hasError) return errorBorderColor || colors.border.danger;
|
|
111
|
+
if (isFocused && showFocusBorder) return focusBorderColor || colors.border.active;
|
|
112
|
+
return customBorderColor || colors.border.default;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Build custom style overrides
|
|
116
|
+
const customStyles = {
|
|
117
|
+
...(customBorderColor && { borderColor: getBorderColor() }),
|
|
118
|
+
...(customBackgroundColor && { backgroundColor: customBackgroundColor }),
|
|
119
|
+
...(customBorderRadius !== undefined && { borderRadius: customBorderRadius }),
|
|
120
|
+
...(customBorderWidth !== undefined && { borderWidth: customBorderWidth }),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<View style={styles.container}>
|
|
125
|
+
{label && (
|
|
126
|
+
<Text style={[styles.label, isDisabled && styles.labelDisabled]}>
|
|
127
|
+
{label}
|
|
128
|
+
{isRequired && <Text style={styles.required}> *</Text>}
|
|
129
|
+
</Text>
|
|
130
|
+
)}
|
|
131
|
+
<View
|
|
132
|
+
style={[
|
|
133
|
+
styles.inputWrapper,
|
|
134
|
+
styles[variant],
|
|
135
|
+
styles[size],
|
|
136
|
+
isFocused && showFocusBorder && !customBorderColor && styles.focused,
|
|
137
|
+
hasError && !customBorderColor && styles.error,
|
|
138
|
+
isDisabled && styles.disabled,
|
|
139
|
+
customStyles,
|
|
140
|
+
isFocused && showFocusBorder && customBorderColor && { borderColor: focusBorderColor || customBorderColor },
|
|
141
|
+
hasError && customBorderColor && { borderColor: errorBorderColor || colors.border.danger },
|
|
142
|
+
inputWrapperStyle,
|
|
143
|
+
]}
|
|
144
|
+
>
|
|
145
|
+
{leftElement && <View style={styles.leftElement}>{leftElement}</View>}
|
|
146
|
+
<TextInput
|
|
147
|
+
ref={ref}
|
|
148
|
+
style={[
|
|
149
|
+
styles.input,
|
|
150
|
+
styles[`${size}Input` as keyof typeof styles],
|
|
151
|
+
isDisabled && styles.disabledText,
|
|
152
|
+
inputStyle,
|
|
153
|
+
]}
|
|
154
|
+
editable={!isDisabled && !isReadOnly}
|
|
155
|
+
placeholderTextColor={colors.text.disabled}
|
|
156
|
+
secureTextEntry={isPassword ? !showPassword : secureTextEntry}
|
|
157
|
+
onFocus={handleFocus}
|
|
158
|
+
onBlur={handleBlur}
|
|
159
|
+
{...props}
|
|
160
|
+
/>
|
|
161
|
+
{isPassword && (
|
|
162
|
+
<Pressable onPress={() => setShowPassword(!showPassword)} style={styles.rightElement}>
|
|
163
|
+
<Text style={styles.toggleText}>{showPassword ? '👁' : '👁🗨'}</Text>
|
|
164
|
+
</Pressable>
|
|
165
|
+
)}
|
|
166
|
+
{rightElement && !isPassword && (
|
|
167
|
+
<View style={styles.rightElement}>{rightElement}</View>
|
|
168
|
+
)}
|
|
169
|
+
</View>
|
|
170
|
+
{(helperText || errorMessage) && (
|
|
171
|
+
<Text style={[styles.helperText, hasError && styles.errorText]}>
|
|
172
|
+
{errorMessage || helperText}
|
|
173
|
+
</Text>
|
|
174
|
+
)}
|
|
175
|
+
</View>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
Input.displayName = 'Input';
|
|
181
|
+
|
|
182
|
+
const styles = StyleSheet.create({
|
|
183
|
+
container: {
|
|
184
|
+
width: '100%',
|
|
185
|
+
},
|
|
186
|
+
label: {
|
|
187
|
+
fontSize: typography.fontSize.componentLabel,
|
|
188
|
+
fontWeight: typography.fontWeight.semiBold,
|
|
189
|
+
color: colors.text.default,
|
|
190
|
+
marginBottom: spacing.base,
|
|
191
|
+
},
|
|
192
|
+
labelDisabled: {
|
|
193
|
+
color: colors.text.disabled,
|
|
194
|
+
},
|
|
195
|
+
required: {
|
|
196
|
+
color: colors.feedback.error.content,
|
|
197
|
+
},
|
|
198
|
+
inputWrapper: {
|
|
199
|
+
flexDirection: 'row',
|
|
200
|
+
alignItems: 'center',
|
|
201
|
+
borderRadius: borderRadius.md,
|
|
202
|
+
},
|
|
203
|
+
outline: {
|
|
204
|
+
borderWidth: 1,
|
|
205
|
+
borderColor: colors.border.default,
|
|
206
|
+
backgroundColor: colors.background.default,
|
|
207
|
+
},
|
|
208
|
+
filled: {
|
|
209
|
+
backgroundColor: colors.background.secondary,
|
|
210
|
+
},
|
|
211
|
+
flushed: {
|
|
212
|
+
borderBottomWidth: 1,
|
|
213
|
+
borderBottomColor: colors.border.default,
|
|
214
|
+
borderRadius: 0,
|
|
215
|
+
},
|
|
216
|
+
sm: {
|
|
217
|
+
height: 32,
|
|
218
|
+
paddingHorizontal: spacing['2x'],
|
|
219
|
+
},
|
|
220
|
+
md: {
|
|
221
|
+
height: 40,
|
|
222
|
+
paddingHorizontal: spacing['3x'],
|
|
223
|
+
},
|
|
224
|
+
lg: {
|
|
225
|
+
height: 48,
|
|
226
|
+
paddingHorizontal: spacing['4x'],
|
|
227
|
+
},
|
|
228
|
+
focused: {
|
|
229
|
+
borderColor: colors.border.active,
|
|
230
|
+
},
|
|
231
|
+
error: {
|
|
232
|
+
borderColor: colors.border.danger,
|
|
233
|
+
},
|
|
234
|
+
disabled: {
|
|
235
|
+
backgroundColor: colors.background.secondary,
|
|
236
|
+
borderColor: colors.border.disabled,
|
|
237
|
+
},
|
|
238
|
+
input: {
|
|
239
|
+
flex: 1,
|
|
240
|
+
color: colors.text.default,
|
|
241
|
+
// Remove browser focus outline on web
|
|
242
|
+
outlineStyle: 'none',
|
|
243
|
+
} as any,
|
|
244
|
+
smInput: {
|
|
245
|
+
fontSize: typography.fontSize.small,
|
|
246
|
+
},
|
|
247
|
+
mdInput: {
|
|
248
|
+
fontSize: typography.fontSize.body,
|
|
249
|
+
},
|
|
250
|
+
lgInput: {
|
|
251
|
+
fontSize: typography.fontSize.body,
|
|
252
|
+
},
|
|
253
|
+
disabledText: {
|
|
254
|
+
color: colors.text.disabled,
|
|
255
|
+
},
|
|
256
|
+
leftElement: {
|
|
257
|
+
marginRight: spacing['2x'],
|
|
258
|
+
},
|
|
259
|
+
rightElement: {
|
|
260
|
+
marginLeft: spacing['2x'],
|
|
261
|
+
},
|
|
262
|
+
toggleText: {
|
|
263
|
+
fontSize: 16,
|
|
264
|
+
},
|
|
265
|
+
helperText: {
|
|
266
|
+
fontSize: typography.fontSize.caption,
|
|
267
|
+
color: colors.text.secondary,
|
|
268
|
+
marginTop: spacing.base,
|
|
269
|
+
},
|
|
270
|
+
errorText: {
|
|
271
|
+
color: colors.feedback.error.content,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Link } from './Link';
|
|
3
|
+
import { VStack } from '../VStack';
|
|
4
|
+
import { HStack } from '../HStack';
|
|
5
|
+
import { Text } from '../Text';
|
|
6
|
+
import { Box } from '../Box';
|
|
7
|
+
import { colors, spacing, borderRadius, elevation } from '../../styles/tokens';
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof Link> = {
|
|
10
|
+
title: 'Typography/Link',
|
|
11
|
+
component: Link,
|
|
12
|
+
argTypes: {
|
|
13
|
+
size: {
|
|
14
|
+
control: 'select',
|
|
15
|
+
options: ['sm', 'md', 'lg'],
|
|
16
|
+
},
|
|
17
|
+
underline: {
|
|
18
|
+
control: 'select',
|
|
19
|
+
options: ['always', 'hover', 'none'],
|
|
20
|
+
},
|
|
21
|
+
color: { control: 'color' },
|
|
22
|
+
isExternal: { control: 'boolean' },
|
|
23
|
+
},
|
|
24
|
+
args: {
|
|
25
|
+
children: 'Click me',
|
|
26
|
+
size: 'md',
|
|
27
|
+
underline: 'always',
|
|
28
|
+
isExternal: false,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default meta;
|
|
33
|
+
|
|
34
|
+
type Story = StoryObj<typeof Link>;
|
|
35
|
+
|
|
36
|
+
export const Default: Story = {
|
|
37
|
+
render: (args) => <Link {...args} />,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const Sizes: Story = {
|
|
41
|
+
render: () => (
|
|
42
|
+
<VStack space={spacing.lg}>
|
|
43
|
+
<Text weight="semiBold">Link Sizes</Text>
|
|
44
|
+
<VStack space={spacing.md}>
|
|
45
|
+
<HStack space={spacing.sm} alignItems="center">
|
|
46
|
+
<Box w={60}><Text size="sm" color={colors.text.secondary}>Small:</Text></Box>
|
|
47
|
+
<Link size="sm">Small link</Link>
|
|
48
|
+
</HStack>
|
|
49
|
+
<HStack space={spacing.sm} alignItems="center">
|
|
50
|
+
<Box w={60}><Text size="sm" color={colors.text.secondary}>Medium:</Text></Box>
|
|
51
|
+
<Link size="md">Medium link</Link>
|
|
52
|
+
</HStack>
|
|
53
|
+
<HStack space={spacing.sm} alignItems="center">
|
|
54
|
+
<Box w={60}><Text size="sm" color={colors.text.secondary}>Large:</Text></Box>
|
|
55
|
+
<Link size="lg">Large link</Link>
|
|
56
|
+
</HStack>
|
|
57
|
+
</VStack>
|
|
58
|
+
</VStack>
|
|
59
|
+
),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const UnderlineStyles: Story = {
|
|
63
|
+
render: () => (
|
|
64
|
+
<VStack space={spacing.lg}>
|
|
65
|
+
<Text weight="semiBold">Underline Styles</Text>
|
|
66
|
+
<VStack space={spacing.md}>
|
|
67
|
+
<HStack space={spacing.sm} alignItems="center">
|
|
68
|
+
<Box w={80}><Text size="sm" color={colors.text.secondary}>Always:</Text></Box>
|
|
69
|
+
<Link underline="always">Always underlined</Link>
|
|
70
|
+
</HStack>
|
|
71
|
+
<HStack space={spacing.sm} alignItems="center">
|
|
72
|
+
<Box w={80}><Text size="sm" color={colors.text.secondary}>On Hover:</Text></Box>
|
|
73
|
+
<Link underline="hover">Underline on hover</Link>
|
|
74
|
+
</HStack>
|
|
75
|
+
<HStack space={spacing.sm} alignItems="center">
|
|
76
|
+
<Box w={80}><Text size="sm" color={colors.text.secondary}>None:</Text></Box>
|
|
77
|
+
<Link underline="none">No underline</Link>
|
|
78
|
+
</HStack>
|
|
79
|
+
</VStack>
|
|
80
|
+
</VStack>
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const Colors: Story = {
|
|
85
|
+
render: () => (
|
|
86
|
+
<VStack space={spacing.lg}>
|
|
87
|
+
<Text weight="semiBold">Custom Colors</Text>
|
|
88
|
+
<VStack space={spacing.md}>
|
|
89
|
+
<Link color={colors.brand.blue}>Blue link</Link>
|
|
90
|
+
<Link color={colors.feedback.success.content}>Green link</Link>
|
|
91
|
+
<Link color={colors.feedback.error.content}>Red link</Link>
|
|
92
|
+
<Link color={colors.badge.purple}>Purple link</Link>
|
|
93
|
+
<Link color={colors.feedback.warning.content}>Orange link</Link>
|
|
94
|
+
</VStack>
|
|
95
|
+
</VStack>
|
|
96
|
+
),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const External: Story = {
|
|
100
|
+
render: () => (
|
|
101
|
+
<VStack space={spacing.lg}>
|
|
102
|
+
<Text weight="semiBold">External Links</Text>
|
|
103
|
+
<VStack space={spacing.md}>
|
|
104
|
+
<Link href="https://example.com">Internal link</Link>
|
|
105
|
+
<Link href="https://example.com" isExternal>
|
|
106
|
+
External link (opens in new tab)
|
|
107
|
+
</Link>
|
|
108
|
+
</VStack>
|
|
109
|
+
</VStack>
|
|
110
|
+
),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const InlineWithText: Story = {
|
|
114
|
+
render: () => (
|
|
115
|
+
<VStack space={spacing.lg}>
|
|
116
|
+
<Text weight="semiBold">Inline Links</Text>
|
|
117
|
+
<Box p={spacing.lg} bg={colors.background.secondary} rounded="md">
|
|
118
|
+
<Text>
|
|
119
|
+
Please read our{' '}
|
|
120
|
+
<Link onPress={() => console.log('Terms clicked')}>Terms of Service</Link>
|
|
121
|
+
{' '}and{' '}
|
|
122
|
+
<Link onPress={() => console.log('Privacy clicked')}>Privacy Policy</Link>
|
|
123
|
+
{' '}before continuing.
|
|
124
|
+
</Text>
|
|
125
|
+
</Box>
|
|
126
|
+
<Box p={spacing.lg} bg={colors.background.secondary} rounded="md">
|
|
127
|
+
<Text size="sm" color={colors.text.secondary}>
|
|
128
|
+
For more information, visit our{' '}
|
|
129
|
+
<Link size="sm" href="https://help.example.com" isExternal>
|
|
130
|
+
Help Center
|
|
131
|
+
</Link>
|
|
132
|
+
{' '}or{' '}
|
|
133
|
+
<Link size="sm" onPress={() => console.log('Contact clicked')}>
|
|
134
|
+
contact support
|
|
135
|
+
</Link>.
|
|
136
|
+
</Text>
|
|
137
|
+
</Box>
|
|
138
|
+
</VStack>
|
|
139
|
+
),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const WithCustomPress: Story = {
|
|
143
|
+
render: () => (
|
|
144
|
+
<VStack space={spacing.lg}>
|
|
145
|
+
<Text weight="semiBold">Custom Press Handler</Text>
|
|
146
|
+
<Link onPress={() => alert('Link clicked!')}>
|
|
147
|
+
Click to trigger alert
|
|
148
|
+
</Link>
|
|
149
|
+
</VStack>
|
|
150
|
+
),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const Navigation: Story = {
|
|
154
|
+
render: () => (
|
|
155
|
+
<VStack space={spacing.lg}>
|
|
156
|
+
<Text weight="semiBold">Navigation Links</Text>
|
|
157
|
+
<Box p={spacing.lg} bg={colors.white} rounded="lg" shadow="10">
|
|
158
|
+
<HStack space={spacing.xl}>
|
|
159
|
+
<Link underline="none" onPress={() => console.log('Home')}>Home</Link>
|
|
160
|
+
<Link underline="none" onPress={() => console.log('About')}>About</Link>
|
|
161
|
+
<Link underline="none" onPress={() => console.log('Services')}>Services</Link>
|
|
162
|
+
<Link underline="none" onPress={() => console.log('Contact')}>Contact</Link>
|
|
163
|
+
</HStack>
|
|
164
|
+
</Box>
|
|
165
|
+
</VStack>
|
|
166
|
+
),
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const Footer: Story = {
|
|
170
|
+
render: () => (
|
|
171
|
+
<VStack space={spacing.lg}>
|
|
172
|
+
<Text weight="semiBold">Footer Links</Text>
|
|
173
|
+
<Box p={spacing.lg} bg={colors.background.tertiary} rounded="lg">
|
|
174
|
+
<VStack space={spacing.sm}>
|
|
175
|
+
<Link color={colors.text.disabled} size="sm" underline="hover">About Us</Link>
|
|
176
|
+
<Link color={colors.text.disabled} size="sm" underline="hover">Careers</Link>
|
|
177
|
+
<Link color={colors.text.disabled} size="sm" underline="hover">Press</Link>
|
|
178
|
+
<Link color={colors.text.disabled} size="sm" underline="hover">Blog</Link>
|
|
179
|
+
<Link color={colors.text.disabled} size="sm" underline="hover" isExternal>
|
|
180
|
+
Twitter
|
|
181
|
+
</Link>
|
|
182
|
+
</VStack>
|
|
183
|
+
</Box>
|
|
184
|
+
</VStack>
|
|
185
|
+
),
|
|
186
|
+
};
|
|
187
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link Component
|
|
3
|
+
* Styled text that acts as a link
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { forwardRef, useState } from 'react';
|
|
7
|
+
import { Text as RNText, TextProps as RNTextProps, Pressable, Linking, StyleSheet } from 'react-native';
|
|
8
|
+
import { colors, typography } from '../../styles/tokens';
|
|
9
|
+
|
|
10
|
+
export interface LinkProps extends Omit<RNTextProps, 'onPress'> {
|
|
11
|
+
/** URL to open */
|
|
12
|
+
href?: string;
|
|
13
|
+
/** Open in external browser */
|
|
14
|
+
isExternal?: boolean;
|
|
15
|
+
/** Custom press handler */
|
|
16
|
+
onPress?: () => void;
|
|
17
|
+
/** Link color */
|
|
18
|
+
color?: string;
|
|
19
|
+
/** Size variant */
|
|
20
|
+
size?: 'sm' | 'md' | 'lg';
|
|
21
|
+
/** Underline style */
|
|
22
|
+
underline?: 'always' | 'hover' | 'none';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Link = forwardRef<RNText, LinkProps>(
|
|
26
|
+
(
|
|
27
|
+
{
|
|
28
|
+
style,
|
|
29
|
+
href,
|
|
30
|
+
isExternal = false,
|
|
31
|
+
onPress,
|
|
32
|
+
color,
|
|
33
|
+
size = 'md',
|
|
34
|
+
underline = 'always',
|
|
35
|
+
children,
|
|
36
|
+
...props
|
|
37
|
+
},
|
|
38
|
+
ref
|
|
39
|
+
) => {
|
|
40
|
+
const [isPressed, setIsPressed] = useState(false);
|
|
41
|
+
|
|
42
|
+
const handlePress = async () => {
|
|
43
|
+
if (onPress) {
|
|
44
|
+
onPress();
|
|
45
|
+
} else if (href) {
|
|
46
|
+
const canOpen = await Linking.canOpenURL(href);
|
|
47
|
+
if (canOpen) {
|
|
48
|
+
await Linking.openURL(href);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const linkColor = color || colors.brand.blue;
|
|
54
|
+
const showUnderline = underline === 'always' || (underline === 'hover' && isPressed);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Pressable
|
|
58
|
+
onPress={handlePress}
|
|
59
|
+
onPressIn={() => setIsPressed(true)}
|
|
60
|
+
onPressOut={() => setIsPressed(false)}
|
|
61
|
+
accessibilityRole="link"
|
|
62
|
+
>
|
|
63
|
+
<RNText
|
|
64
|
+
ref={ref}
|
|
65
|
+
style={[
|
|
66
|
+
styles.base,
|
|
67
|
+
styles[size],
|
|
68
|
+
{ color: linkColor },
|
|
69
|
+
showUnderline && styles.underline,
|
|
70
|
+
isPressed && styles.pressed,
|
|
71
|
+
style,
|
|
72
|
+
]}
|
|
73
|
+
{...props}
|
|
74
|
+
>
|
|
75
|
+
{children}
|
|
76
|
+
{isExternal && ' ↗'}
|
|
77
|
+
</RNText>
|
|
78
|
+
</Pressable>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
Link.displayName = 'Link';
|
|
84
|
+
|
|
85
|
+
const styles = StyleSheet.create({
|
|
86
|
+
base: {
|
|
87
|
+
fontWeight: typography.fontWeight.medium,
|
|
88
|
+
},
|
|
89
|
+
sm: {
|
|
90
|
+
fontSize: typography.fontSize.small,
|
|
91
|
+
},
|
|
92
|
+
md: {
|
|
93
|
+
fontSize: typography.fontSize.body,
|
|
94
|
+
},
|
|
95
|
+
lg: {
|
|
96
|
+
fontSize: typography.fontSize.h4,
|
|
97
|
+
},
|
|
98
|
+
underline: {
|
|
99
|
+
textDecorationLine: 'underline',
|
|
100
|
+
},
|
|
101
|
+
pressed: {
|
|
102
|
+
opacity: 0.7,
|
|
103
|
+
},
|
|
104
|
+
});
|