@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,415 @@
1
+ import React, { useState } from 'react'
2
+ import type { Meta, StoryObj } from '@storybook/react-vite'
3
+ import { View, Text } from 'react-native'
4
+ import { ToolbarButton, ToolbarButtonGroup } from './ToolbarButton'
5
+
6
+ const meta: Meta<typeof ToolbarButton> = {
7
+ title: 'Components/ToolbarButton',
8
+ component: ToolbarButton,
9
+ tags: ['autodocs'],
10
+ argTypes: {
11
+ variant: {
12
+ control: 'select',
13
+ options: ['default', 'neumorphic'],
14
+ },
15
+ isActive: {
16
+ control: 'select',
17
+ options: [undefined, true, false],
18
+ description: 'undefined/true = pressed look, false = raised look',
19
+ },
20
+ isDisabled: {
21
+ control: 'boolean',
22
+ },
23
+ showLabel: {
24
+ control: 'boolean',
25
+ },
26
+ },
27
+ decorators: [
28
+ (Story) => (
29
+ <View className="bg-surface-elevated p-6 rounded-lg">
30
+ <Story />
31
+ </View>
32
+ ),
33
+ ],
34
+ }
35
+
36
+ export default meta
37
+ type Story = StoryObj<typeof ToolbarButton>
38
+
39
+ // Icon components matching MUI icon sizing (20px for fontSize="small")
40
+ // These simulate MUI SvgIcon components at 20x20
41
+ const SettingsIcon = ({ color }: { color?: string }) => (
42
+ <View style={{ width: 20, height: 20, alignItems: 'center', justifyContent: 'center' }}>
43
+ <Text className={color ? undefined : 'text-text-secondary'} style={{ fontSize: 16, ...(color ? { color } : {}) }}>⚙</Text>
44
+ </View>
45
+ )
46
+
47
+ const FilterIcon = ({ color }: { color?: string }) => (
48
+ <View style={{ width: 20, height: 20, alignItems: 'center', justifyContent: 'center' }}>
49
+ <Text className={color ? undefined : 'text-text-secondary'} style={{ fontSize: 16, ...(color ? { color } : {}) }}>⏣</Text>
50
+ </View>
51
+ )
52
+
53
+ const ViewIcon = ({ color }: { color?: string }) => (
54
+ <View style={{ width: 20, height: 20, alignItems: 'center', justifyContent: 'center' }}>
55
+ <Text className={color ? undefined : 'text-text-secondary'} style={{ fontSize: 16, ...(color ? { color } : {}) }}>◉</Text>
56
+ </View>
57
+ )
58
+
59
+ export const Default: Story = {
60
+ args: {
61
+ label: 'Settings',
62
+ icon: <SettingsIcon />,
63
+ variant: 'neumorphic',
64
+ showLabel: true,
65
+ },
66
+ }
67
+
68
+ export const ActiveVsInactive: Story = {
69
+ render: () => (
70
+ <View style={{ gap: 16 }}>
71
+ <View>
72
+ <Text className="text-text-secondary text-xs mb-1">
73
+ isActive=true - sunken/dark look, ORANGE icon, white text
74
+ </Text>
75
+ <ToolbarButton
76
+ label="Active (orange icon)"
77
+ icon={<FilterIcon />}
78
+ isActive={true}
79
+ />
80
+ </View>
81
+ <View>
82
+ <Text className="text-text-secondary text-xs mb-1">
83
+ isActive=undefined - sunken/dark look, white icon (default state)
84
+ </Text>
85
+ <ToolbarButton
86
+ label="Default (white icon)"
87
+ icon={<FilterIcon />}
88
+ />
89
+ </View>
90
+ <View>
91
+ <Text className="text-text-secondary text-xs mb-1">
92
+ isActive=false - elevated/light look, gray icon
93
+ </Text>
94
+ <ToolbarButton
95
+ label="Inactive (gray icon)"
96
+ icon={<FilterIcon />}
97
+ isActive={false}
98
+ />
99
+ </View>
100
+ </View>
101
+ ),
102
+ }
103
+
104
+ export const Interactive: Story = {
105
+ render: function Render() {
106
+ const [active, setActive] = useState(false)
107
+ return (
108
+ <View style={{ gap: 16 }}>
109
+ <Text className="text-text-secondary text-xs">
110
+ Click to toggle. When active=true, icon turns orange:
111
+ </Text>
112
+ <ToolbarButton
113
+ label="Toggle Me"
114
+ icon={<SettingsIcon />}
115
+ isActive={active}
116
+ onPress={() => setActive(!active)}
117
+ />
118
+ <Text className="text-text-secondary text-xs">
119
+ isActive: {active ? 'true (sunken, orange icon)' : 'false (elevated, gray icon)'}
120
+ </Text>
121
+ </View>
122
+ )
123
+ },
124
+ }
125
+
126
+ export const IconOnly: Story = {
127
+ render: () => (
128
+ <View style={{ gap: 16 }}>
129
+ <Text className="text-text-secondary text-xs">
130
+ Icon-only buttons. Active button has orange icon:
131
+ </Text>
132
+ <View style={{ flexDirection: 'row', gap: 8 }}>
133
+ <ToolbarButton
134
+ label="Settings"
135
+ icon={<SettingsIcon />}
136
+ showLabel={false}
137
+ isActive={false}
138
+ />
139
+ <ToolbarButton
140
+ label="Filter (active)"
141
+ icon={<FilterIcon />}
142
+ showLabel={false}
143
+ isActive={true}
144
+ />
145
+ <ToolbarButton
146
+ label="View"
147
+ icon={<ViewIcon />}
148
+ showLabel={false}
149
+ isActive={false}
150
+ />
151
+ </View>
152
+ </View>
153
+ ),
154
+ }
155
+
156
+ export const Disabled: Story = {
157
+ render: () => (
158
+ <View style={{ gap: 16 }}>
159
+ <Text className="text-text-secondary text-xs">
160
+ Disabled buttons use MUI&apos;s default dark theme styling.
161
+ </Text>
162
+ <Text className="text-text-secondary text-[11px]">
163
+ Background: rgba(255,255,255,0.12) | No shadows | Colors preserved with opacity
164
+ </Text>
165
+ <Text className="text-text-secondary text-[11px]">
166
+ Note: Orange icon IS shown when isActive=true (matching original XP behavior)
167
+ </Text>
168
+ <View style={{ flexDirection: 'row', gap: 8 }}>
169
+ <ToolbarButton
170
+ label="Disabled Active"
171
+ icon={<SettingsIcon />}
172
+ isActive={true}
173
+ isDisabled={true}
174
+ />
175
+ <ToolbarButton
176
+ label="Disabled Inactive"
177
+ icon={<SettingsIcon />}
178
+ isActive={false}
179
+ isDisabled={true}
180
+ />
181
+ </View>
182
+ </View>
183
+ ),
184
+ }
185
+
186
+ export const HoverStates: Story = {
187
+ render: () => (
188
+ <View style={{ gap: 16 }}>
189
+ <Text className="text-text-secondary text-xs">
190
+ Hover over buttons to see the hover effect (background lightens, shadows removed)
191
+ </Text>
192
+ <Text className="text-text-secondary text-[11px]">
193
+ Active: #3C3C3C → #4C4C4C | Inactive: #6E6E6E → #5D5D5D | Shadows: removed
194
+ </Text>
195
+ <View style={{ flexDirection: 'row', gap: 8 }}>
196
+ <ToolbarButton
197
+ label="Hover Me (Active)"
198
+ icon={<SettingsIcon />}
199
+ isActive={true}
200
+ />
201
+ <ToolbarButton
202
+ label="Hover Me (Inactive)"
203
+ icon={<SettingsIcon />}
204
+ isActive={false}
205
+ />
206
+ </View>
207
+ </View>
208
+ ),
209
+ }
210
+
211
+ export const WithMenu: Story = {
212
+ render: function Render() {
213
+ return (
214
+ <ToolbarButton
215
+ label="View Options"
216
+ icon={<ViewIcon />}
217
+ isActive={false}
218
+ menuContent={
219
+ <View style={{ paddingVertical: 8 }}>
220
+ <Text className="text-text-primary" style={{ paddingHorizontal: 16, paddingVertical: 8 }}>Grid View</Text>
221
+ <Text className="text-text-primary" style={{ paddingHorizontal: 16, paddingVertical: 8 }}>List View</Text>
222
+ <Text className="text-text-primary" style={{ paddingHorizontal: 16, paddingVertical: 8 }}>Compact View</Text>
223
+ </View>
224
+ }
225
+ />
226
+ )
227
+ },
228
+ }
229
+
230
+ export const ButtonGroup: Story = {
231
+ render: function Render() {
232
+ const [activeIndex, setActiveIndex] = useState<number | null>(1)
233
+
234
+ return (
235
+ <View style={{ gap: 16 }}>
236
+ <Text className="text-text-secondary text-xs mb-1">
237
+ Click to select (one active at a time):
238
+ </Text>
239
+ <ToolbarButtonGroup gap="sm">
240
+ <ToolbarButton
241
+ label="Filter"
242
+ icon={<FilterIcon />}
243
+ isActive={activeIndex === 0}
244
+ onPress={() => setActiveIndex(activeIndex === 0 ? null : 0)}
245
+ />
246
+ <ToolbarButton
247
+ label="Settings"
248
+ icon={<SettingsIcon />}
249
+ isActive={activeIndex === 1}
250
+ onPress={() => setActiveIndex(activeIndex === 1 ? null : 1)}
251
+ />
252
+ <ToolbarButton
253
+ label="View"
254
+ icon={<ViewIcon />}
255
+ isActive={activeIndex === 2}
256
+ onPress={() => setActiveIndex(activeIndex === 2 ? null : 2)}
257
+ />
258
+ </ToolbarButtonGroup>
259
+ </View>
260
+ )
261
+ },
262
+ }
263
+
264
+ export const ToolbarExample: Story = {
265
+ render: function Render() {
266
+ const [filterActive, setFilterActive] = useState(true)
267
+ const [settingsActive, setSettingsActive] = useState(false)
268
+
269
+ return (
270
+ <View>
271
+ <Text className="text-text-secondary text-xs mb-3">
272
+ Typical toolbar usage - filters active, settings inactive:
273
+ </Text>
274
+ <ToolbarButtonGroup gap="sm">
275
+ <ToolbarButton
276
+ label="Filters"
277
+ icon={<FilterIcon />}
278
+ isActive={filterActive}
279
+ onPress={() => setFilterActive(!filterActive)}
280
+ />
281
+ <ToolbarButton
282
+ label="Settings"
283
+ icon={<SettingsIcon />}
284
+ isActive={settingsActive}
285
+ onPress={() => setSettingsActive(!settingsActive)}
286
+ />
287
+ <ToolbarButton
288
+ label="View"
289
+ icon={<ViewIcon />}
290
+ isActive={false}
291
+ menuContent={
292
+ <View style={{ paddingVertical: 8 }}>
293
+ <Text className="text-text-primary" style={{ paddingHorizontal: 16, paddingVertical: 8 }}>Grid</Text>
294
+ <Text className="text-text-primary" style={{ paddingHorizontal: 16, paddingVertical: 8 }}>List</Text>
295
+ </View>
296
+ }
297
+ />
298
+ </ToolbarButtonGroup>
299
+ </View>
300
+ )
301
+ },
302
+ }
303
+
304
+ export const CompactMode: Story = {
305
+ render: function Render() {
306
+ const [isCompact, setIsCompact] = useState(false)
307
+
308
+ return (
309
+ <View style={{ gap: 16 }}>
310
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
311
+ <Text className="text-text-secondary text-xs">
312
+ Mode: {isCompact ? 'Compact (icon only)' : 'Full (icon + label)'}
313
+ </Text>
314
+ <ToolbarButton
315
+ label="Toggle"
316
+ icon={<ViewIcon />}
317
+ showLabel={false}
318
+ isActive={isCompact}
319
+ onPress={() => setIsCompact(!isCompact)}
320
+ />
321
+ </View>
322
+ <View className="h-px bg-border-default" />
323
+ <ToolbarButtonGroup gap="sm">
324
+ <ToolbarButton
325
+ label="Filter"
326
+ icon={<FilterIcon />}
327
+ showLabel={!isCompact}
328
+ isActive={true}
329
+ />
330
+ <ToolbarButton
331
+ label="Settings"
332
+ icon={<SettingsIcon />}
333
+ showLabel={!isCompact}
334
+ isActive={false}
335
+ />
336
+ <ToolbarButton
337
+ label="View"
338
+ icon={<ViewIcon />}
339
+ showLabel={!isCompact}
340
+ isActive={false}
341
+ />
342
+ </ToolbarButtonGroup>
343
+ </View>
344
+ )
345
+ },
346
+ }
347
+
348
+ export const Variants: Story = {
349
+ render: () => (
350
+ <View style={{ gap: 24 }}>
351
+ <View>
352
+ <Text className="text-text-secondary text-xs mb-2">Neumorphic (with 3D inset shadows)</Text>
353
+ <View style={{ flexDirection: 'row', gap: 8 }}>
354
+ <ToolbarButton label="Inactive" icon={<SettingsIcon />} variant="neumorphic" isActive={false} />
355
+ <ToolbarButton label="Active" icon={<SettingsIcon />} variant="neumorphic" isActive={true} />
356
+ </View>
357
+ </View>
358
+ <View>
359
+ <Text className="text-text-secondary text-xs mb-2">Flat (no shadows)</Text>
360
+ <View style={{ flexDirection: 'row', gap: 8 }}>
361
+ <ToolbarButton label="Inactive" icon={<SettingsIcon />} variant="default" isActive={false} />
362
+ <ToolbarButton label="Active" icon={<SettingsIcon />} variant="default" isActive={true} />
363
+ </View>
364
+ </View>
365
+ </View>
366
+ ),
367
+ }
368
+
369
+ export const CSSReference: Story = {
370
+ render: () => (
371
+ <View style={{ gap: 16 }}>
372
+ <Text className="text-text-secondary text-xs">
373
+ Computed CSS properties (derived from original SettingsButton):
374
+ </Text>
375
+ <View className="bg-surface-base p-4 rounded-lg">
376
+ <Text className="text-text-secondary text-[11px] font-mono">
377
+ {`/* Button base */
378
+ padding: 6px 16px;
379
+ border-radius: 4px;
380
+ display: flex;
381
+ align-items: center;
382
+
383
+ /* Active (isActive !== false) - sunken look */
384
+ background-color: #3C3C3C;
385
+ box-shadow: inset -1px -1px 6px #3C3C3C,
386
+ inset 1px 2px 1px #6E6E6E,
387
+ 1px 1px 1px #6E6E6E;
388
+ color: #FFFFFF;
389
+
390
+ /* Inactive (isActive === false) - elevated look */
391
+ background-color: #6E6E6E;
392
+ box-shadow: inset 2px 2px 2px #1F1F1F,
393
+ inset -1px -1px 2px #6E6E6E;
394
+ color: #D1D1D1;
395
+
396
+ /* Text (Typography subtitle2 + bold) */
397
+ font-size: 14px;
398
+ font-weight: 700;
399
+ font-family: Nunito Sans;
400
+ line-height: 1.57;
401
+
402
+ /* Icon */
403
+ width: 20px;
404
+ height: 20px;
405
+ margin-right: 8px; /* when label shown */
406
+ color: #FF7900; /* when isActive=true */`}
407
+ </Text>
408
+ </View>
409
+ <View style={{ flexDirection: 'row', gap: 8 }}>
410
+ <ToolbarButton label="Inactive" icon={<SettingsIcon />} isActive={false} />
411
+ <ToolbarButton label="Active" icon={<SettingsIcon />} isActive={true} />
412
+ </View>
413
+ </View>
414
+ ),
415
+ }
@@ -0,0 +1,224 @@
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 { ToolbarButton, ToolbarButtonGroup } from './ToolbarButton'
5
+
6
+ describe('ToolbarButton', () => {
7
+ it('renders with label', () => {
8
+ render(<ToolbarButton label="Settings" />)
9
+ expect(screen.getByRole('button', { name: 'Settings' })).toBeInTheDocument()
10
+ expect(screen.getByText('Settings')).toBeInTheDocument()
11
+ })
12
+
13
+ it('renders icon when provided', () => {
14
+ render(
15
+ <ToolbarButton
16
+ label="Settings"
17
+ icon={<span data-testid="icon">gear</span>}
18
+ />
19
+ )
20
+ expect(screen.getByTestId('icon')).toBeInTheDocument()
21
+ })
22
+
23
+ it('hides label when showLabel is false', () => {
24
+ render(<ToolbarButton label="Settings" showLabel={false} />)
25
+ expect(screen.getByRole('button', { name: 'Settings' })).toBeInTheDocument()
26
+ expect(screen.queryByText('Settings')).not.toBeInTheDocument()
27
+ })
28
+
29
+ describe('press handling', () => {
30
+ it('calls onPress when clicked', () => {
31
+ const onPress = vi.fn()
32
+ render(<ToolbarButton label="Action" onPress={onPress} />)
33
+
34
+ fireEvent.click(screen.getByRole('button', { name: 'Action' }))
35
+ expect(onPress).toHaveBeenCalledTimes(1)
36
+ })
37
+
38
+ it('does not call onPress when disabled', () => {
39
+ const onPress = vi.fn()
40
+ render(<ToolbarButton label="Disabled" isDisabled onPress={onPress} />)
41
+
42
+ fireEvent.click(screen.getByRole('button', { name: 'Disabled' }))
43
+ expect(onPress).not.toHaveBeenCalled()
44
+ })
45
+ })
46
+
47
+ describe('active state', () => {
48
+ it('communicates selected state when isActive is true', () => {
49
+ render(<ToolbarButton label="Active" isActive />)
50
+ const button = screen.getByRole('button', { name: 'Active' })
51
+ expect(button).toBeInTheDocument()
52
+ })
53
+
54
+ it('communicates unselected state when isActive is false', () => {
55
+ render(<ToolbarButton label="Inactive" isActive={false} />)
56
+ const button = screen.getByRole('button', { name: 'Inactive' })
57
+ expect(button).toBeInTheDocument()
58
+ })
59
+
60
+ it('renders without explicit active state', () => {
61
+ render(<ToolbarButton label="Default" />)
62
+ expect(screen.getByRole('button', { name: 'Default' })).toBeInTheDocument()
63
+ })
64
+ })
65
+
66
+ describe('disabled state', () => {
67
+ it('renders as disabled when isDisabled is true', () => {
68
+ render(<ToolbarButton label="Disabled" isDisabled />)
69
+ const button = screen.getByRole('button', { name: 'Disabled' })
70
+ expect(button).toBeDisabled()
71
+ })
72
+
73
+ it('communicates disabled state to assistive technology', () => {
74
+ render(<ToolbarButton label="Disabled" isDisabled />)
75
+ const button = screen.getByRole('button', { name: 'Disabled' })
76
+ expect(button).toHaveAttribute('aria-disabled', 'true')
77
+ })
78
+ })
79
+
80
+ describe('sizes', () => {
81
+ it('renders with sm size', () => {
82
+ render(<ToolbarButton label="Small" size="sm" />)
83
+ expect(screen.getByRole('button', { name: 'Small' })).toBeInTheDocument()
84
+ })
85
+
86
+ it('renders with md size (default)', () => {
87
+ render(<ToolbarButton label="Medium" size="md" />)
88
+ expect(screen.getByRole('button', { name: 'Medium' })).toBeInTheDocument()
89
+ })
90
+
91
+ it('renders with lg size', () => {
92
+ render(<ToolbarButton label="Large" size="lg" />)
93
+ expect(screen.getByRole('button', { name: 'Large' })).toBeInTheDocument()
94
+ })
95
+ })
96
+
97
+ describe('variants', () => {
98
+ it('renders with neumorphic variant (default)', () => {
99
+ render(<ToolbarButton label="Neumorphic" variant="neumorphic" />)
100
+ expect(screen.getByRole('button', { name: 'Neumorphic' })).toBeInTheDocument()
101
+ })
102
+
103
+ it('renders with default variant', () => {
104
+ render(<ToolbarButton label="Default" variant="default" />)
105
+ expect(screen.getByRole('button', { name: 'Default' })).toBeInTheDocument()
106
+ })
107
+ })
108
+
109
+ describe('menu content', () => {
110
+ it('shows menu when pressed with menuContent', () => {
111
+ render(
112
+ <ToolbarButton
113
+ label="Options"
114
+ menuContent={<span>Menu item</span>}
115
+ />
116
+ )
117
+
118
+ fireEvent.click(screen.getByRole('button', { name: 'Options' }))
119
+ expect(screen.getByText('Menu item')).toBeInTheDocument()
120
+ })
121
+
122
+ it('hides menu when pressed again', () => {
123
+ render(
124
+ <ToolbarButton
125
+ label="Options"
126
+ menuContent={<span>Menu item</span>}
127
+ />
128
+ )
129
+
130
+ const button = screen.getByRole('button', { name: 'Options' })
131
+ fireEvent.click(button)
132
+ expect(screen.getByText('Menu item')).toBeInTheDocument()
133
+
134
+ fireEvent.click(button)
135
+ expect(screen.queryByText('Menu item')).not.toBeInTheDocument()
136
+ })
137
+ })
138
+
139
+ describe('accessibility', () => {
140
+ it('has no accessibility violations', async () => {
141
+ const { container } = render(<ToolbarButton label="Accessible Button" />)
142
+ const results = await axe(container)
143
+ expect(results).toHaveNoViolations()
144
+ })
145
+
146
+ it('has correct accessibility label', () => {
147
+ render(<ToolbarButton label="Settings" />)
148
+ expect(screen.getByLabelText('Settings')).toBeInTheDocument()
149
+ })
150
+
151
+ it('applies tooltip as accessibility hint', () => {
152
+ render(<ToolbarButton label="Help" tooltip="Show help panel" />)
153
+ const button = screen.getByRole('button', { name: 'Help' })
154
+ expect(button).toBeInTheDocument()
155
+ })
156
+ })
157
+ })
158
+
159
+ describe('ToolbarButtonGroup', () => {
160
+ it('renders children', () => {
161
+ render(
162
+ <ToolbarButtonGroup>
163
+ <ToolbarButton label="One" />
164
+ <ToolbarButton label="Two" />
165
+ </ToolbarButtonGroup>
166
+ )
167
+ expect(screen.getByText('One')).toBeInTheDocument()
168
+ expect(screen.getByText('Two')).toBeInTheDocument()
169
+ })
170
+
171
+ it('has toolbar accessibility role', () => {
172
+ render(
173
+ <ToolbarButtonGroup>
174
+ <ToolbarButton label="Action" />
175
+ </ToolbarButtonGroup>
176
+ )
177
+ expect(screen.getByRole('toolbar')).toBeInTheDocument()
178
+ })
179
+
180
+ it('renders with different orientations', () => {
181
+ const { rerender } = render(
182
+ <ToolbarButtonGroup orientation="horizontal">
183
+ <ToolbarButton label="H" />
184
+ </ToolbarButtonGroup>
185
+ )
186
+ expect(screen.getByRole('toolbar')).toBeInTheDocument()
187
+
188
+ rerender(
189
+ <ToolbarButtonGroup orientation="vertical">
190
+ <ToolbarButton label="V" />
191
+ </ToolbarButtonGroup>
192
+ )
193
+ expect(screen.getByRole('toolbar')).toBeInTheDocument()
194
+ })
195
+
196
+ it('renders with different gap sizes', () => {
197
+ const { rerender } = render(
198
+ <ToolbarButtonGroup gap="none">
199
+ <ToolbarButton label="A" />
200
+ </ToolbarButtonGroup>
201
+ )
202
+ expect(screen.getByRole('toolbar')).toBeInTheDocument()
203
+
204
+ rerender(
205
+ <ToolbarButtonGroup gap="md">
206
+ <ToolbarButton label="A" />
207
+ </ToolbarButtonGroup>
208
+ )
209
+ expect(screen.getByRole('toolbar')).toBeInTheDocument()
210
+ })
211
+
212
+ describe('accessibility', () => {
213
+ it('has no accessibility violations', async () => {
214
+ const { container } = render(
215
+ <ToolbarButtonGroup>
216
+ <ToolbarButton label="Bold" />
217
+ <ToolbarButton label="Italic" />
218
+ </ToolbarButtonGroup>
219
+ )
220
+ const results = await axe(container)
221
+ expect(results).toHaveNoViolations()
222
+ })
223
+ })
224
+ })