@titan-design/react-ui 0.6.0 → 0.8.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 (258) 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 +956 -128
  6. package/dist/index.d.ts +956 -128
  7. package/dist/index.js +2873 -981
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2826 -982
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/{pages-MO0JCySL.d.mts → pages-DCFBqp79.d.mts} +289 -57
  12. package/dist/{pages-D7Jlssvp.d.ts → pages-_fI3-x7Z.d.ts} +289 -57
  13. package/dist/pages.d.mts +1 -1
  14. package/dist/pages.d.ts +1 -1
  15. package/dist/pages.js +1659 -799
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1660 -800
  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/CircularTimer/CircularTimer.stories.tsx +90 -0
  34. package/src/components/custom/CircularTimer/CircularTimer.test.tsx +62 -0
  35. package/src/components/custom/CircularTimer/CircularTimer.tsx +112 -0
  36. package/src/components/custom/CircularTimer/index.ts +1 -0
  37. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  38. package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
  39. package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
  40. package/src/components/custom/Metric/Metric.stories.tsx +1 -1
  41. package/src/components/custom/Scatter/Scatter.stories.tsx +1 -1
  42. package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
  43. package/src/components/custom/Table/Table.stories.tsx +1 -1
  44. package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +62 -0
  45. package/src/components/custom/TimerReadout/TimerReadout.test.tsx +72 -0
  46. package/src/components/custom/TimerReadout/TimerReadout.tsx +75 -0
  47. package/src/components/custom/TimerReadout/index.ts +1 -0
  48. package/src/components/custom/Treemap/Treemap.stories.tsx +1 -1
  49. package/src/components/custom/Typography/Typography.stories.tsx +1 -1
  50. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +21 -13
  51. package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +16 -13
  52. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +9 -15
  53. package/src/components/custom/Workout/BaseBadge.stories.tsx +1 -1
  54. package/src/components/custom/Workout/BodyMap.stories.tsx +1 -1
  55. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
  56. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
  57. package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
  58. package/src/components/custom/Workout/ExerciseCard.stories.tsx +51 -66
  59. package/src/components/custom/Workout/ExerciseCard.test.tsx +146 -143
  60. package/src/components/custom/Workout/ExerciseCard.tsx +176 -198
  61. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +99 -0
  62. package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +72 -0
  63. package/src/components/custom/Workout/ExerciseCardHeading.tsx +43 -0
  64. package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +151 -0
  65. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +5 -3
  66. package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +4 -4
  67. package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -5
  68. package/src/components/custom/Workout/ExerciseHeading.stories.tsx +81 -0
  69. package/src/components/custom/Workout/ExerciseHeading.test.tsx +77 -0
  70. package/src/components/custom/Workout/ExerciseHeading.tsx +85 -0
  71. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +110 -0
  72. package/src/components/custom/Workout/ExerciseIndicator.test.tsx +93 -0
  73. package/src/components/custom/Workout/ExerciseIndicator.tsx +120 -0
  74. package/src/components/custom/Workout/FatigueMeter.stories.tsx +51 -0
  75. package/src/components/custom/Workout/FatigueMeter.test.tsx +85 -0
  76. package/src/components/custom/Workout/FatigueMeter.tsx +86 -0
  77. package/src/components/custom/Workout/InputBar.stories.tsx +1 -1
  78. package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
  79. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +153 -0
  80. package/src/components/custom/Workout/LiveAuraFrame.test.tsx +74 -0
  81. package/src/components/custom/Workout/LiveAuraFrame.tsx +105 -0
  82. package/src/components/custom/Workout/MesoCard.stories.tsx +1 -1
  83. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
  84. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
  85. package/src/components/custom/Workout/MetricTiles.stories.tsx +60 -0
  86. package/src/components/custom/Workout/MetricTiles.test.tsx +58 -0
  87. package/src/components/custom/Workout/MetricTiles.tsx +48 -0
  88. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +1 -1
  89. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
  90. package/src/components/custom/Workout/PrBadge.stories.tsx +1 -1
  91. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
  92. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
  93. package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -6
  94. package/src/components/custom/Workout/README.md +193 -4
  95. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
  96. package/src/components/custom/Workout/RestTimer.stories.tsx +133 -3
  97. package/src/components/custom/Workout/RestTimer.test.tsx +73 -0
  98. package/src/components/custom/Workout/RestTimer.tsx +155 -59
  99. package/src/components/custom/Workout/RowInventory.stories.tsx +405 -0
  100. package/src/components/custom/Workout/S3FullRail.stories.tsx +34 -0
  101. package/src/components/custom/Workout/S3SessionPace.stories.tsx +95 -0
  102. package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +61 -0
  103. package/src/components/custom/Workout/S3SetTypes.stories.tsx +176 -0
  104. package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +240 -0
  105. package/src/components/custom/Workout/ScheduleTiles.stories.tsx +40 -0
  106. package/src/components/custom/Workout/ScheduleTiles.test.tsx +34 -0
  107. package/src/components/custom/Workout/ScheduleTiles.tsx +70 -0
  108. package/src/components/custom/Workout/SegmentedBar.stories.tsx +99 -0
  109. package/src/components/custom/Workout/SegmentedBar.test.tsx +77 -0
  110. package/src/components/custom/Workout/SegmentedBar.tsx +172 -0
  111. package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +50 -0
  112. package/src/components/custom/Workout/SegmentedProgressBar.test.tsx +80 -0
  113. package/src/components/custom/Workout/SegmentedProgressBar.tsx +67 -0
  114. package/src/components/custom/Workout/SessionHeader.stories.tsx +121 -0
  115. package/src/components/custom/Workout/SessionHeader.test.tsx +122 -0
  116. package/src/components/custom/Workout/SessionHeader.tsx +152 -0
  117. package/src/components/custom/Workout/SessionRail.stories.tsx +153 -0
  118. package/src/components/custom/Workout/SessionRail.test.tsx +86 -0
  119. package/src/components/custom/Workout/SessionRail.tsx +126 -0
  120. package/src/components/custom/Workout/SetBar.stories.tsx +124 -0
  121. package/src/components/custom/Workout/SetBar.test.tsx +123 -0
  122. package/src/components/custom/Workout/SetBar.tsx +171 -0
  123. package/src/components/custom/Workout/SetRow.stories.tsx +50 -100
  124. package/src/components/custom/Workout/SetRow.test.tsx +94 -166
  125. package/src/components/custom/Workout/SetRow.tsx +169 -232
  126. package/src/components/custom/Workout/SetStrip.stories.tsx +154 -0
  127. package/src/components/custom/Workout/SetStrip.test.tsx +109 -0
  128. package/src/components/custom/Workout/SetStrip.tsx +71 -0
  129. package/src/components/custom/Workout/SetTableHeader.stories.tsx +66 -0
  130. package/src/components/custom/Workout/SetTableHeader.test.tsx +47 -0
  131. package/src/components/custom/Workout/SetTableHeader.tsx +88 -0
  132. package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +47 -0
  133. package/src/components/custom/Workout/SetsRepsLoad.test.tsx +44 -0
  134. package/src/components/custom/Workout/SetsRepsLoad.tsx +52 -0
  135. package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
  136. package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
  137. package/src/components/custom/Workout/StatusPill.stories.tsx +59 -0
  138. package/src/components/custom/Workout/StatusPill.test.tsx +63 -0
  139. package/src/components/custom/Workout/StatusPill.tsx +88 -0
  140. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +1 -1
  141. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
  142. package/src/components/custom/Workout/TempoBar.stories.tsx +70 -1
  143. package/src/components/custom/Workout/TempoBar.test.tsx +119 -4
  144. package/src/components/custom/Workout/TempoBar.tsx +107 -22
  145. package/src/components/custom/Workout/TempoDisplay.stories.tsx +12 -1
  146. package/src/components/custom/Workout/TempoDisplay.tsx +23 -33
  147. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +1 -1
  148. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +254 -0
  149. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +145 -0
  150. package/src/components/custom/Workout/VelocityStrip.stories.tsx +262 -56
  151. package/src/components/custom/Workout/VelocityStrip.test.tsx +440 -141
  152. package/src/components/custom/Workout/VelocityStrip.tsx +776 -141
  153. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +73 -0
  154. package/src/components/custom/Workout/VolumeLandmarkBar.test.tsx +119 -0
  155. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +154 -0
  156. package/src/components/custom/Workout/WeekRow.stories.tsx +1 -1
  157. package/src/components/custom/Workout/WeightBadge.stories.tsx +1 -1
  158. package/src/components/custom/Workout/WorkoutCard.stories.tsx +2 -6
  159. package/src/components/custom/Workout/WorkoutCard.test.tsx +13 -46
  160. package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
  161. package/src/components/custom/Workout/ZoneTrack.stories.tsx +139 -0
  162. package/src/components/custom/Workout/ZoneTrack.test.tsx +234 -0
  163. package/src/components/custom/Workout/ZoneTrack.tsx +373 -0
  164. package/src/components/custom/Workout/index.ts +55 -2
  165. package/src/components/custom/Workout/metricText.stories.tsx +69 -0
  166. package/src/components/custom/Workout/metricText.tsx +34 -0
  167. package/src/components/custom/Workout/paceTone.test.ts +29 -0
  168. package/src/components/custom/Workout/paceTone.ts +27 -0
  169. package/src/components/custom/Workout/setHeadingKit.tsx +177 -0
  170. package/src/components/custom/index.ts +2 -0
  171. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  172. package/src/components/icons/icons.stories.tsx +67 -2
  173. package/src/components/icons/icons.test.tsx +32 -2
  174. package/src/components/icons/icons.tsx +108 -0
  175. package/src/components/shell/BrandLockup.stories.tsx +3 -3
  176. package/src/components/shell/DashboardShell.stories.tsx +65 -0
  177. package/src/components/shell/DashboardShell.test.tsx +42 -0
  178. package/src/components/shell/DashboardShell.tsx +74 -0
  179. package/src/components/shell/DeviceIndicator.stories.tsx +2 -2
  180. package/src/components/shell/DeviceMenu.stories.tsx +5 -5
  181. package/src/components/shell/DeviceRow.stories.tsx +3 -3
  182. package/src/components/shell/NavItem.stories.tsx +52 -0
  183. package/src/components/shell/NavItem.test.tsx +35 -0
  184. package/src/components/shell/NavItem.tsx +75 -0
  185. package/src/components/shell/README.md +30 -0
  186. package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
  187. package/src/components/shell/SessionStatePill.stories.tsx +3 -3
  188. package/src/components/shell/SessionStatePill.tsx +1 -1
  189. package/src/components/shell/SideNav.stories.tsx +81 -0
  190. package/src/components/shell/SideNav.test.tsx +51 -0
  191. package/src/components/shell/SideNav.tsx +74 -0
  192. package/src/components/shell/TopBar.stories.tsx +5 -5
  193. package/src/components/shell/index.ts +5 -0
  194. package/src/components/ui/alert/Alert.stories.tsx +86 -3
  195. package/src/components/ui/alert/Alert.test.tsx +52 -0
  196. package/src/components/ui/alert/Alert.tsx +54 -20
  197. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  198. package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
  199. package/src/components/ui/badge/Badge.stories.tsx +1 -1
  200. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  201. package/src/components/ui/button/Button.stories.tsx +1 -1
  202. package/src/components/ui/card/Card.stories.tsx +1 -1
  203. package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
  204. package/src/components/ui/chip/Chip.stories.tsx +1 -1
  205. package/src/components/ui/collapse/Collapse.stories.tsx +1 -1
  206. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  207. package/src/components/ui/divider/Divider.stories.tsx +1 -1
  208. package/src/components/ui/drawer/Drawer.stories.tsx +1 -1
  209. package/src/components/ui/form-field/FormField.stories.tsx +1 -1
  210. package/src/components/ui/help-tip/HelpTip.stories.tsx +1 -1
  211. package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
  212. package/src/components/ui/index.ts +1 -0
  213. package/src/components/ui/indicator/Indicator.stories.tsx +2 -2
  214. package/src/components/ui/indicator/Indicator.test.tsx +1 -0
  215. package/src/components/ui/indicator/Indicator.tsx +6 -1
  216. package/src/components/ui/input/Input.stories.tsx +1 -1
  217. package/src/components/ui/link/Link.stories.tsx +1 -1
  218. package/src/components/ui/list-item/ListItem.stories.tsx +1 -1
  219. package/src/components/ui/menu/Menu.stories.tsx +1 -1
  220. package/src/components/ui/modal/Modal.stories.tsx +1 -1
  221. package/src/components/ui/pill/Pill.stories.tsx +1 -1
  222. package/src/components/ui/popover/Popover.stories.tsx +1 -1
  223. package/src/components/ui/progress/Progress.stories.tsx +1 -1
  224. package/src/components/ui/progress/Progress.tsx +47 -22
  225. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  226. package/src/components/ui/section/Section.stories.tsx +1 -1
  227. package/src/components/ui/select/Select.stories.tsx +1 -1
  228. package/src/components/ui/skeleton/Skeleton.stories.tsx +1 -1
  229. package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
  230. package/src/components/ui/stack/Stack.stories.tsx +1 -1
  231. package/src/components/ui/surface/Surface.stories.tsx +1 -1
  232. package/src/components/ui/switch/Switch.stories.tsx +1 -1
  233. package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
  234. package/src/components/ui/tile/Tile.stories.tsx +79 -0
  235. package/src/components/ui/tile/Tile.test.tsx +48 -0
  236. package/src/components/ui/tile/Tile.tsx +64 -0
  237. package/src/components/ui/tile/index.ts +2 -0
  238. package/src/components/ui/toast/Toast.stories.tsx +1 -1
  239. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
  240. package/src/components/ui/tooltip/Tooltip.stories.tsx +1 -1
  241. package/src/examples/react-hook-form/ReactHookForm.stories.tsx +1 -1
  242. package/src/hooks/index.ts +2 -0
  243. package/src/hooks/useTimer.test.ts +150 -0
  244. package/src/hooks/useTimer.ts +77 -0
  245. package/src/index.ts +3 -0
  246. package/src/pages.ts +1 -1
  247. package/src/stories/PresetShowcase.stories.tsx +1 -1
  248. package/src/stories/ThemePresets.stories.tsx +1 -1
  249. package/src/theme/ColorSystem.stories.tsx +1 -1
  250. package/src/theme/Colors.stories.tsx +5 -8
  251. package/src/theme/config.ts +4 -0
  252. package/src/theme/elevation.stories.tsx +1 -1
  253. package/src/theme/global.css +5 -1
  254. package/src/theme/shadows.stories.tsx +1 -1
  255. package/src/theme/tokens/primitives.ts +11 -107
  256. package/src/theme/tokens/semantic.ts +8 -0
  257. package/src/theme/workout-tokens.ts +11 -7
  258. package/tailwind.config.js +5 -0
