@titan-design/react-ui 0.1.0 → 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/package.json +10 -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
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react'
|
|
2
|
+
import { View, Text, type ViewProps } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
|
|
5
|
+
export type StepStatus = 'completed' | 'active' | 'upcoming' | 'error'
|
|
6
|
+
export type StepperOrientation = 'horizontal' | 'vertical'
|
|
7
|
+
|
|
8
|
+
interface StepperContextType {
|
|
9
|
+
activeStep: number
|
|
10
|
+
orientation: StepperOrientation
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const StepperContext = createContext<StepperContextType>({
|
|
14
|
+
activeStep: 0,
|
|
15
|
+
orientation: 'horizontal',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export interface StepperProps extends ViewProps {
|
|
19
|
+
/** Current active step (0-indexed) */
|
|
20
|
+
activeStep: number
|
|
21
|
+
/** Stepper orientation */
|
|
22
|
+
orientation?: StepperOrientation
|
|
23
|
+
/** Additional className */
|
|
24
|
+
className?: string
|
|
25
|
+
children?: React.ReactNode
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Stepper component for multi-step flows.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* <Stepper activeStep={1}>
|
|
33
|
+
* <Step>
|
|
34
|
+
* <StepIndicator />
|
|
35
|
+
* <StepLabel>Account</StepLabel>
|
|
36
|
+
* </Step>
|
|
37
|
+
* <Step>
|
|
38
|
+
* <StepIndicator />
|
|
39
|
+
* <StepLabel>Profile</StepLabel>
|
|
40
|
+
* </Step>
|
|
41
|
+
* <Step>
|
|
42
|
+
* <StepIndicator />
|
|
43
|
+
* <StepLabel>Review</StepLabel>
|
|
44
|
+
* </Step>
|
|
45
|
+
* </Stepper>
|
|
46
|
+
*/
|
|
47
|
+
export function Stepper({
|
|
48
|
+
activeStep,
|
|
49
|
+
orientation = 'horizontal',
|
|
50
|
+
className,
|
|
51
|
+
children,
|
|
52
|
+
...props
|
|
53
|
+
}: StepperProps) {
|
|
54
|
+
return (
|
|
55
|
+
<StepperContext.Provider value={{ activeStep, orientation }}>
|
|
56
|
+
<View
|
|
57
|
+
className={cn(
|
|
58
|
+
orientation === 'horizontal' ? 'flex-row items-center' : 'flex-col',
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
{React.Children.map(children, (child, index) => {
|
|
64
|
+
if (React.isValidElement(child)) {
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
{React.cloneElement(child as React.ReactElement<any>, { index })}
|
|
68
|
+
{index < React.Children.count(children) - 1 && (
|
|
69
|
+
<StepConnector index={index} />
|
|
70
|
+
)}
|
|
71
|
+
</>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
return child
|
|
75
|
+
})}
|
|
76
|
+
</View>
|
|
77
|
+
</StepperContext.Provider>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface StepConnectorProps {
|
|
82
|
+
index: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function StepConnector({ index }: StepConnectorProps) {
|
|
86
|
+
const { activeStep, orientation } = useContext(StepperContext)
|
|
87
|
+
const isCompleted = index < activeStep
|
|
88
|
+
|
|
89
|
+
if (orientation === 'horizontal') {
|
|
90
|
+
return (
|
|
91
|
+
<View
|
|
92
|
+
className={cn(
|
|
93
|
+
'flex-1 h-0.5 mx-2',
|
|
94
|
+
isCompleted ? 'bg-brand-primary' : 'bg-border-default'
|
|
95
|
+
)}
|
|
96
|
+
/>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<View
|
|
102
|
+
className={cn(
|
|
103
|
+
'w-0.5 h-8 ml-4 my-1',
|
|
104
|
+
isCompleted ? 'bg-brand-primary' : 'bg-border-default'
|
|
105
|
+
)}
|
|
106
|
+
/>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface StepProps {
|
|
111
|
+
/** Step index (injected by Stepper) */
|
|
112
|
+
index?: number
|
|
113
|
+
/** Override step status */
|
|
114
|
+
status?: StepStatus
|
|
115
|
+
/** Additional className */
|
|
116
|
+
className?: string
|
|
117
|
+
children?: React.ReactNode
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Individual step in the stepper.
|
|
122
|
+
*/
|
|
123
|
+
export function Step({ index = 0, status, className, children }: StepProps) {
|
|
124
|
+
const { activeStep, orientation } = useContext(StepperContext)
|
|
125
|
+
|
|
126
|
+
const derivedStatus: StepStatus = status || (
|
|
127
|
+
index < activeStep ? 'completed' :
|
|
128
|
+
index === activeStep ? 'active' : 'upcoming'
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<View
|
|
133
|
+
className={cn(
|
|
134
|
+
orientation === 'horizontal' ? 'items-center' : 'flex-row items-start',
|
|
135
|
+
className
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
{React.Children.map(children, (child) => {
|
|
139
|
+
if (React.isValidElement(child)) {
|
|
140
|
+
return React.cloneElement(child as React.ReactElement<any>, {
|
|
141
|
+
status: derivedStatus,
|
|
142
|
+
index,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
return child
|
|
146
|
+
})}
|
|
147
|
+
</View>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface StepIndicatorProps {
|
|
152
|
+
/** Step status (injected by Step) */
|
|
153
|
+
status?: StepStatus
|
|
154
|
+
/** Step index (injected by Step) */
|
|
155
|
+
index?: number
|
|
156
|
+
/** Custom icon */
|
|
157
|
+
icon?: React.ReactNode
|
|
158
|
+
/** Additional className */
|
|
159
|
+
className?: string
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Visual indicator for step status.
|
|
164
|
+
*/
|
|
165
|
+
export function StepIndicator({
|
|
166
|
+
status = 'upcoming',
|
|
167
|
+
index = 0,
|
|
168
|
+
icon,
|
|
169
|
+
className,
|
|
170
|
+
}: StepIndicatorProps) {
|
|
171
|
+
const statusStyles: Record<StepStatus, string> = {
|
|
172
|
+
completed: 'bg-brand-primary border-brand-primary',
|
|
173
|
+
active: 'bg-brand-primary border-brand-primary',
|
|
174
|
+
upcoming: 'bg-surface-base border-border-strong',
|
|
175
|
+
error: 'bg-status-error border-status-error',
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<View
|
|
180
|
+
className={cn(
|
|
181
|
+
'w-8 h-8 rounded-full border-2 items-center justify-center',
|
|
182
|
+
statusStyles[status],
|
|
183
|
+
className
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
{icon || (
|
|
187
|
+
<Text
|
|
188
|
+
className={cn(
|
|
189
|
+
'text-sm font-semibold',
|
|
190
|
+
status === 'upcoming' ? 'text-text-secondary' : 'text-white'
|
|
191
|
+
)}
|
|
192
|
+
>
|
|
193
|
+
{status === 'completed' ? '✓' : index + 1}
|
|
194
|
+
</Text>
|
|
195
|
+
)}
|
|
196
|
+
</View>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface StepLabelProps {
|
|
201
|
+
/** Step status (injected by Step) */
|
|
202
|
+
status?: StepStatus
|
|
203
|
+
/** Additional className */
|
|
204
|
+
className?: string
|
|
205
|
+
children?: React.ReactNode
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Label for a step.
|
|
210
|
+
*/
|
|
211
|
+
export function StepLabel({ status = 'upcoming', className, children }: StepLabelProps) {
|
|
212
|
+
const { orientation } = useContext(StepperContext)
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<Text
|
|
216
|
+
className={cn(
|
|
217
|
+
'text-sm font-medium',
|
|
218
|
+
orientation === 'horizontal' ? 'mt-2' : 'ml-3',
|
|
219
|
+
status === 'active' ? 'text-text-primary' :
|
|
220
|
+
status === 'completed' ? 'text-brand-primary' :
|
|
221
|
+
status === 'error' ? 'text-status-error' :
|
|
222
|
+
'text-text-secondary',
|
|
223
|
+
className
|
|
224
|
+
)}
|
|
225
|
+
>
|
|
226
|
+
{children}
|
|
227
|
+
</Text>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface StepContentProps {
|
|
232
|
+
/** Step status (injected by Step) */
|
|
233
|
+
status?: StepStatus
|
|
234
|
+
/** Additional className */
|
|
235
|
+
className?: string
|
|
236
|
+
children?: React.ReactNode
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Content area for vertical stepper.
|
|
241
|
+
*/
|
|
242
|
+
export function StepContent({ status = 'upcoming', className, children }: StepContentProps) {
|
|
243
|
+
if (status !== 'active') return null
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<View className={cn('ml-11 mt-2 pb-4', className)}>
|
|
247
|
+
{children}
|
|
248
|
+
</View>
|
|
249
|
+
)
|
|
250
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { Alert, AlertTitle, AlertDescription } from './Alert'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Alert> = {
|
|
6
|
+
title: 'Components/Alert',
|
|
7
|
+
component: Alert,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
status: {
|
|
11
|
+
control: 'select',
|
|
12
|
+
options: ['success', 'info', 'warning', 'error'],
|
|
13
|
+
},
|
|
14
|
+
variant: {
|
|
15
|
+
control: 'select',
|
|
16
|
+
options: ['subtle', 'outline', 'solid'],
|
|
17
|
+
},
|
|
18
|
+
showIcon: { control: 'boolean' },
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default meta
|
|
23
|
+
type Story = StoryObj<typeof Alert>
|
|
24
|
+
|
|
25
|
+
export const Default: Story = {
|
|
26
|
+
args: {
|
|
27
|
+
status: 'info',
|
|
28
|
+
variant: 'subtle',
|
|
29
|
+
},
|
|
30
|
+
render: (args) => (
|
|
31
|
+
<Alert {...args}>
|
|
32
|
+
<AlertTitle>Information</AlertTitle>
|
|
33
|
+
<AlertDescription>This is an informational alert message.</AlertDescription>
|
|
34
|
+
</Alert>
|
|
35
|
+
),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const Success: Story = {
|
|
39
|
+
render: () => (
|
|
40
|
+
<Alert status="success">
|
|
41
|
+
<AlertTitle>Success!</AlertTitle>
|
|
42
|
+
<AlertDescription>Your changes have been saved successfully.</AlertDescription>
|
|
43
|
+
</Alert>
|
|
44
|
+
),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const Warning: Story = {
|
|
48
|
+
render: () => (
|
|
49
|
+
<Alert status="warning">
|
|
50
|
+
<AlertTitle>Warning</AlertTitle>
|
|
51
|
+
<AlertDescription>Please review before proceeding.</AlertDescription>
|
|
52
|
+
</Alert>
|
|
53
|
+
),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const Error: Story = {
|
|
57
|
+
render: () => (
|
|
58
|
+
<Alert status="error">
|
|
59
|
+
<AlertTitle>Error</AlertTitle>
|
|
60
|
+
<AlertDescription>Something went wrong. Please try again.</AlertDescription>
|
|
61
|
+
</Alert>
|
|
62
|
+
),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const WithCloseButton: Story = {
|
|
66
|
+
render: () => (
|
|
67
|
+
<Alert status="info" onClose={() => console.log('closed')}>
|
|
68
|
+
<AlertTitle>Dismissable Alert</AlertTitle>
|
|
69
|
+
<AlertDescription>Click the X to close this alert.</AlertDescription>
|
|
70
|
+
</Alert>
|
|
71
|
+
),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const AllStatuses: Story = {
|
|
75
|
+
render: () => (
|
|
76
|
+
<View style={{ gap: 12 }}>
|
|
77
|
+
<Alert status="success">
|
|
78
|
+
<AlertTitle>Success</AlertTitle>
|
|
79
|
+
<AlertDescription>Operation completed successfully.</AlertDescription>
|
|
80
|
+
</Alert>
|
|
81
|
+
<Alert status="info">
|
|
82
|
+
<AlertTitle>Info</AlertTitle>
|
|
83
|
+
<AlertDescription>Here is some useful information.</AlertDescription>
|
|
84
|
+
</Alert>
|
|
85
|
+
<Alert status="warning">
|
|
86
|
+
<AlertTitle>Warning</AlertTitle>
|
|
87
|
+
<AlertDescription>Please be careful with this action.</AlertDescription>
|
|
88
|
+
</Alert>
|
|
89
|
+
<Alert status="error">
|
|
90
|
+
<AlertTitle>Error</AlertTitle>
|
|
91
|
+
<AlertDescription>An error occurred.</AlertDescription>
|
|
92
|
+
</Alert>
|
|
93
|
+
</View>
|
|
94
|
+
),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const AllVariants: Story = {
|
|
98
|
+
render: () => (
|
|
99
|
+
<View style={{ gap: 12 }}>
|
|
100
|
+
<Alert status="info" variant="subtle">
|
|
101
|
+
<AlertTitle>Subtle Variant</AlertTitle>
|
|
102
|
+
<AlertDescription>Light background with colored text.</AlertDescription>
|
|
103
|
+
</Alert>
|
|
104
|
+
<Alert status="info" variant="outline">
|
|
105
|
+
<AlertTitle>Outline Variant</AlertTitle>
|
|
106
|
+
<AlertDescription>Border with transparent background.</AlertDescription>
|
|
107
|
+
</Alert>
|
|
108
|
+
<Alert status="info" variant="solid">
|
|
109
|
+
<AlertTitle>Solid Variant</AlertTitle>
|
|
110
|
+
<AlertDescription>Solid background color.</AlertDescription>
|
|
111
|
+
</Alert>
|
|
112
|
+
</View>
|
|
113
|
+
),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const SolidVariants: Story = {
|
|
117
|
+
render: () => (
|
|
118
|
+
<View style={{ gap: 12 }}>
|
|
119
|
+
<Alert status="success" variant="solid">
|
|
120
|
+
<AlertDescription>Success solid alert</AlertDescription>
|
|
121
|
+
</Alert>
|
|
122
|
+
<Alert status="info" variant="solid">
|
|
123
|
+
<AlertDescription>Info solid alert</AlertDescription>
|
|
124
|
+
</Alert>
|
|
125
|
+
<Alert status="warning" variant="solid">
|
|
126
|
+
<AlertDescription>Warning solid alert</AlertDescription>
|
|
127
|
+
</Alert>
|
|
128
|
+
<Alert status="error" variant="solid">
|
|
129
|
+
<AlertDescription>Error solid alert</AlertDescription>
|
|
130
|
+
</Alert>
|
|
131
|
+
</View>
|
|
132
|
+
),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const WithoutIcon: Story = {
|
|
136
|
+
args: {
|
|
137
|
+
showIcon: false,
|
|
138
|
+
status: 'info',
|
|
139
|
+
},
|
|
140
|
+
render: (args) => (
|
|
141
|
+
<Alert {...args}>
|
|
142
|
+
<AlertTitle>No Icon</AlertTitle>
|
|
143
|
+
<AlertDescription>This alert has no icon.</AlertDescription>
|
|
144
|
+
</Alert>
|
|
145
|
+
),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const DescriptionOnly: Story = {
|
|
149
|
+
render: () => (
|
|
150
|
+
<Alert status="info">
|
|
151
|
+
<AlertDescription>Simple alert with description only, no title.</AlertDescription>
|
|
152
|
+
</Alert>
|
|
153
|
+
),
|
|
154
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
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 { Alert, AlertTitle, AlertDescription } from './Alert'
|
|
5
|
+
|
|
6
|
+
describe('Alert', () => {
|
|
7
|
+
it('renders children correctly', () => {
|
|
8
|
+
render(
|
|
9
|
+
<Alert>
|
|
10
|
+
<AlertTitle>Alert Title</AlertTitle>
|
|
11
|
+
<AlertDescription>Alert message</AlertDescription>
|
|
12
|
+
</Alert>
|
|
13
|
+
)
|
|
14
|
+
expect(screen.getByText('Alert Title')).toBeInTheDocument()
|
|
15
|
+
expect(screen.getByText('Alert message')).toBeInTheDocument()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('has alert accessibility role', () => {
|
|
19
|
+
render(
|
|
20
|
+
<Alert>
|
|
21
|
+
<AlertDescription>Info message</AlertDescription>
|
|
22
|
+
</Alert>
|
|
23
|
+
)
|
|
24
|
+
expect(screen.getByRole('alert')).toBeInTheDocument()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('status variants', () => {
|
|
28
|
+
it('renders with success status', () => {
|
|
29
|
+
render(
|
|
30
|
+
<Alert status="success">
|
|
31
|
+
<AlertDescription>Success</AlertDescription>
|
|
32
|
+
</Alert>
|
|
33
|
+
)
|
|
34
|
+
expect(screen.getByRole('alert')).toBeInTheDocument()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('renders with info status (default)', () => {
|
|
38
|
+
render(
|
|
39
|
+
<Alert>
|
|
40
|
+
<AlertDescription>Info</AlertDescription>
|
|
41
|
+
</Alert>
|
|
42
|
+
)
|
|
43
|
+
expect(screen.getByRole('alert')).toBeInTheDocument()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('renders with warning status', () => {
|
|
47
|
+
render(
|
|
48
|
+
<Alert status="warning">
|
|
49
|
+
<AlertDescription>Warning</AlertDescription>
|
|
50
|
+
</Alert>
|
|
51
|
+
)
|
|
52
|
+
expect(screen.getByRole('alert')).toBeInTheDocument()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('renders with error status', () => {
|
|
56
|
+
render(
|
|
57
|
+
<Alert status="error">
|
|
58
|
+
<AlertDescription>Error</AlertDescription>
|
|
59
|
+
</Alert>
|
|
60
|
+
)
|
|
61
|
+
expect(screen.getByRole('alert')).toBeInTheDocument()
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('visual variants', () => {
|
|
66
|
+
it('renders with subtle variant (default)', () => {
|
|
67
|
+
render(
|
|
68
|
+
<Alert variant="subtle">
|
|
69
|
+
<AlertDescription>Subtle</AlertDescription>
|
|
70
|
+
</Alert>
|
|
71
|
+
)
|
|
72
|
+
expect(screen.getByRole('alert')).toBeInTheDocument()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('renders with outline variant', () => {
|
|
76
|
+
render(
|
|
77
|
+
<Alert variant="outline">
|
|
78
|
+
<AlertDescription>Outline</AlertDescription>
|
|
79
|
+
</Alert>
|
|
80
|
+
)
|
|
81
|
+
expect(screen.getByRole('alert')).toBeInTheDocument()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('renders with solid variant', () => {
|
|
85
|
+
render(
|
|
86
|
+
<Alert variant="solid">
|
|
87
|
+
<AlertDescription>Solid</AlertDescription>
|
|
88
|
+
</Alert>
|
|
89
|
+
)
|
|
90
|
+
expect(screen.getByRole('alert')).toBeInTheDocument()
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('icon behavior', () => {
|
|
95
|
+
it('shows default icon by default', () => {
|
|
96
|
+
render(
|
|
97
|
+
<Alert status="info">
|
|
98
|
+
<AlertDescription>With icon</AlertDescription>
|
|
99
|
+
</Alert>
|
|
100
|
+
)
|
|
101
|
+
// Default info icon is the info symbol
|
|
102
|
+
expect(screen.getByRole('alert')).toBeInTheDocument()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('hides icon when showIcon is false', () => {
|
|
106
|
+
render(
|
|
107
|
+
<Alert status="success" showIcon={false}>
|
|
108
|
+
<AlertDescription>No icon</AlertDescription>
|
|
109
|
+
</Alert>
|
|
110
|
+
)
|
|
111
|
+
expect(screen.getByRole('alert')).toBeInTheDocument()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('renders custom icon when provided', () => {
|
|
115
|
+
render(
|
|
116
|
+
<Alert icon={<span data-testid="custom-icon">!</span>}>
|
|
117
|
+
<AlertDescription>Custom icon</AlertDescription>
|
|
118
|
+
</Alert>
|
|
119
|
+
)
|
|
120
|
+
expect(screen.getByTestId('custom-icon')).toBeInTheDocument()
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('close button', () => {
|
|
125
|
+
it('renders close button when onClose is provided', () => {
|
|
126
|
+
render(
|
|
127
|
+
<Alert onClose={() => {}}>
|
|
128
|
+
<AlertDescription>Closeable</AlertDescription>
|
|
129
|
+
</Alert>
|
|
130
|
+
)
|
|
131
|
+
expect(screen.getByLabelText('Close alert')).toBeInTheDocument()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('calls onClose when close button is clicked', () => {
|
|
135
|
+
const onClose = vi.fn()
|
|
136
|
+
render(
|
|
137
|
+
<Alert onClose={onClose}>
|
|
138
|
+
<AlertDescription>Closeable</AlertDescription>
|
|
139
|
+
</Alert>
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
fireEvent.click(screen.getByLabelText('Close alert'))
|
|
143
|
+
expect(onClose).toHaveBeenCalledTimes(1)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('does not render close button when onClose is not provided', () => {
|
|
147
|
+
render(
|
|
148
|
+
<Alert>
|
|
149
|
+
<AlertDescription>Not closeable</AlertDescription>
|
|
150
|
+
</Alert>
|
|
151
|
+
)
|
|
152
|
+
expect(screen.queryByLabelText('Close alert')).not.toBeInTheDocument()
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('compound sub-components', () => {
|
|
157
|
+
it('renders AlertTitle', () => {
|
|
158
|
+
render(<AlertTitle>Title Text</AlertTitle>)
|
|
159
|
+
expect(screen.getByText('Title Text')).toBeInTheDocument()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('renders AlertDescription', () => {
|
|
163
|
+
render(<AlertDescription>Description text</AlertDescription>)
|
|
164
|
+
expect(screen.getByText('Description text')).toBeInTheDocument()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('accepts custom className on sub-components', () => {
|
|
168
|
+
render(
|
|
169
|
+
<Alert className="custom-alert">
|
|
170
|
+
<AlertTitle className="custom-title">Title</AlertTitle>
|
|
171
|
+
<AlertDescription className="custom-desc">Desc</AlertDescription>
|
|
172
|
+
</Alert>
|
|
173
|
+
)
|
|
174
|
+
expect(screen.getByText('Title')).toBeInTheDocument()
|
|
175
|
+
expect(screen.getByText('Desc')).toBeInTheDocument()
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('accessibility', () => {
|
|
180
|
+
it('has no accessibility violations', async () => {
|
|
181
|
+
const { container } = render(
|
|
182
|
+
<Alert status="info">
|
|
183
|
+
<AlertTitle>Info Alert</AlertTitle>
|
|
184
|
+
<AlertDescription>This is an informational message.</AlertDescription>
|
|
185
|
+
</Alert>
|
|
186
|
+
)
|
|
187
|
+
const results = await axe(container)
|
|
188
|
+
expect(results).toHaveNoViolations()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('has no accessibility violations with close button', async () => {
|
|
192
|
+
const { container } = render(
|
|
193
|
+
<Alert status="error" onClose={() => {}}>
|
|
194
|
+
<AlertTitle>Error</AlertTitle>
|
|
195
|
+
<AlertDescription>Something went wrong.</AlertDescription>
|
|
196
|
+
</Alert>
|
|
197
|
+
)
|
|
198
|
+
const results = await axe(container)
|
|
199
|
+
expect(results).toHaveNoViolations()
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
})
|