@titan-design/react-ui 0.1.0 → 0.2.5
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/README.md +47 -1
- package/dist/{chunk-MGW37MPO.mjs → chunk-UXVRPOME.mjs} +173 -3
- package/dist/chunk-UXVRPOME.mjs.map +1 -0
- package/dist/index.d.mts +101 -12
- package/dist/index.d.ts +101 -12
- package/dist/index.js +1267 -468
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +734 -112
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +86 -1
- package/dist/theme/index.d.ts +86 -1
- package/dist/theme/index.js +166 -0
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +1 -1
- package/package.json +22 -6
- package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
- package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
- package/src/components/custom/DateTime/DateTime.tsx +235 -0
- package/src/components/custom/DateTime/index.ts +2 -0
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
- package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
- package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
- package/src/components/custom/EmptyState/index.ts +2 -0
- package/src/components/custom/Metric/Metric.stories.tsx +133 -0
- package/src/components/custom/Metric/Metric.test.tsx +120 -0
- package/src/components/custom/Metric/Metric.tsx +92 -0
- package/src/components/custom/Metric/index.ts +2 -0
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
- package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
- package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
- package/src/components/custom/Sidebar/index.ts +18 -0
- package/src/components/custom/Table/Table.stories.tsx +481 -0
- package/src/components/custom/Table/Table.test.tsx +531 -0
- package/src/components/custom/Table/Table.tsx +696 -0
- package/src/components/custom/Table/index.ts +22 -0
- package/src/components/custom/Typography/Typography.stories.tsx +153 -0
- package/src/components/custom/Typography/Typography.test.tsx +107 -0
- package/src/components/custom/Typography/Typography.tsx +218 -0
- package/src/components/custom/Typography/index.ts +19 -0
- package/src/components/custom/Workout/RestTimer.stories.tsx +71 -0
- package/src/components/custom/Workout/RestTimer.test.tsx +145 -0
- package/src/components/custom/Workout/RestTimer.tsx +172 -0
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +144 -0
- package/src/components/custom/Workout/SupersetWrapper.test.tsx +130 -0
- package/src/components/custom/Workout/SupersetWrapper.tsx +63 -0
- package/src/components/custom/Workout/index.ts +2 -0
- package/src/components/custom/index.ts +9 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
- package/src/components/custom/stepper/Stepper.test.tsx +230 -0
- package/src/components/custom/stepper/Stepper.tsx +250 -0
- package/src/components/custom/stepper/index.ts +10 -0
- package/src/components/index.ts +5 -0
- package/src/components/ui/alert/Alert.stories.tsx +154 -0
- package/src/components/ui/alert/Alert.test.tsx +202 -0
- package/src/components/ui/alert/Alert.tsx +170 -0
- package/src/components/ui/alert/index.ts +8 -0
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
- package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
- package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
- package/src/components/ui/autocomplete/index.ts +2 -0
- package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
- package/src/components/ui/avatar/Avatar.test.tsx +152 -0
- package/src/components/ui/avatar/Avatar.tsx +176 -0
- package/src/components/ui/avatar/index.ts +2 -0
- package/src/components/ui/badge/Badge.stories.tsx +100 -0
- package/src/components/ui/badge/Badge.test.tsx +103 -0
- package/src/components/ui/badge/Badge.tsx +119 -0
- package/src/components/ui/badge/index.ts +3 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
- package/src/components/ui/breadcrumbs/index.ts +2 -0
- package/src/components/ui/button/Button.stories.tsx +216 -0
- package/src/components/ui/button/Button.test.tsx +132 -0
- package/src/components/ui/button/Button.tsx +297 -0
- package/src/components/ui/button/index.ts +10 -0
- package/src/components/ui/card/Card.stories.tsx +443 -0
- package/src/components/ui/card/Card.test.tsx +294 -0
- package/src/components/ui/card/Card.tsx +401 -0
- package/src/components/ui/card/index.ts +20 -0
- package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
- package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
- package/src/components/ui/checkbox/Checkbox.tsx +163 -0
- package/src/components/ui/checkbox/index.ts +2 -0
- package/src/components/ui/chip/Chip.stories.tsx +112 -0
- package/src/components/ui/chip/Chip.test.tsx +119 -0
- package/src/components/ui/chip/Chip.tsx +151 -0
- package/src/components/ui/chip/index.ts +2 -0
- package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
- package/src/components/ui/collapse/Collapse.test.tsx +298 -0
- package/src/components/ui/collapse/Collapse.tsx +275 -0
- package/src/components/ui/collapse/index.ts +18 -0
- package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
- package/src/components/ui/data-row/DataRow.test.tsx +78 -0
- package/src/components/ui/data-row/DataRow.tsx +29 -0
- package/src/components/ui/data-row/index.ts +2 -0
- package/src/components/ui/divider/Divider.stories.tsx +71 -0
- package/src/components/ui/divider/Divider.test.tsx +55 -0
- package/src/components/ui/divider/Divider.tsx +37 -0
- package/src/components/ui/divider/index.ts +2 -0
- package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
- package/src/components/ui/drawer/Drawer.test.tsx +142 -0
- package/src/components/ui/drawer/Drawer.tsx +228 -0
- package/src/components/ui/drawer/index.ts +9 -0
- package/src/components/ui/form-field/FormField.stories.tsx +260 -0
- package/src/components/ui/form-field/FormField.test.tsx +280 -0
- package/src/components/ui/form-field/FormField.tsx +308 -0
- package/src/components/ui/form-field/index.ts +2 -0
- package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
- package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
- package/src/components/ui/help-tip/HelpTip.tsx +229 -0
- package/src/components/ui/help-tip/index.ts +2 -0
- package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
- package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
- package/src/components/ui/icon-box/IconBox.tsx +57 -0
- package/src/components/ui/icon-box/index.ts +1 -0
- package/src/components/ui/index.ts +38 -0
- package/src/components/ui/indicator/Indicator.stories.tsx +74 -0
- package/src/components/ui/indicator/Indicator.test.tsx +55 -0
- package/src/components/ui/indicator/Indicator.tsx +73 -0
- package/src/components/ui/indicator/index.ts +2 -0
- package/src/components/ui/input/Input.stories.tsx +192 -0
- package/src/components/ui/input/Input.test.tsx +86 -0
- package/src/components/ui/input/Input.tsx +273 -0
- package/src/components/ui/input/index.ts +2 -0
- package/src/components/ui/link/Link.stories.tsx +78 -0
- package/src/components/ui/link/Link.test.tsx +94 -0
- package/src/components/ui/link/Link.tsx +86 -0
- package/src/components/ui/link/index.ts +2 -0
- package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
- package/src/components/ui/list-item/ListItem.test.tsx +128 -0
- package/src/components/ui/list-item/ListItem.tsx +89 -0
- package/src/components/ui/list-item/index.ts +14 -0
- package/src/components/ui/menu/Menu.stories.tsx +180 -0
- package/src/components/ui/menu/Menu.test.tsx +353 -0
- package/src/components/ui/menu/Menu.tsx +222 -0
- package/src/components/ui/menu/index.ts +2 -0
- package/src/components/ui/modal/Modal.stories.tsx +350 -0
- package/src/components/ui/modal/Modal.test.tsx +159 -0
- package/src/components/ui/modal/Modal.tsx +254 -0
- package/src/components/ui/modal/index.ts +19 -0
- package/src/components/ui/pill/Pill.stories.tsx +71 -0
- package/src/components/ui/pill/Pill.test.tsx +69 -0
- package/src/components/ui/pill/Pill.tsx +104 -0
- package/src/components/ui/pill/index.ts +2 -0
- package/src/components/ui/popover/Popover.stories.tsx +190 -0
- package/src/components/ui/popover/Popover.test.tsx +419 -0
- package/src/components/ui/popover/Popover.tsx +237 -0
- package/src/components/ui/popover/index.ts +2 -0
- package/src/components/ui/progress/Progress.stories.tsx +190 -0
- package/src/components/ui/progress/Progress.test.tsx +210 -0
- package/src/components/ui/progress/Progress.tsx +339 -0
- package/src/components/ui/progress/index.ts +9 -0
- package/src/components/ui/radio/Radio.stories.tsx +199 -0
- package/src/components/ui/radio/Radio.test.tsx +166 -0
- package/src/components/ui/radio/Radio.tsx +200 -0
- package/src/components/ui/radio/index.ts +2 -0
- package/src/components/ui/section/Section.stories.tsx +111 -0
- package/src/components/ui/section/Section.test.tsx +121 -0
- package/src/components/ui/section/Section.tsx +52 -0
- package/src/components/ui/section/index.ts +6 -0
- package/src/components/ui/select/Select.stories.tsx +286 -0
- package/src/components/ui/select/Select.test.tsx +200 -0
- package/src/components/ui/select/Select.tsx +243 -0
- package/src/components/ui/select/index.ts +2 -0
- package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
- package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
- package/src/components/ui/skeleton/Skeleton.tsx +229 -0
- package/src/components/ui/skeleton/index.ts +16 -0
- package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
- package/src/components/ui/spinner/Spinner.test.tsx +67 -0
- package/src/components/ui/spinner/Spinner.tsx +72 -0
- package/src/components/ui/spinner/index.ts +2 -0
- package/src/components/ui/stack/Stack.stories.tsx +127 -0
- package/src/components/ui/stack/Stack.test.tsx +184 -0
- package/src/components/ui/stack/Stack.tsx +76 -0
- package/src/components/ui/stack/index.ts +2 -0
- package/src/components/ui/surface/Surface.stories.tsx +91 -0
- package/src/components/ui/surface/Surface.test.tsx +63 -0
- package/src/components/ui/surface/Surface.tsx +51 -0
- package/src/components/ui/surface/index.ts +1 -0
- package/src/components/ui/switch/Switch.stories.tsx +111 -0
- package/src/components/ui/switch/Switch.test.tsx +112 -0
- package/src/components/ui/switch/Switch.tsx +103 -0
- package/src/components/ui/switch/index.ts +2 -0
- package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
- package/src/components/ui/tabs/Tabs.test.tsx +192 -0
- package/src/components/ui/tabs/Tabs.tsx +253 -0
- package/src/components/ui/tabs/index.ts +16 -0
- package/src/components/ui/toast/Toast.stories.tsx +255 -0
- package/src/components/ui/toast/Toast.test.tsx +207 -0
- package/src/components/ui/toast/Toast.tsx +327 -0
- package/src/components/ui/toast/index.ts +9 -0
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
- package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
- package/src/components/ui/toolbar-button/index.ts +7 -0
- package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
- package/src/components/ui/tooltip/Tooltip.test.tsx +262 -0
- package/src/components/ui/tooltip/Tooltip.tsx +234 -0
- package/src/components/ui/tooltip/index.ts +2 -0
- package/src/index.ts +10 -0
- package/src/stories/PresetShowcase.stories.tsx +462 -0
- package/src/stories/ThemePresets.stories.tsx +85 -0
- package/src/test/setup.ts +19 -0
- package/src/theme/Colors.stories.tsx +258 -0
- package/src/theme/config.ts +193 -0
- package/src/theme/elevation.stories.tsx +346 -0
- package/src/theme/elevation.test.ts +27 -0
- package/src/theme/elevation.ts +567 -0
- package/src/theme/global.css +129 -1
- package/src/theme/index.ts +40 -0
- package/src/theme/presets/apply.ts +97 -0
- package/src/theme/presets/audiobook.ts +93 -0
- package/src/theme/presets/default.ts +6 -0
- package/src/theme/presets/index.ts +4 -0
- package/src/theme/presets/presets.test.ts +51 -0
- package/src/theme/presets/types.ts +76 -0
- package/src/theme/shadows.stories.tsx +523 -0
- package/src/theme/shadows.ts +439 -0
- package/src/theme/tokens/index.ts +2 -0
- package/src/theme/tokens/primitives.ts +354 -0
- package/src/theme/tokens/semantic.ts +381 -0
- package/src/types/jest-axe.d.ts +16 -0
- package/src/types/nativewind.d.ts +54 -0
- package/src/utils/avatar-color.test.ts +30 -0
- package/src/utils/avatar-color.ts +20 -0
- package/src/utils/cn.ts +14 -0
- package/src/utils/colors.ts +140 -0
- package/src/utils/form.ts +188 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/useTheme.ts +72 -0
- package/src/web-jsx/jsx-dev-runtime.ts +28 -0
- package/src/web-jsx/jsx-runtime.ts +41 -0
- package/tailwind.config.js +53 -3
- package/dist/chunk-MGW37MPO.mjs.map +0 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import { Input, InputGroup, PasswordInput } from './Input'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Input> = {
|
|
6
|
+
title: 'Components/Input',
|
|
7
|
+
component: Input,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
variant: {
|
|
11
|
+
control: 'select',
|
|
12
|
+
options: ['outline', 'filled', 'underline'],
|
|
13
|
+
},
|
|
14
|
+
size: {
|
|
15
|
+
control: 'select',
|
|
16
|
+
options: ['sm', 'md', 'lg'],
|
|
17
|
+
},
|
|
18
|
+
multiline: { control: 'boolean' },
|
|
19
|
+
isDisabled: { control: 'boolean' },
|
|
20
|
+
isInvalid: { control: 'boolean' },
|
|
21
|
+
isReadOnly: { control: 'boolean' },
|
|
22
|
+
isRequired: { control: 'boolean' },
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default meta
|
|
27
|
+
type Story = StoryObj<typeof Input>
|
|
28
|
+
|
|
29
|
+
export const Default: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
placeholder: 'Enter text...',
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const WithLabel: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
label: 'Email',
|
|
38
|
+
placeholder: 'Enter your email',
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const WithHelperText: Story = {
|
|
43
|
+
args: {
|
|
44
|
+
label: 'Password',
|
|
45
|
+
placeholder: 'Enter your password',
|
|
46
|
+
helperText: 'Must be at least 8 characters',
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const WithError: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
label: 'Email',
|
|
53
|
+
placeholder: 'Enter your email',
|
|
54
|
+
isInvalid: true,
|
|
55
|
+
errorMessage: 'Please enter a valid email address',
|
|
56
|
+
defaultValue: 'invalid-email',
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const Required: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
label: 'Username',
|
|
63
|
+
placeholder: 'Choose a username',
|
|
64
|
+
isRequired: true,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const Disabled: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
label: 'Disabled Input',
|
|
71
|
+
placeholder: 'This is disabled',
|
|
72
|
+
isDisabled: true,
|
|
73
|
+
defaultValue: 'Disabled value',
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const ReadOnly: Story = {
|
|
78
|
+
args: {
|
|
79
|
+
label: 'Read Only',
|
|
80
|
+
isReadOnly: true,
|
|
81
|
+
defaultValue: 'This value cannot be changed',
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const AllVariants: Story = {
|
|
86
|
+
render: () => (
|
|
87
|
+
<View style={{ gap: 16, width: 300 }}>
|
|
88
|
+
<Input variant="outline" label="Outline" placeholder="Outline variant" />
|
|
89
|
+
<Input variant="filled" label="Filled" placeholder="Filled variant" />
|
|
90
|
+
<Input variant="underline" label="Underline" placeholder="Underline variant" />
|
|
91
|
+
</View>
|
|
92
|
+
),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const AllSizes: Story = {
|
|
96
|
+
render: () => (
|
|
97
|
+
<View style={{ gap: 16, width: 300 }}>
|
|
98
|
+
<Input size="sm" placeholder="Small input" />
|
|
99
|
+
<Input size="md" placeholder="Medium input" />
|
|
100
|
+
<Input size="lg" placeholder="Large input" />
|
|
101
|
+
</View>
|
|
102
|
+
),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const Multiline: Story = {
|
|
106
|
+
args: {
|
|
107
|
+
label: 'Description',
|
|
108
|
+
placeholder: 'Enter a description...',
|
|
109
|
+
multiline: true,
|
|
110
|
+
numberOfLines: 4,
|
|
111
|
+
helperText: 'Maximum 500 characters',
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const MultilineSizes: Story = {
|
|
116
|
+
render: () => (
|
|
117
|
+
<View style={{ gap: 16, width: 400 }}>
|
|
118
|
+
<Input size="sm" multiline label="Small" placeholder="Small textarea" />
|
|
119
|
+
<Input size="md" multiline label="Medium" placeholder="Medium textarea" />
|
|
120
|
+
<Input size="lg" multiline label="Large" placeholder="Large textarea" />
|
|
121
|
+
</View>
|
|
122
|
+
),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const Password: Story = {
|
|
126
|
+
render: () => (
|
|
127
|
+
<View style={{ gap: 16, width: 300 }}>
|
|
128
|
+
<PasswordInput
|
|
129
|
+
label="Password"
|
|
130
|
+
placeholder="Enter your password"
|
|
131
|
+
helperText="Must be at least 8 characters"
|
|
132
|
+
/>
|
|
133
|
+
</View>
|
|
134
|
+
),
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const PasswordWithError: Story = {
|
|
138
|
+
render: () => (
|
|
139
|
+
<View style={{ gap: 16, width: 300 }}>
|
|
140
|
+
<PasswordInput
|
|
141
|
+
label="Password"
|
|
142
|
+
placeholder="Enter your password"
|
|
143
|
+
isInvalid
|
|
144
|
+
errorMessage="Password must be at least 8 characters"
|
|
145
|
+
defaultValue="short"
|
|
146
|
+
/>
|
|
147
|
+
</View>
|
|
148
|
+
),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Simple search icon for demo
|
|
152
|
+
function SearchIcon() {
|
|
153
|
+
return (
|
|
154
|
+
<View className="w-4 h-4 items-center justify-center">
|
|
155
|
+
<View className="w-3 h-3 rounded-full border-2 border-text-secondary" />
|
|
156
|
+
<View className="absolute bottom-0 right-0 w-1.5 h-0.5 bg-text-secondary rotate-45 origin-left" />
|
|
157
|
+
</View>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const WithLeftElement: Story = {
|
|
162
|
+
render: () => (
|
|
163
|
+
<View style={{ gap: 16, width: 300 }}>
|
|
164
|
+
<Input
|
|
165
|
+
placeholder="Search..."
|
|
166
|
+
leftElement={<SearchIcon />}
|
|
167
|
+
/>
|
|
168
|
+
</View>
|
|
169
|
+
),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const FormExample: Story = {
|
|
173
|
+
render: () => (
|
|
174
|
+
<InputGroup>
|
|
175
|
+
<Input label="First Name" placeholder="John" isRequired />
|
|
176
|
+
<Input label="Last Name" placeholder="Doe" isRequired />
|
|
177
|
+
<Input label="Email" placeholder="john@example.com" isRequired />
|
|
178
|
+
<PasswordInput
|
|
179
|
+
label="Password"
|
|
180
|
+
placeholder="••••••••"
|
|
181
|
+
helperText="Must be at least 8 characters"
|
|
182
|
+
isRequired
|
|
183
|
+
/>
|
|
184
|
+
<Input
|
|
185
|
+
label="Bio"
|
|
186
|
+
placeholder="Tell us about yourself..."
|
|
187
|
+
multiline
|
|
188
|
+
numberOfLines={3}
|
|
189
|
+
/>
|
|
190
|
+
</InputGroup>
|
|
191
|
+
),
|
|
192
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { Input } from './Input'
|
|
5
|
+
|
|
6
|
+
describe('Input', () => {
|
|
7
|
+
it('renders correctly', () => {
|
|
8
|
+
render(<Input placeholder="Enter text" />)
|
|
9
|
+
expect(screen.getByPlaceholderText('Enter text')).toBeInTheDocument()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('displays label when provided', () => {
|
|
13
|
+
render(<Input label="Email" placeholder="Enter email" />)
|
|
14
|
+
expect(screen.getByText('Email')).toBeInTheDocument()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('displays required indicator when isRequired', () => {
|
|
18
|
+
render(<Input label="Email" isRequired />)
|
|
19
|
+
expect(screen.getByText('*')).toBeInTheDocument()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('displays helper text when provided', () => {
|
|
23
|
+
render(<Input helperText="This is helper text" />)
|
|
24
|
+
expect(screen.getByText('This is helper text')).toBeInTheDocument()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('displays error message when isInvalid', () => {
|
|
28
|
+
render(
|
|
29
|
+
<Input
|
|
30
|
+
isInvalid
|
|
31
|
+
errorMessage="Invalid input"
|
|
32
|
+
helperText="This should not show"
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
expect(screen.getByText('Invalid input')).toBeInTheDocument()
|
|
36
|
+
expect(screen.queryByText('This should not show')).not.toBeInTheDocument()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('handles text changes', () => {
|
|
40
|
+
const onChangeText = vi.fn()
|
|
41
|
+
render(<Input onChangeText={onChangeText} placeholder="Type here" />)
|
|
42
|
+
|
|
43
|
+
const input = screen.getByPlaceholderText('Type here')
|
|
44
|
+
// react-native-web renders as HTML input, use change event
|
|
45
|
+
fireEvent.change(input, { target: { value: 'Hello' } })
|
|
46
|
+
|
|
47
|
+
expect(onChangeText).toHaveBeenCalled()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('respects isDisabled prop', () => {
|
|
51
|
+
render(<Input isDisabled placeholder="Disabled" />)
|
|
52
|
+
const input = screen.getByPlaceholderText('Disabled')
|
|
53
|
+
// react-native-web renders isDisabled as readonly, not disabled
|
|
54
|
+
expect(input).toHaveAttribute('readonly')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('respects isReadOnly prop', () => {
|
|
58
|
+
render(<Input isReadOnly defaultValue="Read only" />)
|
|
59
|
+
const input = screen.getByDisplayValue('Read only')
|
|
60
|
+
expect(input).toHaveAttribute('readonly')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('accessibility', () => {
|
|
64
|
+
it('has no accessibility violations', async () => {
|
|
65
|
+
const { container } = render(
|
|
66
|
+
<Input label="Email" placeholder="Enter your email" />
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const results = await axe(container)
|
|
70
|
+
expect(results).toHaveNoViolations()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('associates label with input', () => {
|
|
74
|
+
render(<Input label="Username" placeholder="Enter username" />)
|
|
75
|
+
const input = screen.getByPlaceholderText('Enter username')
|
|
76
|
+
expect(input).toHaveAccessibleName('Username')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('communicates disabled state', () => {
|
|
80
|
+
render(<Input isDisabled placeholder="Disabled input" />)
|
|
81
|
+
const input = screen.getByPlaceholderText('Disabled input')
|
|
82
|
+
// react-native-web renders isDisabled as readonly
|
|
83
|
+
expect(input).toHaveAttribute('readonly')
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import React, { forwardRef, useState } from 'react'
|
|
2
|
+
import { TextInput, View, Text, Pressable, type TextInputProps } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
|
|
5
|
+
export type InputSize = 'sm' | 'md' | 'lg'
|
|
6
|
+
export type InputVariant = 'outline' | 'filled' | 'underline'
|
|
7
|
+
|
|
8
|
+
export interface InputProps extends Omit<TextInputProps, 'editable'> {
|
|
9
|
+
/** Input size */
|
|
10
|
+
size?: InputSize
|
|
11
|
+
/** Visual variant */
|
|
12
|
+
variant?: InputVariant
|
|
13
|
+
/** Whether the input is disabled */
|
|
14
|
+
isDisabled?: boolean
|
|
15
|
+
/** Whether the input has an error */
|
|
16
|
+
isInvalid?: boolean
|
|
17
|
+
/** Whether the input is read-only */
|
|
18
|
+
isReadOnly?: boolean
|
|
19
|
+
/** Whether the input is required */
|
|
20
|
+
isRequired?: boolean
|
|
21
|
+
/** Whether this is a multiline textarea */
|
|
22
|
+
multiline?: boolean
|
|
23
|
+
/** Number of visible lines for multiline */
|
|
24
|
+
numberOfLines?: number
|
|
25
|
+
/** Label text */
|
|
26
|
+
label?: string
|
|
27
|
+
/** Helper text shown below input */
|
|
28
|
+
helperText?: string
|
|
29
|
+
/** Error message (shown when isInvalid is true) */
|
|
30
|
+
errorMessage?: string
|
|
31
|
+
/** Left element/icon */
|
|
32
|
+
leftElement?: React.ReactNode
|
|
33
|
+
/** Right element/icon */
|
|
34
|
+
rightElement?: React.ReactNode
|
|
35
|
+
/** Additional className for the container */
|
|
36
|
+
className?: string
|
|
37
|
+
/** Additional className for the input */
|
|
38
|
+
inputClassName?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sizeStyles: Record<InputSize, string> = {
|
|
42
|
+
sm: 'h-8 px-3 text-sm',
|
|
43
|
+
md: 'h-10 px-4 text-base',
|
|
44
|
+
lg: 'h-12 px-4 text-lg',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const multilineSizeStyles: Record<InputSize, string> = {
|
|
48
|
+
sm: 'min-h-[80px] px-3 py-2 text-sm',
|
|
49
|
+
md: 'min-h-[100px] px-4 py-3 text-base',
|
|
50
|
+
lg: 'min-h-[120px] px-4 py-3 text-lg',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const variantStyles: Record<InputVariant, { base: string; focus: string; hover: string; error: string }> = {
|
|
54
|
+
outline: {
|
|
55
|
+
base: 'border border-border-input rounded-md bg-transparent',
|
|
56
|
+
hover: 'web:hover:border-border-input-hover',
|
|
57
|
+
focus: 'border-border-input-focus',
|
|
58
|
+
error: 'border-border-input-error',
|
|
59
|
+
},
|
|
60
|
+
filled: {
|
|
61
|
+
base: 'border border-transparent rounded-md bg-surface-input',
|
|
62
|
+
hover: 'web:hover:border-border-input-hover',
|
|
63
|
+
focus: 'border-border-input-focus bg-transparent',
|
|
64
|
+
error: 'border-border-input-error',
|
|
65
|
+
},
|
|
66
|
+
underline: {
|
|
67
|
+
base: 'border-b border-border-input rounded-none bg-transparent',
|
|
68
|
+
hover: 'web:hover:border-border-input-hover',
|
|
69
|
+
focus: 'border-border-input-focus',
|
|
70
|
+
error: 'border-border-input-error',
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Input component for text entry.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* <Input
|
|
79
|
+
* label="Email"
|
|
80
|
+
* placeholder="Enter your email"
|
|
81
|
+
* helperText="We'll never share your email"
|
|
82
|
+
* />
|
|
83
|
+
*
|
|
84
|
+
* // With error state
|
|
85
|
+
* <Input
|
|
86
|
+
* label="Password"
|
|
87
|
+
* isInvalid
|
|
88
|
+
* errorMessage="Password is required"
|
|
89
|
+
* />
|
|
90
|
+
*/
|
|
91
|
+
export const Input = forwardRef<TextInput, InputProps>(function Input(
|
|
92
|
+
{
|
|
93
|
+
size = 'md',
|
|
94
|
+
variant = 'outline',
|
|
95
|
+
isDisabled = false,
|
|
96
|
+
isInvalid = false,
|
|
97
|
+
isReadOnly = false,
|
|
98
|
+
isRequired = false,
|
|
99
|
+
multiline = false,
|
|
100
|
+
numberOfLines = 4,
|
|
101
|
+
label,
|
|
102
|
+
helperText,
|
|
103
|
+
errorMessage,
|
|
104
|
+
leftElement,
|
|
105
|
+
rightElement,
|
|
106
|
+
className,
|
|
107
|
+
inputClassName,
|
|
108
|
+
onFocus,
|
|
109
|
+
onBlur,
|
|
110
|
+
...props
|
|
111
|
+
},
|
|
112
|
+
ref
|
|
113
|
+
) {
|
|
114
|
+
const [isFocused, setIsFocused] = useState(false)
|
|
115
|
+
|
|
116
|
+
const handleFocus = (e: any) => {
|
|
117
|
+
setIsFocused(true)
|
|
118
|
+
onFocus?.(e)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const handleBlur = (e: any) => {
|
|
122
|
+
setIsFocused(false)
|
|
123
|
+
onBlur?.(e)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const showError = isInvalid && errorMessage
|
|
127
|
+
const showHelper = !showError && helperText
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<View className={cn('w-full', className)}>
|
|
131
|
+
{label && (
|
|
132
|
+
<Text className="mb-1.5 text-sm font-medium text-text-primary">
|
|
133
|
+
{label}
|
|
134
|
+
{isRequired && <Text className="text-status-error ml-0.5">*</Text>}
|
|
135
|
+
</Text>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
<View
|
|
139
|
+
className={cn(
|
|
140
|
+
'flex-row transition-colors duration-150',
|
|
141
|
+
multiline ? 'items-start' : 'items-center',
|
|
142
|
+
variantStyles[variant].base,
|
|
143
|
+
!isDisabled && !isInvalid && variantStyles[variant].hover,
|
|
144
|
+
isFocused && variantStyles[variant].focus,
|
|
145
|
+
isInvalid && variantStyles[variant].error,
|
|
146
|
+
isDisabled && 'opacity-40 cursor-not-allowed'
|
|
147
|
+
)}
|
|
148
|
+
>
|
|
149
|
+
{leftElement && (
|
|
150
|
+
<View className={cn('pl-3 pr-1', multiline && 'pt-3')}>
|
|
151
|
+
{leftElement}
|
|
152
|
+
</View>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
<TextInput
|
|
156
|
+
ref={ref}
|
|
157
|
+
editable={!isDisabled && !isReadOnly}
|
|
158
|
+
accessibilityLabel={label}
|
|
159
|
+
accessibilityState={{
|
|
160
|
+
disabled: isDisabled,
|
|
161
|
+
}}
|
|
162
|
+
multiline={multiline}
|
|
163
|
+
numberOfLines={multiline ? numberOfLines : 1}
|
|
164
|
+
textAlignVertical={multiline ? 'top' : 'center'}
|
|
165
|
+
placeholderTextColor="var(--color-text-tertiary)"
|
|
166
|
+
onFocus={handleFocus}
|
|
167
|
+
onBlur={handleBlur}
|
|
168
|
+
className={cn(
|
|
169
|
+
'flex-1 text-text-primary',
|
|
170
|
+
multiline ? multilineSizeStyles[size] : sizeStyles[size],
|
|
171
|
+
leftElement && 'pl-1',
|
|
172
|
+
rightElement && 'pr-1',
|
|
173
|
+
inputClassName
|
|
174
|
+
)}
|
|
175
|
+
{...props}
|
|
176
|
+
/>
|
|
177
|
+
|
|
178
|
+
{rightElement && (
|
|
179
|
+
<View className={cn('pl-1 pr-3', multiline && 'pt-3')}>
|
|
180
|
+
{rightElement}
|
|
181
|
+
</View>
|
|
182
|
+
)}
|
|
183
|
+
</View>
|
|
184
|
+
|
|
185
|
+
{showHelper && (
|
|
186
|
+
<Text className="mt-1.5 text-xs text-text-secondary">
|
|
187
|
+
{helperText}
|
|
188
|
+
</Text>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{showError && (
|
|
192
|
+
<Text className="mt-1.5 text-xs text-status-error">
|
|
193
|
+
{errorMessage}
|
|
194
|
+
</Text>
|
|
195
|
+
)}
|
|
196
|
+
</View>
|
|
197
|
+
)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
export interface InputGroupProps {
|
|
201
|
+
children: React.ReactNode
|
|
202
|
+
className?: string
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Container for grouping related inputs.
|
|
207
|
+
*/
|
|
208
|
+
export function InputGroup({ children, className }: InputGroupProps) {
|
|
209
|
+
return (
|
|
210
|
+
<View className={cn('flex-col gap-4', className)}>
|
|
211
|
+
{children}
|
|
212
|
+
</View>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface PasswordInputProps extends Omit<InputProps, 'secureTextEntry' | 'rightElement'> {
|
|
217
|
+
/** Custom show password icon */
|
|
218
|
+
showIcon?: React.ReactNode
|
|
219
|
+
/** Custom hide password icon */
|
|
220
|
+
hideIcon?: React.ReactNode
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Password input with visibility toggle.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* <PasswordInput
|
|
228
|
+
* label="Password"
|
|
229
|
+
* placeholder="Enter your password"
|
|
230
|
+
* />
|
|
231
|
+
*/
|
|
232
|
+
export const PasswordInput = forwardRef<TextInput, PasswordInputProps>(function PasswordInput(
|
|
233
|
+
{ showIcon, hideIcon, ...props },
|
|
234
|
+
ref
|
|
235
|
+
) {
|
|
236
|
+
const [isVisible, setIsVisible] = useState(false)
|
|
237
|
+
|
|
238
|
+
const toggleVisibility = () => setIsVisible(!isVisible)
|
|
239
|
+
|
|
240
|
+
// Default eye icons (simple SVG-like views)
|
|
241
|
+
const defaultShowIcon = (
|
|
242
|
+
<View className="w-5 h-5 items-center justify-center">
|
|
243
|
+
<View className="w-3 h-3 rounded-full border-2 border-text-secondary" />
|
|
244
|
+
<View className="absolute w-1.5 h-1.5 rounded-full bg-text-secondary" />
|
|
245
|
+
</View>
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
const defaultHideIcon = (
|
|
249
|
+
<View className="w-5 h-5 items-center justify-center">
|
|
250
|
+
<View className="w-3 h-3 rounded-full border-2 border-text-secondary" />
|
|
251
|
+
<View className="absolute w-1.5 h-1.5 rounded-full bg-text-secondary" />
|
|
252
|
+
<View className="absolute w-5 h-0.5 bg-text-secondary rotate-45" />
|
|
253
|
+
</View>
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<Input
|
|
258
|
+
ref={ref}
|
|
259
|
+
secureTextEntry={!isVisible}
|
|
260
|
+
rightElement={
|
|
261
|
+
<Pressable
|
|
262
|
+
onPress={toggleVisibility}
|
|
263
|
+
accessibilityRole="button"
|
|
264
|
+
accessibilityLabel={isVisible ? 'Hide password' : 'Show password'}
|
|
265
|
+
className="p-1 rounded active:opacity-70"
|
|
266
|
+
>
|
|
267
|
+
{isVisible ? (hideIcon || defaultHideIcon) : (showIcon || defaultShowIcon)}
|
|
268
|
+
</Pressable>
|
|
269
|
+
}
|
|
270
|
+
{...props}
|
|
271
|
+
/>
|
|
272
|
+
)
|
|
273
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { Link } from './Link'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Link> = {
|
|
6
|
+
title: 'Components/Link',
|
|
7
|
+
component: Link,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
color: {
|
|
11
|
+
control: 'select',
|
|
12
|
+
options: ['default', 'primary', 'secondary', 'inherit'],
|
|
13
|
+
description: 'Link color',
|
|
14
|
+
},
|
|
15
|
+
underline: {
|
|
16
|
+
control: 'select',
|
|
17
|
+
options: ['always', 'hover', 'none'],
|
|
18
|
+
description: 'Underline behavior',
|
|
19
|
+
},
|
|
20
|
+
isExternal: {
|
|
21
|
+
control: 'boolean',
|
|
22
|
+
description: 'Whether the link opens externally',
|
|
23
|
+
},
|
|
24
|
+
isDisabled: {
|
|
25
|
+
control: 'boolean',
|
|
26
|
+
description: 'Whether the link is disabled',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default meta
|
|
32
|
+
type Story = StoryObj<typeof Link>
|
|
33
|
+
|
|
34
|
+
export const Default: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
children: 'Click here',
|
|
37
|
+
color: 'default',
|
|
38
|
+
underline: 'hover',
|
|
39
|
+
onPress: () => {},
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const AllColors: Story = {
|
|
44
|
+
render: () => (
|
|
45
|
+
<View style={{ flexDirection: 'row', gap: 16 }}>
|
|
46
|
+
<Link color="default" onPress={() => {}}>Default</Link>
|
|
47
|
+
<Link color="primary" onPress={() => {}}>Primary</Link>
|
|
48
|
+
<Link color="secondary" onPress={() => {}}>Secondary</Link>
|
|
49
|
+
</View>
|
|
50
|
+
),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const UnderlineStyles: Story = {
|
|
54
|
+
render: () => (
|
|
55
|
+
<View style={{ flexDirection: 'row', gap: 16 }}>
|
|
56
|
+
<Link underline="always" onPress={() => {}}>Always</Link>
|
|
57
|
+
<Link underline="hover" onPress={() => {}}>Hover</Link>
|
|
58
|
+
<Link underline="none" onPress={() => {}}>None</Link>
|
|
59
|
+
</View>
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const External: Story = {
|
|
64
|
+
args: {
|
|
65
|
+
children: 'Visit Example',
|
|
66
|
+
href: 'https://example.com',
|
|
67
|
+
isExternal: true,
|
|
68
|
+
onPress: () => {},
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const Disabled: Story = {
|
|
73
|
+
args: {
|
|
74
|
+
children: 'Disabled Link',
|
|
75
|
+
isDisabled: true,
|
|
76
|
+
onPress: () => {},
|
|
77
|
+
},
|
|
78
|
+
}
|