@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.
Files changed (206) hide show
  1. package/dist/{chunk-NA3EES23.mjs → chunk-MGW37MPO.mjs} +26 -4
  2. package/dist/chunk-MGW37MPO.mjs.map +1 -0
  3. package/dist/index.d.mts +115 -2
  4. package/dist/index.d.ts +115 -2
  5. package/dist/index.js +262 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +225 -3
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/theme/index.d.mts +124 -109
  10. package/dist/theme/index.d.ts +124 -109
  11. package/dist/theme/index.js +24 -1
  12. package/dist/theme/index.js.map +1 -1
  13. package/dist/theme/index.mjs +1 -1
  14. package/package.json +12 -5
  15. package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
  16. package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
  17. package/src/components/custom/DateTime/DateTime.tsx +235 -0
  18. package/src/components/custom/DateTime/index.ts +2 -0
  19. package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
  20. package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
  21. package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
  22. package/src/components/custom/EmptyState/index.ts +2 -0
  23. package/src/components/custom/Metric/Metric.stories.tsx +133 -0
  24. package/src/components/custom/Metric/Metric.test.tsx +120 -0
  25. package/src/components/custom/Metric/Metric.tsx +92 -0
  26. package/src/components/custom/Metric/index.ts +2 -0
  27. package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
  28. package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
  29. package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
  30. package/src/components/custom/Sidebar/index.ts +18 -0
  31. package/src/components/custom/Table/Table.stories.tsx +481 -0
  32. package/src/components/custom/Table/Table.test.tsx +531 -0
  33. package/src/components/custom/Table/Table.tsx +696 -0
  34. package/src/components/custom/Table/index.ts +22 -0
  35. package/src/components/custom/Typography/Typography.stories.tsx +153 -0
  36. package/src/components/custom/Typography/Typography.test.tsx +107 -0
  37. package/src/components/custom/Typography/Typography.tsx +218 -0
  38. package/src/components/custom/Typography/index.ts +19 -0
  39. package/src/components/custom/index.ts +8 -0
  40. package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
  41. package/src/components/custom/stepper/Stepper.test.tsx +230 -0
  42. package/src/components/custom/stepper/Stepper.tsx +250 -0
  43. package/src/components/custom/stepper/index.ts +10 -0
  44. package/src/components/index.ts +5 -0
  45. package/src/components/ui/alert/Alert.stories.tsx +154 -0
  46. package/src/components/ui/alert/Alert.test.tsx +202 -0
  47. package/src/components/ui/alert/Alert.tsx +170 -0
  48. package/src/components/ui/alert/index.ts +8 -0
  49. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
  50. package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
  51. package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
  52. package/src/components/ui/autocomplete/index.ts +2 -0
  53. package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
  54. package/src/components/ui/avatar/Avatar.test.tsx +135 -0
  55. package/src/components/ui/avatar/Avatar.tsx +168 -0
  56. package/src/components/ui/avatar/index.ts +2 -0
  57. package/src/components/ui/badge/Badge.stories.tsx +100 -0
  58. package/src/components/ui/badge/Badge.test.tsx +86 -0
  59. package/src/components/ui/badge/Badge.tsx +105 -0
  60. package/src/components/ui/badge/index.ts +2 -0
  61. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
  62. package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
  63. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
  64. package/src/components/ui/breadcrumbs/index.ts +2 -0
  65. package/src/components/ui/button/Button.stories.tsx +216 -0
  66. package/src/components/ui/button/Button.test.tsx +132 -0
  67. package/src/components/ui/button/Button.tsx +242 -0
  68. package/src/components/ui/button/index.ts +10 -0
  69. package/src/components/ui/card/Card.stories.tsx +443 -0
  70. package/src/components/ui/card/Card.test.tsx +265 -0
  71. package/src/components/ui/card/Card.tsx +380 -0
  72. package/src/components/ui/card/index.ts +20 -0
  73. package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
  74. package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
  75. package/src/components/ui/checkbox/Checkbox.tsx +163 -0
  76. package/src/components/ui/checkbox/index.ts +2 -0
  77. package/src/components/ui/chip/Chip.stories.tsx +112 -0
  78. package/src/components/ui/chip/Chip.test.tsx +119 -0
  79. package/src/components/ui/chip/Chip.tsx +151 -0
  80. package/src/components/ui/chip/index.ts +2 -0
  81. package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
  82. package/src/components/ui/collapse/Collapse.test.tsx +298 -0
  83. package/src/components/ui/collapse/Collapse.tsx +275 -0
  84. package/src/components/ui/collapse/index.ts +18 -0
  85. package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
  86. package/src/components/ui/data-row/DataRow.test.tsx +78 -0
  87. package/src/components/ui/data-row/DataRow.tsx +29 -0
  88. package/src/components/ui/data-row/index.ts +2 -0
  89. package/src/components/ui/divider/Divider.stories.tsx +71 -0
  90. package/src/components/ui/divider/Divider.test.tsx +55 -0
  91. package/src/components/ui/divider/Divider.tsx +37 -0
  92. package/src/components/ui/divider/index.ts +2 -0
  93. package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
  94. package/src/components/ui/drawer/Drawer.test.tsx +142 -0
  95. package/src/components/ui/drawer/Drawer.tsx +228 -0
  96. package/src/components/ui/drawer/index.ts +9 -0
  97. package/src/components/ui/form-field/FormField.stories.tsx +260 -0
  98. package/src/components/ui/form-field/FormField.test.tsx +280 -0
  99. package/src/components/ui/form-field/FormField.tsx +308 -0
  100. package/src/components/ui/form-field/index.ts +2 -0
  101. package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
  102. package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
  103. package/src/components/ui/help-tip/HelpTip.tsx +229 -0
  104. package/src/components/ui/help-tip/index.ts +2 -0
  105. package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
  106. package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
  107. package/src/components/ui/icon-box/IconBox.tsx +57 -0
  108. package/src/components/ui/icon-box/index.ts +1 -0
  109. package/src/components/ui/index.ts +36 -0
  110. package/src/components/ui/input/Input.stories.tsx +192 -0
  111. package/src/components/ui/input/Input.test.tsx +86 -0
  112. package/src/components/ui/input/Input.tsx +273 -0
  113. package/src/components/ui/input/index.ts +2 -0
  114. package/src/components/ui/link/Link.stories.tsx +78 -0
  115. package/src/components/ui/link/Link.test.tsx +94 -0
  116. package/src/components/ui/link/Link.tsx +86 -0
  117. package/src/components/ui/link/index.ts +2 -0
  118. package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
  119. package/src/components/ui/list-item/ListItem.test.tsx +128 -0
  120. package/src/components/ui/list-item/ListItem.tsx +89 -0
  121. package/src/components/ui/list-item/index.ts +14 -0
  122. package/src/components/ui/menu/Menu.stories.tsx +180 -0
  123. package/src/components/ui/menu/Menu.test.tsx +353 -0
  124. package/src/components/ui/menu/Menu.tsx +222 -0
  125. package/src/components/ui/menu/index.ts +2 -0
  126. package/src/components/ui/modal/Modal.stories.tsx +350 -0
  127. package/src/components/ui/modal/Modal.test.tsx +159 -0
  128. package/src/components/ui/modal/Modal.tsx +254 -0
  129. package/src/components/ui/modal/index.ts +19 -0
  130. package/src/components/ui/popover/Popover.stories.tsx +190 -0
  131. package/src/components/ui/popover/Popover.test.tsx +277 -0
  132. package/src/components/ui/popover/Popover.tsx +162 -0
  133. package/src/components/ui/popover/index.ts +2 -0
  134. package/src/components/ui/progress/Progress.stories.tsx +190 -0
  135. package/src/components/ui/progress/Progress.test.tsx +181 -0
  136. package/src/components/ui/progress/Progress.tsx +320 -0
  137. package/src/components/ui/progress/index.ts +9 -0
  138. package/src/components/ui/radio/Radio.stories.tsx +199 -0
  139. package/src/components/ui/radio/Radio.test.tsx +166 -0
  140. package/src/components/ui/radio/Radio.tsx +200 -0
  141. package/src/components/ui/radio/index.ts +2 -0
  142. package/src/components/ui/section/Section.stories.tsx +111 -0
  143. package/src/components/ui/section/Section.test.tsx +121 -0
  144. package/src/components/ui/section/Section.tsx +52 -0
  145. package/src/components/ui/section/index.ts +6 -0
  146. package/src/components/ui/select/Select.stories.tsx +286 -0
  147. package/src/components/ui/select/Select.test.tsx +180 -0
  148. package/src/components/ui/select/Select.tsx +236 -0
  149. package/src/components/ui/select/index.ts +2 -0
  150. package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
  151. package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
  152. package/src/components/ui/skeleton/Skeleton.tsx +229 -0
  153. package/src/components/ui/skeleton/index.ts +16 -0
  154. package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
  155. package/src/components/ui/spinner/Spinner.test.tsx +67 -0
  156. package/src/components/ui/spinner/Spinner.tsx +72 -0
  157. package/src/components/ui/spinner/index.ts +2 -0
  158. package/src/components/ui/stack/Stack.stories.tsx +127 -0
  159. package/src/components/ui/stack/Stack.test.tsx +184 -0
  160. package/src/components/ui/stack/Stack.tsx +76 -0
  161. package/src/components/ui/stack/index.ts +2 -0
  162. package/src/components/ui/surface/Surface.stories.tsx +91 -0
  163. package/src/components/ui/surface/Surface.test.tsx +63 -0
  164. package/src/components/ui/surface/Surface.tsx +51 -0
  165. package/src/components/ui/surface/index.ts +1 -0
  166. package/src/components/ui/switch/Switch.stories.tsx +111 -0
  167. package/src/components/ui/switch/Switch.test.tsx +112 -0
  168. package/src/components/ui/switch/Switch.tsx +103 -0
  169. package/src/components/ui/switch/index.ts +2 -0
  170. package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
  171. package/src/components/ui/tabs/Tabs.test.tsx +192 -0
  172. package/src/components/ui/tabs/Tabs.tsx +253 -0
  173. package/src/components/ui/tabs/index.ts +16 -0
  174. package/src/components/ui/toast/Toast.stories.tsx +255 -0
  175. package/src/components/ui/toast/Toast.test.tsx +207 -0
  176. package/src/components/ui/toast/Toast.tsx +327 -0
  177. package/src/components/ui/toast/index.ts +8 -0
  178. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
  179. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
  180. package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
  181. package/src/components/ui/toolbar-button/index.ts +7 -0
  182. package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
  183. package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
  184. package/src/components/ui/tooltip/Tooltip.tsx +143 -0
  185. package/src/components/ui/tooltip/index.ts +2 -0
  186. package/src/index.ts +10 -0
  187. package/src/test/setup.ts +19 -0
  188. package/src/theme/Colors.stories.tsx +258 -0
  189. package/src/theme/config.ts +193 -0
  190. package/src/theme/elevation.stories.tsx +346 -0
  191. package/src/theme/elevation.test.ts +27 -0
  192. package/src/theme/elevation.ts +567 -0
  193. package/src/theme/index.ts +39 -0
  194. package/src/theme/shadows.stories.tsx +523 -0
  195. package/src/theme/shadows.ts +439 -0
  196. package/src/theme/tokens/index.ts +2 -0
  197. package/src/theme/tokens/primitives.ts +354 -0
  198. package/src/theme/tokens/semantic.ts +381 -0
  199. package/src/types/jest-axe.d.ts +16 -0
  200. package/src/types/nativewind.d.ts +54 -0
  201. package/src/utils/cn.ts +14 -0
  202. package/src/utils/colors.ts +140 -0
  203. package/src/utils/form.ts +188 -0
  204. package/src/utils/index.ts +29 -0
  205. package/src/utils/useTheme.ts +72 -0
  206. package/dist/chunk-NA3EES23.mjs.map +0 -1
