@titan-design/react-ui 0.6.0 → 0.7.0

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 (248) hide show
  1. package/dist/bodymap.js +12 -7
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +12 -7
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +855 -124
  6. package/dist/index.d.ts +855 -124
  7. package/dist/index.js +2276 -801
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2230 -802
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/{pages-MO0JCySL.d.mts → pages-8u_4WbL-.d.mts} +280 -57
  12. package/dist/{pages-D7Jlssvp.d.ts → pages-DaWiBOGa.d.ts} +280 -57
  13. package/dist/pages.d.mts +1 -1
  14. package/dist/pages.d.ts +1 -1
  15. package/dist/pages.js +1255 -798
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1255 -798
  18. package/dist/pages.mjs.map +1 -1
  19. package/dist/theme/index.d.mts +16 -82
  20. package/dist/theme/index.d.ts +16 -82
  21. package/dist/theme/index.js +13 -115
  22. package/dist/theme/index.js.map +1 -1
  23. package/dist/theme/index.mjs +13 -115
  24. package/dist/theme/index.mjs.map +1 -1
  25. package/dist/theme/tokens-css.js +14 -6
  26. package/dist/theme/tokens-css.js.map +1 -1
  27. package/dist/theme/tokens-css.mjs +14 -6
  28. package/dist/theme/tokens-css.mjs.map +1 -1
  29. package/package.json +1 -1
  30. package/src/arch/Architecture.stories.tsx +870 -0
  31. package/src/arch/annotations.json +48 -0
  32. package/src/arch/arch-graph.json +4301 -0
  33. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  34. package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
  35. package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
  36. package/src/components/custom/Metric/Metric.stories.tsx +1 -1
  37. package/src/components/custom/Scatter/Scatter.stories.tsx +1 -1
  38. package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
  39. package/src/components/custom/Table/Table.stories.tsx +1 -1
  40. package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +62 -0
  41. package/src/components/custom/TimerReadout/TimerReadout.test.tsx +72 -0
  42. package/src/components/custom/TimerReadout/TimerReadout.tsx +75 -0
  43. package/src/components/custom/TimerReadout/index.ts +1 -0
  44. package/src/components/custom/Treemap/Treemap.stories.tsx +1 -1
  45. package/src/components/custom/Typography/Typography.stories.tsx +1 -1
  46. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +21 -13
  47. package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +16 -13
  48. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +9 -15
  49. package/src/components/custom/Workout/BaseBadge.stories.tsx +1 -1
  50. package/src/components/custom/Workout/BodyMap.stories.tsx +1 -1
  51. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
  52. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
  53. package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
  54. package/src/components/custom/Workout/ExerciseCard.stories.tsx +51 -66
  55. package/src/components/custom/Workout/ExerciseCard.test.tsx +146 -143
  56. package/src/components/custom/Workout/ExerciseCard.tsx +176 -198
  57. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +99 -0
  58. package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +72 -0
  59. package/src/components/custom/Workout/ExerciseCardHeading.tsx +43 -0
  60. package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +151 -0
  61. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +5 -3
  62. package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +4 -4
  63. package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -5
  64. package/src/components/custom/Workout/ExerciseHeading.stories.tsx +81 -0
  65. package/src/components/custom/Workout/ExerciseHeading.test.tsx +77 -0
  66. package/src/components/custom/Workout/ExerciseHeading.tsx +85 -0
  67. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +110 -0
  68. package/src/components/custom/Workout/ExerciseIndicator.test.tsx +93 -0
  69. package/src/components/custom/Workout/ExerciseIndicator.tsx +120 -0
  70. package/src/components/custom/Workout/FatigueMeter.stories.tsx +51 -0
  71. package/src/components/custom/Workout/FatigueMeter.test.tsx +73 -0
  72. package/src/components/custom/Workout/FatigueMeter.tsx +82 -0
  73. package/src/components/custom/Workout/InputBar.stories.tsx +1 -1
  74. package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
  75. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +153 -0
  76. package/src/components/custom/Workout/LiveAuraFrame.test.tsx +74 -0
  77. package/src/components/custom/Workout/LiveAuraFrame.tsx +105 -0
  78. package/src/components/custom/Workout/MesoCard.stories.tsx +1 -1
  79. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
  80. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
  81. package/src/components/custom/Workout/MetricTiles.stories.tsx +60 -0
  82. package/src/components/custom/Workout/MetricTiles.test.tsx +58 -0
  83. package/src/components/custom/Workout/MetricTiles.tsx +48 -0
  84. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +1 -1
  85. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
  86. package/src/components/custom/Workout/PrBadge.stories.tsx +1 -1
  87. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
  88. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
  89. package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -6
  90. package/src/components/custom/Workout/README.md +146 -4
  91. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
  92. package/src/components/custom/Workout/RestTimer.stories.tsx +1 -1
  93. package/src/components/custom/Workout/RestTimer.tsx +15 -10
  94. package/src/components/custom/Workout/RowInventory.stories.tsx +405 -0
  95. package/src/components/custom/Workout/S3FullRail.stories.tsx +34 -0
  96. package/src/components/custom/Workout/S3SessionPace.stories.tsx +95 -0
  97. package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +61 -0
  98. package/src/components/custom/Workout/S3SetTypes.stories.tsx +176 -0
  99. package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +240 -0
  100. package/src/components/custom/Workout/ScheduleTiles.stories.tsx +40 -0
  101. package/src/components/custom/Workout/ScheduleTiles.test.tsx +34 -0
  102. package/src/components/custom/Workout/ScheduleTiles.tsx +70 -0
  103. package/src/components/custom/Workout/SegmentedBar.stories.tsx +99 -0
  104. package/src/components/custom/Workout/SegmentedBar.test.tsx +77 -0
  105. package/src/components/custom/Workout/SegmentedBar.tsx +172 -0
  106. package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +50 -0
  107. package/src/components/custom/Workout/SegmentedProgressBar.test.tsx +80 -0
  108. package/src/components/custom/Workout/SegmentedProgressBar.tsx +67 -0
  109. package/src/components/custom/Workout/SessionHeader.stories.tsx +121 -0
  110. package/src/components/custom/Workout/SessionHeader.test.tsx +122 -0
  111. package/src/components/custom/Workout/SessionHeader.tsx +152 -0
  112. package/src/components/custom/Workout/SessionRail.stories.tsx +153 -0
  113. package/src/components/custom/Workout/SessionRail.test.tsx +86 -0
  114. package/src/components/custom/Workout/SessionRail.tsx +126 -0
  115. package/src/components/custom/Workout/SetBar.stories.tsx +124 -0
  116. package/src/components/custom/Workout/SetBar.test.tsx +123 -0
  117. package/src/components/custom/Workout/SetBar.tsx +171 -0
  118. package/src/components/custom/Workout/SetRow.stories.tsx +50 -100
  119. package/src/components/custom/Workout/SetRow.test.tsx +94 -166
  120. package/src/components/custom/Workout/SetRow.tsx +169 -232
  121. package/src/components/custom/Workout/SetStrip.stories.tsx +154 -0
  122. package/src/components/custom/Workout/SetStrip.test.tsx +109 -0
  123. package/src/components/custom/Workout/SetStrip.tsx +71 -0
  124. package/src/components/custom/Workout/SetTableHeader.stories.tsx +66 -0
  125. package/src/components/custom/Workout/SetTableHeader.test.tsx +47 -0
  126. package/src/components/custom/Workout/SetTableHeader.tsx +88 -0
  127. package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +47 -0
  128. package/src/components/custom/Workout/SetsRepsLoad.test.tsx +44 -0
  129. package/src/components/custom/Workout/SetsRepsLoad.tsx +52 -0
  130. package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
  131. package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
  132. package/src/components/custom/Workout/StatusPill.stories.tsx +59 -0
  133. package/src/components/custom/Workout/StatusPill.test.tsx +63 -0
  134. package/src/components/custom/Workout/StatusPill.tsx +88 -0
  135. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +1 -1
  136. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
  137. package/src/components/custom/Workout/TempoBar.stories.tsx +1 -1
  138. package/src/components/custom/Workout/TempoDisplay.stories.tsx +12 -1
  139. package/src/components/custom/Workout/TempoDisplay.tsx +23 -33
  140. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +1 -1
  141. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +254 -0
  142. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +145 -0
  143. package/src/components/custom/Workout/VelocityStrip.stories.tsx +129 -66
  144. package/src/components/custom/Workout/VelocityStrip.test.tsx +376 -141
  145. package/src/components/custom/Workout/VelocityStrip.tsx +508 -120
  146. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +73 -0
  147. package/src/components/custom/Workout/VolumeLandmarkBar.test.tsx +119 -0
  148. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +154 -0
  149. package/src/components/custom/Workout/WeekRow.stories.tsx +1 -1
  150. package/src/components/custom/Workout/WeightBadge.stories.tsx +1 -1
  151. package/src/components/custom/Workout/WorkoutCard.stories.tsx +2 -6
  152. package/src/components/custom/Workout/WorkoutCard.test.tsx +13 -46
  153. package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
  154. package/src/components/custom/Workout/ZoneTrack.stories.tsx +140 -0
  155. package/src/components/custom/Workout/ZoneTrack.test.tsx +200 -0
  156. package/src/components/custom/Workout/ZoneTrack.tsx +315 -0
  157. package/src/components/custom/Workout/index.ts +55 -2
  158. package/src/components/custom/Workout/metricText.stories.tsx +69 -0
  159. package/src/components/custom/Workout/metricText.tsx +34 -0
  160. package/src/components/custom/Workout/paceTone.test.ts +29 -0
  161. package/src/components/custom/Workout/paceTone.ts +27 -0
  162. package/src/components/custom/Workout/setHeadingKit.tsx +177 -0
  163. package/src/components/custom/index.ts +1 -0
  164. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  165. package/src/components/icons/icons.stories.tsx +67 -2
  166. package/src/components/icons/icons.test.tsx +32 -2
  167. package/src/components/icons/icons.tsx +108 -0
  168. package/src/components/shell/BrandLockup.stories.tsx +3 -3
  169. package/src/components/shell/DashboardShell.stories.tsx +65 -0
  170. package/src/components/shell/DashboardShell.test.tsx +42 -0
  171. package/src/components/shell/DashboardShell.tsx +74 -0
  172. package/src/components/shell/DeviceIndicator.stories.tsx +2 -2
  173. package/src/components/shell/DeviceMenu.stories.tsx +5 -5
  174. package/src/components/shell/DeviceRow.stories.tsx +3 -3
  175. package/src/components/shell/NavItem.stories.tsx +52 -0
  176. package/src/components/shell/NavItem.test.tsx +35 -0
  177. package/src/components/shell/NavItem.tsx +75 -0
  178. package/src/components/shell/README.md +30 -0
  179. package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
  180. package/src/components/shell/SessionStatePill.stories.tsx +3 -3
  181. package/src/components/shell/SessionStatePill.tsx +1 -1
  182. package/src/components/shell/SideNav.stories.tsx +81 -0
  183. package/src/components/shell/SideNav.test.tsx +51 -0
  184. package/src/components/shell/SideNav.tsx +74 -0
  185. package/src/components/shell/TopBar.stories.tsx +5 -5
  186. package/src/components/shell/index.ts +5 -0
  187. package/src/components/ui/alert/Alert.stories.tsx +1 -1
  188. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  189. package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
  190. package/src/components/ui/badge/Badge.stories.tsx +1 -1
  191. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  192. package/src/components/ui/button/Button.stories.tsx +1 -1
  193. package/src/components/ui/card/Card.stories.tsx +1 -1
  194. package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
  195. package/src/components/ui/chip/Chip.stories.tsx +1 -1
  196. package/src/components/ui/collapse/Collapse.stories.tsx +1 -1
  197. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  198. package/src/components/ui/divider/Divider.stories.tsx +1 -1
  199. package/src/components/ui/drawer/Drawer.stories.tsx +1 -1
  200. package/src/components/ui/form-field/FormField.stories.tsx +1 -1
  201. package/src/components/ui/help-tip/HelpTip.stories.tsx +1 -1
  202. package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
  203. package/src/components/ui/index.ts +1 -0
  204. package/src/components/ui/indicator/Indicator.stories.tsx +2 -2
  205. package/src/components/ui/indicator/Indicator.test.tsx +1 -0
  206. package/src/components/ui/indicator/Indicator.tsx +6 -1
  207. package/src/components/ui/input/Input.stories.tsx +1 -1
  208. package/src/components/ui/link/Link.stories.tsx +1 -1
  209. package/src/components/ui/list-item/ListItem.stories.tsx +1 -1
  210. package/src/components/ui/menu/Menu.stories.tsx +1 -1
  211. package/src/components/ui/modal/Modal.stories.tsx +1 -1
  212. package/src/components/ui/pill/Pill.stories.tsx +1 -1
  213. package/src/components/ui/popover/Popover.stories.tsx +1 -1
  214. package/src/components/ui/progress/Progress.stories.tsx +1 -1
  215. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  216. package/src/components/ui/section/Section.stories.tsx +1 -1
  217. package/src/components/ui/select/Select.stories.tsx +1 -1
  218. package/src/components/ui/skeleton/Skeleton.stories.tsx +1 -1
  219. package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
  220. package/src/components/ui/stack/Stack.stories.tsx +1 -1
  221. package/src/components/ui/surface/Surface.stories.tsx +1 -1
  222. package/src/components/ui/switch/Switch.stories.tsx +1 -1
  223. package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
  224. package/src/components/ui/tile/Tile.stories.tsx +79 -0
  225. package/src/components/ui/tile/Tile.test.tsx +48 -0
  226. package/src/components/ui/tile/Tile.tsx +64 -0
  227. package/src/components/ui/tile/index.ts +2 -0
  228. package/src/components/ui/toast/Toast.stories.tsx +1 -1
  229. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
  230. package/src/components/ui/tooltip/Tooltip.stories.tsx +1 -1
  231. package/src/examples/react-hook-form/ReactHookForm.stories.tsx +1 -1
  232. package/src/hooks/index.ts +2 -0
  233. package/src/hooks/useTimer.test.ts +150 -0
  234. package/src/hooks/useTimer.ts +77 -0
  235. package/src/index.ts +3 -0
  236. package/src/pages.ts +1 -1
  237. package/src/stories/PresetShowcase.stories.tsx +1 -1
  238. package/src/stories/ThemePresets.stories.tsx +1 -1
  239. package/src/theme/ColorSystem.stories.tsx +1 -1
  240. package/src/theme/Colors.stories.tsx +5 -8
  241. package/src/theme/config.ts +4 -0
  242. package/src/theme/elevation.stories.tsx +1 -1
  243. package/src/theme/global.css +5 -1
  244. package/src/theme/shadows.stories.tsx +1 -1
  245. package/src/theme/tokens/primitives.ts +11 -107
  246. package/src/theme/tokens/semantic.ts +8 -0
  247. package/src/theme/workout-tokens.ts +11 -7
  248. package/tailwind.config.js +5 -0
