@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,243 @@
1
+ import React, { useState, createContext, useContext } from 'react'
2
+ import { View, Text, Pressable, ScrollView, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export interface SelectOption<T = string> {
6
+ value: T
7
+ label: string
8
+ isDisabled?: boolean
9
+ }
10
+
11
+ interface SelectContextType<T = string> {
12
+ value: T | T[] | null
13
+ isMulti: boolean
14
+ isOpen: boolean
15
+ setIsOpen: (open: boolean) => void
16
+ selectValue: (val: T) => void
17
+ isSelected: (val: T) => boolean
18
+ }
19
+
20
+ const SelectContext = createContext<SelectContextType<any>>({
21
+ value: null,
22
+ isMulti: false,
23
+ isOpen: false,
24
+ setIsOpen: () => {},
25
+ selectValue: () => {},
26
+ isSelected: () => false,
27
+ })
28
+
29
+ export interface SelectProps<T = string> extends ViewProps {
30
+ /** Selected value (single mode) */
31
+ value?: T | null
32
+ /** Selected values (multi mode) */
33
+ values?: T[]
34
+ /** Callback when value changes (single mode) */
35
+ onChange?: (value: T | null) => void
36
+ /** Callback when values change (multi mode) */
37
+ onChangeMulti?: (values: T[]) => void
38
+ /** Enable multi-select */
39
+ isMulti?: boolean
40
+ /** Placeholder text */
41
+ placeholder?: string
42
+ /** Whether the select is disabled */
43
+ isDisabled?: boolean
44
+ /** Whether the select has an error */
45
+ isInvalid?: boolean
46
+ /** Options to display */
47
+ options: SelectOption<T>[]
48
+ /** Visual variant — use 'filled' on dark/elevated surfaces */
49
+ variant?: 'default' | 'filled'
50
+ /** Additional className */
51
+ className?: string
52
+ }
53
+
54
+ /**
55
+ * Select component for single or multi-selection.
56
+ *
57
+ * @example
58
+ * // Single select
59
+ * <Select
60
+ * value={value}
61
+ * onChange={setValue}
62
+ * options={[
63
+ * { value: '1', label: 'Option 1' },
64
+ * { value: '2', label: 'Option 2' },
65
+ * ]}
66
+ * />
67
+ *
68
+ * // Multi select
69
+ * <Select
70
+ * isMulti
71
+ * values={values}
72
+ * onChangeMulti={setValues}
73
+ * options={options}
74
+ * />
75
+ */
76
+ export function Select<T extends string = string>({
77
+ value,
78
+ values = [],
79
+ onChange,
80
+ onChangeMulti,
81
+ isMulti = false,
82
+ placeholder = 'Select...',
83
+ isDisabled = false,
84
+ isInvalid = false,
85
+ options,
86
+ variant = 'default',
87
+ className,
88
+ ...props
89
+ }: SelectProps<T>) {
90
+ const [isOpen, setIsOpen] = useState(false)
91
+
92
+ const selectValue = (val: T) => {
93
+ if (isMulti) {
94
+ const newValues = values.includes(val)
95
+ ? values.filter((v) => v !== val)
96
+ : [...values, val]
97
+ onChangeMulti?.(newValues)
98
+ } else {
99
+ onChange?.(val)
100
+ setIsOpen(false)
101
+ }
102
+ }
103
+
104
+ const isSelected = (val: T) => {
105
+ if (isMulti) {
106
+ return values.includes(val)
107
+ }
108
+ return value === val
109
+ }
110
+
111
+ const getDisplayValue = () => {
112
+ if (isMulti) {
113
+ if (values.length === 0) return placeholder
114
+ if (values.length === 1) {
115
+ return options.find((o) => o.value === values[0])?.label || placeholder
116
+ }
117
+ return `${values.length} selected`
118
+ }
119
+ if (value === null || value === undefined) return placeholder
120
+ return options.find((o) => o.value === value)?.label || placeholder
121
+ }
122
+
123
+ const clearValue = () => {
124
+ if (isMulti) {
125
+ onChangeMulti?.([])
126
+ } else {
127
+ onChange?.(null)
128
+ }
129
+ }
130
+
131
+ const hasValue = isMulti ? values.length > 0 : value !== null && value !== undefined
132
+
133
+ return (
134
+ <SelectContext.Provider value={{ value, isMulti, isOpen, setIsOpen, selectValue, isSelected }}>
135
+ <View className={cn('relative w-full', className)} {...props}>
136
+ {/* Trigger */}
137
+ <Pressable
138
+ onPress={() => !isDisabled && setIsOpen(!isOpen)}
139
+ disabled={isDisabled}
140
+ accessibilityRole="combobox"
141
+ accessibilityState={{ expanded: isOpen, disabled: isDisabled }}
142
+ className={cn(
143
+ 'flex-row items-center justify-between px-4 py-2.5 rounded-md border',
144
+ variant === 'filled' ? 'bg-black/30' : 'bg-surface-base',
145
+ isInvalid
146
+ ? 'border-border-input-error'
147
+ : variant === 'filled'
148
+ ? 'border-border-subtle'
149
+ : 'border-border-input',
150
+ !isDisabled && !isInvalid && 'web:hover:border-border-input-hover',
151
+ isOpen && 'border-border-input-focus',
152
+ isDisabled && 'opacity-50 cursor-not-allowed'
153
+ )}
154
+ >
155
+ <Text
156
+ className={cn(
157
+ 'flex-1',
158
+ hasValue ? 'text-text-primary' : 'text-text-tertiary'
159
+ )}
160
+ >
161
+ {getDisplayValue()}
162
+ </Text>
163
+ <View className="flex-row items-center gap-2">
164
+ {hasValue && (
165
+ <Pressable
166
+ onPress={(e) => {
167
+ e.stopPropagation?.()
168
+ clearValue()
169
+ }}
170
+ className="p-1"
171
+ >
172
+ <Text className="text-text-secondary text-xs">×</Text>
173
+ </Pressable>
174
+ )}
175
+ <Text className={cn('text-text-secondary', isOpen && 'rotate-180')}>▼</Text>
176
+ </View>
177
+ </Pressable>
178
+
179
+ {/* Dropdown */}
180
+ {isOpen && (
181
+ <>
182
+ <Pressable
183
+ onPress={() => setIsOpen(false)}
184
+ className="fixed inset-0 z-40"
185
+ style={{ position: 'absolute' }}
186
+ />
187
+ <View
188
+ className={cn(
189
+ 'absolute z-50 top-full left-0 right-0 mt-1',
190
+ 'bg-surface-elevated rounded-md shadow-lg border border-border-default',
191
+ 'max-h-60 overflow-hidden'
192
+ )}
193
+ >
194
+ <ScrollView className="py-1">
195
+ {options.map((option) => (
196
+ <SelectOption key={option.value} option={option} />
197
+ ))}
198
+ </ScrollView>
199
+ </View>
200
+ </>
201
+ )}
202
+ </View>
203
+ </SelectContext.Provider>
204
+ )
205
+ }
206
+
207
+ interface SelectOptionComponentProps<T> {
208
+ option: SelectOption<T>
209
+ }
210
+
211
+ function SelectOption<T>({ option }: SelectOptionComponentProps<T>) {
212
+ const { selectValue, isSelected, isMulti } = useContext(SelectContext)
213
+ const selected = isSelected(option.value)
214
+
215
+ return (
216
+ <Pressable
217
+ onPress={() => !option.isDisabled && selectValue(option.value)}
218
+ disabled={option.isDisabled}
219
+ accessibilityRole={isMulti ? 'checkbox' : ('option' as any)}
220
+ accessibilityState={{ selected, disabled: option.isDisabled }}
221
+ className={cn(
222
+ 'flex-row items-center px-4 py-2',
223
+ 'web:hover:bg-interactive-hover active:bg-interactive-active',
224
+ selected && 'bg-interactive-selected',
225
+ option.isDisabled && 'opacity-50 cursor-not-allowed'
226
+ )}
227
+ >
228
+ {isMulti && (
229
+ <View
230
+ className={cn(
231
+ 'w-4 h-4 mr-3 rounded border',
232
+ selected ? 'bg-brand-primary border-brand-primary' : 'border-border-strong'
233
+ )}
234
+ >
235
+ {selected && <Text className="text-white text-xs text-center">✓</Text>}
236
+ </View>
237
+ )}
238
+ <Text className={cn('text-sm', selected && !isMulti ? 'text-brand-primary font-medium' : 'text-text-primary')}>
239
+ {option.label}
240
+ </Text>
241
+ </Pressable>
242
+ )
243
+ }
@@ -0,0 +1,2 @@
1
+ export { Select } from './Select'
2
+ export type { SelectProps, SelectOption } from './Select'
@@ -0,0 +1,153 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import {
4
+ Skeleton,
5
+ SkeletonText,
6
+ SkeletonCircle,
7
+ SkeletonCard,
8
+ SkeletonListItem,
9
+ } from './Skeleton'
10
+
11
+ const meta: Meta<typeof Skeleton> = {
12
+ title: 'Components/Skeleton',
13
+ component: Skeleton,
14
+ tags: ['autodocs'],
15
+ argTypes: {
16
+ variant: {
17
+ control: 'select',
18
+ options: ['text', 'circle', 'rect', 'rounded'],
19
+ },
20
+ animation: {
21
+ control: 'select',
22
+ options: ['pulse', 'shimmer', 'none'],
23
+ },
24
+ },
25
+ }
26
+
27
+ export default meta
28
+ type Story = StoryObj<typeof Skeleton>
29
+
30
+ export const Default: Story = {
31
+ args: {
32
+ variant: 'text',
33
+ width: 200,
34
+ },
35
+ }
36
+
37
+ export const TextVariant: Story = {
38
+ render: () => (
39
+ <View style={{ gap: 8, width: 300 }}>
40
+ <Skeleton variant="text" width="100%" />
41
+ <Skeleton variant="text" width="80%" />
42
+ <Skeleton variant="text" width="60%" />
43
+ </View>
44
+ ),
45
+ }
46
+
47
+ export const CircleVariant: Story = {
48
+ render: () => (
49
+ <View style={{ flexDirection: 'row', gap: 12 }}>
50
+ <Skeleton variant="circle" width={32} height={32} />
51
+ <Skeleton variant="circle" width={48} height={48} />
52
+ <Skeleton variant="circle" width={64} height={64} />
53
+ </View>
54
+ ),
55
+ }
56
+
57
+ export const RectVariant: Story = {
58
+ render: () => (
59
+ <View style={{ gap: 12 }}>
60
+ <Skeleton variant="rect" width={200} height={100} />
61
+ <Skeleton variant="rect" width={200} height={100} borderRadius={8} />
62
+ </View>
63
+ ),
64
+ }
65
+
66
+ export const RoundedVariant: Story = {
67
+ render: () => (
68
+ <Skeleton variant="rounded" width={300} height={200} />
69
+ ),
70
+ }
71
+
72
+ export const TextBlock: Story = {
73
+ render: () => (
74
+ <View style={{ width: 300 }}>
75
+ <SkeletonText lines={4} />
76
+ </View>
77
+ ),
78
+ }
79
+
80
+ export const Avatar: Story = {
81
+ render: () => (
82
+ <View style={{ flexDirection: 'row', gap: 12, alignItems: 'center' }}>
83
+ <SkeletonCircle size={40} />
84
+ <SkeletonCircle size={48} />
85
+ <SkeletonCircle size={56} />
86
+ </View>
87
+ ),
88
+ }
89
+
90
+ export const CardSkeleton: Story = {
91
+ render: () => (
92
+ <View style={{ width: 300 }}>
93
+ <SkeletonCard />
94
+ </View>
95
+ ),
96
+ }
97
+
98
+ export const CardWithoutImage: Story = {
99
+ render: () => (
100
+ <View style={{ width: 300 }}>
101
+ <SkeletonCard hasImage={false} />
102
+ </View>
103
+ ),
104
+ }
105
+
106
+ export const ListItemSkeleton: Story = {
107
+ render: () => (
108
+ <View style={{ width: 350 }}>
109
+ <SkeletonListItem />
110
+ <SkeletonListItem />
111
+ <SkeletonListItem />
112
+ </View>
113
+ ),
114
+ }
115
+
116
+ export const ListWithoutAvatar: Story = {
117
+ render: () => (
118
+ <View style={{ width: 350 }}>
119
+ <SkeletonListItem hasAvatar={false} />
120
+ <SkeletonListItem hasAvatar={false} />
121
+ <SkeletonListItem hasAvatar={false} />
122
+ </View>
123
+ ),
124
+ }
125
+
126
+ export const NoAnimation: Story = {
127
+ render: () => (
128
+ <View style={{ gap: 12 }}>
129
+ <Skeleton variant="text" width={200} animation="none" />
130
+ <SkeletonCircle size={48} animation="none" />
131
+ <Skeleton variant="rounded" width={200} height={100} animation="none" />
132
+ </View>
133
+ ),
134
+ }
135
+
136
+ export const ComposedExample: Story = {
137
+ render: () => (
138
+ <View className="w-[400px] p-4 bg-surface-elevated rounded-xl">
139
+ <View style={{ flexDirection: 'row', gap: 12, marginBottom: 16 }}>
140
+ <SkeletonCircle size={56} />
141
+ <View style={{ flex: 1, justifyContent: 'center', gap: 8 }}>
142
+ <Skeleton variant="text" width="60%" />
143
+ <Skeleton variant="text" width="40%" />
144
+ </View>
145
+ </View>
146
+ <SkeletonText lines={3} />
147
+ <View style={{ flexDirection: 'row', gap: 8, marginTop: 16 }}>
148
+ <Skeleton variant="rounded" width={80} height={36} />
149
+ <Skeleton variant="rounded" width={80} height={36} />
150
+ </View>
151
+ </View>
152
+ ),
153
+ }
@@ -0,0 +1,162 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { Skeleton, SkeletonText, SkeletonCircle, SkeletonCard, SkeletonListItem } from './Skeleton'
5
+
6
+ describe('Skeleton', () => {
7
+ it('renders correctly', () => {
8
+ const { container } = render(<Skeleton />)
9
+ expect(container.firstChild).toBeInTheDocument()
10
+ })
11
+
12
+ it('defaults to text variant with pulse animation', () => {
13
+ const { container } = render(<Skeleton />)
14
+ expect(container.firstChild).toBeInTheDocument()
15
+ })
16
+
17
+ it('renders with all variant options', () => {
18
+ const variants = ['text', 'circle', 'rect', 'rounded'] as const
19
+ variants.forEach((variant) => {
20
+ const { unmount } = render(<Skeleton variant={variant} />)
21
+ unmount()
22
+ })
23
+ })
24
+
25
+ it('renders with all animation options', () => {
26
+ const animations = ['pulse', 'shimmer', 'none'] as const
27
+ animations.forEach((animation) => {
28
+ const { unmount } = render(<Skeleton animation={animation} />)
29
+ unmount()
30
+ })
31
+ })
32
+
33
+ it('accepts width and height props', () => {
34
+ const { container } = render(<Skeleton width={200} height={100} />)
35
+ expect(container.firstChild).toBeInTheDocument()
36
+ })
37
+
38
+ it('accepts string width and height', () => {
39
+ const { container } = render(<Skeleton width="50%" height="100px" />)
40
+ expect(container.firstChild).toBeInTheDocument()
41
+ })
42
+
43
+ it('makes circle variant square when only width is provided', () => {
44
+ const { container } = render(<Skeleton variant="circle" width={48} />)
45
+ expect(container.firstChild).toBeInTheDocument()
46
+ })
47
+
48
+ it('makes circle variant square when only height is provided', () => {
49
+ const { container } = render(<Skeleton variant="circle" height={48} />)
50
+ expect(container.firstChild).toBeInTheDocument()
51
+ })
52
+
53
+ it('accepts custom borderRadius', () => {
54
+ const { container } = render(<Skeleton variant="rect" borderRadius={8} />)
55
+ expect(container.firstChild).toBeInTheDocument()
56
+ })
57
+
58
+ it('applies custom className', () => {
59
+ const { container } = render(<Skeleton className="extra" />)
60
+ expect(container.firstChild).toBeInTheDocument()
61
+ })
62
+
63
+ it('has accessibility label of Loading...', () => {
64
+ const { container } = render(<Skeleton />)
65
+ expect(container.firstChild).toHaveAttribute('aria-label', 'Loading...')
66
+ })
67
+
68
+ describe('SkeletonText', () => {
69
+ it('renders default 3 lines', () => {
70
+ const { container } = render(<SkeletonText />)
71
+ // 3 skeleton lines
72
+ expect(container.firstChild?.childNodes).toHaveLength(3)
73
+ })
74
+
75
+ it('renders custom number of lines', () => {
76
+ const { container } = render(<SkeletonText lines={5} />)
77
+ expect(container.firstChild?.childNodes).toHaveLength(5)
78
+ })
79
+
80
+ it('accepts custom lastLineWidth', () => {
81
+ const { container } = render(<SkeletonText lastLineWidth="40%" />)
82
+ expect(container.firstChild).toBeInTheDocument()
83
+ })
84
+
85
+ it('accepts custom animation', () => {
86
+ const { container } = render(<SkeletonText animation="none" />)
87
+ expect(container.firstChild).toBeInTheDocument()
88
+ })
89
+ })
90
+
91
+ describe('SkeletonCircle', () => {
92
+ it('renders with default size', () => {
93
+ const { container } = render(<SkeletonCircle />)
94
+ expect(container.firstChild).toBeInTheDocument()
95
+ })
96
+
97
+ it('renders with custom size', () => {
98
+ const { container } = render(<SkeletonCircle size={64} />)
99
+ expect(container.firstChild).toBeInTheDocument()
100
+ })
101
+ })
102
+
103
+ describe('SkeletonCard', () => {
104
+ it('renders with default props', () => {
105
+ const { container } = render(<SkeletonCard />)
106
+ expect(container.firstChild).toBeInTheDocument()
107
+ })
108
+
109
+ it('renders without image', () => {
110
+ const { container } = render(<SkeletonCard hasImage={false} />)
111
+ expect(container.firstChild).toBeInTheDocument()
112
+ })
113
+
114
+ it('accepts custom imageHeight and lines', () => {
115
+ const { container } = render(<SkeletonCard imageHeight={200} lines={4} />)
116
+ expect(container.firstChild).toBeInTheDocument()
117
+ })
118
+ })
119
+
120
+ describe('SkeletonListItem', () => {
121
+ it('renders with default props', () => {
122
+ const { container } = render(<SkeletonListItem />)
123
+ expect(container.firstChild).toBeInTheDocument()
124
+ })
125
+
126
+ it('renders without avatar', () => {
127
+ const { container } = render(<SkeletonListItem hasAvatar={false} />)
128
+ expect(container.firstChild).toBeInTheDocument()
129
+ })
130
+
131
+ it('accepts custom avatarSize and lines', () => {
132
+ const { container } = render(<SkeletonListItem avatarSize={56} lines={3} />)
133
+ expect(container.firstChild).toBeInTheDocument()
134
+ })
135
+ })
136
+
137
+ describe('accessibility', () => {
138
+ it('has no accessibility violations', async () => {
139
+ const { container } = render(<Skeleton />)
140
+ const results = await axe(container, {
141
+ rules: { 'aria-prohibited-attr': { enabled: false } },
142
+ })
143
+ expect(results).toHaveNoViolations()
144
+ })
145
+
146
+ it('has no accessibility violations for SkeletonText', async () => {
147
+ const { container } = render(<SkeletonText />)
148
+ const results = await axe(container, {
149
+ rules: { 'aria-prohibited-attr': { enabled: false } },
150
+ })
151
+ expect(results).toHaveNoViolations()
152
+ })
153
+
154
+ it('has no accessibility violations for SkeletonCard', async () => {
155
+ const { container } = render(<SkeletonCard />)
156
+ const results = await axe(container, {
157
+ rules: { 'aria-prohibited-attr': { enabled: false } },
158
+ })
159
+ expect(results).toHaveNoViolations()
160
+ })
161
+ })
162
+ })