@@ -0,0 +1,152 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import { View, Text, type ViewProps } from 'react-native'
3
+ import { primitiveColors } from '../../../theme/tokens/primitives'
4
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
5
+ import { Typography } from '../Typography'
6
+ import { TimerReadout } from '../TimerReadout'
7
+ import { SegmentedProgressBar } from './SegmentedProgressBar'
8
+ import { MetricTiles, type MetricTileData } from './MetricTiles'
9
+ import { ScheduleTiles } from './ScheduleTiles'
10
+ import { paceTone, paceToneColor } from './paceTone'
11
+
12
+ const t = getSemanticColors('dark')
13
+
14
+ // The raised charcoal heading plane (charcoal 400) — reads as one flat surface with
15
+ // the nav; depth lives on the sunk list below it, not here.
16
+ const RAISED = primitiveColors.charcoal[400] // #1F1F1F
17
+
18
+ // The chunked pace bar matches the SetStrip language but sits tighter than the default
19
+ // SetStrip gap (5) — the locked design uses a 3px gap and a 9px track.
20
+ const BAR_HEIGHT = 9
21
+ const BAR_GAP = 3
22
+
23
+ export interface SessionHeaderPlanEntry {
24
+ /** Exercise name (currently informational — chunk widths come from `sets`). */
25
+ name?: string
26
+ /** Planned set count — drives this exercise's chunk width (∝ sets). */
27
+ sets: number
28
+ }
29
+
30
+ export interface SessionHeaderProps extends ViewProps {
31
+ /** Session title — the live session's name, or the next session's name when `next` is set. */
32
+ title: string
33
+ /** Exercises in order → chunk widths (∝ sets). */
34
+ plan: SessionHeaderPlanEntry[]
35
+ /** Fractional sets completed (live). 0/undefined leaves the bar empty. */
36
+ setsDone?: number
37
+ /** Live elapsed time in ms — drives the ⏱ readout and the pace marker. */
38
+ elapsedMs?: number
39
+ /** Optional time budget in ms → adds the pace marker and the ` / budget` suffix. Omit → no marker, plain (steel) progress. */
40
+ budgetMs?: number
41
+ /** Elapsed readout goes live-green while true. Default true (live). */
42
+ running?: boolean
43
+ /** Live stat tiles (Volume / Load / Fatigue). Ignored when `next` is set. */
44
+ metrics?: MetricTileData[]
45
+ /** When set → UPCOMING layout: schedule tiles + empty bar; `title` is the next session's name. */
46
+ next?: number | Date
47
+ className?: string
48
+ }
49
+
50
+ /**
51
+ * The session-rail heading: the session title over a stat row and a chunked pace bar.
52
+ * Live, it shows Volume/Load/Fatigue tiles, a per-exercise progress bar (fill ∝ sets
53
+ * done, coloured green/amber against the elapsed-vs-budget pace, with a live marker),
54
+ * a mono `n/total sets` label and a ⏱ elapsed readout. Upcoming (`next` set), the tiles
55
+ * become Date/Time/Until, the bar is the empty plan shape, and the readout shows the
56
+ * planned duration. Rendered on the raised charcoal plane; presentational. Composes
57
+ * {@link MetricTiles} / {@link ScheduleTiles} + {@link SegmentedProgressBar} +
58
+ * {@link TimerReadout}.
59
+ */
60
+ export function SessionHeader({
61
+ title,
62
+ plan,
63
+ setsDone = 0,
64
+ elapsedMs,
65
+ budgetMs,
66
+ running = true,
67
+ metrics = [],
68
+ next,
69
+ className,
70
+ style,
71
+ ...props
72
+ }: SessionHeaderProps) {
73
+ const upcoming = next != null
74
+ const totalSets = plan.reduce((sum, e) => sum + e.sets, 0)
75
+
76
+ const hasPace = !upcoming && budgetMs != null && elapsedMs != null
77
+ const target = hasPace ? (elapsedMs as number) / (budgetMs as number) : undefined
78
+
79
+ const setsLabel = upcoming ? `${totalSets} sets` : `${Math.floor(setsDone)}/${totalSets} sets`
80
+ const setsLabelColor = upcoming
81
+ ? t['text-secondary']
82
+ : paceToneColor(paceTone(totalSets > 0 ? setsDone / totalSets : 0, target))
83
+
84
+ return (
85
+ <View
86
+ className={className}
87
+ style={[
88
+ {
89
+ backgroundColor: RAISED,
90
+ paddingTop: 11,
91
+ paddingRight: 12,
92
+ paddingBottom: 12,
93
+ paddingLeft: 12,
94
+ zIndex: 2,
95
+ },
96
+ style,
97
+ ]}
98
+ testID="session-rail-header"
99
+ {...props}
100
+ >
101
+ <Text
102
+ accessibilityRole="header"
103
+ style={{
104
+ fontSize: 16,
105
+ lineHeight: 18,
106
+ fontWeight: '700',
107
+ fontFamily: '"Space Grotesk", sans-serif',
108
+ color: t['text-primary'],
109
+ marginBottom: 10,
110
+ }}
111
+ testID="session-rail-title"
112
+ >
113
+ {title}
114
+ </Text>
115
+
116
+ {upcoming ? <ScheduleTiles when={next} /> : <MetricTiles metrics={metrics} />}
117
+
118
+ <View style={{ marginTop: 10 }}>
119
+ <SegmentedProgressBar
120
+ segments={plan.map((e) => ({ weight: e.sets }))}
121
+ value={upcoming ? 0 : setsDone}
122
+ target={target}
123
+ height={BAR_HEIGHT}
124
+ gap={BAR_GAP}
125
+ />
126
+ <View
127
+ style={{
128
+ flexDirection: 'row',
129
+ justifyContent: 'space-between',
130
+ alignItems: 'baseline',
131
+ marginTop: 5,
132
+ }}
133
+ >
134
+ <Typography
135
+ variant="mono"
136
+ style={{ fontSize: 10, color: setsLabelColor }}
137
+ testID="session-rail-sets"
138
+ >
139
+ {setsLabel}
140
+ </Typography>
141
+ <TimerReadout
142
+ mode="up"
143
+ elapsedMs={upcoming ? budgetMs : elapsedMs}
144
+ durationMs={budgetMs}
145
+ running={running && !upcoming}
146
+ showTotal={budgetMs != null && !upcoming}
147
+ />
148
+ </View>
149
+ </View>
150
+ </View>
151
+ )
152
+ }
@@ -0,0 +1,153 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import { primitiveRamps } from '../../../theme/tokens/primitives'
4
+ import { SessionRail, type SessionRailExercise } from './SessionRail'
5
+
6
+ /**
7
+ * `SessionRail` (shell organism) — the live-workout exercise list: a flat raised
8
+ * `SessionHeader` glance (title, stat tiles, chunked pace bar) over a sunk, inset list
9
+ * of ExerciseCard rail headings. The header plan (chunk widths ∝ sets) is derived from
10
+ * `exercises`. Surfaces bind to the charcoal ramp; the list depth is a subtle neumorphic
11
+ * inset. Driven entirely by props.
12
+ */
13
+ const meta: Meta<typeof SessionRail> = {
14
+ title: 'Shell/SessionRail',
15
+ component: SessionRail,
16
+ tags: ['autodocs'],
17
+ argTypes: {
18
+ stripHeight: { control: { type: 'range', min: 2, max: 16, step: 1 } },
19
+ onExercisePress: { action: 'exercise-press' },
20
+ },
21
+ decorators: [
22
+ (Story) => (
23
+ <View className="min-h-screen flex-row" style={{ backgroundColor: '#101010' }}>
24
+ <Story />
25
+ <View className="flex-1 items-center justify-center">
26
+ <Text style={{ color: '#6B7280', fontSize: 12 }}>main viewport</Text>
27
+ </View>
28
+ </View>
29
+ ),
30
+ ],
31
+ parameters: {
32
+ layout: 'fullscreen',
33
+ docs: {
34
+ description: {
35
+ component:
36
+ '**Organism** (shell S3). The live-workout exercise list: raised header glance over ' +
37
+ 'a sunk inset list, subtle neumorphic depth (charcoal ramp + `neumorphicShadows`). ' +
38
+ 'Composes ' +
39
+ '[SessionHeader](?path=/docs/shell-sessionrail-sessionheader--docs) + ' +
40
+ '[ExerciseCardHeading](?path=/docs/workout-exercisecardheading--docs) × N. ' +
41
+ 'Sits beside [SideNav](?path=/docs/shell-sidenav--docs) in the dashboard shell.',
42
+ },
43
+ },
44
+ },
45
+ }
46
+
47
+ export default meta
48
+ type Story = StoryObj<typeof SessionRail>
49
+
50
+ const AMBER = primitiveRamps.amber[300]
51
+
52
+ const decay = (n: number, start: number, span = 0.4): number[] =>
53
+ Array.from({ length: n }, (_, r) => +(start - (span * r) / Math.max(1, n - 1)).toFixed(3))
54
+
55
+ const SESSION: SessionRailExercise[] = [
56
+ {
57
+ name: 'Seated Cable Row',
58
+ summary: { sets: 5, reps: 8, weight: 145, unit: 'lbs' },
59
+ tempo: [3, 1, 2, 0],
60
+ indicator: 'pr',
61
+ setStates: [1.0, 0.9, 0.8, 0.7, 0.6].map((s) => ({ status: 'done', velocities: decay(8, s) })),
62
+ },
63
+ {
64
+ name: 'Incline DB Press',
65
+ summary: { sets: 3, reps: 8, weight: 70, unit: 'lbs' },
66
+ tempo: [3, 0, 3, 0],
67
+ setStates: [0.72, 0.62, 0.54].map((s) => ({ status: 'done', velocities: decay(8, s) })),
68
+ },
69
+ {
70
+ name: 'Cable Chest Press',
71
+ summary: { sets: 3, reps: 10, weight: 90, unit: 'lbs' },
72
+ tempo: [2, 1, 2, 0],
73
+ indicator: 'info',
74
+ setStates: [
75
+ { status: 'done', velocities: decay(10, 0.72) },
76
+ { status: 'active', velocities: decay(5, 0.62), planned: 10 },
77
+ { status: 'todo', planned: 10 },
78
+ ],
79
+ },
80
+ {
81
+ name: 'Standing Calf Raise',
82
+ summary: { sets: 5, reps: 20, weight: 25, unit: 'lbs' },
83
+ tempo: [2, 1, 2, 0],
84
+ upcoming: true,
85
+ setStates: [1, 2, 3, 4, 5].map(() => ({ status: 'todo', planned: 20 })),
86
+ },
87
+ {
88
+ name: 'Face Pull',
89
+ summary: { sets: 3, reps: '15-20', weight: 40, unit: 'lbs' },
90
+ tempo: [2, 1, 2, 0],
91
+ upcoming: true,
92
+ setStates: [1, 2, 3].map(() => ({ status: 'todo', planned: 18 })),
93
+ },
94
+ ]
95
+
96
+ const LIVE_METRICS = [
97
+ { label: 'Volume', value: '76%' },
98
+ { label: 'Load', value: '7.3k' },
99
+ { label: 'Fatigue', value: 'MOD', valueColor: AMBER },
100
+ ]
101
+
102
+ const baseArgs = {
103
+ title: 'Pull A · Intensification',
104
+ exercises: SESSION,
105
+ stripHeight: 8,
106
+ }
107
+
108
+ export const Default: Story = {
109
+ args: {
110
+ ...baseArgs,
111
+ setsDone: 7.2,
112
+ elapsedMs: (42 * 60 + 18) * 1000,
113
+ budgetMs: 60 * 60 * 1000,
114
+ metrics: LIVE_METRICS,
115
+ },
116
+ parameters: {
117
+ docs: {
118
+ description: {
119
+ story: 'Live, behind pace — the header carries the session glance over the exercise list.',
120
+ },
121
+ },
122
+ },
123
+ }
124
+
125
+ export const NoTimeTarget: Story = {
126
+ args: {
127
+ ...baseArgs,
128
+ setsDone: 7.2,
129
+ elapsedMs: (42 * 60 + 18) * 1000,
130
+ metrics: LIVE_METRICS,
131
+ },
132
+ parameters: {
133
+ docs: {
134
+ description: { story: 'Live with no time budget — plain (steel) progress, ⏱ elapsed only.' },
135
+ },
136
+ },
137
+ }
138
+
139
+ export const Upcoming: Story = {
140
+ args: {
141
+ ...baseArgs,
142
+ title: 'Lower B · Volume',
143
+ budgetMs: 52 * 60 * 1000,
144
+ next: Date.now() + 2 * 24 * 60 * 60 * 1000,
145
+ },
146
+ parameters: {
147
+ docs: {
148
+ description: {
149
+ story: 'Upcoming session — the header shows Date / Time / Until + the empty plan bar.',
150
+ },
151
+ },
152
+ },
153
+ }
@@ -0,0 +1,86 @@
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 { SessionRail, type SessionRailExercise } from './SessionRail'
5
+
6
+ const exercises: SessionRailExercise[] = [
7
+ {
8
+ name: 'Seated Cable Row',
9
+ summary: { sets: 5, reps: 8, weight: 145, unit: 'lbs' },
10
+ tempo: [3, 1, 2, 0],
11
+ indicator: 'pr',
12
+ setStates: [{ status: 'done', velocities: [1, 0.9] }],
13
+ },
14
+ {
15
+ name: 'Cable Chest Press',
16
+ summary: { sets: 3, reps: 10, weight: 90, unit: 'lbs' },
17
+ setStates: [{ status: 'active', velocities: [0.6], planned: 10 }],
18
+ },
19
+ {
20
+ name: 'Face Pull',
21
+ summary: { sets: 3, reps: '15-20', weight: 40, unit: 'lbs' },
22
+ upcoming: true,
23
+ setStates: [{ status: 'todo', planned: 18 }],
24
+ },
25
+ ] // total = 11 sets
26
+
27
+ const baseProps = {
28
+ title: 'Pull A · Intensification',
29
+ exercises,
30
+ setsDone: 4,
31
+ elapsedMs: 20 * 60 * 1000,
32
+ budgetMs: 60 * 60 * 1000,
33
+ metrics: [
34
+ { label: 'Volume', value: '40%' },
35
+ { label: 'Load', value: '3.1k' },
36
+ { label: 'Fatigue', value: 'LOW' },
37
+ ],
38
+ }
39
+
40
+ describe('SessionRail', () => {
41
+ it('renders the session title', () => {
42
+ render(<SessionRail {...baseProps} />)
43
+ expect(screen.getByTestId('session-rail-title')).toHaveTextContent('Pull A · Intensification')
44
+ })
45
+
46
+ it('derives the header sets label from the exercises', () => {
47
+ render(<SessionRail {...baseProps} />)
48
+ expect(screen.getByTestId('session-rail-sets')).toHaveTextContent('4/11 sets')
49
+ })
50
+
51
+ it('feeds the live metric tiles into the header', () => {
52
+ render(<SessionRail {...baseProps} />)
53
+ expect(screen.getByText('Volume')).toBeInTheDocument()
54
+ expect(screen.getByText('Fatigue')).toBeInTheDocument()
55
+ })
56
+
57
+ it('renders the upcoming glance when next is set', () => {
58
+ render(
59
+ <SessionRail title="Lower B" exercises={exercises} next={new Date('2026-07-11T18:30:00')} />
60
+ )
61
+ expect(screen.getByText('Date')).toBeInTheDocument()
62
+ expect(screen.getByText('Until')).toBeInTheDocument()
63
+ })
64
+
65
+ it('renders one rail heading per exercise', () => {
66
+ render(<SessionRail {...baseProps} />)
67
+ expect(screen.getAllByTestId('exercise-card')).toHaveLength(3)
68
+ expect(screen.getByText('Seated Cable Row')).toBeInTheDocument()
69
+ expect(screen.getByText('Face Pull')).toBeInTheDocument()
70
+ })
71
+
72
+ it('fires onExercisePress with the exercise and its index', () => {
73
+ const onExercisePress = vi.fn()
74
+ render(<SessionRail {...baseProps} onExercisePress={onExercisePress} />)
75
+ fireEvent.click(screen.getAllByTestId('exercise-card-header')[1])
76
+ expect(onExercisePress).toHaveBeenCalledWith(exercises[1], 1)
77
+ })
78
+
79
+ describe('accessibility', () => {
80
+ it('has no accessibility violations', async () => {
81
+ const { container } = render(<SessionRail {...baseProps} />)
82
+ const results = await axe(container)
83
+ expect(results).toHaveNoViolations()
84
+ })
85
+ })
86
+ })
@@ -0,0 +1,126 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import { View, type ViewProps } from 'react-native'
3
+ import { primitiveColors } from '../../../theme/tokens/primitives'
4
+ import { neumorphicShadows } from '../../../theme/shadows'
5
+ import { ExerciseCardHeading } from './ExerciseCardHeading'
6
+ import { SessionHeader } from './SessionHeader'
7
+ import type { MetricTileData } from './MetricTiles'
8
+ import type { SetStripSet } from './SetStrip'
9
+ import type { ExerciseIndicatorKind } from './ExerciseIndicator'
10
+
11
+ // Charcoal-ramp surfaces (the locked S3 shell shades): the nav + the heading plane
12
+ // read as ONE flat raised surface; the exercise list is sunk. Depth comes only from
13
+ // the list's subtle inset shadow (top cast by the heading, faint left edge by the nav).
14
+ const INSET = primitiveColors.charcoal[800] // #131313 — sunk exercise list
15
+ const DIVIDER = primitiveColors.charcoal[500] // #1C1C1C — row divider
16
+
17
+ const DEFAULT_WIDTH = 246
18
+
19
+ export interface SessionRailExercise {
20
+ /** Stable key + press payload. Falls back to list index when absent. */
21
+ id?: string
22
+ name: string
23
+ summary: { sets: number; reps: number | string; weight: number; unit: 'lbs' | 'kg' }
24
+ tempo?: [number, number, number, number]
25
+ indicator?: ExerciseIndicatorKind
26
+ /** Per-set performance data driving the heading strip. */
27
+ setStates: SetStripSet[]
28
+ /** Dim the row as a not-yet-reached exercise. */
29
+ upcoming?: boolean
30
+ }
31
+
32
+ export interface SessionRailProps extends ViewProps {
33
+ /** Session title — the live session's name, or the next session's name when `next` is set. */
34
+ title: string
35
+ exercises: SessionRailExercise[]
36
+ /** Fractional sets completed (live) — drives the header's pace bar + sets label. */
37
+ setsDone?: number
38
+ /** Live elapsed time in ms — drives the header's ⏱ readout and pace marker. */
39
+ elapsedMs?: number
40
+ /** Optional time budget in ms → header pace marker + ` / budget`. */
41
+ budgetMs?: number
42
+ /** Header elapsed readout goes live-green while true. Default true (live). */
43
+ running?: boolean
44
+ /** Live stat tiles (Volume / Load / Fatigue). Ignored when `next` is set. */
45
+ metrics?: MetricTileData[]
46
+ /** When set → the header renders the UPCOMING glance (Date/Time/Until + empty bar). */
47
+ next?: number | Date
48
+ /** Heading strip height in px, forwarded to each row. Default 8. */
49
+ stripHeight?: number
50
+ /** Rail width in px. Default 246. */
51
+ width?: number
52
+ onExercisePress?: (exercise: SessionRailExercise, index: number) => void
53
+ className?: string
54
+ }
55
+
56
+ /**
57
+ * The live-workout session rail: a flat raised {@link SessionHeader} glance (title,
58
+ * stat tiles, chunked pace bar) over a sunk, inset list of {@link ExerciseCardHeading}
59
+ * exercise headings. The header's plan (chunk widths ∝ sets) is derived from `exercises`.
60
+ * Presentational — driven entirely by props.
61
+ */
62
+ export function SessionRail({
63
+ title,
64
+ exercises,
65
+ setsDone,
66
+ elapsedMs,
67
+ budgetMs,
68
+ running,
69
+ metrics,
70
+ next,
71
+ stripHeight = 8,
72
+ width = DEFAULT_WIDTH,
73
+ onExercisePress,
74
+ className,
75
+ style,
76
+ ...props
77
+ }: SessionRailProps) {
78
+ return (
79
+ <View
80
+ className={className}
81
+ style={[{ width, flexDirection: 'column', backgroundColor: INSET }, style]}
82
+ testID="session-rail"
83
+ {...props}
84
+ >
85
+ <SessionHeader
86
+ title={title}
87
+ plan={exercises.map((ex) => ({ name: ex.name, sets: ex.summary.sets }))}
88
+ setsDone={setsDone}
89
+ elapsedMs={elapsedMs}
90
+ budgetMs={budgetMs}
91
+ running={running}
92
+ metrics={metrics}
93
+ next={next}
94
+ />
95
+
96
+ <View
97
+ style={[{ flex: 1, backgroundColor: INSET }, neumorphicShadows.charcoal.pressed.subtle]}
98
+ testID="session-rail-list"
99
+ >
100
+ {exercises.map((ex, i) => (
101
+ <View
102
+ key={ex.id ?? i}
103
+ style={{
104
+ borderBottomWidth: i < exercises.length - 1 ? 1 : 0,
105
+ borderBottomColor: DIVIDER,
106
+ }}
107
+ >
108
+ <ExerciseCardHeading
109
+ name={ex.name}
110
+ sets={ex.summary.sets}
111
+ reps={ex.summary.reps}
112
+ load={ex.summary.weight}
113
+ unit={ex.summary.unit}
114
+ tempo={ex.tempo}
115
+ indicator={ex.indicator}
116
+ setStates={ex.setStates}
117
+ stripHeight={stripHeight}
118
+ dimmed={ex.upcoming}
119
+ onPress={() => onExercisePress?.(ex, i)}
120
+ />
121
+ </View>
122
+ ))}
123
+ </View>
124
+ </View>
125
+ )
126
+ }
@@ -0,0 +1,124 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { SetBar, type SetStripSet } from './SetBar'
4
+
5
+ /**
6
+ * `SetBar` — ONE set's multi-coloured bar: the butted per-rep colour segments for a
7
+ * single set. `done` = velocity-coloured reps · `active` = performed reps pulse with
8
+ * a grey remainder · `todo` = a solid grey bar. Colours are the real titan ramp pins
9
+ * (red-600 · orange-400 · amber-300 · green-300). {@link SetStrip} lays several side by side.
10
+ */
11
+ const meta: Meta<typeof SetBar> = {
12
+ title: 'Workout/SetBar',
13
+ component: SetBar,
14
+ tags: ['autodocs'],
15
+ parameters: {
16
+ docs: {
17
+ description: {
18
+ component:
19
+ '**Atom.** ONE set’s multi-coloured bar — the per-rep colour segments for a ' +
20
+ 'single set (done / active-with-pulse / todo). Colours are the real titan ramp pins. ' +
21
+ 'Used-by ↑ [SetStrip](?path=/docs/workout-setstrip--docs).',
22
+ },
23
+ },
24
+ },
25
+ argTypes: {
26
+ height: { control: { type: 'range', min: 2, max: 16, step: 1 } },
27
+ },
28
+ decorators: [
29
+ (Story) => (
30
+ <View style={{ width: 180, flexDirection: 'row', padding: 16, backgroundColor: '#131313' }}>
31
+ <Story />
32
+ </View>
33
+ ),
34
+ ],
35
+ }
36
+
37
+ export default meta
38
+ type Story = StoryObj<typeof SetBar>
39
+
40
+ const decay = (n: number, start: number, span = 0.4): number[] =>
41
+ Array.from({ length: n }, (_, r) => +(start - (span * r) / Math.max(1, n - 1)).toFixed(3))
42
+
43
+ export const Done: Story = {
44
+ args: { height: 8, set: { status: 'done', velocities: decay(8, 0.9) } as SetStripSet },
45
+ }
46
+
47
+ export const Active: Story = {
48
+ args: {
49
+ height: 8,
50
+ set: { status: 'active', velocities: decay(5, 0.62), planned: 10 } as SetStripSet,
51
+ },
52
+ parameters: {
53
+ docs: { description: { story: 'Performed reps pulse; the planned remainder is greyed.' } },
54
+ },
55
+ }
56
+
57
+ export const Todo: Story = {
58
+ args: { height: 8, set: { status: 'todo', planned: 10 } as SetStripSet },
59
+ }
60
+
61
+ export const Range: Story = {
62
+ args: {
63
+ height: 8,
64
+ set: { status: 'range', floor: 15, max: 20, doneVels: decay(17, 0.85) } as SetStripSet,
65
+ },
66
+ parameters: {
67
+ docs: {
68
+ description: {
69
+ story:
70
+ 'Variable rep-range (e.g. 15–20): range-max segments. Done reps are velocity-' +
71
+ 'coloured (even past the floor), the committed-todo zone is grey, and the variable-' +
72
+ 'todo zone (floor→max) is the cyan variable/opportunity pin.',
73
+ },
74
+ },
75
+ },
76
+ }
77
+
78
+ export const Drop: Story = {
79
+ args: {
80
+ height: 8,
81
+ set: { status: 'drop', subloads: [decay(10, 0.85), decay(8, 0.7), decay(6, 0.6)] } as SetStripSet,
82
+ },
83
+ parameters: {
84
+ docs: {
85
+ description: {
86
+ story: 'One set, no rest: sub-loads butted internally and split by 2px notches.',
87
+ },
88
+ },
89
+ },
90
+ }
91
+
92
+ export const Myo: Story = {
93
+ args: {
94
+ height: 8,
95
+ set: {
96
+ status: 'myo',
97
+ activation: decay(15, 0.75),
98
+ clusters: [decay(5, 0.6), decay(5, 0.55), decay(5, 0.5)],
99
+ } as SetStripSet,
100
+ },
101
+ parameters: {
102
+ docs: {
103
+ description: {
104
+ story: 'Myo-rep / rest-pause (done): activation chunk + mini-clusters split by 3px gaps.',
105
+ },
106
+ },
107
+ },
108
+ }
109
+
110
+ export const MyoUpcoming: Story = {
111
+ args: {
112
+ height: 8,
113
+ set: { status: 'myo-upcoming', activationLen: 15 } as SetStripSet,
114
+ },
115
+ parameters: {
116
+ docs: {
117
+ description: {
118
+ story:
119
+ 'Planned myo, length unknown: grey activation + a fading cyan "clusters-to-failure" ' +
120
+ 'trail whose right edge is left open — "more coming, count unknown".',
121
+ },
122
+ },
123
+ },
124
+ }