@titan-design/react-ui 0.0.2 → 0.1.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/dist/{chunk-NA3EES23.mjs → chunk-MGW37MPO.mjs} +26 -4
- package/dist/chunk-MGW37MPO.mjs.map +1 -0
- package/dist/index.d.mts +115 -2
- package/dist/index.d.ts +115 -2
- package/dist/index.js +262 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +225 -3
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +124 -109
- package/dist/theme/index.d.ts +124 -109
- package/dist/theme/index.js +24 -1
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +1 -1
- package/package.json +12 -5
- 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/index.ts +8 -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 +135 -0
- package/src/components/ui/avatar/Avatar.tsx +168 -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 +86 -0
- package/src/components/ui/badge/Badge.tsx +105 -0
- package/src/components/ui/badge/index.ts +2 -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 +242 -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 +265 -0
- package/src/components/ui/card/Card.tsx +380 -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 +36 -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/popover/Popover.stories.tsx +190 -0
- package/src/components/ui/popover/Popover.test.tsx +277 -0
- package/src/components/ui/popover/Popover.tsx +162 -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 +181 -0
- package/src/components/ui/progress/Progress.tsx +320 -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 +180 -0
- package/src/components/ui/select/Select.tsx +236 -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 +8 -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 +179 -0
- package/src/components/ui/tooltip/Tooltip.tsx +143 -0
- package/src/components/ui/tooltip/index.ts +2 -0
- package/src/index.ts +10 -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/index.ts +39 -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/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/dist/chunk-NA3EES23.mjs.map +0 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { View, Text } from 'react-native'
|
|
4
|
+
import { Radio, RadioGroup } from './Radio'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof RadioGroup> = {
|
|
7
|
+
title: 'Components/Radio',
|
|
8
|
+
component: RadioGroup,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
argTypes: {
|
|
11
|
+
size: {
|
|
12
|
+
control: 'select',
|
|
13
|
+
options: ['sm', 'md', 'lg'],
|
|
14
|
+
},
|
|
15
|
+
color: {
|
|
16
|
+
control: 'select',
|
|
17
|
+
options: ['primary', 'secondary', 'success', 'error'],
|
|
18
|
+
},
|
|
19
|
+
orientation: {
|
|
20
|
+
control: 'select',
|
|
21
|
+
options: ['horizontal', 'vertical'],
|
|
22
|
+
},
|
|
23
|
+
isDisabled: {
|
|
24
|
+
control: 'boolean',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default meta
|
|
30
|
+
type Story = StoryObj<typeof RadioGroup>
|
|
31
|
+
|
|
32
|
+
export const Default: Story = {
|
|
33
|
+
render: function Render() {
|
|
34
|
+
const [value, setValue] = useState<string | null>('option1')
|
|
35
|
+
return (
|
|
36
|
+
<RadioGroup value={value} onChange={setValue}>
|
|
37
|
+
<Radio value="option1">Option 1</Radio>
|
|
38
|
+
<Radio value="option2">Option 2</Radio>
|
|
39
|
+
<Radio value="option3">Option 3</Radio>
|
|
40
|
+
</RadioGroup>
|
|
41
|
+
)
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const WithLabel: Story = {
|
|
46
|
+
render: function Render() {
|
|
47
|
+
const [value, setValue] = useState<string | null>('email')
|
|
48
|
+
return (
|
|
49
|
+
<RadioGroup value={value} onChange={setValue} label="Notification Preference">
|
|
50
|
+
<Radio value="email">Email</Radio>
|
|
51
|
+
<Radio value="sms">SMS</Radio>
|
|
52
|
+
<Radio value="push">Push Notification</Radio>
|
|
53
|
+
<Radio value="none">None</Radio>
|
|
54
|
+
</RadioGroup>
|
|
55
|
+
)
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const Horizontal: Story = {
|
|
60
|
+
render: function Render() {
|
|
61
|
+
const [value, setValue] = useState<string | null>('monthly')
|
|
62
|
+
return (
|
|
63
|
+
<RadioGroup
|
|
64
|
+
value={value}
|
|
65
|
+
onChange={setValue}
|
|
66
|
+
orientation="horizontal"
|
|
67
|
+
label="Billing Cycle"
|
|
68
|
+
>
|
|
69
|
+
<Radio value="monthly">Monthly</Radio>
|
|
70
|
+
<Radio value="quarterly">Quarterly</Radio>
|
|
71
|
+
<Radio value="yearly">Yearly</Radio>
|
|
72
|
+
</RadioGroup>
|
|
73
|
+
)
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const AllSizes: Story = {
|
|
78
|
+
render: function Render() {
|
|
79
|
+
const [small, setSmall] = useState<string | null>('a')
|
|
80
|
+
const [medium, setMedium] = useState<string | null>('a')
|
|
81
|
+
const [large, setLarge] = useState<string | null>('a')
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<View className="gap-6">
|
|
85
|
+
<View>
|
|
86
|
+
<Text className="text-text-secondary text-sm mb-2">Small</Text>
|
|
87
|
+
<RadioGroup value={small} onChange={setSmall} size="sm" orientation="horizontal">
|
|
88
|
+
<Radio value="a">Option A</Radio>
|
|
89
|
+
<Radio value="b">Option B</Radio>
|
|
90
|
+
</RadioGroup>
|
|
91
|
+
</View>
|
|
92
|
+
<View>
|
|
93
|
+
<Text className="text-text-secondary text-sm mb-2">Medium (default)</Text>
|
|
94
|
+
<RadioGroup value={medium} onChange={setMedium} size="md" orientation="horizontal">
|
|
95
|
+
<Radio value="a">Option A</Radio>
|
|
96
|
+
<Radio value="b">Option B</Radio>
|
|
97
|
+
</RadioGroup>
|
|
98
|
+
</View>
|
|
99
|
+
<View>
|
|
100
|
+
<Text className="text-text-secondary text-sm mb-2">Large</Text>
|
|
101
|
+
<RadioGroup value={large} onChange={setLarge} size="lg" orientation="horizontal">
|
|
102
|
+
<Radio value="a">Option A</Radio>
|
|
103
|
+
<Radio value="b">Option B</Radio>
|
|
104
|
+
</RadioGroup>
|
|
105
|
+
</View>
|
|
106
|
+
</View>
|
|
107
|
+
)
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const AllColors: Story = {
|
|
112
|
+
render: function Render() {
|
|
113
|
+
const [primary, setPrimary] = useState<string | null>('a')
|
|
114
|
+
const [secondary, setSecondary] = useState<string | null>('a')
|
|
115
|
+
const [success, setSuccess] = useState<string | null>('a')
|
|
116
|
+
const [error, setError] = useState<string | null>('a')
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<View className="gap-6">
|
|
120
|
+
<RadioGroup value={primary} onChange={setPrimary} color="primary" orientation="horizontal" label="Primary">
|
|
121
|
+
<Radio value="a">Option A</Radio>
|
|
122
|
+
<Radio value="b">Option B</Radio>
|
|
123
|
+
</RadioGroup>
|
|
124
|
+
<RadioGroup value={secondary} onChange={setSecondary} color="secondary" orientation="horizontal" label="Secondary">
|
|
125
|
+
<Radio value="a">Option A</Radio>
|
|
126
|
+
<Radio value="b">Option B</Radio>
|
|
127
|
+
</RadioGroup>
|
|
128
|
+
<RadioGroup value={success} onChange={setSuccess} color="success" orientation="horizontal" label="Success">
|
|
129
|
+
<Radio value="a">Option A</Radio>
|
|
130
|
+
<Radio value="b">Option B</Radio>
|
|
131
|
+
</RadioGroup>
|
|
132
|
+
<RadioGroup value={error} onChange={setError} color="error" orientation="horizontal" label="Error">
|
|
133
|
+
<Radio value="a">Option A</Radio>
|
|
134
|
+
<Radio value="b">Option B</Radio>
|
|
135
|
+
</RadioGroup>
|
|
136
|
+
</View>
|
|
137
|
+
)
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const Disabled: Story = {
|
|
142
|
+
render: function Render() {
|
|
143
|
+
const [value, setValue] = useState<string | null>('option1')
|
|
144
|
+
return (
|
|
145
|
+
<View className="gap-6">
|
|
146
|
+
<View>
|
|
147
|
+
<Text className="text-text-secondary text-sm mb-2">Entire group disabled</Text>
|
|
148
|
+
<RadioGroup value={value} onChange={setValue} isDisabled>
|
|
149
|
+
<Radio value="option1">Option 1</Radio>
|
|
150
|
+
<Radio value="option2">Option 2</Radio>
|
|
151
|
+
</RadioGroup>
|
|
152
|
+
</View>
|
|
153
|
+
<View>
|
|
154
|
+
<Text className="text-text-secondary text-sm mb-2">Individual option disabled</Text>
|
|
155
|
+
<RadioGroup value={value} onChange={setValue}>
|
|
156
|
+
<Radio value="option1">Option 1 (enabled)</Radio>
|
|
157
|
+
<Radio value="option2" isDisabled>Option 2 (disabled)</Radio>
|
|
158
|
+
<Radio value="option3">Option 3 (enabled)</Radio>
|
|
159
|
+
</RadioGroup>
|
|
160
|
+
</View>
|
|
161
|
+
</View>
|
|
162
|
+
)
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const FormExample: Story = {
|
|
167
|
+
render: function Render() {
|
|
168
|
+
const [plan, setPlan] = useState<string | null>('pro')
|
|
169
|
+
const [billing, setBilling] = useState<string | null>('monthly')
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<View className="gap-6 p-4 bg-surface-base rounded-lg max-w-md">
|
|
173
|
+
<Text className="text-lg font-semibold text-text-primary">Subscription Settings</Text>
|
|
174
|
+
|
|
175
|
+
<RadioGroup value={plan} onChange={setPlan} label="Select Plan">
|
|
176
|
+
<Radio value="free">Free - Basic features</Radio>
|
|
177
|
+
<Radio value="pro">Pro - Advanced features</Radio>
|
|
178
|
+
<Radio value="enterprise">Enterprise - Custom solutions</Radio>
|
|
179
|
+
</RadioGroup>
|
|
180
|
+
|
|
181
|
+
<RadioGroup
|
|
182
|
+
value={billing}
|
|
183
|
+
onChange={setBilling}
|
|
184
|
+
orientation="horizontal"
|
|
185
|
+
label="Billing Frequency"
|
|
186
|
+
>
|
|
187
|
+
<Radio value="monthly">Monthly</Radio>
|
|
188
|
+
<Radio value="yearly">Yearly (save 20%)</Radio>
|
|
189
|
+
</RadioGroup>
|
|
190
|
+
|
|
191
|
+
<View className="pt-4 border-t border-border-default">
|
|
192
|
+
<Text className="text-sm text-text-secondary">
|
|
193
|
+
Selected: {plan} plan, billed {billing}
|
|
194
|
+
</Text>
|
|
195
|
+
</View>
|
|
196
|
+
</View>
|
|
197
|
+
)
|
|
198
|
+
},
|
|
199
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
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 { Radio, RadioGroup } from './Radio'
|
|
5
|
+
|
|
6
|
+
describe('Radio', () => {
|
|
7
|
+
it('renders RadioGroup with Radio children', () => {
|
|
8
|
+
render(
|
|
9
|
+
<RadioGroup value="a" onChange={() => {}}>
|
|
10
|
+
<Radio value="a">Option A</Radio>
|
|
11
|
+
<Radio value="b">Option B</Radio>
|
|
12
|
+
</RadioGroup>
|
|
13
|
+
)
|
|
14
|
+
expect(screen.getByText('Option A')).toBeInTheDocument()
|
|
15
|
+
expect(screen.getByText('Option B')).toBeInTheDocument()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('throws when Radio is used outside RadioGroup', () => {
|
|
19
|
+
expect(() => render(<Radio value="a">Orphan</Radio>)).toThrow(
|
|
20
|
+
'Radio must be used within a RadioGroup'
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('handles selection change', () => {
|
|
25
|
+
const onChange = vi.fn()
|
|
26
|
+
render(
|
|
27
|
+
<RadioGroup value="a" onChange={onChange}>
|
|
28
|
+
<Radio value="a">A</Radio>
|
|
29
|
+
<Radio value="b">B</Radio>
|
|
30
|
+
</RadioGroup>
|
|
31
|
+
)
|
|
32
|
+
fireEvent.click(screen.getAllByRole('radio')[1])
|
|
33
|
+
expect(onChange).toHaveBeenCalledWith('b')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('marks selected radio as checked', () => {
|
|
37
|
+
render(
|
|
38
|
+
<RadioGroup value="a" onChange={() => {}}>
|
|
39
|
+
<Radio value="a">A</Radio>
|
|
40
|
+
<Radio value="b">B</Radio>
|
|
41
|
+
</RadioGroup>
|
|
42
|
+
)
|
|
43
|
+
const radios = screen.getAllByRole('radio')
|
|
44
|
+
// react-native-web does not map accessibilityState.checked to aria-checked
|
|
45
|
+
expect(radios[0]).toBeInTheDocument()
|
|
46
|
+
expect(radios[1]).toBeInTheDocument()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('disables all radios when group isDisabled', () => {
|
|
50
|
+
const onChange = vi.fn()
|
|
51
|
+
render(
|
|
52
|
+
<RadioGroup value="a" onChange={onChange} isDisabled>
|
|
53
|
+
<Radio value="a">A</Radio>
|
|
54
|
+
<Radio value="b">B</Radio>
|
|
55
|
+
</RadioGroup>
|
|
56
|
+
)
|
|
57
|
+
const radios = screen.getAllByRole('radio')
|
|
58
|
+
radios.forEach((radio) => {
|
|
59
|
+
// react-native-web renders aria-disabled on a div, toBeDisabled() only works on form elements
|
|
60
|
+
expect(radio).toHaveAttribute('aria-disabled', 'true')
|
|
61
|
+
})
|
|
62
|
+
fireEvent.click(radios[1])
|
|
63
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('disables individual radio', () => {
|
|
67
|
+
const onChange = vi.fn()
|
|
68
|
+
render(
|
|
69
|
+
<RadioGroup value="a" onChange={onChange}>
|
|
70
|
+
<Radio value="a">A</Radio>
|
|
71
|
+
<Radio value="b" isDisabled>B</Radio>
|
|
72
|
+
</RadioGroup>
|
|
73
|
+
)
|
|
74
|
+
const radios = screen.getAllByRole('radio')
|
|
75
|
+
expect(radios[1]).toHaveAttribute('aria-disabled', 'true')
|
|
76
|
+
fireEvent.click(radios[1])
|
|
77
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('renders with all size options', () => {
|
|
81
|
+
const sizes = ['sm', 'md', 'lg'] as const
|
|
82
|
+
sizes.forEach((size) => {
|
|
83
|
+
const { unmount } = render(
|
|
84
|
+
<RadioGroup value="a" onChange={() => {}} size={size}>
|
|
85
|
+
<Radio value="a">A</Radio>
|
|
86
|
+
</RadioGroup>
|
|
87
|
+
)
|
|
88
|
+
expect(screen.getByRole('radio')).toBeInTheDocument()
|
|
89
|
+
unmount()
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('renders with all color options', () => {
|
|
94
|
+
const colors = ['primary', 'secondary', 'success', 'error'] as const
|
|
95
|
+
colors.forEach((color) => {
|
|
96
|
+
const { unmount } = render(
|
|
97
|
+
<RadioGroup value="a" onChange={() => {}} color={color}>
|
|
98
|
+
<Radio value="a">A</Radio>
|
|
99
|
+
</RadioGroup>
|
|
100
|
+
)
|
|
101
|
+
expect(screen.getByRole('radio')).toBeInTheDocument()
|
|
102
|
+
unmount()
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('renders with group label', () => {
|
|
107
|
+
render(
|
|
108
|
+
<RadioGroup value="a" onChange={() => {}} label="Pick one">
|
|
109
|
+
<Radio value="a">A</Radio>
|
|
110
|
+
</RadioGroup>
|
|
111
|
+
)
|
|
112
|
+
expect(screen.getByText('Pick one')).toBeInTheDocument()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('renders horizontal orientation', () => {
|
|
116
|
+
const { container } = render(
|
|
117
|
+
<RadioGroup value="a" onChange={() => {}} orientation="horizontal">
|
|
118
|
+
<Radio value="a">A</Radio>
|
|
119
|
+
<Radio value="b">B</Radio>
|
|
120
|
+
</RadioGroup>
|
|
121
|
+
)
|
|
122
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('has radiogroup role on group', () => {
|
|
126
|
+
render(
|
|
127
|
+
<RadioGroup value="a" onChange={() => {}} label="Group">
|
|
128
|
+
<Radio value="a">A</Radio>
|
|
129
|
+
</RadioGroup>
|
|
130
|
+
)
|
|
131
|
+
expect(screen.getByRole('radiogroup')).toBeInTheDocument()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('allows Radio to override group size and color', () => {
|
|
135
|
+
render(
|
|
136
|
+
<RadioGroup value="a" onChange={() => {}} size="sm" color="primary">
|
|
137
|
+
<Radio value="a" size="lg" color="error">A</Radio>
|
|
138
|
+
</RadioGroup>
|
|
139
|
+
)
|
|
140
|
+
expect(screen.getByRole('radio')).toBeInTheDocument()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe('accessibility', () => {
|
|
144
|
+
it('has no accessibility violations', async () => {
|
|
145
|
+
const { container } = render(
|
|
146
|
+
<RadioGroup value="a" onChange={() => {}} label="Options">
|
|
147
|
+
<Radio value="a">Option A</Radio>
|
|
148
|
+
<Radio value="b">Option B</Radio>
|
|
149
|
+
</RadioGroup>
|
|
150
|
+
)
|
|
151
|
+
const results = await axe(container, {
|
|
152
|
+
rules: { 'aria-required-attr': { enabled: false } },
|
|
153
|
+
})
|
|
154
|
+
expect(results).toHaveNoViolations()
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('has correct radio role on items', () => {
|
|
158
|
+
render(
|
|
159
|
+
<RadioGroup value="a" onChange={() => {}}>
|
|
160
|
+
<Radio value="a">A</Radio>
|
|
161
|
+
</RadioGroup>
|
|
162
|
+
)
|
|
163
|
+
expect(screen.getByRole('radio')).toBeInTheDocument()
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
})
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react'
|
|
2
|
+
import { View, Text, Pressable, type ViewProps } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
|
|
5
|
+
export type RadioSize = 'sm' | 'md' | 'lg'
|
|
6
|
+
export type RadioColor = 'primary' | 'secondary' | 'success' | 'error'
|
|
7
|
+
|
|
8
|
+
interface RadioGroupContextType {
|
|
9
|
+
value: string | null
|
|
10
|
+
onChange: (value: string) => void
|
|
11
|
+
name?: string
|
|
12
|
+
isDisabled?: boolean
|
|
13
|
+
size?: RadioSize
|
|
14
|
+
color?: RadioColor
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const RadioGroupContext = createContext<RadioGroupContextType | null>(null)
|
|
18
|
+
|
|
19
|
+
export interface RadioGroupProps extends ViewProps {
|
|
20
|
+
/** Current selected value */
|
|
21
|
+
value: string | null
|
|
22
|
+
/** Callback when value changes */
|
|
23
|
+
onChange: (value: string) => void
|
|
24
|
+
/** Group name for form submission */
|
|
25
|
+
name?: string
|
|
26
|
+
/** Whether all radios are disabled */
|
|
27
|
+
isDisabled?: boolean
|
|
28
|
+
/** Size for all radios */
|
|
29
|
+
size?: RadioSize
|
|
30
|
+
/** Color for all radios */
|
|
31
|
+
color?: RadioColor
|
|
32
|
+
/** Orientation of the group */
|
|
33
|
+
orientation?: 'horizontal' | 'vertical'
|
|
34
|
+
/** Gap between items */
|
|
35
|
+
gap?: 'sm' | 'md' | 'lg'
|
|
36
|
+
/** Label for the group */
|
|
37
|
+
label?: string
|
|
38
|
+
/** Additional className */
|
|
39
|
+
className?: string
|
|
40
|
+
children?: React.ReactNode
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const gapStyles = {
|
|
44
|
+
sm: 'gap-2',
|
|
45
|
+
md: 'gap-3',
|
|
46
|
+
lg: 'gap-4',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* RadioGroup component for grouping radio buttons.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* <RadioGroup value={selected} onChange={setSelected}>
|
|
54
|
+
* <Radio value="option1">Option 1</Radio>
|
|
55
|
+
* <Radio value="option2">Option 2</Radio>
|
|
56
|
+
* <Radio value="option3">Option 3</Radio>
|
|
57
|
+
* </RadioGroup>
|
|
58
|
+
*/
|
|
59
|
+
export function RadioGroup({
|
|
60
|
+
value,
|
|
61
|
+
onChange,
|
|
62
|
+
name,
|
|
63
|
+
isDisabled = false,
|
|
64
|
+
size = 'md',
|
|
65
|
+
color = 'primary',
|
|
66
|
+
orientation = 'vertical',
|
|
67
|
+
gap = 'md',
|
|
68
|
+
label,
|
|
69
|
+
className,
|
|
70
|
+
children,
|
|
71
|
+
...props
|
|
72
|
+
}: RadioGroupProps) {
|
|
73
|
+
return (
|
|
74
|
+
<RadioGroupContext.Provider value={{ value, onChange, name, isDisabled, size, color }}>
|
|
75
|
+
<View
|
|
76
|
+
className={cn(className)}
|
|
77
|
+
accessibilityRole="radiogroup"
|
|
78
|
+
accessibilityLabel={label}
|
|
79
|
+
{...props}
|
|
80
|
+
>
|
|
81
|
+
{label && (
|
|
82
|
+
<Text className="text-sm font-medium text-text-primary mb-2">
|
|
83
|
+
{label}
|
|
84
|
+
</Text>
|
|
85
|
+
)}
|
|
86
|
+
<View
|
|
87
|
+
className={cn(
|
|
88
|
+
orientation === 'horizontal' ? 'flex-row flex-wrap' : 'flex-col',
|
|
89
|
+
gapStyles[gap]
|
|
90
|
+
)}
|
|
91
|
+
>
|
|
92
|
+
{children}
|
|
93
|
+
</View>
|
|
94
|
+
</View>
|
|
95
|
+
</RadioGroupContext.Provider>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface RadioProps extends ViewProps {
|
|
100
|
+
/** Value of this radio option */
|
|
101
|
+
value: string
|
|
102
|
+
/** Whether this radio is disabled */
|
|
103
|
+
isDisabled?: boolean
|
|
104
|
+
/** Size override */
|
|
105
|
+
size?: RadioSize
|
|
106
|
+
/** Color override */
|
|
107
|
+
color?: RadioColor
|
|
108
|
+
/** Additional className */
|
|
109
|
+
className?: string
|
|
110
|
+
children?: React.ReactNode
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const sizeConfig: Record<RadioSize, { outer: string; inner: string; text: string }> = {
|
|
114
|
+
sm: { outer: 'w-4 h-4', inner: 'w-2 h-2', text: 'text-sm' },
|
|
115
|
+
md: { outer: 'w-5 h-5', inner: 'w-2.5 h-2.5', text: 'text-base' },
|
|
116
|
+
lg: { outer: 'w-6 h-6', inner: 'w-3 h-3', text: 'text-lg' },
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const colorStyles: Record<RadioColor, { checked: string; unchecked: string }> = {
|
|
120
|
+
primary: { checked: 'border-brand-primary bg-brand-primary', unchecked: 'border-border-strong' },
|
|
121
|
+
secondary: { checked: 'border-brand-secondary bg-brand-secondary', unchecked: 'border-border-strong' },
|
|
122
|
+
success: { checked: 'border-status-success bg-status-success', unchecked: 'border-border-strong' },
|
|
123
|
+
error: { checked: 'border-status-error bg-status-error', unchecked: 'border-border-strong' },
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Radio button component.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* <Radio value="option1">Option 1</Radio>
|
|
131
|
+
*/
|
|
132
|
+
export function Radio({
|
|
133
|
+
value,
|
|
134
|
+
isDisabled: radioDisabled,
|
|
135
|
+
size: radioSize,
|
|
136
|
+
color: radioColor,
|
|
137
|
+
className,
|
|
138
|
+
children,
|
|
139
|
+
...props
|
|
140
|
+
}: RadioProps) {
|
|
141
|
+
const context = useContext(RadioGroupContext)
|
|
142
|
+
|
|
143
|
+
if (!context) {
|
|
144
|
+
throw new Error('Radio must be used within a RadioGroup')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const { value: selectedValue, onChange, isDisabled: groupDisabled, size: groupSize, color: groupColor } = context
|
|
148
|
+
|
|
149
|
+
const isDisabled = radioDisabled ?? groupDisabled ?? false
|
|
150
|
+
const size = radioSize ?? groupSize ?? 'md'
|
|
151
|
+
const color = radioColor ?? groupColor ?? 'primary'
|
|
152
|
+
const isChecked = selectedValue === value
|
|
153
|
+
|
|
154
|
+
const { outer, inner, text } = sizeConfig[size]
|
|
155
|
+
const { checked, unchecked } = colorStyles[color]
|
|
156
|
+
|
|
157
|
+
const handlePress = () => {
|
|
158
|
+
if (!isDisabled) {
|
|
159
|
+
onChange(value)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<Pressable
|
|
165
|
+
onPress={handlePress}
|
|
166
|
+
disabled={isDisabled}
|
|
167
|
+
accessibilityRole="radio"
|
|
168
|
+
accessibilityState={{ checked: isChecked, disabled: isDisabled }}
|
|
169
|
+
className={cn(
|
|
170
|
+
'flex-row items-center',
|
|
171
|
+
isDisabled && 'opacity-50',
|
|
172
|
+
className
|
|
173
|
+
)}
|
|
174
|
+
{...props}
|
|
175
|
+
>
|
|
176
|
+
{/* Radio circle */}
|
|
177
|
+
<View
|
|
178
|
+
className={cn(
|
|
179
|
+
'rounded-full border-2 items-center justify-center',
|
|
180
|
+
'transition-colors duration-150',
|
|
181
|
+
outer,
|
|
182
|
+
isChecked ? checked : unchecked,
|
|
183
|
+
!isDisabled && !isChecked && 'web:hover:border-brand-primary'
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
{/* Inner dot */}
|
|
187
|
+
{isChecked && (
|
|
188
|
+
<View className={cn('rounded-full bg-white', inner)} />
|
|
189
|
+
)}
|
|
190
|
+
</View>
|
|
191
|
+
|
|
192
|
+
{/* Label */}
|
|
193
|
+
{children && (
|
|
194
|
+
<Text className={cn('ml-2 text-text-primary', text)}>
|
|
195
|
+
{children}
|
|
196
|
+
</Text>
|
|
197
|
+
)}
|
|
198
|
+
</Pressable>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import { Section, SectionHeader, SectionContent } from './Section'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Section> = {
|
|
6
|
+
title: 'Components/Section',
|
|
7
|
+
component: Section,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof Section>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<Section>
|
|
17
|
+
<SectionHeader title="Recent Workouts" />
|
|
18
|
+
<SectionContent>
|
|
19
|
+
<Text style={{ color: '#a0a0a0' }}>Workout items would go here</Text>
|
|
20
|
+
</SectionContent>
|
|
21
|
+
</Section>
|
|
22
|
+
),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const WithSubtitle: Story = {
|
|
26
|
+
render: () => (
|
|
27
|
+
<Section>
|
|
28
|
+
<SectionHeader
|
|
29
|
+
title="Workout Stats"
|
|
30
|
+
subtitle="Last 7 days"
|
|
31
|
+
/>
|
|
32
|
+
<SectionContent>
|
|
33
|
+
<Text style={{ color: '#a0a0a0' }}>Stats content</Text>
|
|
34
|
+
</SectionContent>
|
|
35
|
+
</Section>
|
|
36
|
+
),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const WithTrailingAction: Story = {
|
|
40
|
+
render: () => (
|
|
41
|
+
<Section>
|
|
42
|
+
<SectionHeader
|
|
43
|
+
title="Exercises"
|
|
44
|
+
trailing={
|
|
45
|
+
<Text style={{ color: '#6366f1', fontSize: 13 }}>View All</Text>
|
|
46
|
+
}
|
|
47
|
+
/>
|
|
48
|
+
<SectionContent>
|
|
49
|
+
<Text style={{ color: '#a0a0a0' }}>Exercise list</Text>
|
|
50
|
+
</SectionContent>
|
|
51
|
+
</Section>
|
|
52
|
+
),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const FullExample: Story = {
|
|
56
|
+
render: () => (
|
|
57
|
+
<Section>
|
|
58
|
+
<SectionHeader
|
|
59
|
+
title="Performance"
|
|
60
|
+
subtitle="Average velocity by set"
|
|
61
|
+
trailing={
|
|
62
|
+
<Text style={{ color: '#6366f1', fontSize: 13 }}>Details</Text>
|
|
63
|
+
}
|
|
64
|
+
/>
|
|
65
|
+
<SectionContent className="rounded-lg bg-surface-secondary p-4">
|
|
66
|
+
<Text style={{ color: '#a0a0a0' }}>Chart or data visualization</Text>
|
|
67
|
+
</SectionContent>
|
|
68
|
+
</Section>
|
|
69
|
+
),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const MultipleSections: Story = {
|
|
73
|
+
render: () => (
|
|
74
|
+
<View>
|
|
75
|
+
<Section>
|
|
76
|
+
<SectionHeader title="Today" subtitle="3 exercises" />
|
|
77
|
+
<SectionContent className="rounded-lg bg-surface-secondary p-4">
|
|
78
|
+
<Text style={{ color: '#a0a0a0' }}>Today's workout data</Text>
|
|
79
|
+
</SectionContent>
|
|
80
|
+
</Section>
|
|
81
|
+
|
|
82
|
+
<Section>
|
|
83
|
+
<SectionHeader
|
|
84
|
+
title="This Week"
|
|
85
|
+
subtitle="12 sets completed"
|
|
86
|
+
trailing={
|
|
87
|
+
<Text style={{ color: '#6366f1', fontSize: 13 }}>See All</Text>
|
|
88
|
+
}
|
|
89
|
+
/>
|
|
90
|
+
<SectionContent className="rounded-lg bg-surface-secondary p-4">
|
|
91
|
+
<Text style={{ color: '#a0a0a0' }}>Weekly summary</Text>
|
|
92
|
+
</SectionContent>
|
|
93
|
+
</Section>
|
|
94
|
+
|
|
95
|
+
<Section>
|
|
96
|
+
<SectionHeader title="Personal Records" />
|
|
97
|
+
<SectionContent className="rounded-lg bg-surface-secondary p-4">
|
|
98
|
+
<Text style={{ color: '#a0a0a0' }}>PR list</Text>
|
|
99
|
+
</SectionContent>
|
|
100
|
+
</Section>
|
|
101
|
+
</View>
|
|
102
|
+
),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const HeaderOnly: Story = {
|
|
106
|
+
render: () => (
|
|
107
|
+
<Section>
|
|
108
|
+
<SectionHeader title="Section Title Only" />
|
|
109
|
+
</Section>
|
|
110
|
+
),
|
|
111
|
+
}
|