@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,103 @@
1
+ import React, { forwardRef } from 'react'
2
+ import { Pressable, View, Text, type PressableProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export type SwitchSize = 'sm' | 'md' | 'lg'
6
+
7
+ export interface SwitchProps extends Omit<PressableProps, 'children'> {
8
+ /** Whether the switch is on */
9
+ isChecked?: boolean
10
+ /** Whether the switch is disabled */
11
+ isDisabled?: boolean
12
+ /** Switch size */
13
+ size?: SwitchSize
14
+ /** Label text */
15
+ label?: string
16
+ /** Label position */
17
+ labelPosition?: 'left' | 'right'
18
+ /** Callback when switch state changes */
19
+ onCheckedChange?: (checked: boolean) => void
20
+ /** Additional className */
21
+ className?: string
22
+ }
23
+
24
+ const sizeStyles: Record<SwitchSize, { track: string; thumb: string; translate: string }> = {
25
+ sm: { track: 'w-8 h-4', thumb: 'w-3 h-3', translate: 'translate-x-4' },
26
+ md: { track: 'w-10 h-5', thumb: 'w-4 h-4', translate: 'translate-x-5' },
27
+ lg: { track: 'w-12 h-6', thumb: 'w-5 h-5', translate: 'translate-x-6' },
28
+ }
29
+
30
+ /**
31
+ * Switch component for toggling boolean values.
32
+ *
33
+ * @example
34
+ * <Switch
35
+ * isChecked={enabled}
36
+ * onCheckedChange={setEnabled}
37
+ * label="Enable notifications"
38
+ * />
39
+ */
40
+ export const Switch = forwardRef<View, SwitchProps>(function Switch(
41
+ {
42
+ isChecked = false,
43
+ isDisabled = false,
44
+ size = 'md',
45
+ label,
46
+ labelPosition = 'right',
47
+ onCheckedChange,
48
+ className,
49
+ ...props
50
+ },
51
+ ref
52
+ ) {
53
+ const styles = sizeStyles[size]
54
+
55
+ const handlePress = () => {
56
+ if (!isDisabled) {
57
+ onCheckedChange?.(!isChecked)
58
+ }
59
+ }
60
+
61
+ const switchElement = (
62
+ <View
63
+ className={cn(
64
+ 'rounded-full p-0.5 transition-colors',
65
+ styles.track,
66
+ isChecked ? 'bg-brand-primary' : 'bg-border-strong'
67
+ )}
68
+ >
69
+ <View
70
+ className={cn(
71
+ 'rounded-full bg-white shadow-sm transition-transform',
72
+ styles.thumb,
73
+ isChecked && styles.translate
74
+ )}
75
+ />
76
+ </View>
77
+ )
78
+
79
+ return (
80
+ <Pressable
81
+ ref={ref}
82
+ disabled={isDisabled}
83
+ onPress={handlePress}
84
+ accessibilityRole="switch"
85
+ accessibilityState={{ checked: isChecked, disabled: isDisabled }}
86
+ accessibilityLabel={label}
87
+ className={cn(
88
+ 'flex-row items-center gap-2',
89
+ isDisabled && 'opacity-50',
90
+ className
91
+ )}
92
+ {...props}
93
+ >
94
+ {label && labelPosition === 'left' && (
95
+ <Text className="text-text-primary font-medium">{label}</Text>
96
+ )}
97
+ {switchElement}
98
+ {label && labelPosition === 'right' && (
99
+ <Text className="text-text-primary font-medium">{label}</Text>
100
+ )}
101
+ </Pressable>
102
+ )
103
+ })
@@ -0,0 +1,2 @@
1
+ export { Switch } from './Switch'
2
+ export type { SwitchProps, SwitchSize } from './Switch'
@@ -0,0 +1,213 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import { Tabs, TabList, Tab, TabPanels, TabPanel } from './Tabs'
4
+
5
+ const meta: Meta<typeof Tabs> = {
6
+ title: 'Components/Tabs',
7
+ component: Tabs,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ variant: {
11
+ control: 'select',
12
+ options: ['line', 'enclosed', 'soft-rounded'],
13
+ },
14
+ orientation: {
15
+ control: 'select',
16
+ options: ['horizontal', 'vertical'],
17
+ },
18
+ },
19
+ }
20
+
21
+ export default meta
22
+ type Story = StoryObj<typeof Tabs>
23
+
24
+ export const Default: Story = {
25
+ render: () => (
26
+ <Tabs defaultIndex={0}>
27
+ <TabList>
28
+ <Tab>Account</Tab>
29
+ <Tab>Security</Tab>
30
+ <Tab>Notifications</Tab>
31
+ </TabList>
32
+ <TabPanels>
33
+ <TabPanel>
34
+ <Text className="text-text-primary">Account settings content</Text>
35
+ </TabPanel>
36
+ <TabPanel>
37
+ <Text className="text-text-primary">Security settings content</Text>
38
+ </TabPanel>
39
+ <TabPanel>
40
+ <Text className="text-text-primary">Notification settings content</Text>
41
+ </TabPanel>
42
+ </TabPanels>
43
+ </Tabs>
44
+ ),
45
+ }
46
+
47
+ export const LineVariant: Story = {
48
+ render: () => (
49
+ <Tabs variant="line">
50
+ <TabList>
51
+ <Tab>Overview</Tab>
52
+ <Tab>Analytics</Tab>
53
+ <Tab>Reports</Tab>
54
+ <Tab>Settings</Tab>
55
+ </TabList>
56
+ <TabPanels>
57
+ <TabPanel>
58
+ <Text className="text-text-primary">Overview content</Text>
59
+ </TabPanel>
60
+ <TabPanel>
61
+ <Text className="text-text-primary">Analytics content</Text>
62
+ </TabPanel>
63
+ <TabPanel>
64
+ <Text className="text-text-primary">Reports content</Text>
65
+ </TabPanel>
66
+ <TabPanel>
67
+ <Text className="text-text-primary">Settings content</Text>
68
+ </TabPanel>
69
+ </TabPanels>
70
+ </Tabs>
71
+ ),
72
+ }
73
+
74
+ export const EnclosedVariant: Story = {
75
+ render: () => (
76
+ <Tabs variant="enclosed">
77
+ <TabList>
78
+ <Tab>Day</Tab>
79
+ <Tab>Week</Tab>
80
+ <Tab>Month</Tab>
81
+ <Tab>Year</Tab>
82
+ </TabList>
83
+ <TabPanels>
84
+ <TabPanel>
85
+ <Text className="text-text-primary">Daily view</Text>
86
+ </TabPanel>
87
+ <TabPanel>
88
+ <Text className="text-text-primary">Weekly view</Text>
89
+ </TabPanel>
90
+ <TabPanel>
91
+ <Text className="text-text-primary">Monthly view</Text>
92
+ </TabPanel>
93
+ <TabPanel>
94
+ <Text className="text-text-primary">Yearly view</Text>
95
+ </TabPanel>
96
+ </TabPanels>
97
+ </Tabs>
98
+ ),
99
+ }
100
+
101
+ export const SoftRoundedVariant: Story = {
102
+ render: () => (
103
+ <Tabs variant="soft-rounded">
104
+ <TabList>
105
+ <Tab>All</Tab>
106
+ <Tab>Active</Tab>
107
+ <Tab>Completed</Tab>
108
+ </TabList>
109
+ <TabPanels>
110
+ <TabPanel>
111
+ <Text className="text-text-primary">All items</Text>
112
+ </TabPanel>
113
+ <TabPanel>
114
+ <Text className="text-text-primary">Active items</Text>
115
+ </TabPanel>
116
+ <TabPanel>
117
+ <Text className="text-text-primary">Completed items</Text>
118
+ </TabPanel>
119
+ </TabPanels>
120
+ </Tabs>
121
+ ),
122
+ }
123
+
124
+ export const VerticalOrientation: Story = {
125
+ render: () => (
126
+ <View style={{ height: 300 }}>
127
+ <Tabs orientation="vertical" variant="line">
128
+ <TabList>
129
+ <Tab>Profile</Tab>
130
+ <Tab>Account</Tab>
131
+ <Tab>Security</Tab>
132
+ <Tab>Billing</Tab>
133
+ </TabList>
134
+ <TabPanels>
135
+ <TabPanel>
136
+ <Text className="text-text-primary">Profile settings</Text>
137
+ </TabPanel>
138
+ <TabPanel>
139
+ <Text className="text-text-primary">Account settings</Text>
140
+ </TabPanel>
141
+ <TabPanel>
142
+ <Text className="text-text-primary">Security settings</Text>
143
+ </TabPanel>
144
+ <TabPanel>
145
+ <Text className="text-text-primary">Billing settings</Text>
146
+ </TabPanel>
147
+ </TabPanels>
148
+ </Tabs>
149
+ </View>
150
+ ),
151
+ }
152
+
153
+ export const WithDisabledTab: Story = {
154
+ render: () => (
155
+ <Tabs>
156
+ <TabList>
157
+ <Tab>Available</Tab>
158
+ <Tab isDisabled>Disabled</Tab>
159
+ <Tab>Also Available</Tab>
160
+ </TabList>
161
+ <TabPanels>
162
+ <TabPanel>
163
+ <Text className="text-text-primary">First tab content</Text>
164
+ </TabPanel>
165
+ <TabPanel>
166
+ <Text className="text-text-primary">This won&apos;t show</Text>
167
+ </TabPanel>
168
+ <TabPanel>
169
+ <Text className="text-text-primary">Third tab content</Text>
170
+ </TabPanel>
171
+ </TabPanels>
172
+ </Tabs>
173
+ ),
174
+ }
175
+
176
+ export const AllVariants: Story = {
177
+ render: () => (
178
+ <View style={{ gap: 32 }}>
179
+ <View>
180
+ <Text className="text-text-secondary mb-2">Line Variant</Text>
181
+ <Tabs variant="line">
182
+ <TabList>
183
+ <Tab>Tab 1</Tab>
184
+ <Tab>Tab 2</Tab>
185
+ <Tab>Tab 3</Tab>
186
+ </TabList>
187
+ </Tabs>
188
+ </View>
189
+
190
+ <View>
191
+ <Text className="text-text-secondary mb-2">Enclosed Variant</Text>
192
+ <Tabs variant="enclosed">
193
+ <TabList>
194
+ <Tab>Tab 1</Tab>
195
+ <Tab>Tab 2</Tab>
196
+ <Tab>Tab 3</Tab>
197
+ </TabList>
198
+ </Tabs>
199
+ </View>
200
+
201
+ <View>
202
+ <Text className="text-text-secondary mb-2">Soft Rounded Variant</Text>
203
+ <Tabs variant="soft-rounded">
204
+ <TabList>
205
+ <Tab>Tab 1</Tab>
206
+ <Tab>Tab 2</Tab>
207
+ <Tab>Tab 3</Tab>
208
+ </TabList>
209
+ </Tabs>
210
+ </View>
211
+ </View>
212
+ ),
213
+ }
@@ -0,0 +1,192 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { Tabs, TabList, Tab, TabPanels, TabPanel } from './Tabs'
5
+
6
+ function renderTabs(props: Record<string, unknown> = {}) {
7
+ return render(
8
+ <Tabs {...props}>
9
+ <TabList>
10
+ <Tab>Tab 1</Tab>
11
+ <Tab>Tab 2</Tab>
12
+ <Tab>Tab 3</Tab>
13
+ </TabList>
14
+ <TabPanels>
15
+ <TabPanel>Content 1</TabPanel>
16
+ <TabPanel>Content 2</TabPanel>
17
+ <TabPanel>Content 3</TabPanel>
18
+ </TabPanels>
19
+ </Tabs>
20
+ )
21
+ }
22
+
23
+ describe('Tabs', () => {
24
+ it('renders all tab buttons', () => {
25
+ renderTabs()
26
+ expect(screen.getByText('Tab 1')).toBeInTheDocument()
27
+ expect(screen.getByText('Tab 2')).toBeInTheDocument()
28
+ expect(screen.getByText('Tab 3')).toBeInTheDocument()
29
+ })
30
+
31
+ it('shows first tab panel by default', () => {
32
+ renderTabs()
33
+ expect(screen.getByText('Content 1')).toBeInTheDocument()
34
+ expect(screen.queryByText('Content 2')).not.toBeInTheDocument()
35
+ })
36
+
37
+ it('shows correct panel for defaultIndex', () => {
38
+ renderTabs({ defaultIndex: 1 })
39
+ expect(screen.queryByText('Content 1')).not.toBeInTheDocument()
40
+ expect(screen.getByText('Content 2')).toBeInTheDocument()
41
+ })
42
+
43
+ it('switches tab on click', () => {
44
+ renderTabs()
45
+ fireEvent.click(screen.getAllByRole('tab')[1])
46
+ expect(screen.queryByText('Content 1')).not.toBeInTheDocument()
47
+ expect(screen.getByText('Content 2')).toBeInTheDocument()
48
+ })
49
+
50
+ it('calls onChange when tab is clicked', () => {
51
+ const onChange = vi.fn()
52
+ renderTabs({ onChange })
53
+ fireEvent.click(screen.getAllByRole('tab')[2])
54
+ expect(onChange).toHaveBeenCalledWith(2)
55
+ })
56
+
57
+ it('supports controlled index', () => {
58
+ const onChange = vi.fn()
59
+ const { rerender } = render(
60
+ <Tabs index={0} onChange={onChange}>
61
+ <TabList>
62
+ <Tab>A</Tab>
63
+ <Tab>B</Tab>
64
+ </TabList>
65
+ <TabPanels>
66
+ <TabPanel>Panel A</TabPanel>
67
+ <TabPanel>Panel B</TabPanel>
68
+ </TabPanels>
69
+ </Tabs>
70
+ )
71
+ expect(screen.getByText('Panel A')).toBeInTheDocument()
72
+
73
+ fireEvent.click(screen.getAllByRole('tab')[1])
74
+ expect(onChange).toHaveBeenCalledWith(1)
75
+ // Still shows Panel A because controlled
76
+ expect(screen.getByText('Panel A')).toBeInTheDocument()
77
+
78
+ rerender(
79
+ <Tabs index={1} onChange={onChange}>
80
+ <TabList>
81
+ <Tab>A</Tab>
82
+ <Tab>B</Tab>
83
+ </TabList>
84
+ <TabPanels>
85
+ <TabPanel>Panel A</TabPanel>
86
+ <TabPanel>Panel B</TabPanel>
87
+ </TabPanels>
88
+ </Tabs>
89
+ )
90
+ expect(screen.getByText('Panel B')).toBeInTheDocument()
91
+ })
92
+
93
+ it('renders with all variant options', () => {
94
+ const variants = ['line', 'enclosed', 'soft-rounded'] as const
95
+ variants.forEach((variant) => {
96
+ const { unmount } = render(
97
+ <Tabs variant={variant}>
98
+ <TabList>
99
+ <Tab>A</Tab>
100
+ </TabList>
101
+ <TabPanels>
102
+ <TabPanel>Panel</TabPanel>
103
+ </TabPanels>
104
+ </Tabs>
105
+ )
106
+ expect(screen.getByText('A')).toBeInTheDocument()
107
+ unmount()
108
+ })
109
+ })
110
+
111
+ it('renders with vertical orientation', () => {
112
+ const { container } = render(
113
+ <Tabs orientation="vertical">
114
+ <TabList>
115
+ <Tab>A</Tab>
116
+ </TabList>
117
+ <TabPanels>
118
+ <TabPanel>Panel</TabPanel>
119
+ </TabPanels>
120
+ </Tabs>
121
+ )
122
+ expect(container.firstChild).toBeInTheDocument()
123
+ })
124
+
125
+ it('disables individual tabs', () => {
126
+ render(
127
+ <Tabs>
128
+ <TabList>
129
+ <Tab>Enabled</Tab>
130
+ <Tab isDisabled>Disabled</Tab>
131
+ </TabList>
132
+ <TabPanels>
133
+ <TabPanel>Panel 1</TabPanel>
134
+ <TabPanel>Panel 2</TabPanel>
135
+ </TabPanels>
136
+ </Tabs>
137
+ )
138
+ const tabs = screen.getAllByRole('tab')
139
+ expect(tabs[1]).toHaveAttribute('aria-disabled', 'true')
140
+ })
141
+
142
+ it('does not switch to disabled tab on click', () => {
143
+ render(
144
+ <Tabs>
145
+ <TabList>
146
+ <Tab>A</Tab>
147
+ <Tab isDisabled>B</Tab>
148
+ </TabList>
149
+ <TabPanels>
150
+ <TabPanel>Panel A</TabPanel>
151
+ <TabPanel>Panel B</TabPanel>
152
+ </TabPanels>
153
+ </Tabs>
154
+ )
155
+ fireEvent.click(screen.getAllByRole('tab')[1])
156
+ expect(screen.getByText('Panel A')).toBeInTheDocument()
157
+ })
158
+
159
+ it('applies custom className to Tabs', () => {
160
+ const { container } = renderTabs({ className: 'extra' })
161
+ expect(container.firstChild).toBeInTheDocument()
162
+ })
163
+
164
+ describe('accessibility', () => {
165
+ it('has no accessibility violations', async () => {
166
+ const { container } = renderTabs()
167
+ // react-native-web does not render role="tablist" on the TabList container
168
+ // or aria-selected on tabs, so we disable related rules
169
+ const results = await axe(container, {
170
+ rules: {
171
+ 'aria-required-parent': { enabled: false },
172
+ 'aria-required-attr': { enabled: false },
173
+ },
174
+ })
175
+ expect(results).toHaveNoViolations()
176
+ })
177
+
178
+ it('has correct tab role on tabs', () => {
179
+ renderTabs()
180
+ const tabs = screen.getAllByRole('tab')
181
+ expect(tabs).toHaveLength(3)
182
+ })
183
+
184
+ it('marks active tab as selected', () => {
185
+ renderTabs()
186
+ const tabs = screen.getAllByRole('tab')
187
+ // react-native-web does not output aria-selected for accessibilityState.selected on tabs
188
+ expect(tabs[0]).toBeInTheDocument()
189
+ expect(tabs[1]).toBeInTheDocument()
190
+ })
191
+ })
192
+ })