@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,227 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import {
4
+ Sidebar,
5
+ SidebarHeader,
6
+ SidebarContent,
7
+ SidebarFooter,
8
+ SidebarSection,
9
+ SidebarItem,
10
+ SidebarDivider,
11
+ } from './Sidebar'
12
+
13
+ const meta: Meta<typeof Sidebar> = {
14
+ title: 'Custom/Sidebar',
15
+ component: Sidebar,
16
+ tags: ['autodocs'],
17
+ argTypes: {
18
+ isCollapsed: {
19
+ control: 'boolean',
20
+ description: 'Whether sidebar is collapsed (icon-only mode)',
21
+ },
22
+ activeItem: {
23
+ control: 'text',
24
+ description: 'ID of the currently active item',
25
+ },
26
+ width: {
27
+ control: 'number',
28
+ description: 'Width when expanded',
29
+ },
30
+ collapsedWidth: {
31
+ control: 'number',
32
+ description: 'Width when collapsed',
33
+ },
34
+ },
35
+ decorators: [
36
+ (Story) => (
37
+ <View style={{ height: 500 }}>
38
+ <Story />
39
+ </View>
40
+ ),
41
+ ],
42
+ }
43
+
44
+ export default meta
45
+ type Story = StoryObj<typeof Sidebar>
46
+
47
+ // Simple icon components for demos
48
+ function HomeIcon({ size = 20, className }: { size?: number; className?: string }) {
49
+ return <Text style={{ fontSize: size }} className={className}>&#8962;</Text>
50
+ }
51
+
52
+ function ChartIcon({ size = 20, className }: { size?: number; className?: string }) {
53
+ return <Text style={{ fontSize: size }} className={className}>&#9632;</Text>
54
+ }
55
+
56
+ function UserIcon({ size = 20, className }: { size?: number; className?: string }) {
57
+ return <Text style={{ fontSize: size }} className={className}>&#9786;</Text>
58
+ }
59
+
60
+ function GearIcon({ size = 20, className }: { size?: number; className?: string }) {
61
+ return <Text style={{ fontSize: size }} className={className}>&#9881;</Text>
62
+ }
63
+
64
+ function MailIcon({ size = 20, className }: { size?: number; className?: string }) {
65
+ return <Text style={{ fontSize: size }} className={className}>&#9993;</Text>
66
+ }
67
+
68
+ function FileIcon({ size = 20, className }: { size?: number; className?: string }) {
69
+ return <Text style={{ fontSize: size }} className={className}>&#128196;</Text>
70
+ }
71
+
72
+ export const Default: Story = {
73
+ args: {
74
+ activeItem: 'home',
75
+ },
76
+ render: (args) => (
77
+ <Sidebar {...args}>
78
+ <SidebarHeader>
79
+ <Text className="text-text-primary text-lg font-bold">Titan</Text>
80
+ </SidebarHeader>
81
+ <SidebarContent>
82
+ <SidebarSection>
83
+ <SidebarItem id="home" label="Home" />
84
+ <SidebarItem id="dashboard" label="Dashboard" />
85
+ <SidebarItem id="projects" label="Projects" />
86
+ <SidebarItem id="settings" label="Settings" />
87
+ </SidebarSection>
88
+ </SidebarContent>
89
+ </Sidebar>
90
+ ),
91
+ }
92
+
93
+ export const WithIcons: Story = {
94
+ args: {
95
+ activeItem: 'dashboard',
96
+ },
97
+ render: (args) => (
98
+ <Sidebar {...args}>
99
+ <SidebarHeader>
100
+ <Text className="text-text-primary text-lg font-bold">Titan</Text>
101
+ </SidebarHeader>
102
+ <SidebarContent>
103
+ <SidebarSection>
104
+ <SidebarItem id="home" icon={HomeIcon} label="Home" />
105
+ <SidebarItem id="dashboard" icon={ChartIcon} label="Dashboard" />
106
+ <SidebarItem id="users" icon={UserIcon} label="Users" />
107
+ <SidebarItem id="messages" icon={MailIcon} label="Messages" />
108
+ </SidebarSection>
109
+ </SidebarContent>
110
+ <SidebarFooter>
111
+ <SidebarItem id="settings" icon={GearIcon} label="Settings" />
112
+ </SidebarFooter>
113
+ </Sidebar>
114
+ ),
115
+ }
116
+
117
+ export const Collapsed: Story = {
118
+ args: {
119
+ isCollapsed: true,
120
+ activeItem: 'home',
121
+ },
122
+ render: (args) => (
123
+ <Sidebar {...args}>
124
+ <SidebarHeader>
125
+ <Text className="text-text-primary text-lg font-bold text-center">T</Text>
126
+ </SidebarHeader>
127
+ <SidebarContent>
128
+ <SidebarSection>
129
+ <SidebarItem id="home" icon={HomeIcon} label="Home" />
130
+ <SidebarItem id="dashboard" icon={ChartIcon} label="Dashboard" />
131
+ <SidebarItem id="users" icon={UserIcon} label="Users" />
132
+ <SidebarItem id="messages" icon={MailIcon} label="Messages" />
133
+ </SidebarSection>
134
+ </SidebarContent>
135
+ <SidebarFooter>
136
+ <SidebarItem id="settings" icon={GearIcon} label="Settings" />
137
+ </SidebarFooter>
138
+ </Sidebar>
139
+ ),
140
+ }
141
+
142
+ export const WithSections: Story = {
143
+ args: {
144
+ activeItem: 'dashboard',
145
+ },
146
+ render: (args) => (
147
+ <Sidebar {...args}>
148
+ <SidebarHeader>
149
+ <Text className="text-text-primary text-lg font-bold">Titan</Text>
150
+ </SidebarHeader>
151
+ <SidebarContent>
152
+ <SidebarSection title="Main">
153
+ <SidebarItem id="home" icon={HomeIcon} label="Home" />
154
+ <SidebarItem id="dashboard" icon={ChartIcon} label="Dashboard" />
155
+ </SidebarSection>
156
+ <SidebarDivider />
157
+ <SidebarSection title="Content">
158
+ <SidebarItem id="files" icon={FileIcon} label="Files" />
159
+ <SidebarItem id="messages" icon={MailIcon} label="Messages" />
160
+ </SidebarSection>
161
+ <SidebarDivider />
162
+ <SidebarSection title="Account">
163
+ <SidebarItem id="profile" icon={UserIcon} label="Profile" />
164
+ <SidebarItem id="settings" icon={GearIcon} label="Settings" />
165
+ </SidebarSection>
166
+ </SidebarContent>
167
+ </Sidebar>
168
+ ),
169
+ }
170
+
171
+ export const WithBadge: Story = {
172
+ args: {
173
+ activeItem: 'home',
174
+ },
175
+ render: (args) => (
176
+ <Sidebar {...args}>
177
+ <SidebarHeader>
178
+ <Text className="text-text-primary text-lg font-bold">Titan</Text>
179
+ </SidebarHeader>
180
+ <SidebarContent>
181
+ <SidebarSection>
182
+ <SidebarItem id="home" icon={HomeIcon} label="Home" />
183
+ <SidebarItem id="dashboard" icon={ChartIcon} label="Dashboard" />
184
+ <SidebarItem
185
+ id="messages"
186
+ icon={MailIcon}
187
+ label="Messages"
188
+ badge={
189
+ <View className="bg-status-error rounded-full px-1.5 py-0.5">
190
+ <Text className="text-white text-xs font-bold">5</Text>
191
+ </View>
192
+ }
193
+ />
194
+ <SidebarItem id="users" icon={UserIcon} label="Users" />
195
+ </SidebarSection>
196
+ </SidebarContent>
197
+ <SidebarFooter>
198
+ <SidebarItem id="settings" icon={GearIcon} label="Settings" />
199
+ </SidebarFooter>
200
+ </Sidebar>
201
+ ),
202
+ }
203
+
204
+ export const WithActiveItem: Story = {
205
+ render: () => (
206
+ <View style={{ flexDirection: 'row', gap: 16, height: 400 }}>
207
+ <Sidebar activeItem="home" width={200}>
208
+ <SidebarContent>
209
+ <SidebarSection>
210
+ <SidebarItem id="home" icon={HomeIcon} label="Home" />
211
+ <SidebarItem id="dashboard" icon={ChartIcon} label="Dashboard" />
212
+ <SidebarItem id="settings" icon={GearIcon} label="Settings" />
213
+ </SidebarSection>
214
+ </SidebarContent>
215
+ </Sidebar>
216
+ <Sidebar activeItem="dashboard" width={200}>
217
+ <SidebarContent>
218
+ <SidebarSection>
219
+ <SidebarItem id="home" icon={HomeIcon} label="Home" />
220
+ <SidebarItem id="dashboard" icon={ChartIcon} label="Dashboard" />
221
+ <SidebarItem id="settings" icon={GearIcon} label="Settings" />
222
+ </SidebarSection>
223
+ </SidebarContent>
224
+ </Sidebar>
225
+ </View>
226
+ ),
227
+ }
@@ -0,0 +1,185 @@
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 {
5
+ Sidebar,
6
+ SidebarHeader,
7
+ SidebarContent,
8
+ SidebarFooter,
9
+ SidebarSection,
10
+ SidebarItem,
11
+ SidebarDivider,
12
+ } from './Sidebar'
13
+
14
+ function MockIcon({ size, className }: { size?: number; className?: string }) {
15
+ return <span data-testid="mock-icon" data-size={size} className={className}>I</span>
16
+ }
17
+
18
+ function renderSidebar(props: Partial<React.ComponentProps<typeof Sidebar>> = {}) {
19
+ return render(
20
+ <Sidebar activeItem="home" onItemSelect={vi.fn()} {...props}>
21
+ <SidebarHeader>
22
+ <span>Logo</span>
23
+ </SidebarHeader>
24
+ <SidebarContent>
25
+ <SidebarSection title="Main">
26
+ <SidebarItem id="home" icon={MockIcon} label="Home" />
27
+ <SidebarItem id="dashboard" icon={MockIcon} label="Dashboard" />
28
+ </SidebarSection>
29
+ <SidebarDivider />
30
+ <SidebarSection title="Settings">
31
+ <SidebarItem id="settings" icon={MockIcon} label="Settings" />
32
+ </SidebarSection>
33
+ </SidebarContent>
34
+ <SidebarFooter>
35
+ <SidebarItem id="logout" label="Logout" />
36
+ </SidebarFooter>
37
+ </Sidebar>
38
+ )
39
+ }
40
+
41
+ describe('Sidebar', () => {
42
+ it('renders with all sections', () => {
43
+ renderSidebar()
44
+ expect(screen.getByText('Logo')).toBeInTheDocument()
45
+ expect(screen.getByText('Home')).toBeInTheDocument()
46
+ expect(screen.getByText('Dashboard')).toBeInTheDocument()
47
+ expect(screen.getAllByText('Settings').length).toBeGreaterThanOrEqual(1)
48
+ expect(screen.getByText('Logout')).toBeInTheDocument()
49
+ })
50
+
51
+ it('renders section titles', () => {
52
+ renderSidebar()
53
+ expect(screen.getByText('Main')).toBeInTheDocument()
54
+ })
55
+
56
+ it('hides section titles when collapsed', () => {
57
+ renderSidebar({ isCollapsed: true })
58
+ expect(screen.queryByText('Main')).not.toBeInTheDocument()
59
+ expect(screen.queryByText('Settings')).not.toBeInTheDocument()
60
+ })
61
+
62
+ it('hides item labels when collapsed', () => {
63
+ renderSidebar({ isCollapsed: true })
64
+ expect(screen.queryByText('Home')).not.toBeInTheDocument()
65
+ expect(screen.queryByText('Dashboard')).not.toBeInTheDocument()
66
+ })
67
+
68
+ it('shows item labels when expanded', () => {
69
+ renderSidebar({ isCollapsed: false })
70
+ expect(screen.getByText('Home')).toBeInTheDocument()
71
+ expect(screen.getByText('Dashboard')).toBeInTheDocument()
72
+ })
73
+
74
+ describe('SidebarItem', () => {
75
+ it('calls onItemSelect when item is pressed', () => {
76
+ const onItemSelect = vi.fn()
77
+ renderSidebar({ onItemSelect })
78
+
79
+ fireEvent.click(screen.getByLabelText('Dashboard'))
80
+ expect(onItemSelect).toHaveBeenCalledWith('dashboard')
81
+ })
82
+
83
+ it('highlights active item', () => {
84
+ renderSidebar({ activeItem: 'home' })
85
+ const homeItem = screen.getByLabelText('Home')
86
+ expect(homeItem).toBeInTheDocument()
87
+ })
88
+
89
+ it('renders icon', () => {
90
+ renderSidebar()
91
+ const icons = screen.getAllByTestId('mock-icon')
92
+ expect(icons.length).toBeGreaterThan(0)
93
+ })
94
+
95
+ it('renders badge content', () => {
96
+ render(
97
+ <Sidebar>
98
+ <SidebarContent>
99
+ <SidebarItem id="inbox" label="Inbox" badge={<span>5</span>} />
100
+ </SidebarContent>
101
+ </Sidebar>
102
+ )
103
+ expect(screen.getByText('5')).toBeInTheDocument()
104
+ })
105
+
106
+ it('renders expand indicator for items with children', () => {
107
+ render(
108
+ <Sidebar>
109
+ <SidebarContent>
110
+ <SidebarItem id="nav" label="Navigation" hasChildren isExpanded={false} />
111
+ </SidebarContent>
112
+ </Sidebar>
113
+ )
114
+ expect(screen.getByLabelText('Navigation')).toBeInTheDocument()
115
+ })
116
+
117
+ it('calls custom onPress handler alongside onItemSelect', () => {
118
+ const onItemSelect = vi.fn()
119
+ const onPress = vi.fn()
120
+ render(
121
+ <Sidebar onItemSelect={onItemSelect}>
122
+ <SidebarContent>
123
+ <SidebarItem id="custom" label="Custom" onPress={onPress} />
124
+ </SidebarContent>
125
+ </Sidebar>
126
+ )
127
+
128
+ fireEvent.click(screen.getByLabelText('Custom'))
129
+ expect(onItemSelect).toHaveBeenCalledWith('custom')
130
+ expect(onPress).toHaveBeenCalled()
131
+ })
132
+ })
133
+
134
+ describe('SidebarDivider', () => {
135
+ it('renders divider', () => {
136
+ renderSidebar()
137
+ // The divider renders as a View with a separator style, just verify the sidebar renders completely
138
+ expect(screen.getByText('Logout')).toBeInTheDocument()
139
+ })
140
+ })
141
+
142
+ describe('width props', () => {
143
+ it('uses default width', () => {
144
+ renderSidebar()
145
+ expect(screen.getByText('Logo')).toBeInTheDocument()
146
+ })
147
+
148
+ it('uses custom width', () => {
149
+ renderSidebar({ width: 300 })
150
+ expect(screen.getByText('Logo')).toBeInTheDocument()
151
+ })
152
+
153
+ it('uses collapsed width when collapsed', () => {
154
+ renderSidebar({ isCollapsed: true, collapsedWidth: 80 })
155
+ // Items still render but labels are hidden
156
+ expect(screen.queryByText('Home')).not.toBeInTheDocument()
157
+ })
158
+ })
159
+
160
+ describe('accessibility', () => {
161
+ it('has no accessibility violations', async () => {
162
+ const { container } = renderSidebar()
163
+ const results = await axe(container)
164
+ expect(results).toHaveNoViolations()
165
+ })
166
+
167
+ it('items have button role', () => {
168
+ renderSidebar()
169
+ const buttons = screen.getAllByRole('button')
170
+ expect(buttons.length).toBeGreaterThan(0)
171
+ })
172
+
173
+ it('items have accessible labels', () => {
174
+ renderSidebar()
175
+ expect(screen.getByLabelText('Home')).toBeInTheDocument()
176
+ expect(screen.getByLabelText('Dashboard')).toBeInTheDocument()
177
+ })
178
+
179
+ it('communicates selected state', () => {
180
+ renderSidebar({ activeItem: 'home' })
181
+ const homeButton = screen.getByLabelText('Home')
182
+ expect(homeButton).toBeInTheDocument()
183
+ })
184
+ })
185
+ })
@@ -0,0 +1,257 @@
1
+ import React, { createContext, useContext } from 'react'
2
+ import { View, Text, Pressable, ScrollView, type ViewProps, type PressableProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ interface SidebarContextType {
6
+ isCollapsed: boolean
7
+ activeItem?: string
8
+ onItemSelect?: (id: string) => void
9
+ }
10
+
11
+ const SidebarContext = createContext<SidebarContextType>({
12
+ isCollapsed: false,
13
+ })
14
+
15
+ export interface SidebarProps extends ViewProps {
16
+ /** Whether sidebar is collapsed (icon-only mode) */
17
+ isCollapsed?: boolean
18
+ /** Currently active item ID */
19
+ activeItem?: string
20
+ /** Callback when an item is selected */
21
+ onItemSelect?: (id: string) => void
22
+ /** Width when expanded */
23
+ width?: number
24
+ /** Width when collapsed */
25
+ collapsedWidth?: number
26
+ /** Additional className */
27
+ className?: string
28
+ children?: React.ReactNode
29
+ }
30
+
31
+ /**
32
+ * Sidebar navigation component.
33
+ *
34
+ * @example
35
+ * <Sidebar activeItem={activeNav} onItemSelect={setActiveNav}>
36
+ * <SidebarHeader>
37
+ * <Logo />
38
+ * </SidebarHeader>
39
+ * <SidebarContent>
40
+ * <SidebarSection title="Main">
41
+ * <SidebarItem id="home" icon={HomeIcon} label="Home" />
42
+ * <SidebarItem id="dashboard" icon={DashboardIcon} label="Dashboard" />
43
+ * </SidebarSection>
44
+ * </SidebarContent>
45
+ * <SidebarFooter>
46
+ * <SidebarItem id="settings" icon={SettingsIcon} label="Settings" />
47
+ * </SidebarFooter>
48
+ * </Sidebar>
49
+ */
50
+ export function Sidebar({
51
+ isCollapsed = false,
52
+ activeItem,
53
+ onItemSelect,
54
+ width = 256,
55
+ collapsedWidth = 64,
56
+ className,
57
+ children,
58
+ ...props
59
+ }: SidebarProps) {
60
+ const currentWidth = isCollapsed ? collapsedWidth : width
61
+
62
+ return (
63
+ <SidebarContext.Provider value={{ isCollapsed, activeItem, onItemSelect }}>
64
+ <View
65
+ style={{ width: currentWidth }}
66
+ className={cn(
67
+ 'h-full flex-col bg-surface-base border-r border-border-default transition-all duration-200',
68
+ className
69
+ )}
70
+ {...props}
71
+ >
72
+ {children}
73
+ </View>
74
+ </SidebarContext.Provider>
75
+ )
76
+ }
77
+
78
+ export interface SidebarHeaderProps {
79
+ children?: React.ReactNode
80
+ className?: string
81
+ }
82
+
83
+ /**
84
+ * Header section of sidebar (typically contains logo).
85
+ */
86
+ export function SidebarHeader({ children, className }: SidebarHeaderProps) {
87
+ return (
88
+ <View className={cn('px-4 py-4 border-b border-border-subtle', className)}>
89
+ {children}
90
+ </View>
91
+ )
92
+ }
93
+
94
+ export interface SidebarContentProps {
95
+ children?: React.ReactNode
96
+ className?: string
97
+ }
98
+
99
+ /**
100
+ * Scrollable content area of sidebar.
101
+ */
102
+ export function SidebarContent({ children, className }: SidebarContentProps) {
103
+ return (
104
+ <ScrollView
105
+ className={cn('flex-1', className)}
106
+ showsVerticalScrollIndicator={false}
107
+ >
108
+ {children}
109
+ </ScrollView>
110
+ )
111
+ }
112
+
113
+ export interface SidebarFooterProps {
114
+ children?: React.ReactNode
115
+ className?: string
116
+ }
117
+
118
+ /**
119
+ * Footer section of sidebar.
120
+ */
121
+ export function SidebarFooter({ children, className }: SidebarFooterProps) {
122
+ return (
123
+ <View className={cn('px-2 py-4 border-t border-border-subtle', className)}>
124
+ {children}
125
+ </View>
126
+ )
127
+ }
128
+
129
+ export interface SidebarSectionProps {
130
+ /** Section title */
131
+ title?: string
132
+ children?: React.ReactNode
133
+ className?: string
134
+ }
135
+
136
+ /**
137
+ * Section grouping for sidebar items.
138
+ */
139
+ export function SidebarSection({ title, children, className }: SidebarSectionProps) {
140
+ const { isCollapsed } = useContext(SidebarContext)
141
+
142
+ return (
143
+ <View className={cn('px-2 py-2', className)}>
144
+ {title && !isCollapsed && (
145
+ <Text className="px-3 py-2 text-xs font-semibold uppercase tracking-wider text-text-tertiary">
146
+ {title}
147
+ </Text>
148
+ )}
149
+ <View className="gap-0.5">
150
+ {children}
151
+ </View>
152
+ </View>
153
+ )
154
+ }
155
+
156
+ export interface SidebarItemProps extends Omit<PressableProps, 'children'> {
157
+ /** Unique identifier for the item */
158
+ id: string
159
+ /** Icon component */
160
+ icon?: React.ComponentType<{ size?: number; className?: string }>
161
+ /** Label text */
162
+ label: string
163
+ /** Badge content (e.g., notification count) */
164
+ badge?: React.ReactNode
165
+ /** Whether this item has nested items */
166
+ hasChildren?: boolean
167
+ /** Whether nested items are expanded */
168
+ isExpanded?: boolean
169
+ /** Additional className */
170
+ className?: string
171
+ }
172
+
173
+ /**
174
+ * Individual navigation item in sidebar.
175
+ */
176
+ export function SidebarItem({
177
+ id,
178
+ icon: Icon,
179
+ label,
180
+ badge,
181
+ hasChildren,
182
+ isExpanded,
183
+ className,
184
+ onPress,
185
+ ...props
186
+ }: SidebarItemProps) {
187
+ const { isCollapsed, activeItem, onItemSelect } = useContext(SidebarContext)
188
+ const isActive = activeItem === id
189
+
190
+ const handlePress = (e: any) => {
191
+ onItemSelect?.(id)
192
+ onPress?.(e)
193
+ }
194
+
195
+ return (
196
+ <Pressable
197
+ accessibilityRole="button"
198
+ accessibilityState={{ selected: isActive }}
199
+ accessibilityLabel={label}
200
+ onPress={handlePress}
201
+ className={cn(
202
+ 'flex-row items-center gap-3 px-3 py-2.5 rounded-md transition-colors',
203
+ isActive
204
+ ? 'bg-brand-primary-subtle text-brand-primary'
205
+ : 'text-text-secondary web:hover:bg-interactive-hover active:bg-interactive-active',
206
+ isCollapsed && 'justify-center px-2',
207
+ className
208
+ )}
209
+ {...props}
210
+ >
211
+ {Icon && (
212
+ <Icon
213
+ size={20}
214
+ className={cn(
215
+ isActive ? 'text-brand-primary' : 'text-text-secondary'
216
+ )}
217
+ />
218
+ )}
219
+ {!isCollapsed && (
220
+ <>
221
+ <Text
222
+ numberOfLines={1}
223
+ className={cn(
224
+ 'flex-1 text-sm font-medium',
225
+ isActive ? 'text-brand-primary' : 'text-text-primary'
226
+ )}
227
+ >
228
+ {label}
229
+ </Text>
230
+ {badge && (
231
+ <View className="ml-auto">
232
+ {badge}
233
+ </View>
234
+ )}
235
+ {hasChildren && (
236
+ <Text className="text-text-tertiary ml-1">
237
+ {isExpanded ? '▼' : '▶'}
238
+ </Text>
239
+ )}
240
+ </>
241
+ )}
242
+ </Pressable>
243
+ )
244
+ }
245
+
246
+ export interface SidebarDividerProps {
247
+ className?: string
248
+ }
249
+
250
+ /**
251
+ * Divider between sidebar sections.
252
+ */
253
+ export function SidebarDivider({ className }: SidebarDividerProps) {
254
+ return (
255
+ <View className={cn('h-px mx-4 my-2 bg-border-subtle', className)} />
256
+ )
257
+ }
@@ -0,0 +1,18 @@
1
+ export {
2
+ Sidebar,
3
+ SidebarHeader,
4
+ SidebarContent,
5
+ SidebarFooter,
6
+ SidebarSection,
7
+ SidebarItem,
8
+ SidebarDivider,
9
+ } from './Sidebar'
10
+ export type {
11
+ SidebarProps,
12
+ SidebarHeaderProps,
13
+ SidebarContentProps,
14
+ SidebarFooterProps,
15
+ SidebarSectionProps,
16
+ SidebarItemProps,
17
+ SidebarDividerProps,
18
+ } from './Sidebar'