@@ -0,0 +1,236 @@
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
+ /** Additional className */
49
+ className?: string
50
+ }
51
+
52
+ /**
53
+ * Select component for single or multi-selection.
54
+ *
55
+ * @example
56
+ * // Single select
57
+ * <Select
58
+ * value={value}
59
+ * onChange={setValue}
60
+ * options={[
61
+ * { value: '1', label: 'Option 1' },
62
+ * { value: '2', label: 'Option 2' },
63
+ * ]}
64
+ * />
65
+ *
66
+ * // Multi select
67
+ * <Select
68
+ * isMulti
69
+ * values={values}
70
+ * onChangeMulti={setValues}
71
+ * options={options}
72
+ * />
73
+ */
74
+ export function Select<T extends string = string>({
75
+ value,
76
+ values = [],
77
+ onChange,
78
+ onChangeMulti,
79
+ isMulti = false,
80
+ placeholder = 'Select...',
81
+ isDisabled = false,
82
+ isInvalid = false,
83
+ options,
84
+ className,
85
+ ...props
86
+ }: SelectProps<T>) {
87
+ const [isOpen, setIsOpen] = useState(false)
88
+
89
+ const selectValue = (val: T) => {
90
+ if (isMulti) {
91
+ const newValues = values.includes(val)
92
+ ? values.filter((v) => v !== val)
93
+ : [...values, val]
94
+ onChangeMulti?.(newValues)
95
+ } else {
96
+ onChange?.(val)
97
+ setIsOpen(false)
98
+ }
99
+ }
100
+
101
+ const isSelected = (val: T) => {
102
+ if (isMulti) {
103
+ return values.includes(val)
104
+ }
105
+ return value === val
106
+ }
107
+
108
+ const getDisplayValue = () => {
109
+ if (isMulti) {
110
+ if (values.length === 0) return placeholder
111
+ if (values.length === 1) {
112
+ return options.find((o) => o.value === values[0])?.label || placeholder
113
+ }
114
+ return `${values.length} selected`
115
+ }
116
+ if (value === null || value === undefined) return placeholder
117
+ return options.find((o) => o.value === value)?.label || placeholder
118
+ }
119
+
120
+ const clearValue = () => {
121
+ if (isMulti) {
122
+ onChangeMulti?.([])
123
+ } else {
124
+ onChange?.(null)
125
+ }
126
+ }
127
+
128
+ const hasValue = isMulti ? values.length > 0 : value !== null && value !== undefined
129
+
130
+ return (
131
+ <SelectContext.Provider value={{ value, isMulti, isOpen, setIsOpen, selectValue, isSelected }}>
132
+ <View className={cn('relative w-full', className)} {...props}>
133
+ {/* Trigger */}
134
+ <Pressable
135
+ onPress={() => !isDisabled && setIsOpen(!isOpen)}
136
+ disabled={isDisabled}
137
+ accessibilityRole="combobox"
138
+ accessibilityState={{ expanded: isOpen, disabled: isDisabled }}
139
+ className={cn(
140
+ 'flex-row items-center justify-between px-4 py-2.5 rounded-md border',
141
+ 'bg-surface-base',
142
+ isInvalid ? 'border-border-input-error' : 'border-border-input',
143
+ !isDisabled && !isInvalid && 'web:hover:border-border-input-hover',
144
+ isOpen && 'border-border-input-focus',
145
+ isDisabled && 'opacity-50 cursor-not-allowed'
146
+ )}
147
+ >
148
+ <Text
149
+ className={cn(
150
+ 'flex-1',
151
+ hasValue ? 'text-text-primary' : 'text-text-tertiary'
152
+ )}
153
+ >
154
+ {getDisplayValue()}
155
+ </Text>
156
+ <View className="flex-row items-center gap-2">
157
+ {hasValue && (
158
+ <Pressable
159
+ onPress={(e) => {
160
+ e.stopPropagation?.()
161
+ clearValue()
162
+ }}
163
+ className="p-1"
164
+ >
165
+ <Text className="text-text-secondary text-xs">×</Text>
166
+ </Pressable>
167
+ )}
168
+ <Text className={cn('text-text-secondary', isOpen && 'rotate-180')}>▼</Text>
169
+ </View>
170
+ </Pressable>
171
+
172
+ {/* Dropdown */}
173
+ {isOpen && (
174
+ <>
175
+ <Pressable
176
+ onPress={() => setIsOpen(false)}
177
+ className="fixed inset-0 z-40"
178
+ style={{ position: 'absolute' }}
179
+ />
180
+ <View
181
+ className={cn(
182
+ 'absolute z-50 top-full left-0 right-0 mt-1',
183
+ 'bg-surface-elevated rounded-md shadow-lg border border-border-default',
184
+ 'max-h-60 overflow-hidden'
185
+ )}
186
+ >
187
+ <ScrollView className="py-1">
188
+ {options.map((option) => (
189
+ <SelectOption key={option.value} option={option} />
190
+ ))}
191
+ </ScrollView>
192
+ </View>
193
+ </>
194
+ )}
195
+ </View>
196
+ </SelectContext.Provider>
197
+ )
198
+ }
199
+
200
+ interface SelectOptionComponentProps<T> {
201
+ option: SelectOption<T>
202
+ }
203
+
204
+ function SelectOption<T>({ option }: SelectOptionComponentProps<T>) {
205
+ const { selectValue, isSelected, isMulti } = useContext(SelectContext)
206
+ const selected = isSelected(option.value)
207
+
208
+ return (
209
+ <Pressable
210
+ onPress={() => !option.isDisabled && selectValue(option.value)}
211
+ disabled={option.isDisabled}
212
+ accessibilityRole={isMulti ? 'checkbox' : ('option' as any)}
213
+ accessibilityState={{ selected, disabled: option.isDisabled }}
214
+ className={cn(
215
+ 'flex-row items-center px-4 py-2',
216
+ 'web:hover:bg-interactive-hover active:bg-interactive-active',
217
+ selected && 'bg-interactive-selected',
218
+ option.isDisabled && 'opacity-50 cursor-not-allowed'
219
+ )}
220
+ >
221
+ {isMulti && (
222
+ <View
223
+ className={cn(
224
+ 'w-4 h-4 mr-3 rounded border',
225
+ selected ? 'bg-brand-primary border-brand-primary' : 'border-border-strong'
226
+ )}
227
+ >
228
+ {selected && <Text className="text-white text-xs text-center">✓</Text>}
229
+ </View>
230
+ )}
231
+ <Text className={cn('text-sm', selected && !isMulti ? 'text-brand-primary font-medium' : 'text-text-primary')}>
232
+ {option.label}
233
+ </Text>
234
+ </Pressable>
235
+ )
236
+ }
@@ -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
+ })