@@ -0,0 +1,200 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { ZoneTrack, type ZoneTrackZone } from './ZoneTrack'
5
+ import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
6
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
7
+
8
+ const { green, yellow, orange, red } = WORKOUT_TOKENS.scale
9
+ const t = getSemanticColors('dark')
10
+
11
+ const ZONES: ZoneTrackZone[] = [
12
+ { upTo: 10, color: green },
13
+ { upTo: 20, color: yellow },
14
+ { upTo: 30, color: orange },
15
+ { upTo: 40, color: red },
16
+ ]
17
+
18
+ describe('ZoneTrack', () => {
19
+ it('renders one band per zone', () => {
20
+ render(<ZoneTrack zones={ZONES} max={40} />)
21
+ expect(screen.getAllByTestId('zone-track-band')).toHaveLength(4)
22
+ })
23
+
24
+ it('colours each band from the supplied zone token (literal hex, not a var ref)', () => {
25
+ render(<ZoneTrack zones={ZONES} max={40} />)
26
+ const bands = screen.getAllByTestId('zone-track-band')
27
+ expect(bands[0]).toHaveStyle({ backgroundColor: green })
28
+ expect(bands[3]).toHaveStyle({ backgroundColor: red })
29
+ })
30
+
31
+ it('weights each band by its domain span', () => {
32
+ render(
33
+ <ZoneTrack
34
+ zones={[
35
+ { upTo: 30, color: green },
36
+ { upTo: 40, color: red },
37
+ ]}
38
+ max={40}
39
+ />
40
+ )
41
+ const bands = screen.getAllByTestId('zone-track-band')
42
+ expect(bands[0]).toHaveStyle({ flexGrow: 30 })
43
+ expect(bands[1]).toHaveStyle({ flexGrow: 10 })
44
+ })
45
+
46
+ it('renders no marker when none is supplied', () => {
47
+ render(<ZoneTrack zones={ZONES} max={40} />)
48
+ expect(screen.queryByTestId('zone-track-needle')).not.toBeInTheDocument()
49
+ expect(screen.queryByTestId('zone-track-fill')).not.toBeInTheDocument()
50
+ expect(screen.queryByTestId('zone-track-unfilled')).not.toBeInTheDocument()
51
+ })
52
+
53
+ it('positions a needle marker at the value fraction of the domain', () => {
54
+ render(<ZoneTrack zones={ZONES} max={40} marker={{ type: 'needle', value: 10 }} />)
55
+ expect(screen.getByTestId('zone-track-needle')).toHaveStyle({ left: '25%' })
56
+ })
57
+
58
+ it('clamps a needle past the max to the track end', () => {
59
+ render(<ZoneTrack zones={ZONES} max={40} marker={{ type: 'needle', value: 100 }} />)
60
+ expect(screen.getByTestId('zone-track-needle')).toHaveStyle({ left: '100%' })
61
+ })
62
+
63
+ it('clip-reveals the gradient for a fill marker without a colour', () => {
64
+ render(<ZoneTrack zones={ZONES} max={40} marker={{ type: 'fill', value: 20 }} />)
65
+ expect(screen.getByTestId('zone-track-unfilled')).toHaveStyle({ left: '50%' })
66
+ expect(screen.queryByTestId('zone-track-fill')).not.toBeInTheDocument()
67
+ })
68
+
69
+ it('draws a solid trend fill when the fill marker has a colour', () => {
70
+ render(<ZoneTrack zones={ZONES} max={40} marker={{ type: 'fill', value: 30, color: red }} />)
71
+ const fill = screen.getByTestId('zone-track-fill')
72
+ expect(fill).toHaveStyle({ width: '75%', backgroundColor: red })
73
+ })
74
+
75
+ it('renders a tick with its label per entry', () => {
76
+ render(
77
+ <ZoneTrack
78
+ zones={ZONES}
79
+ max={40}
80
+ ticks={[
81
+ { value: 0, label: 'fresh' },
82
+ { value: 40, label: 'stop' },
83
+ ]}
84
+ />
85
+ )
86
+ expect(screen.getAllByTestId('zone-track-tick')).toHaveLength(2)
87
+ expect(screen.getByText('fresh')).toBeInTheDocument()
88
+ expect(screen.getByText('stop')).toBeInTheDocument()
89
+ })
90
+
91
+ it('draws a range highlight only when a band is supplied', () => {
92
+ const { rerender } = render(<ZoneTrack zones={ZONES} max={40} />)
93
+ expect(screen.queryByTestId('zone-track-band-highlight')).not.toBeInTheDocument()
94
+ rerender(
95
+ <ZoneTrack
96
+ zones={ZONES}
97
+ max={40}
98
+ band={{ from: 10, to: 20, color: 'rgba(255,255,255,0.2)' }}
99
+ />
100
+ )
101
+ expect(screen.getByTestId('zone-track-band-highlight')).toHaveStyle({
102
+ left: '25%',
103
+ width: '25%',
104
+ })
105
+ })
106
+
107
+ // RNW does not map accessibilityValue → aria-valuenow under jsdom (gotcha #11), so the
108
+ // value is verified via the marker position + the accessible name, not aria-valuenow.
109
+ it('exposes the progressbar role with an accessible name', () => {
110
+ render(
111
+ <ZoneTrack
112
+ zones={ZONES}
113
+ max={40}
114
+ marker={{ type: 'needle', value: 12 }}
115
+ accessibilityLabel="Fatigue meter"
116
+ />
117
+ )
118
+ expect(screen.getByRole('progressbar', { name: 'Fatigue meter' })).toBeInTheDocument()
119
+ })
120
+
121
+ describe('accessibility', () => {
122
+ it('has no accessibility violations', async () => {
123
+ const { container } = render(
124
+ <ZoneTrack
125
+ zones={ZONES}
126
+ max={40}
127
+ marker={{ type: 'needle', value: 21 }}
128
+ ticks={[
129
+ { value: 0, label: 'fresh' },
130
+ { value: 40, label: 'stop' },
131
+ ]}
132
+ accessibilityLabel="Fatigue meter"
133
+ />
134
+ )
135
+ const results = await axe(container)
136
+ expect(results).toHaveNoViolations()
137
+ })
138
+ })
139
+
140
+ describe('glow', () => {
141
+ it('haloes the track when the marker has glow', () => {
142
+ render(
143
+ <ZoneTrack
144
+ zones={ZONES}
145
+ max={40}
146
+ marker={{ type: 'fill', value: 24, color: orange, glow: true }}
147
+ />
148
+ )
149
+ expect((screen.getByTestId('zone-track-track') as HTMLElement).style.boxShadow).not.toBe('')
150
+ })
151
+ it('has no glow by default', () => {
152
+ render(
153
+ <ZoneTrack zones={ZONES} max={40} marker={{ type: 'fill', value: 24, color: orange }} />
154
+ )
155
+ expect((screen.getByTestId('zone-track-track') as HTMLElement).style.boxShadow).toBe('')
156
+ })
157
+ })
158
+
159
+ describe('ticks (colored lines + labels + tooltip)', () => {
160
+ it('renders a line and label per tick', () => {
161
+ render(
162
+ <ZoneTrack
163
+ zones={ZONES}
164
+ max={40}
165
+ ticks={[
166
+ { value: 10, label: 'A' },
167
+ { value: 30, label: 'B' },
168
+ ]}
169
+ />
170
+ )
171
+ expect(screen.getAllByTestId('zone-track-tick-line')).toHaveLength(2)
172
+ expect(screen.getAllByTestId('zone-track-tick-label').map((e) => e.textContent)).toEqual([
173
+ 'A',
174
+ 'B',
175
+ ])
176
+ })
177
+ it('colours an emphasized tick line with the brand accent', () => {
178
+ render(
179
+ <ZoneTrack zones={ZONES} max={40} ticks={[{ value: 20, label: 'T', emphasized: true }]} />
180
+ )
181
+ expect(screen.getByTestId('zone-track-tick-line')).toHaveStyle({
182
+ backgroundColor: t['brand-primary'],
183
+ })
184
+ })
185
+ it('applies a per-tick color override to the line', () => {
186
+ render(<ZoneTrack zones={ZONES} max={40} ticks={[{ value: 20, label: 'X', color: red }]} />)
187
+ expect(screen.getByTestId('zone-track-tick-line')).toHaveStyle({ backgroundColor: red })
188
+ })
189
+ it('renders a tooltip label trigger', () => {
190
+ render(
191
+ <ZoneTrack
192
+ zones={ZONES}
193
+ max={40}
194
+ ticks={[{ value: 20, label: 'MAV', tooltip: 'Maximum Adaptive Volume' }]}
195
+ />
196
+ )
197
+ expect(screen.getByText('MAV')).toBeInTheDocument()
198
+ })
199
+ })
200
+ })
@@ -0,0 +1,315 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import { View, Text, type ViewProps, type DimensionValue } from 'react-native'
3
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
4
+ import { primitiveColors } from '../../../theme/tokens/primitives'
5
+ import { alpha } from '../../../utils/colors'
6
+ import { Tooltip } from '../../ui/tooltip/Tooltip'
7
+
8
+ const t = getSemanticColors('dark')
9
+
10
+ /** Soft two-stop glow in `color`, for a marker's optional `glow`. */
11
+ function glowShadow(color: string): string {
12
+ return `0 0 5px 1px ${alpha(color, 0.3)}, 0 0 10px 3px ${alpha(color, 0.12)}`
13
+ }
14
+
15
+ /** Muted, un-reached track colour — a charcoal step, matches the IntensityBar track family. */
16
+ const DEFAULT_TRACK_COLOR = primitiveColors.charcoal[200]
17
+ /** Default needle / fill-marker colour. */
18
+ const DEFAULT_MARKER_COLOR = primitiveColors.white
19
+ /** Tick mark + tick label colour. */
20
+ const TICK_COLOR = t['text-tertiary']
21
+
22
+ const DEFAULT_TRACK_HEIGHT = 14
23
+ const DEFAULT_NEEDLE_OVERHANG = 6
24
+ const NEEDLE_WIDTH = 4
25
+
26
+ /** A single colour band of the zone gradient, spanning `[prev.upTo, upTo]` of the domain. */
27
+ export interface ZoneTrackZone {
28
+ /** Upper bound of this band in domain units. The last band's `upTo` should reach `max`. */
29
+ upTo: number
30
+ /** Band fill colour — a literal hex (RNW-safe), sourced from a ramp/effort-scale token. */
31
+ color: string
32
+ }
33
+
34
+ /**
35
+ * A tick at `value`: a colored line over the track + an optional compact label
36
+ * below, with an optional tooltip (to expand an acronym / show the raw value so
37
+ * the label footer can stay light). Multiple ticks are supported.
38
+ */
39
+ export interface ZoneTrackTick {
40
+ /** Position of the tick in domain units. */
41
+ value: number
42
+ /** Optional compact label rendered under the tick. */
43
+ label?: string
44
+ /** Line + label colour. Defaults to a muted tick colour (or brand when `emphasized`). */
45
+ color?: string
46
+ /** Emphasize this tick (thicker line, brand colour by default) — e.g. a target landmark. */
47
+ emphasized?: boolean
48
+ /** Tooltip content shown on hover (web) / long-press (native) — expand the acronym, show the raw value. */
49
+ tooltip?: string
50
+ }
51
+
52
+ /**
53
+ * The value marker sitting on the track.
54
+ * - `needle`: a vertical line at `value`, overhanging the track (FatigueMeter / TrainingLoadGauge).
55
+ * - `fill`: a left-anchored fill from `min` to `value`. Omit `color` to clip-reveal the zone
56
+ * gradient up to `value`; supply `color` for a solid trend-coloured fill (FatigueGauge-style).
57
+ */
58
+ export type ZoneTrackMarker =
59
+ | { type: 'needle'; value: number; color?: string; glow?: boolean }
60
+ | { type: 'fill'; value: number; color?: string; glow?: boolean }
61
+
62
+ /** An optional translucent range highlight (e.g. RpeCalibration's predicted CI band). */
63
+ export interface ZoneTrackBand {
64
+ /** Range start in domain units. */
65
+ from: number
66
+ /** Range end in domain units. */
67
+ to: number
68
+ /** Highlight colour (typically a translucent token). */
69
+ color: string
70
+ }
71
+
72
+ export interface ZoneTrackProps extends ViewProps {
73
+ /** Ordered zone bands, laid left→right; each covers `[previous.upTo, upTo]` of the domain. */
74
+ zones: ZoneTrackZone[]
75
+ /** Domain maximum (right edge). */
76
+ max: number
77
+ /** Domain minimum (left edge). Default 0. */
78
+ min?: number
79
+ /** The value marker — a needle line or a left-anchored fill. Omit for a bare gradient track. */
80
+ marker?: ZoneTrackMarker | null
81
+ /** Optional tick marks with labels below the track. */
82
+ ticks?: ZoneTrackTick[]
83
+ /** Optional translucent range highlight drawn over the gradient. */
84
+ band?: ZoneTrackBand | null
85
+ /** Track height in px. Default 14. */
86
+ trackHeight?: number
87
+ /** How far a needle marker overhangs the track top + bottom, in px. Default 6. */
88
+ needleOverhang?: number
89
+ /** Muted colour of the track behind / beyond the fill. Default a charcoal step. */
90
+ trackColor?: string
91
+ className?: string
92
+ }
93
+
94
+ /** Map a domain value to a clamped 0..1 fraction of the track width. */
95
+ function fraction(value: number, min: number, max: number): number {
96
+ if (max <= min) return 0
97
+ return Math.max(0, Math.min(1, (value - min) / (max - min)))
98
+ }
99
+
100
+ function pct(frac: number): DimensionValue {
101
+ return `${frac * 100}%`
102
+ }
103
+
104
+ /**
105
+ * Low-level gauge primitive: a horizontal pill track carrying an N-band zone gradient,
106
+ * optional tick marks with labels, and a single value marker (a needle line or a
107
+ * left-anchored fill). The zone bands are weighted by their domain span, so thresholds
108
+ * need not be evenly spaced. This is the shared visual base for the linear gauge family
109
+ * (FatigueMeter needle, TrainingLoadGauge zone bar, RpeCalibration band + marker) — pass
110
+ * the domain (`min`/`max`), the `zones`, and a `marker`; colours are literal hex from the
111
+ * ramp tokens (never `var()` refs, so they survive the RNW/vitest alias).
112
+ */
113
+ export function ZoneTrack({
114
+ zones,
115
+ max,
116
+ min = 0,
117
+ marker = null,
118
+ ticks,
119
+ band = null,
120
+ trackHeight = DEFAULT_TRACK_HEIGHT,
121
+ needleOverhang = DEFAULT_NEEDLE_OVERHANG,
122
+ trackColor = DEFAULT_TRACK_COLOR,
123
+ className,
124
+ style,
125
+ accessibilityLabel,
126
+ ...props
127
+ }: ZoneTrackProps) {
128
+ const isNeedle = marker?.type === 'needle'
129
+ const markerHeight = isNeedle ? trackHeight + needleOverhang * 2 : trackHeight
130
+ const markerFrac = marker != null ? fraction(marker.value, min, max) : 0
131
+
132
+ const bands = zones.map((zone, i) => {
133
+ const prev = i === 0 ? min : zones[i - 1].upTo
134
+ const start = Math.max(min, Math.min(max, prev))
135
+ const end = Math.max(min, Math.min(max, zone.upTo))
136
+ return { color: zone.color, weight: Math.max(0, end - start) }
137
+ })
138
+
139
+ return (
140
+ <View
141
+ className={className}
142
+ style={[{ width: '100%' }, style]}
143
+ accessibilityRole="progressbar"
144
+ accessibilityValue={marker != null ? { min, max, now: marker.value } : { min, max, now: min }}
145
+ accessibilityLabel={accessibilityLabel ?? `Zone track: ${marker?.value ?? min}`}
146
+ testID="zone-track"
147
+ {...props}
148
+ >
149
+ <View style={{ position: 'relative', height: markerHeight }}>
150
+ <View
151
+ testID="zone-track-track"
152
+ accessibilityElementsHidden
153
+ style={{
154
+ position: 'absolute',
155
+ top: (markerHeight - trackHeight) / 2,
156
+ left: 0,
157
+ right: 0,
158
+ height: trackHeight,
159
+ borderRadius: trackHeight / 2,
160
+ backgroundColor: trackColor,
161
+ overflow: 'hidden',
162
+ flexDirection: 'row',
163
+ // The pill's own box-shadow renders outside its overflow:hidden, so
164
+ // `glow` haloes the whole track in the marker colour.
165
+ ...(marker?.glow
166
+ ? { boxShadow: glowShadow(marker.color ?? DEFAULT_MARKER_COLOR) }
167
+ : null),
168
+ }}
169
+ >
170
+ {bands.map((b, i) => (
171
+ <View
172
+ key={i}
173
+ testID="zone-track-band"
174
+ style={{
175
+ flexGrow: b.weight,
176
+ flexShrink: 1,
177
+ flexBasis: 0,
178
+ minWidth: 0,
179
+ height: '100%',
180
+ backgroundColor: b.color,
181
+ }}
182
+ />
183
+ ))}
184
+
185
+ {band != null && (
186
+ <View
187
+ testID="zone-track-band-highlight"
188
+ style={{
189
+ position: 'absolute',
190
+ top: 0,
191
+ bottom: 0,
192
+ left: pct(fraction(band.from, min, max)),
193
+ width: pct(fraction(band.to, min, max) - fraction(band.from, min, max)),
194
+ backgroundColor: band.color,
195
+ }}
196
+ />
197
+ )}
198
+
199
+ {marker?.type === 'fill' && (
200
+ <View
201
+ testID="zone-track-unfilled"
202
+ style={{
203
+ position: 'absolute',
204
+ top: 0,
205
+ bottom: 0,
206
+ left: pct(markerFrac),
207
+ right: 0,
208
+ backgroundColor: trackColor,
209
+ }}
210
+ />
211
+ )}
212
+ {marker?.type === 'fill' && marker.color != null && (
213
+ <View
214
+ testID="zone-track-fill"
215
+ style={{
216
+ position: 'absolute',
217
+ top: 0,
218
+ bottom: 0,
219
+ left: 0,
220
+ width: pct(markerFrac),
221
+ backgroundColor: marker.color,
222
+ }}
223
+ />
224
+ )}
225
+ </View>
226
+
227
+ {isNeedle && (
228
+ <View
229
+ testID="zone-track-needle"
230
+ style={{
231
+ position: 'absolute',
232
+ top: 0,
233
+ left: pct(markerFrac),
234
+ width: NEEDLE_WIDTH,
235
+ height: markerHeight,
236
+ borderRadius: NEEDLE_WIDTH / 2,
237
+ backgroundColor: marker.color ?? DEFAULT_MARKER_COLOR,
238
+ transform: [{ translateX: -NEEDLE_WIDTH / 2 }],
239
+ }}
240
+ />
241
+ )}
242
+
243
+ {/* Colored tick lines over the track (decorative; the labels below carry the a11y). */}
244
+ {ticks?.map((tick, i) => {
245
+ const emphasized = tick.emphasized === true
246
+ const lineColor = tick.color ?? (emphasized ? t['brand-primary'] : TICK_COLOR)
247
+ const lineWidth = emphasized ? 2 : 1.5
248
+ return (
249
+ <View
250
+ key={`line-${i}`}
251
+ testID="zone-track-tick-line"
252
+ accessibilityElementsHidden
253
+ style={{
254
+ position: 'absolute',
255
+ top: (markerHeight - trackHeight) / 2 - 2,
256
+ left: pct(fraction(tick.value, min, max)),
257
+ width: lineWidth,
258
+ height: trackHeight + 4,
259
+ borderRadius: lineWidth / 2,
260
+ backgroundColor: lineColor,
261
+ transform: [{ translateX: -lineWidth / 2 }],
262
+ zIndex: 3,
263
+ }}
264
+ />
265
+ )
266
+ })}
267
+ </View>
268
+
269
+ {ticks != null && ticks.length > 0 && (
270
+ <View style={{ position: 'relative', height: 16, marginTop: 5 }}>
271
+ {ticks.map((tick, i) => {
272
+ if (tick.label == null) return null
273
+ const emphasized = tick.emphasized === true
274
+ const labelColor = tick.color ?? (emphasized ? t['brand-primary'] : TICK_COLOR)
275
+ const labelNode = (
276
+ <Text
277
+ testID="zone-track-tick-label"
278
+ style={{
279
+ fontSize: 9,
280
+ letterSpacing: 0.5,
281
+ color: labelColor,
282
+ fontFamily: 'monospace',
283
+ fontWeight: emphasized ? '700' : '400',
284
+ }}
285
+ >
286
+ {tick.label}
287
+ </Text>
288
+ )
289
+ return (
290
+ <View
291
+ key={`label-${i}`}
292
+ testID="zone-track-tick"
293
+ style={{
294
+ position: 'absolute',
295
+ left: pct(fraction(tick.value, min, max)),
296
+ transform: [{ translateX: -14 }],
297
+ width: 28,
298
+ alignItems: 'center',
299
+ }}
300
+ >
301
+ {tick.tooltip != null ? (
302
+ <Tooltip label={tick.tooltip} placement="top">
303
+ {labelNode}
304
+ </Tooltip>
305
+ ) : (
306
+ labelNode
307
+ )}
308
+ </View>
309
+ )
310
+ })}
311
+ </View>
312
+ )}
313
+ </View>
314
+ )
315
+ }
@@ -29,6 +29,7 @@ export { WorkoutPill, type WorkoutPillProps, type WorkoutPillStatus } from './Wo
29
29
  export {
30
30
  VelocityStrip,
31
31
  type VelocityStripProps,
32
+ type VelocitySet,
32
33
  type VelocityZoneBandProp,
33
34
  getVelocityZoneColor,
34
35
  getVelocityZoneName,
@@ -37,10 +38,46 @@ export {
37
38
  } from './VelocityStrip'
38
39
  export { MuscleGroupChip, type MuscleGroupChipProps, type VolumeStatus } from './MuscleGroupChip'
39
40
  export { Sparkline, type SparklineProps } from './Sparkline'
40
- export { SetRow, type SetRowProps, type SetRowMode } from './SetRow'
41
+ export { SetRow, type SetRowProps, type SetRowState, type SetRowUnit } from './SetRow'
41
42
  export { InputBar, type InputBarProps } from './InputBar'
42
43
  export { RestTimer, type RestTimerProps } from './RestTimer'
43
- export { ExerciseCard, type ExerciseCardProps, type ExerciseCardState } from './ExerciseCard'
44
+ export { MetricCell, type MetricCellProps, METRIC_FONT } from './metricText'
45
+ export { SetsRepsLoad, type SetsRepsLoadProps } from './SetsRepsLoad'
46
+ export { SegmentedBar, type SegmentedBarProps, type SegmentedBarSegment } from './SegmentedBar'
47
+ export { paceTone, paceToneColor, type PaceTone } from './paceTone'
48
+ export {
49
+ SegmentedProgressBar,
50
+ type SegmentedProgressBarProps,
51
+ type SegmentedProgressBarSegment,
52
+ } from './SegmentedProgressBar'
53
+ export { MetricTiles, type MetricTilesProps, type MetricTileData } from './MetricTiles'
54
+ export { ScheduleTiles, type ScheduleTilesProps } from './ScheduleTiles'
55
+ export { SetBar, type SetBarProps } from './SetBar'
56
+ export {
57
+ SetStrip,
58
+ type SetStripProps,
59
+ type SetStripSet,
60
+ SET_STRIP_ZONES,
61
+ SET_STRIP_VARIABLE_COLOR,
62
+ velocityZoneColor,
63
+ } from './SetStrip'
64
+ export {
65
+ ExerciseIndicator,
66
+ resolveIndicator,
67
+ INDICATOR_PRECEDENCE,
68
+ type ExerciseIndicatorProps,
69
+ type ExerciseIndicatorKind,
70
+ } from './ExerciseIndicator'
71
+ export { ExerciseHeading, type ExerciseHeadingProps } from './ExerciseHeading'
72
+ export { ExerciseCardHeading, type ExerciseCardHeadingProps } from './ExerciseCardHeading'
73
+ export { ExerciseCard, type ExerciseCardProps } from './ExerciseCard'
74
+ export { SetTableHeader, type SetTableHeaderProps } from './SetTableHeader'
75
+ export {
76
+ SessionHeader,
77
+ type SessionHeaderProps,
78
+ type SessionHeaderPlanEntry,
79
+ } from './SessionHeader'
80
+ export { SessionRail, type SessionRailProps, type SessionRailExercise } from './SessionRail'
44
81
  export { SupersetWrapper, type SupersetWrapperProps } from './SupersetWrapper'
45
82
  export {
46
83
  MesoProgressBar,
@@ -152,3 +189,19 @@ export type {
152
189
  WorkoutProgress,
153
190
  WorkoutGroup,
154
191
  } from './ActiveWorkoutPage'
192
+ export { ZoneTrack } from './ZoneTrack'
193
+ export type {
194
+ ZoneTrackProps,
195
+ ZoneTrackZone,
196
+ ZoneTrackTick,
197
+ ZoneTrackMarker,
198
+ ZoneTrackBand,
199
+ } from './ZoneTrack'
200
+ export { FatigueMeter } from './FatigueMeter'
201
+ export type { FatigueMeterProps } from './FatigueMeter'
202
+ export { VolumeLandmarkBar } from './VolumeLandmarkBar'
203
+ export type { VolumeLandmarkBarProps, VolumeZone } from './VolumeLandmarkBar'
204
+ export { StatusPill, statusPillColor } from './StatusPill'
205
+ export type { StatusPillProps, StatusPillStatus } from './StatusPill'
206
+ export { LiveAuraFrame, liveAuraColor } from './LiveAuraFrame'
207
+ export type { LiveAuraFrameProps, LiveAuraCategory } from './LiveAuraFrame'
@@ -0,0 +1,69 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { MetricCell } from './metricText'
4
+
5
+ /**
6
+ * `MetricCell` — the shared "segmented metric" typography primitive (Inter · 600 ·
7
+ * letter-spacing 1). Value cells take a primary color, separators a muted one. The
8
+ * single source of the value/separator look shared by SetsRepsLoad and TempoDisplay.
9
+ */
10
+ const meta: Meta<typeof MetricCell> = {
11
+ title: 'Workout/MetricCell',
12
+ component: MetricCell,
13
+ tags: ['autodocs'],
14
+ parameters: {
15
+ docs: {
16
+ description: {
17
+ component:
18
+ '**Primitive.** The shared Inter·600·letter-spacing-1 value/separator cell. ' +
19
+ 'Extracted so the two prescription readouts read as one visual language instead of ' +
20
+ 'drifting copies. Used-by ↑ ' +
21
+ '[SetsRepsLoad](?path=/docs/workout-setsrepsload--docs) + ' +
22
+ '[TempoDisplay](?path=/docs/workout-tempodisplay--docs).',
23
+ },
24
+ },
25
+ },
26
+ argTypes: {
27
+ color: { control: 'color' },
28
+ fontSize: { control: { type: 'range', min: 9, max: 24, step: 1 } },
29
+ },
30
+ decorators: [
31
+ (Story) => (
32
+ <View style={{ padding: 20, backgroundColor: '#131313' }}>
33
+ <Story />
34
+ </View>
35
+ ),
36
+ ],
37
+ }
38
+
39
+ export default meta
40
+ type Story = StoryObj<typeof MetricCell>
41
+
42
+ export const ValueCell: Story = {
43
+ args: { children: '145', color: '#F3F4F6', fontSize: 11 },
44
+ }
45
+
46
+ export const Separator: Story = {
47
+ args: { children: '×', color: '#6B7280', fontSize: 11 },
48
+ parameters: { docs: { description: { story: 'A muted separator cell (× / @ / -).' } } },
49
+ }
50
+
51
+ export const InContext: Story = {
52
+ render: () => (
53
+ <View style={{ flexDirection: 'row', alignItems: 'baseline' }}>
54
+ <MetricCell color="#F3F4F6">5</MetricCell>
55
+ <MetricCell color="#6B7280">&nbsp;×&nbsp;</MetricCell>
56
+ <MetricCell color="#F3F4F6">8</MetricCell>
57
+ <MetricCell color="#6B7280">&nbsp;@&nbsp;</MetricCell>
58
+ <MetricCell color="#F3F4F6">145</MetricCell>
59
+ <MetricCell color="#6B7280">&nbsp;lb</MetricCell>
60
+ </View>
61
+ ),
62
+ parameters: {
63
+ docs: {
64
+ description: {
65
+ story: 'Value + separator cells composed into the sets × reps @ load language.',
66
+ },
67
+ },
68
+ },
69
+ }
@@ -0,0 +1,34 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import type { ReactNode } from 'react'
3
+ import { Text } from 'react-native'
4
+
5
+ /**
6
+ * Shared "segmented metric" typography primitive — the Inter · 600 · letter-spacing 1
7
+ * value/separator cell used by both {@link TempoDisplay} (tempo digits + dashes) and
8
+ * {@link SetsRepsLoad} (sets × reps @ load). Extracted so the two read as one visual
9
+ * language instead of drifting copies.
10
+ */
11
+ export const METRIC_FONT = 'Inter, sans-serif'
12
+
13
+ export interface MetricCellProps {
14
+ children: ReactNode
15
+ /** Cell text color — value cells take a primary color, separators a muted one. */
16
+ color: string
17
+ fontSize?: number
18
+ }
19
+
20
+ export function MetricCell({ children, color, fontSize = 11 }: MetricCellProps) {
21
+ return (
22
+ <Text
23
+ style={{
24
+ fontFamily: METRIC_FONT,
25
+ fontSize,
26
+ color,
27
+ fontWeight: '600',
28
+ letterSpacing: 1,
29
+ }}
30
+ >
31
+ {children}
32
+ </Text>
33
+ )
34
+ }