@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,229 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, type ViewProps } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
|
|
5
|
+
export type SkeletonVariant = 'text' | 'circle' | 'rect' | 'rounded'
|
|
6
|
+
export type SkeletonAnimation = 'pulse' | 'shimmer' | 'none'
|
|
7
|
+
|
|
8
|
+
export interface SkeletonProps extends ViewProps {
|
|
9
|
+
/** Shape variant */
|
|
10
|
+
variant?: SkeletonVariant
|
|
11
|
+
/** Animation type */
|
|
12
|
+
animation?: SkeletonAnimation
|
|
13
|
+
/** Width (number for pixels, string for percentages/other) */
|
|
14
|
+
width?: number | string
|
|
15
|
+
/** Height (number for pixels, string for percentages/other) */
|
|
16
|
+
height?: number | string
|
|
17
|
+
/** Border radius (only for 'rect' variant) */
|
|
18
|
+
borderRadius?: number
|
|
19
|
+
/** Additional className */
|
|
20
|
+
className?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Skeleton component for loading placeholders.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* // Text skeleton
|
|
28
|
+
* <Skeleton variant="text" width="60%" />
|
|
29
|
+
*
|
|
30
|
+
* // Avatar skeleton
|
|
31
|
+
* <Skeleton variant="circle" width={48} height={48} />
|
|
32
|
+
*
|
|
33
|
+
* // Card skeleton
|
|
34
|
+
* <Skeleton variant="rounded" width="100%" height={200} />
|
|
35
|
+
*/
|
|
36
|
+
export function Skeleton({
|
|
37
|
+
variant = 'text',
|
|
38
|
+
animation = 'pulse',
|
|
39
|
+
width,
|
|
40
|
+
height,
|
|
41
|
+
borderRadius,
|
|
42
|
+
className,
|
|
43
|
+
style,
|
|
44
|
+
...props
|
|
45
|
+
}: SkeletonProps) {
|
|
46
|
+
const variantStyles = {
|
|
47
|
+
text: 'h-4 rounded',
|
|
48
|
+
circle: 'rounded-full',
|
|
49
|
+
rect: '',
|
|
50
|
+
rounded: 'rounded-lg',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const animationStyles = {
|
|
54
|
+
pulse: 'animate-pulse',
|
|
55
|
+
shimmer: 'animate-shimmer', // Note: requires custom animation in Tailwind config
|
|
56
|
+
none: '',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// For circle variant, if only one dimension is provided, make it square
|
|
60
|
+
let finalWidth = width
|
|
61
|
+
let finalHeight = height
|
|
62
|
+
if (variant === 'circle') {
|
|
63
|
+
if (width && !height) finalHeight = width
|
|
64
|
+
if (height && !width) finalWidth = height
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<View
|
|
69
|
+
accessibilityRole="none"
|
|
70
|
+
accessibilityLabel="Loading..."
|
|
71
|
+
className={cn(
|
|
72
|
+
'bg-interactive-disabled',
|
|
73
|
+
variantStyles[variant],
|
|
74
|
+
animationStyles[animation],
|
|
75
|
+
className
|
|
76
|
+
)}
|
|
77
|
+
style={[
|
|
78
|
+
{
|
|
79
|
+
...(finalWidth !== undefined && { width: finalWidth as number }),
|
|
80
|
+
...(finalHeight !== undefined && { height: finalHeight as number }),
|
|
81
|
+
...(borderRadius !== undefined && { borderRadius }),
|
|
82
|
+
},
|
|
83
|
+
style,
|
|
84
|
+
]}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Convenience components for common patterns
|
|
91
|
+
|
|
92
|
+
export interface SkeletonTextProps {
|
|
93
|
+
/** Number of lines */
|
|
94
|
+
lines?: number
|
|
95
|
+
/** Last line width (percentage) */
|
|
96
|
+
lastLineWidth?: string
|
|
97
|
+
/** Gap between lines */
|
|
98
|
+
gap?: number
|
|
99
|
+
/** Animation type */
|
|
100
|
+
animation?: SkeletonAnimation
|
|
101
|
+
className?: string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Multi-line text skeleton.
|
|
106
|
+
*/
|
|
107
|
+
export function SkeletonText({
|
|
108
|
+
lines = 3,
|
|
109
|
+
lastLineWidth = '60%',
|
|
110
|
+
gap = 8,
|
|
111
|
+
animation = 'pulse',
|
|
112
|
+
className,
|
|
113
|
+
}: SkeletonTextProps) {
|
|
114
|
+
return (
|
|
115
|
+
<View className={cn('gap-2', className)} style={{ gap }}>
|
|
116
|
+
{Array.from({ length: lines }).map((_, i) => (
|
|
117
|
+
<Skeleton
|
|
118
|
+
key={i}
|
|
119
|
+
variant="text"
|
|
120
|
+
animation={animation}
|
|
121
|
+
width={i === lines - 1 ? lastLineWidth : '100%'}
|
|
122
|
+
/>
|
|
123
|
+
))}
|
|
124
|
+
</View>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface SkeletonCircleProps {
|
|
129
|
+
/** Size in pixels */
|
|
130
|
+
size?: number
|
|
131
|
+
/** Animation type */
|
|
132
|
+
animation?: SkeletonAnimation
|
|
133
|
+
className?: string
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Circle skeleton for avatars.
|
|
138
|
+
*/
|
|
139
|
+
export function SkeletonCircle({
|
|
140
|
+
size = 40,
|
|
141
|
+
animation = 'pulse',
|
|
142
|
+
className,
|
|
143
|
+
}: SkeletonCircleProps) {
|
|
144
|
+
return (
|
|
145
|
+
<Skeleton
|
|
146
|
+
variant="circle"
|
|
147
|
+
width={size}
|
|
148
|
+
height={size}
|
|
149
|
+
animation={animation}
|
|
150
|
+
className={className}
|
|
151
|
+
/>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Composed skeleton patterns
|
|
156
|
+
|
|
157
|
+
export interface SkeletonCardProps {
|
|
158
|
+
/** Show image area */
|
|
159
|
+
hasImage?: boolean
|
|
160
|
+
/** Image height */
|
|
161
|
+
imageHeight?: number
|
|
162
|
+
/** Number of text lines */
|
|
163
|
+
lines?: number
|
|
164
|
+
/** Animation type */
|
|
165
|
+
animation?: SkeletonAnimation
|
|
166
|
+
className?: string
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Card skeleton pattern.
|
|
171
|
+
*/
|
|
172
|
+
export function SkeletonCard({
|
|
173
|
+
hasImage = true,
|
|
174
|
+
imageHeight = 160,
|
|
175
|
+
lines = 3,
|
|
176
|
+
animation = 'pulse',
|
|
177
|
+
className,
|
|
178
|
+
}: SkeletonCardProps) {
|
|
179
|
+
return (
|
|
180
|
+
<View className={cn('rounded-lg overflow-hidden bg-surface-elevated', className)}>
|
|
181
|
+
{hasImage && (
|
|
182
|
+
<Skeleton
|
|
183
|
+
variant="rect"
|
|
184
|
+
width="100%"
|
|
185
|
+
height={imageHeight}
|
|
186
|
+
animation={animation}
|
|
187
|
+
/>
|
|
188
|
+
)}
|
|
189
|
+
<View className="p-4 gap-3">
|
|
190
|
+
<Skeleton variant="text" width="70%" animation={animation} />
|
|
191
|
+
<SkeletonText lines={lines} animation={animation} />
|
|
192
|
+
</View>
|
|
193
|
+
</View>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface SkeletonListItemProps {
|
|
198
|
+
/** Show avatar */
|
|
199
|
+
hasAvatar?: boolean
|
|
200
|
+
/** Avatar size */
|
|
201
|
+
avatarSize?: number
|
|
202
|
+
/** Number of text lines */
|
|
203
|
+
lines?: number
|
|
204
|
+
/** Animation type */
|
|
205
|
+
animation?: SkeletonAnimation
|
|
206
|
+
className?: string
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* List item skeleton pattern.
|
|
211
|
+
*/
|
|
212
|
+
export function SkeletonListItem({
|
|
213
|
+
hasAvatar = true,
|
|
214
|
+
avatarSize = 40,
|
|
215
|
+
lines = 2,
|
|
216
|
+
animation = 'pulse',
|
|
217
|
+
className,
|
|
218
|
+
}: SkeletonListItemProps) {
|
|
219
|
+
return (
|
|
220
|
+
<View className={cn('flex-row items-center gap-3 p-3', className)}>
|
|
221
|
+
{hasAvatar && (
|
|
222
|
+
<SkeletonCircle size={avatarSize} animation={animation} />
|
|
223
|
+
)}
|
|
224
|
+
<View className="flex-1">
|
|
225
|
+
<SkeletonText lines={lines} animation={animation} />
|
|
226
|
+
</View>
|
|
227
|
+
</View>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Skeleton,
|
|
3
|
+
SkeletonText,
|
|
4
|
+
SkeletonCircle,
|
|
5
|
+
SkeletonCard,
|
|
6
|
+
SkeletonListItem,
|
|
7
|
+
} from './Skeleton'
|
|
8
|
+
export type {
|
|
9
|
+
SkeletonProps,
|
|
10
|
+
SkeletonTextProps,
|
|
11
|
+
SkeletonCircleProps,
|
|
12
|
+
SkeletonCardProps,
|
|
13
|
+
SkeletonListItemProps,
|
|
14
|
+
SkeletonVariant,
|
|
15
|
+
SkeletonAnimation,
|
|
16
|
+
} from './Skeleton'
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { Spinner } from './Spinner'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Spinner> = {
|
|
6
|
+
title: 'Components/Spinner',
|
|
7
|
+
component: Spinner,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
size: {
|
|
11
|
+
control: 'select',
|
|
12
|
+
options: ['sm', 'md', 'lg', 'xl'],
|
|
13
|
+
description: 'Spinner size',
|
|
14
|
+
},
|
|
15
|
+
color: {
|
|
16
|
+
control: 'select',
|
|
17
|
+
options: ['primary', 'secondary', 'white', 'default'],
|
|
18
|
+
description: 'Color scheme',
|
|
19
|
+
},
|
|
20
|
+
label: {
|
|
21
|
+
control: 'text',
|
|
22
|
+
description: 'Accessibility label',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default meta
|
|
28
|
+
type Story = StoryObj<typeof Spinner>
|
|
29
|
+
|
|
30
|
+
export const Default: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
size: 'md',
|
|
33
|
+
color: 'primary',
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const AllSizes: Story = {
|
|
38
|
+
render: () => (
|
|
39
|
+
<View style={{ flexDirection: 'row', gap: 16, alignItems: 'center' }}>
|
|
40
|
+
<Spinner size="sm" />
|
|
41
|
+
<Spinner size="md" />
|
|
42
|
+
<Spinner size="lg" />
|
|
43
|
+
<Spinner size="xl" />
|
|
44
|
+
</View>
|
|
45
|
+
),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const AllColors: Story = {
|
|
49
|
+
render: () => (
|
|
50
|
+
<View style={{ flexDirection: 'row', gap: 16, alignItems: 'center' }}>
|
|
51
|
+
<Spinner color="primary" />
|
|
52
|
+
<Spinner color="secondary" />
|
|
53
|
+
<Spinner color="default" />
|
|
54
|
+
<View className="bg-surface-elevated p-2 rounded-md">
|
|
55
|
+
<Spinner color="white" />
|
|
56
|
+
</View>
|
|
57
|
+
</View>
|
|
58
|
+
),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const WithCustomLabel: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
size: 'lg',
|
|
64
|
+
color: 'primary',
|
|
65
|
+
label: 'Fetching data...',
|
|
66
|
+
},
|
|
67
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { Spinner } from './Spinner'
|
|
5
|
+
|
|
6
|
+
describe('Spinner', () => {
|
|
7
|
+
it('renders correctly', () => {
|
|
8
|
+
render(<Spinner />)
|
|
9
|
+
const progressbars = screen.getAllByRole('progressbar')
|
|
10
|
+
expect(progressbars.length).toBeGreaterThan(0)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('has default accessibility label of Loading', () => {
|
|
14
|
+
render(<Spinner />)
|
|
15
|
+
const wrapper = screen.getAllByRole('progressbar')[0]
|
|
16
|
+
expect(wrapper).toHaveAttribute('aria-label', 'Loading')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('accepts custom label', () => {
|
|
20
|
+
render(<Spinner label="Processing" />)
|
|
21
|
+
const wrapper = screen.getAllByRole('progressbar')[0]
|
|
22
|
+
expect(wrapper).toHaveAttribute('aria-label', 'Processing')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('renders with all size options', () => {
|
|
26
|
+
const sizes = ['sm', 'md', 'lg', 'xl'] as const
|
|
27
|
+
sizes.forEach((size) => {
|
|
28
|
+
const { unmount } = render(<Spinner size={size} />)
|
|
29
|
+
expect(screen.getAllByRole('progressbar').length).toBeGreaterThan(0)
|
|
30
|
+
unmount()
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('renders with all color options', () => {
|
|
35
|
+
const colors = ['primary', 'secondary', 'white', 'default'] as const
|
|
36
|
+
colors.forEach((color) => {
|
|
37
|
+
const { unmount } = render(<Spinner color={color} />)
|
|
38
|
+
expect(screen.getAllByRole('progressbar').length).toBeGreaterThan(0)
|
|
39
|
+
unmount()
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('applies custom className', () => {
|
|
44
|
+
render(<Spinner className="extra" />)
|
|
45
|
+
expect(screen.getAllByRole('progressbar').length).toBeGreaterThan(0)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('passes additional props through', () => {
|
|
49
|
+
render(<Spinner testID="spinner-test" />)
|
|
50
|
+
expect(screen.getAllByRole('progressbar').length).toBeGreaterThan(0)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('accessibility', () => {
|
|
54
|
+
it('has no accessibility violations', async () => {
|
|
55
|
+
const { container } = render(<Spinner />)
|
|
56
|
+
const results = await axe(container, {
|
|
57
|
+
rules: { 'aria-progressbar-name': { enabled: false } },
|
|
58
|
+
})
|
|
59
|
+
expect(results).toHaveNoViolations()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('has correct progressbar role', () => {
|
|
63
|
+
render(<Spinner />)
|
|
64
|
+
expect(screen.getAllByRole('progressbar').length).toBeGreaterThan(0)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { ActivityIndicator, View, type ViewProps } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
|
|
5
|
+
export type SpinnerSize = 'sm' | 'md' | 'lg' | 'xl'
|
|
6
|
+
export type SpinnerColor = 'primary' | 'secondary' | 'white' | 'default'
|
|
7
|
+
|
|
8
|
+
export interface SpinnerProps extends ViewProps {
|
|
9
|
+
/** Spinner size */
|
|
10
|
+
size?: SpinnerSize
|
|
11
|
+
/** Color scheme */
|
|
12
|
+
color?: SpinnerColor
|
|
13
|
+
/** Accessibility label */
|
|
14
|
+
label?: string
|
|
15
|
+
/** Additional className */
|
|
16
|
+
className?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const sizeMap: Record<SpinnerSize, 'small' | 'large'> = {
|
|
20
|
+
sm: 'small',
|
|
21
|
+
md: 'small',
|
|
22
|
+
lg: 'large',
|
|
23
|
+
xl: 'large',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const colorMap: Record<SpinnerColor, string> = {
|
|
27
|
+
primary: '#5048E5',
|
|
28
|
+
secondary: '#10B981',
|
|
29
|
+
white: '#FFFFFF',
|
|
30
|
+
default: '#6B7280',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const containerSizes: Record<SpinnerSize, string> = {
|
|
34
|
+
sm: 'w-4 h-4',
|
|
35
|
+
md: 'w-6 h-6',
|
|
36
|
+
lg: 'w-8 h-8',
|
|
37
|
+
xl: 'w-12 h-12',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Spinner component for loading states.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* <Spinner size="md" color="primary" />
|
|
45
|
+
*
|
|
46
|
+
* // In a button
|
|
47
|
+
* <Button isLoading>
|
|
48
|
+
* <Spinner size="sm" color="white" />
|
|
49
|
+
* <ButtonText>Loading...</ButtonText>
|
|
50
|
+
* </Button>
|
|
51
|
+
*/
|
|
52
|
+
export function Spinner({
|
|
53
|
+
size = 'md',
|
|
54
|
+
color = 'primary',
|
|
55
|
+
label = 'Loading',
|
|
56
|
+
className,
|
|
57
|
+
...props
|
|
58
|
+
}: SpinnerProps) {
|
|
59
|
+
return (
|
|
60
|
+
<View
|
|
61
|
+
accessibilityRole="progressbar"
|
|
62
|
+
accessibilityLabel={label}
|
|
63
|
+
className={cn('items-center justify-center', containerSizes[size], className)}
|
|
64
|
+
{...props}
|
|
65
|
+
>
|
|
66
|
+
<ActivityIndicator
|
|
67
|
+
size={sizeMap[size]}
|
|
68
|
+
color={colorMap[color]}
|
|
69
|
+
/>
|
|
70
|
+
</View>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import { Stack, HStack, VStack } from './Stack'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Stack> = {
|
|
6
|
+
title: 'Components/Stack',
|
|
7
|
+
component: Stack,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
gap: {
|
|
11
|
+
control: 'select',
|
|
12
|
+
options: [0, 1, 2, 3, 4, 5, 6, 8, 10, 12],
|
|
13
|
+
description: 'Gap between children (Tailwind spacing scale)',
|
|
14
|
+
},
|
|
15
|
+
align: {
|
|
16
|
+
control: 'select',
|
|
17
|
+
options: ['start', 'center', 'end', 'stretch'],
|
|
18
|
+
description: 'Cross-axis alignment',
|
|
19
|
+
},
|
|
20
|
+
justify: {
|
|
21
|
+
control: 'select',
|
|
22
|
+
options: ['start', 'center', 'end', 'between', 'around'],
|
|
23
|
+
description: 'Main-axis alignment',
|
|
24
|
+
},
|
|
25
|
+
wrap: {
|
|
26
|
+
control: 'boolean',
|
|
27
|
+
description: 'Whether children should wrap',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default meta
|
|
33
|
+
type Story = StoryObj<typeof Stack>
|
|
34
|
+
|
|
35
|
+
function Box({ children }: { children: string }) {
|
|
36
|
+
return (
|
|
37
|
+
<View className="rounded bg-primary-500 px-4 py-2">
|
|
38
|
+
<Text className="text-sm text-white">{children}</Text>
|
|
39
|
+
</View>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const Default: Story = {
|
|
44
|
+
args: {
|
|
45
|
+
gap: 4,
|
|
46
|
+
},
|
|
47
|
+
render: (args) => (
|
|
48
|
+
<Stack {...args}>
|
|
49
|
+
<Box>Item 1</Box>
|
|
50
|
+
<Box>Item 2</Box>
|
|
51
|
+
<Box>Item 3</Box>
|
|
52
|
+
</Stack>
|
|
53
|
+
),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const Horizontal: Story = {
|
|
57
|
+
render: () => (
|
|
58
|
+
<HStack gap={4} align="center">
|
|
59
|
+
<Box>Left</Box>
|
|
60
|
+
<Box>Center</Box>
|
|
61
|
+
<Box>Right</Box>
|
|
62
|
+
</HStack>
|
|
63
|
+
),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const Vertical: Story = {
|
|
67
|
+
render: () => (
|
|
68
|
+
<VStack gap={4} align="start">
|
|
69
|
+
<Box>Top</Box>
|
|
70
|
+
<Box>Middle</Box>
|
|
71
|
+
<Box>Bottom</Box>
|
|
72
|
+
</VStack>
|
|
73
|
+
),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const WithAlignment: Story = {
|
|
77
|
+
render: () => (
|
|
78
|
+
<VStack gap={6}>
|
|
79
|
+
<Text className="text-sm font-medium text-neutral-400">justify="between"</Text>
|
|
80
|
+
<HStack gap={2} justify="between" className="w-full">
|
|
81
|
+
<Box>A</Box>
|
|
82
|
+
<Box>B</Box>
|
|
83
|
+
<Box>C</Box>
|
|
84
|
+
</HStack>
|
|
85
|
+
|
|
86
|
+
<Text className="text-sm font-medium text-neutral-400">align="center"</Text>
|
|
87
|
+
<HStack gap={2} align="center">
|
|
88
|
+
<View className="rounded bg-primary-500 px-4 py-1">
|
|
89
|
+
<Text className="text-sm text-white">Short</Text>
|
|
90
|
+
</View>
|
|
91
|
+
<View className="rounded bg-primary-500 px-4 py-4">
|
|
92
|
+
<Text className="text-sm text-white">Tall</Text>
|
|
93
|
+
</View>
|
|
94
|
+
<View className="rounded bg-primary-500 px-4 py-2">
|
|
95
|
+
<Text className="text-sm text-white">Medium</Text>
|
|
96
|
+
</View>
|
|
97
|
+
</HStack>
|
|
98
|
+
</VStack>
|
|
99
|
+
),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const WithWrapping: Story = {
|
|
103
|
+
render: () => (
|
|
104
|
+
<HStack gap={2} wrap className="max-w-xs">
|
|
105
|
+
{Array.from({ length: 8 }, (_, i) => (
|
|
106
|
+
<Box key={i}>Tag {i + 1}</Box>
|
|
107
|
+
))}
|
|
108
|
+
</HStack>
|
|
109
|
+
),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const GapScale: Story = {
|
|
113
|
+
render: () => (
|
|
114
|
+
<VStack gap={6}>
|
|
115
|
+
{([0, 1, 2, 4, 6, 8, 12] as const).map((g) => (
|
|
116
|
+
<VStack key={g} gap={1}>
|
|
117
|
+
<Text className="text-xs text-neutral-400">gap={g}</Text>
|
|
118
|
+
<HStack gap={g}>
|
|
119
|
+
<Box>A</Box>
|
|
120
|
+
<Box>B</Box>
|
|
121
|
+
<Box>C</Box>
|
|
122
|
+
</HStack>
|
|
123
|
+
</VStack>
|
|
124
|
+
))}
|
|
125
|
+
</VStack>
|
|
126
|
+
),
|
|
127
|
+
}
|