@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,145 @@
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 { RestTimer } from './RestTimer'
5
+
6
+ const defaultProps = {
7
+ totalSeconds: 150,
8
+ elapsedMs: 30000,
9
+ onSkip: vi.fn(),
10
+ onAddTime: vi.fn(),
11
+ visible: true,
12
+ }
13
+
14
+ describe('RestTimer', () => {
15
+ describe('time display formatting', () => {
16
+ it('displays 2:30 for 150s total with 0 elapsed', () => {
17
+ render(<RestTimer {...defaultProps} elapsedMs={0} />)
18
+ expect(screen.getByTestId('rest-timer-time')).toHaveTextContent('2:30')
19
+ })
20
+
21
+ it('displays 2:00 for 150s total with 30s elapsed', () => {
22
+ render(<RestTimer {...defaultProps} elapsedMs={30000} />)
23
+ expect(screen.getByTestId('rest-timer-time')).toHaveTextContent('2:00')
24
+ })
25
+
26
+ it('displays 0:05 for 150s total with 145s elapsed', () => {
27
+ render(<RestTimer {...defaultProps} elapsedMs={145000} />)
28
+ expect(screen.getByTestId('rest-timer-time')).toHaveTextContent('0:05')
29
+ })
30
+
31
+ it('pads seconds with leading zero', () => {
32
+ render(<RestTimer {...defaultProps} totalSeconds={65} elapsedMs={0} />)
33
+ expect(screen.getByTestId('rest-timer-time')).toHaveTextContent('1:05')
34
+ })
35
+
36
+ it('displays 0:00 when elapsed equals total', () => {
37
+ render(<RestTimer {...defaultProps} elapsedMs={150000} />)
38
+ expect(screen.getByTestId('rest-timer-time')).toHaveTextContent('0:00')
39
+ })
40
+ })
41
+
42
+ describe('progress bar', () => {
43
+ it('sets width to 0% when elapsed is 0', () => {
44
+ render(<RestTimer {...defaultProps} elapsedMs={0} />)
45
+ const fill = screen.getByTestId('rest-timer-progress-fill')
46
+ expect(fill).toHaveStyle({ width: '0%' })
47
+ })
48
+
49
+ it('sets width to 50% when half elapsed', () => {
50
+ render(<RestTimer {...defaultProps} totalSeconds={100} elapsedMs={50000} />)
51
+ const fill = screen.getByTestId('rest-timer-progress-fill')
52
+ expect(fill).toHaveStyle({ width: '50%' })
53
+ })
54
+
55
+ it('clamps width to 100% when elapsed exceeds total', () => {
56
+ render(<RestTimer {...defaultProps} elapsedMs={200000} />)
57
+ const fill = screen.getByTestId('rest-timer-progress-fill')
58
+ expect(fill).toHaveStyle({ width: '100%' })
59
+ })
60
+ })
61
+
62
+ describe('button interactions', () => {
63
+ it('fires onSkip when skip button is pressed', () => {
64
+ const onSkip = vi.fn()
65
+ render(<RestTimer {...defaultProps} onSkip={onSkip} />)
66
+ fireEvent.click(screen.getByTestId('rest-timer-skip'))
67
+ expect(onSkip).toHaveBeenCalledOnce()
68
+ })
69
+
70
+ it('fires onAddTime when +30s button is pressed', () => {
71
+ const onAddTime = vi.fn()
72
+ render(<RestTimer {...defaultProps} onAddTime={onAddTime} />)
73
+ fireEvent.click(screen.getByTestId('rest-timer-add-time'))
74
+ expect(onAddTime).toHaveBeenCalledOnce()
75
+ })
76
+ })
77
+
78
+ describe('next set info', () => {
79
+ it('displays nextSetInfo when provided', () => {
80
+ render(
81
+ <RestTimer
82
+ {...defaultProps}
83
+ nextSetInfo="Bench Press — Set 3 of 4"
84
+ />,
85
+ )
86
+ expect(screen.getByTestId('rest-timer-next-set')).toHaveTextContent(
87
+ 'Bench Press — Set 3 of 4',
88
+ )
89
+ })
90
+
91
+ it('does not render next set element when nextSetInfo is not provided', () => {
92
+ render(<RestTimer {...defaultProps} />)
93
+ expect(screen.queryByTestId('rest-timer-next-set')).not.toBeInTheDocument()
94
+ })
95
+ })
96
+
97
+ describe('visibility', () => {
98
+ it('renders nothing when visible is false', () => {
99
+ render(<RestTimer {...defaultProps} visible={false} />)
100
+ expect(screen.queryByTestId('rest-timer')).not.toBeInTheDocument()
101
+ })
102
+
103
+ it('renders when visible is true', () => {
104
+ render(<RestTimer {...defaultProps} visible />)
105
+ expect(screen.getByTestId('rest-timer')).toBeInTheDocument()
106
+ })
107
+ })
108
+
109
+ describe('edge cases', () => {
110
+ it('clamps remaining to 0 when elapsed exceeds total', () => {
111
+ render(<RestTimer {...defaultProps} elapsedMs={200000} />)
112
+ expect(screen.getByTestId('rest-timer-time')).toHaveTextContent('0:00')
113
+ })
114
+
115
+ it('displays full time when elapsed is 0', () => {
116
+ render(<RestTimer {...defaultProps} totalSeconds={90} elapsedMs={0} />)
117
+ expect(screen.getByTestId('rest-timer-time')).toHaveTextContent('1:30')
118
+ })
119
+ })
120
+
121
+ describe('accessibility', () => {
122
+ it('has no accessibility violations', async () => {
123
+ const { container } = render(<RestTimer {...defaultProps} />)
124
+ const results = await axe(container)
125
+ expect(results).toHaveNoViolations()
126
+ })
127
+
128
+ it('has timer role with remaining seconds in label', () => {
129
+ render(<RestTimer {...defaultProps} totalSeconds={90} elapsedMs={30000} />)
130
+ expect(
131
+ screen.getByLabelText('Rest timer, 60 seconds remaining'),
132
+ ).toBeInTheDocument()
133
+ })
134
+
135
+ it('skip button has accessible label', () => {
136
+ render(<RestTimer {...defaultProps} />)
137
+ expect(screen.getByLabelText('Skip rest')).toBeInTheDocument()
138
+ })
139
+
140
+ it('add time button has accessible label', () => {
141
+ render(<RestTimer {...defaultProps} />)
142
+ expect(screen.getByLabelText('Add 30 seconds')).toBeInTheDocument()
143
+ })
144
+ })
145
+ })
@@ -0,0 +1,172 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import React from 'react'
3
+ import { View, Text, Pressable } from 'react-native'
4
+
5
+ export interface RestTimerProps {
6
+ totalSeconds: number
7
+ elapsedMs: number
8
+ onSkip: () => void
9
+ onAddTime: () => void
10
+ nextSetInfo?: string
11
+ visible: boolean
12
+ }
13
+
14
+ export function RestTimer({
15
+ totalSeconds,
16
+ elapsedMs,
17
+ onSkip,
18
+ onAddTime,
19
+ nextSetInfo,
20
+ visible,
21
+ }: RestTimerProps) {
22
+ if (!visible) return null
23
+
24
+ const remainingMs = Math.max(0, totalSeconds * 1000 - elapsedMs)
25
+ const remainingSec = Math.ceil(remainingMs / 1000)
26
+ const minutes = Math.floor(remainingSec / 60)
27
+ const seconds = remainingSec % 60
28
+ const timeDisplay = `${minutes}:${String(seconds).padStart(2, '0')}`
29
+ const progressPct = Math.min(100, (elapsedMs / (totalSeconds * 1000)) * 100)
30
+
31
+ return (
32
+ <View
33
+ style={{
34
+ width: '100%',
35
+ backgroundColor: '#1C1C1C',
36
+ borderTopWidth: 1,
37
+ borderTopColor: '#1F1F1F',
38
+ paddingVertical: 12,
39
+ paddingHorizontal: 16,
40
+ }}
41
+ accessibilityRole="timer"
42
+ accessibilityLabel={`Rest timer, ${remainingSec} seconds remaining`}
43
+ testID="rest-timer"
44
+ >
45
+ {/* Top row */}
46
+ <View
47
+ style={{
48
+ flexDirection: 'row',
49
+ justifyContent: 'space-between',
50
+ alignItems: 'center',
51
+ marginBottom: 8,
52
+ }}
53
+ >
54
+ {/* Left side */}
55
+ <View style={{ flexDirection: 'column' }}>
56
+ <Text
57
+ style={{
58
+ fontSize: 11,
59
+ fontFamily: 'Inter, sans-serif',
60
+ fontWeight: '600',
61
+ textTransform: 'uppercase',
62
+ letterSpacing: 1,
63
+ color: '#9CA3AF',
64
+ }}
65
+ testID="rest-timer-label"
66
+ >
67
+ REST
68
+ </Text>
69
+ {nextSetInfo != null && (
70
+ <Text
71
+ style={{
72
+ fontSize: 11,
73
+ fontFamily: 'Inter, sans-serif',
74
+ color: '#6B7280',
75
+ marginTop: 2,
76
+ }}
77
+ testID="rest-timer-next-set"
78
+ >
79
+ {nextSetInfo}
80
+ </Text>
81
+ )}
82
+ </View>
83
+
84
+ {/* Right side - time display */}
85
+ <Text
86
+ style={{
87
+ fontSize: 28,
88
+ fontFamily: '"Space Grotesk", sans-serif',
89
+ fontWeight: '700',
90
+ color: '#F3F4F6',
91
+ fontVariant: ['tabular-nums'],
92
+ letterSpacing: -0.5,
93
+ }}
94
+ testID="rest-timer-time"
95
+ >
96
+ {timeDisplay}
97
+ </Text>
98
+ </View>
99
+
100
+ {/* Progress bar */}
101
+ <View
102
+ style={{
103
+ height: 3,
104
+ backgroundColor: '#1F1F1F',
105
+ borderRadius: 2,
106
+ marginBottom: 12,
107
+ }}
108
+ testID="rest-timer-progress-track"
109
+ >
110
+ <View
111
+ style={{
112
+ height: '100%',
113
+ backgroundColor: '#FF7900',
114
+ borderRadius: 2,
115
+ width: `${progressPct}%`,
116
+ }}
117
+ testID="rest-timer-progress-fill"
118
+ />
119
+ </View>
120
+
121
+ {/* Actions row */}
122
+ <View style={{ flexDirection: 'row', gap: 8 }}>
123
+ <Pressable
124
+ onPress={onAddTime}
125
+ style={{
126
+ backgroundColor: 'rgba(255,255,255,0.06)',
127
+ paddingVertical: 8,
128
+ paddingHorizontal: 20,
129
+ borderRadius: 8,
130
+ }}
131
+ accessibilityRole="button"
132
+ accessibilityLabel="Add 30 seconds"
133
+ testID="rest-timer-add-time"
134
+ >
135
+ <Text
136
+ style={{
137
+ fontSize: 11,
138
+ fontFamily: 'Inter, sans-serif',
139
+ fontWeight: '600',
140
+ color: '#9CA3AF',
141
+ }}
142
+ >
143
+ +30s
144
+ </Text>
145
+ </Pressable>
146
+ <Pressable
147
+ onPress={onSkip}
148
+ style={{
149
+ backgroundColor: 'rgba(255,121,0,0.12)',
150
+ paddingVertical: 8,
151
+ paddingHorizontal: 20,
152
+ borderRadius: 8,
153
+ }}
154
+ accessibilityRole="button"
155
+ accessibilityLabel="Skip rest"
156
+ testID="rest-timer-skip"
157
+ >
158
+ <Text
159
+ style={{
160
+ fontSize: 11,
161
+ fontFamily: 'Inter, sans-serif',
162
+ fontWeight: '600',
163
+ color: '#FF7900',
164
+ }}
165
+ >
166
+ Skip
167
+ </Text>
168
+ </Pressable>
169
+ </View>
170
+ </View>
171
+ )
172
+ }
@@ -0,0 +1,144 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import { SupersetWrapper } from './SupersetWrapper'
4
+ import { ExerciseCard } from './ExerciseCard'
5
+
6
+ const meta: Meta<typeof SupersetWrapper> = {
7
+ title: 'Custom/Workout/SupersetWrapper',
8
+ component: SupersetWrapper,
9
+ tags: ['autodocs'],
10
+ decorators: [
11
+ (Story) => (
12
+ <View style={{ maxWidth: 400, padding: 16, backgroundColor: '#121212' }}>
13
+ <Story />
14
+ </View>
15
+ ),
16
+ ],
17
+ }
18
+
19
+ export default meta
20
+ type Story = StoryObj<typeof SupersetWrapper>
21
+
22
+ export const Default: Story = {
23
+ args: {
24
+ children: (
25
+ <>
26
+ <View
27
+ style={{
28
+ padding: 12,
29
+ backgroundColor: '#1C1C1C',
30
+ borderRadius: 8,
31
+ }}
32
+ >
33
+ <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
34
+ Exercise A
35
+ </Text>
36
+ </View>
37
+ <View
38
+ style={{
39
+ padding: 12,
40
+ backgroundColor: '#1C1C1C',
41
+ borderRadius: 8,
42
+ }}
43
+ >
44
+ <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
45
+ Exercise B
46
+ </Text>
47
+ </View>
48
+ </>
49
+ ),
50
+ },
51
+ }
52
+
53
+ export const CustomLabel: Story = {
54
+ args: {
55
+ label: 'A1/A2',
56
+ children: (
57
+ <>
58
+ <View
59
+ style={{
60
+ padding: 12,
61
+ backgroundColor: '#1C1C1C',
62
+ borderRadius: 8,
63
+ }}
64
+ >
65
+ <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
66
+ Exercise A1
67
+ </Text>
68
+ </View>
69
+ <View
70
+ style={{
71
+ padding: 12,
72
+ backgroundColor: '#1C1C1C',
73
+ borderRadius: 8,
74
+ }}
75
+ >
76
+ <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
77
+ Exercise A2
78
+ </Text>
79
+ </View>
80
+ </>
81
+ ),
82
+ },
83
+ }
84
+
85
+ export const CustomColor: Story = {
86
+ args: {
87
+ color: '#22C55E',
88
+ children: (
89
+ <>
90
+ <View
91
+ style={{
92
+ padding: 12,
93
+ backgroundColor: '#1C1C1C',
94
+ borderRadius: 8,
95
+ }}
96
+ >
97
+ <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
98
+ Exercise A
99
+ </Text>
100
+ </View>
101
+ <View
102
+ style={{
103
+ padding: 12,
104
+ backgroundColor: '#1C1C1C',
105
+ borderRadius: 8,
106
+ }}
107
+ >
108
+ <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
109
+ Exercise B
110
+ </Text>
111
+ </View>
112
+ </>
113
+ ),
114
+ },
115
+ }
116
+
117
+ const exerciseA = {
118
+ name: 'Bench Press',
119
+ state: 'collapsed' as const,
120
+ onToggle: () => {},
121
+ summary: { sets: 3, reps: 8, weight: 185, unit: 'lbs' as const },
122
+ supersetPosition: 'first' as const,
123
+ supersetColor: '#FF7900',
124
+ }
125
+
126
+ const exerciseB = {
127
+ name: 'Bent Over Row',
128
+ state: 'collapsed' as const,
129
+ onToggle: () => {},
130
+ summary: { sets: 3, reps: 8, weight: 155, unit: 'lbs' as const },
131
+ supersetPosition: 'last' as const,
132
+ supersetColor: '#FF7900',
133
+ }
134
+
135
+ export const WithExerciseCards: Story = {
136
+ args: {
137
+ children: (
138
+ <>
139
+ <ExerciseCard {...exerciseA} />
140
+ <ExerciseCard {...exerciseB} />
141
+ </>
142
+ ),
143
+ },
144
+ }
@@ -0,0 +1,130 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { View, Text } from 'react-native'
5
+ import { SupersetWrapper } from './SupersetWrapper'
6
+
7
+ describe('SupersetWrapper', () => {
8
+ describe('label', () => {
9
+ it('renders default label "SS"', () => {
10
+ render(
11
+ <SupersetWrapper>
12
+ <View />
13
+ </SupersetWrapper>,
14
+ )
15
+ expect(screen.getByTestId('superset-label')).toHaveTextContent('SS')
16
+ })
17
+
18
+ it('renders custom label', () => {
19
+ render(
20
+ <SupersetWrapper label="A1/A2">
21
+ <View />
22
+ </SupersetWrapper>,
23
+ )
24
+ expect(screen.getByTestId('superset-label')).toHaveTextContent('A1/A2')
25
+ })
26
+ })
27
+
28
+ describe('color', () => {
29
+ it('applies default color #FF7900 to border and label background', () => {
30
+ render(
31
+ <SupersetWrapper>
32
+ <View />
33
+ </SupersetWrapper>,
34
+ )
35
+ const wrapper = screen.getByTestId('superset-wrapper')
36
+ expect(wrapper).toHaveStyle({ borderLeftColor: '#FF7900' })
37
+
38
+ const label = screen.getByTestId('superset-label')
39
+ expect(label).toHaveStyle({ backgroundColor: '#FF7900' })
40
+ })
41
+
42
+ it('applies custom color to border and label background', () => {
43
+ render(
44
+ <SupersetWrapper color="#22C55E">
45
+ <View />
46
+ </SupersetWrapper>,
47
+ )
48
+ const wrapper = screen.getByTestId('superset-wrapper')
49
+ expect(wrapper).toHaveStyle({ borderLeftColor: '#22C55E' })
50
+
51
+ const label = screen.getByTestId('superset-label')
52
+ expect(label).toHaveStyle({ backgroundColor: '#22C55E' })
53
+ })
54
+ })
55
+
56
+ describe('children', () => {
57
+ it('renders children', () => {
58
+ render(
59
+ <SupersetWrapper>
60
+ <Text testID="child-1">Exercise A</Text>
61
+ <Text testID="child-2">Exercise B</Text>
62
+ </SupersetWrapper>,
63
+ )
64
+ expect(screen.getByTestId('child-1')).toHaveTextContent('Exercise A')
65
+ expect(screen.getByTestId('child-2')).toHaveTextContent('Exercise B')
66
+ })
67
+ })
68
+
69
+ describe('accessibility', () => {
70
+ it('has group role', () => {
71
+ render(
72
+ <SupersetWrapper>
73
+ <View />
74
+ </SupersetWrapper>,
75
+ )
76
+ expect(screen.getByRole('group')).toBeInTheDocument()
77
+ })
78
+
79
+ it('has correct accessibility label with default label', () => {
80
+ render(
81
+ <SupersetWrapper>
82
+ <View />
83
+ </SupersetWrapper>,
84
+ )
85
+ expect(screen.getByLabelText('Superset: SS')).toBeInTheDocument()
86
+ })
87
+
88
+ it('has correct accessibility label with custom label', () => {
89
+ render(
90
+ <SupersetWrapper label="A1/A2">
91
+ <View />
92
+ </SupersetWrapper>,
93
+ )
94
+ expect(screen.getByLabelText('Superset: A1/A2')).toBeInTheDocument()
95
+ })
96
+
97
+ it('has no accessibility violations', async () => {
98
+ const { container } = render(
99
+ <SupersetWrapper>
100
+ <Text>Exercise A</Text>
101
+ <Text>Exercise B</Text>
102
+ </SupersetWrapper>,
103
+ )
104
+ const results = await axe(container)
105
+ expect(results).toHaveNoViolations()
106
+ })
107
+ })
108
+
109
+ describe('label positioning', () => {
110
+ it('positions label absolutely', () => {
111
+ render(
112
+ <SupersetWrapper>
113
+ <View />
114
+ </SupersetWrapper>,
115
+ )
116
+ const label = screen.getByTestId('superset-label')
117
+ expect(label).toHaveStyle({ position: 'absolute' })
118
+ })
119
+
120
+ it('positions label at top -1 and left -3', () => {
121
+ render(
122
+ <SupersetWrapper>
123
+ <View />
124
+ </SupersetWrapper>,
125
+ )
126
+ const label = screen.getByTestId('superset-label')
127
+ expect(label).toHaveStyle({ top: '-1px', left: '-3px' })
128
+ })
129
+ })
130
+ })
@@ -0,0 +1,63 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import React from 'react'
3
+ import { View, Text } from 'react-native'
4
+
5
+ export interface SupersetWrapperProps {
6
+ label?: string
7
+ color?: string
8
+ children: React.ReactNode
9
+ syncExpansion?: boolean
10
+ }
11
+
12
+ const DEFAULTS = {
13
+ label: 'SS',
14
+ color: '#FF7900',
15
+ bgBase: '#101010',
16
+ } as const
17
+
18
+ export function SupersetWrapper({
19
+ label = DEFAULTS.label,
20
+ color = DEFAULTS.color,
21
+ children,
22
+ }: SupersetWrapperProps) {
23
+ return (
24
+ <View
25
+ style={{
26
+ position: 'relative',
27
+ borderLeftWidth: 3,
28
+ borderLeftColor: color,
29
+ paddingLeft: 8,
30
+ marginHorizontal: 12,
31
+ marginBottom: 8,
32
+ overflow: 'visible',
33
+ }}
34
+ accessibilityRole={"group" as any}
35
+ accessibilityLabel={`Superset: ${label}`}
36
+ testID="superset-wrapper"
37
+ >
38
+ <Text
39
+ style={{
40
+ position: 'absolute',
41
+ top: -1,
42
+ left: -3,
43
+ fontSize: 9,
44
+ fontFamily: 'Inter, sans-serif',
45
+ fontWeight: '700',
46
+ backgroundColor: color,
47
+ color: DEFAULTS.bgBase,
48
+ paddingVertical: 2,
49
+ paddingHorizontal: 6,
50
+ borderBottomRightRadius: 4,
51
+ letterSpacing: 0.5,
52
+ zIndex: 2,
53
+ }}
54
+ testID="superset-label"
55
+ >
56
+ {label}
57
+ </Text>
58
+ <View style={{ gap: 2 }} testID="superset-children">
59
+ {children}
60
+ </View>
61
+ </View>
62
+ )
63
+ }
@@ -0,0 +1,2 @@
1
+ export { RestTimer, type RestTimerProps } from './RestTimer'
2
+ export { SupersetWrapper, type SupersetWrapperProps } from './SupersetWrapper'
@@ -0,0 +1,9 @@
1
+ // Custom components
2
+ export * from './Typography'
3
+ export * from './Sidebar'
4
+ export * from './Table'
5
+ export * from './EmptyState'
6
+ export * from './stepper'
7
+ export * from './DateTime'
8
+ export * from './Metric'
9
+ export * from './Workout'