@titan-design/react-ui 0.1.0 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/README.md +47 -1
  2. package/dist/{chunk-MGW37MPO.mjs → chunk-UXVRPOME.mjs} +173 -3
  3. package/dist/chunk-UXVRPOME.mjs.map +1 -0
  4. package/dist/index.d.mts +101 -12
  5. package/dist/index.d.ts +101 -12
  6. package/dist/index.js +1267 -468
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +734 -112
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/theme/index.d.mts +86 -1
  11. package/dist/theme/index.d.ts +86 -1
  12. package/dist/theme/index.js +166 -0
  13. package/dist/theme/index.js.map +1 -1
  14. package/dist/theme/index.mjs +1 -1
  15. package/package.json +22 -6
  16. package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
  17. package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
  18. package/src/components/custom/DateTime/DateTime.tsx +235 -0
  19. package/src/components/custom/DateTime/index.ts +2 -0
  20. package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
  21. package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
  22. package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
  23. package/src/components/custom/EmptyState/index.ts +2 -0
  24. package/src/components/custom/Metric/Metric.stories.tsx +133 -0
  25. package/src/components/custom/Metric/Metric.test.tsx +120 -0
  26. package/src/components/custom/Metric/Metric.tsx +92 -0
  27. package/src/components/custom/Metric/index.ts +2 -0
  28. package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
  29. package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
  30. package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
  31. package/src/components/custom/Sidebar/index.ts +18 -0
  32. package/src/components/custom/Table/Table.stories.tsx +481 -0
  33. package/src/components/custom/Table/Table.test.tsx +531 -0
  34. package/src/components/custom/Table/Table.tsx +696 -0
  35. package/src/components/custom/Table/index.ts +22 -0
  36. package/src/components/custom/Typography/Typography.stories.tsx +153 -0
  37. package/src/components/custom/Typography/Typography.test.tsx +107 -0
  38. package/src/components/custom/Typography/Typography.tsx +218 -0
  39. package/src/components/custom/Typography/index.ts +19 -0
  40. package/src/components/custom/Workout/RestTimer.stories.tsx +71 -0
  41. package/src/components/custom/Workout/RestTimer.test.tsx +145 -0
  42. package/src/components/custom/Workout/RestTimer.tsx +172 -0
  43. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +144 -0
  44. package/src/components/custom/Workout/SupersetWrapper.test.tsx +130 -0
  45. package/src/components/custom/Workout/SupersetWrapper.tsx +63 -0
  46. package/src/components/custom/Workout/index.ts +2 -0
  47. package/src/components/custom/index.ts +9 -0
  48. package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
  49. package/src/components/custom/stepper/Stepper.test.tsx +230 -0
  50. package/src/components/custom/stepper/Stepper.tsx +250 -0
  51. package/src/components/custom/stepper/index.ts +10 -0
  52. package/src/components/index.ts +5 -0
  53. package/src/components/ui/alert/Alert.stories.tsx +154 -0
  54. package/src/components/ui/alert/Alert.test.tsx +202 -0
  55. package/src/components/ui/alert/Alert.tsx +170 -0
  56. package/src/components/ui/alert/index.ts +8 -0
  57. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
  58. package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
  59. package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
  60. package/src/components/ui/autocomplete/index.ts +2 -0
  61. package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
  62. package/src/components/ui/avatar/Avatar.test.tsx +152 -0
  63. package/src/components/ui/avatar/Avatar.tsx +176 -0
  64. package/src/components/ui/avatar/index.ts +2 -0
  65. package/src/components/ui/badge/Badge.stories.tsx +100 -0
  66. package/src/components/ui/badge/Badge.test.tsx +103 -0
  67. package/src/components/ui/badge/Badge.tsx +119 -0
  68. package/src/components/ui/badge/index.ts +3 -0
  69. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
  70. package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
  71. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
  72. package/src/components/ui/breadcrumbs/index.ts +2 -0
  73. package/src/components/ui/button/Button.stories.tsx +216 -0
  74. package/src/components/ui/button/Button.test.tsx +132 -0
  75. package/src/components/ui/button/Button.tsx +297 -0
  76. package/src/components/ui/button/index.ts +10 -0
  77. package/src/components/ui/card/Card.stories.tsx +443 -0
  78. package/src/components/ui/card/Card.test.tsx +294 -0
  79. package/src/components/ui/card/Card.tsx +401 -0
  80. package/src/components/ui/card/index.ts +20 -0
  81. package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
  82. package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
  83. package/src/components/ui/checkbox/Checkbox.tsx +163 -0
  84. package/src/components/ui/checkbox/index.ts +2 -0
  85. package/src/components/ui/chip/Chip.stories.tsx +112 -0
  86. package/src/components/ui/chip/Chip.test.tsx +119 -0
  87. package/src/components/ui/chip/Chip.tsx +151 -0
  88. package/src/components/ui/chip/index.ts +2 -0
  89. package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
  90. package/src/components/ui/collapse/Collapse.test.tsx +298 -0
  91. package/src/components/ui/collapse/Collapse.tsx +275 -0
  92. package/src/components/ui/collapse/index.ts +18 -0
  93. package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
  94. package/src/components/ui/data-row/DataRow.test.tsx +78 -0
  95. package/src/components/ui/data-row/DataRow.tsx +29 -0
  96. package/src/components/ui/data-row/index.ts +2 -0
  97. package/src/components/ui/divider/Divider.stories.tsx +71 -0
  98. package/src/components/ui/divider/Divider.test.tsx +55 -0
  99. package/src/components/ui/divider/Divider.tsx +37 -0
  100. package/src/components/ui/divider/index.ts +2 -0
  101. package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
  102. package/src/components/ui/drawer/Drawer.test.tsx +142 -0
  103. package/src/components/ui/drawer/Drawer.tsx +228 -0
  104. package/src/components/ui/drawer/index.ts +9 -0
  105. package/src/components/ui/form-field/FormField.stories.tsx +260 -0
  106. package/src/components/ui/form-field/FormField.test.tsx +280 -0
  107. package/src/components/ui/form-field/FormField.tsx +308 -0
  108. package/src/components/ui/form-field/index.ts +2 -0
  109. package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
  110. package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
  111. package/src/components/ui/help-tip/HelpTip.tsx +229 -0
  112. package/src/components/ui/help-tip/index.ts +2 -0
  113. package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
  114. package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
  115. package/src/components/ui/icon-box/IconBox.tsx +57 -0
  116. package/src/components/ui/icon-box/index.ts +1 -0
  117. package/src/components/ui/index.ts +38 -0
  118. package/src/components/ui/indicator/Indicator.stories.tsx +74 -0
  119. package/src/components/ui/indicator/Indicator.test.tsx +55 -0
  120. package/src/components/ui/indicator/Indicator.tsx +73 -0
  121. package/src/components/ui/indicator/index.ts +2 -0
  122. package/src/components/ui/input/Input.stories.tsx +192 -0
  123. package/src/components/ui/input/Input.test.tsx +86 -0
  124. package/src/components/ui/input/Input.tsx +273 -0
  125. package/src/components/ui/input/index.ts +2 -0
  126. package/src/components/ui/link/Link.stories.tsx +78 -0
  127. package/src/components/ui/link/Link.test.tsx +94 -0
  128. package/src/components/ui/link/Link.tsx +86 -0
  129. package/src/components/ui/link/index.ts +2 -0
  130. package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
  131. package/src/components/ui/list-item/ListItem.test.tsx +128 -0
  132. package/src/components/ui/list-item/ListItem.tsx +89 -0
  133. package/src/components/ui/list-item/index.ts +14 -0
  134. package/src/components/ui/menu/Menu.stories.tsx +180 -0
  135. package/src/components/ui/menu/Menu.test.tsx +353 -0
  136. package/src/components/ui/menu/Menu.tsx +222 -0
  137. package/src/components/ui/menu/index.ts +2 -0
  138. package/src/components/ui/modal/Modal.stories.tsx +350 -0
  139. package/src/components/ui/modal/Modal.test.tsx +159 -0
  140. package/src/components/ui/modal/Modal.tsx +254 -0
  141. package/src/components/ui/modal/index.ts +19 -0
  142. package/src/components/ui/pill/Pill.stories.tsx +71 -0
  143. package/src/components/ui/pill/Pill.test.tsx +69 -0
  144. package/src/components/ui/pill/Pill.tsx +104 -0
  145. package/src/components/ui/pill/index.ts +2 -0
  146. package/src/components/ui/popover/Popover.stories.tsx +190 -0
  147. package/src/components/ui/popover/Popover.test.tsx +419 -0
  148. package/src/components/ui/popover/Popover.tsx +237 -0
  149. package/src/components/ui/popover/index.ts +2 -0
  150. package/src/components/ui/progress/Progress.stories.tsx +190 -0
  151. package/src/components/ui/progress/Progress.test.tsx +210 -0
  152. package/src/components/ui/progress/Progress.tsx +339 -0
  153. package/src/components/ui/progress/index.ts +9 -0
  154. package/src/components/ui/radio/Radio.stories.tsx +199 -0
  155. package/src/components/ui/radio/Radio.test.tsx +166 -0
  156. package/src/components/ui/radio/Radio.tsx +200 -0
  157. package/src/components/ui/radio/index.ts +2 -0
  158. package/src/components/ui/section/Section.stories.tsx +111 -0
  159. package/src/components/ui/section/Section.test.tsx +121 -0
  160. package/src/components/ui/section/Section.tsx +52 -0
  161. package/src/components/ui/section/index.ts +6 -0
  162. package/src/components/ui/select/Select.stories.tsx +286 -0
  163. package/src/components/ui/select/Select.test.tsx +200 -0
  164. package/src/components/ui/select/Select.tsx +243 -0
  165. package/src/components/ui/select/index.ts +2 -0
  166. package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
  167. package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
  168. package/src/components/ui/skeleton/Skeleton.tsx +229 -0
  169. package/src/components/ui/skeleton/index.ts +16 -0
  170. package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
  171. package/src/components/ui/spinner/Spinner.test.tsx +67 -0
  172. package/src/components/ui/spinner/Spinner.tsx +72 -0
  173. package/src/components/ui/spinner/index.ts +2 -0
  174. package/src/components/ui/stack/Stack.stories.tsx +127 -0
  175. package/src/components/ui/stack/Stack.test.tsx +184 -0
  176. package/src/components/ui/stack/Stack.tsx +76 -0
  177. package/src/components/ui/stack/index.ts +2 -0
  178. package/src/components/ui/surface/Surface.stories.tsx +91 -0
  179. package/src/components/ui/surface/Surface.test.tsx +63 -0
  180. package/src/components/ui/surface/Surface.tsx +51 -0
  181. package/src/components/ui/surface/index.ts +1 -0
  182. package/src/components/ui/switch/Switch.stories.tsx +111 -0
  183. package/src/components/ui/switch/Switch.test.tsx +112 -0
  184. package/src/components/ui/switch/Switch.tsx +103 -0
  185. package/src/components/ui/switch/index.ts +2 -0
  186. package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
  187. package/src/components/ui/tabs/Tabs.test.tsx +192 -0
  188. package/src/components/ui/tabs/Tabs.tsx +253 -0
  189. package/src/components/ui/tabs/index.ts +16 -0
  190. package/src/components/ui/toast/Toast.stories.tsx +255 -0
  191. package/src/components/ui/toast/Toast.test.tsx +207 -0
  192. package/src/components/ui/toast/Toast.tsx +327 -0
  193. package/src/components/ui/toast/index.ts +9 -0
  194. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
  195. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
  196. package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
  197. package/src/components/ui/toolbar-button/index.ts +7 -0
  198. package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
  199. package/src/components/ui/tooltip/Tooltip.test.tsx +262 -0
  200. package/src/components/ui/tooltip/Tooltip.tsx +234 -0
  201. package/src/components/ui/tooltip/index.ts +2 -0
  202. package/src/index.ts +10 -0
  203. package/src/stories/PresetShowcase.stories.tsx +462 -0
  204. package/src/stories/ThemePresets.stories.tsx +85 -0
  205. package/src/test/setup.ts +19 -0
  206. package/src/theme/Colors.stories.tsx +258 -0
  207. package/src/theme/config.ts +193 -0
  208. package/src/theme/elevation.stories.tsx +346 -0
  209. package/src/theme/elevation.test.ts +27 -0
  210. package/src/theme/elevation.ts +567 -0
  211. package/src/theme/global.css +129 -1
  212. package/src/theme/index.ts +40 -0
  213. package/src/theme/presets/apply.ts +97 -0
  214. package/src/theme/presets/audiobook.ts +93 -0
  215. package/src/theme/presets/default.ts +6 -0
  216. package/src/theme/presets/index.ts +4 -0
  217. package/src/theme/presets/presets.test.ts +51 -0
  218. package/src/theme/presets/types.ts +76 -0
  219. package/src/theme/shadows.stories.tsx +523 -0
  220. package/src/theme/shadows.ts +439 -0
  221. package/src/theme/tokens/index.ts +2 -0
  222. package/src/theme/tokens/primitives.ts +354 -0
  223. package/src/theme/tokens/semantic.ts +381 -0
  224. package/src/types/jest-axe.d.ts +16 -0
  225. package/src/types/nativewind.d.ts +54 -0
  226. package/src/utils/avatar-color.test.ts +30 -0
  227. package/src/utils/avatar-color.ts +20 -0
  228. package/src/utils/cn.ts +14 -0
  229. package/src/utils/colors.ts +140 -0
  230. package/src/utils/form.ts +188 -0
  231. package/src/utils/index.ts +29 -0
  232. package/src/utils/useTheme.ts +72 -0
  233. package/src/web-jsx/jsx-dev-runtime.ts +28 -0
  234. package/src/web-jsx/jsx-runtime.ts +41 -0
  235. package/tailwind.config.js +53 -3
  236. package/dist/chunk-MGW37MPO.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,2 @@
1
+ export { Spinner } from './Spinner'
2
+ export type { SpinnerProps, SpinnerSize, SpinnerColor } from './Spinner'
@@ -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=&quot;between&quot;</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=&quot;center&quot;</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
+ }