@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,753 @@
1
+ /**
2
+ * S3 · Session Rail — SPECIMEN (rapid-prototype, not yet a hardened component).
3
+ *
4
+ * A faithful React port of the R2/C3 converged rail
5
+ * (coordination/design-explorations/drilldown-pass/r2-synthesis/gallery.html), brought into
6
+ * titan Storybook so we can iterate with the token system + real titan components + side-by-side.
7
+ * Raw hex lives here on purpose — it mirrors the gallery 1:1; tokenization happens at harden.
8
+ *
9
+ * Stories:
10
+ * - R2Baseline — the converged rail exactly as designed (live-session context)
11
+ * - ContextSwap — the same rail across live / historical / idle (NAV-D02)
12
+ * - Directions — DIRECTION A (this faithful rail) vs DIRECTION B (real titan ExerciseCard)
13
+ * rendered at the real rail width, to ground the "what is a rail item?" call.
14
+ */
15
+ import type { Meta, StoryObj } from '@storybook/react-vite'
16
+ import { useState } from 'react'
17
+ import { ExerciseCard, type ExerciseCardProps } from '../custom/Workout/ExerciseCard'
18
+ import type { SetRowProps } from '../custom/Workout/SetRow'
19
+
20
+ // ----------------------------------------------------------------------------- specimen data
21
+ // Transcribed from the r2-synthesis gallery `EX` model (cable pull/push session, "Pull A").
22
+ type SpecSet = { reps: number; meanV: number; loss: number; live?: boolean }
23
+ type SpecExercise = {
24
+ id: string
25
+ name: string
26
+ wt: number
27
+ e1rm: number
28
+ pr?: boolean
29
+ cat: string
30
+ sets: SpecSet[]
31
+ }
32
+
33
+ const SESSION: SpecExercise[] = [
34
+ {
35
+ id: 'cable-row',
36
+ name: 'Seated Cable Row',
37
+ wt: 145,
38
+ e1rm: 205,
39
+ pr: true,
40
+ cat: 'Back',
41
+ sets: [
42
+ { reps: 8, meanV: 0.56, loss: 11 },
43
+ { reps: 8, meanV: 0.54, loss: 15 },
44
+ { reps: 8, meanV: 0.51, loss: 19 },
45
+ { reps: 8, meanV: 0.49, loss: 0, live: true },
46
+ { reps: 7, meanV: 0.47, loss: 26 },
47
+ ],
48
+ },
49
+ {
50
+ id: 'lat-pull',
51
+ name: 'Lat Pulldown',
52
+ wt: 130,
53
+ e1rm: 188,
54
+ cat: 'Back',
55
+ sets: [
56
+ { reps: 10, meanV: 0.64, loss: 9 },
57
+ { reps: 10, meanV: 0.61, loss: 14 },
58
+ { reps: 9, meanV: 0.58, loss: 17 },
59
+ { reps: 8, meanV: 0.55, loss: 22 },
60
+ ],
61
+ },
62
+ {
63
+ id: 'cable-press',
64
+ name: 'Cable Chest Press',
65
+ wt: 90,
66
+ e1rm: 150,
67
+ cat: 'Chest',
68
+ sets: [
69
+ { reps: 10, meanV: 0.58, loss: 10 },
70
+ { reps: 9, meanV: 0.55, loss: 15 },
71
+ { reps: 8, meanV: 0.51, loss: 20 },
72
+ { reps: 8, meanV: 0.48, loss: 24 },
73
+ ],
74
+ },
75
+ {
76
+ id: 'cable-fly',
77
+ name: 'Cable Fly',
78
+ wt: 45,
79
+ e1rm: 78,
80
+ cat: 'Chest',
81
+ sets: [
82
+ { reps: 12, meanV: 0.74, loss: 8 },
83
+ { reps: 12, meanV: 0.71, loss: 12 },
84
+ { reps: 11, meanV: 0.68, loss: 16 },
85
+ ],
86
+ },
87
+ {
88
+ id: 'face-pull',
89
+ name: 'Face Pull',
90
+ wt: 35,
91
+ e1rm: 60,
92
+ cat: 'Rear Delt',
93
+ sets: [
94
+ { reps: 15, meanV: 0.78, loss: 6 },
95
+ { reps: 15, meanV: 0.74, loss: 10 },
96
+ { reps: 14, meanV: 0.7, loss: 14 },
97
+ ],
98
+ },
99
+ ]
100
+
101
+ const LIVE_EX = 'cable-row'
102
+ const LIVE_SET_IDX = 3
103
+ const LIVE_TARGET = 8
104
+ const LIVE_REP = 5 // current rep of the running set
105
+
106
+ type RailContext = 'live' | 'historical' | 'idle'
107
+
108
+ // ----------------------------------------------------------------------------- helpers (gallery)
109
+ const velColor = (v: number): string =>
110
+ v < 0.5 ? '#ff4757' : v < 0.75 ? '#ffa502' : v < 1.0 ? '#ffd43b' : '#2ed573'
111
+ const f2 = (v: number): string => v.toFixed(2)
112
+ const lossClass = (l: number): string => (l >= 25 ? 'bad' : l >= 18 ? 'warn' : 'ok')
113
+
114
+ // ----------------------------------------------------------------------------- faithful CSS
115
+ // All selectors scoped under `.s3spec` so short class names never leak into the RNW canvas.
116
+ const RAIL_CSS = `
117
+ .s3spec{--brand:#FF7900;--brand-soft:rgba(255,121,0,.14);--spine:#131418;--stage:#0f1012;
118
+ --surface:#17181c;--raised:#1e2025;--border:#24262d;--border-strong:#31343d;
119
+ --hover:rgba(255,255,255,.04);--text:#E8EAED;--muted:#8D95A3;--dim:#5b616d;
120
+ --amber:#FFB020;--sky:#2196F3;--v-green:#2ed573;
121
+ --mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;--ease:cubic-bezier(.22,1,.36,1);
122
+ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;color:var(--text)}
123
+ .s3spec .rail{width:246px;flex:0 0 246px;background:var(--spine);border-right:1px solid var(--border);
124
+ display:flex;flex-direction:column;min-height:0;height:100%}
125
+ .s3spec .railhead{padding:11px 13px 9px;border-bottom:1px solid var(--border)}
126
+ .s3spec .railctx{font-size:9px;font-weight:800;letter-spacing:1px;text-transform:uppercase;color:var(--dim);display:flex;align-items:center;gap:5px}
127
+ .s3spec .railctx .d{width:6px;height:6px;border-radius:50%;background:var(--v-green)}
128
+ .s3spec .railctx.hist .d{background:var(--sky)}
129
+ .s3spec .railtitle{font-size:14px;font-weight:800;margin-top:3px;letter-spacing:-.2px}
130
+ .s3spec .railmeta{font-size:10px;color:var(--muted);margin-top:2px;font-family:var(--mono)}
131
+ .s3spec .railscroll{flex:1;overflow-y:auto;overflow-x:hidden;padding:6px 8px}
132
+ .s3spec .railscroll::-webkit-scrollbar{width:6px}
133
+ .s3spec .railscroll::-webkit-scrollbar-thumb{background:#2a2d34;border-radius:3px}
134
+ .s3spec .rex{border-radius:9px;margin-bottom:3px;border:1px solid transparent;transition:background .15s}
135
+ .s3spec .rex.active{background:var(--brand-soft);border-color:rgba(255,121,0,.28)}
136
+ .s3spec .rexhead{padding:8px 9px;cursor:pointer;display:flex;flex-direction:column;gap:6px;border-radius:9px;transition:background .15s}
137
+ .s3spec .rexhead:hover{background:var(--hover)}
138
+ .s3spec .rextop{display:flex;align-items:center;gap:6px}
139
+ .s3spec .rexidx{font-family:var(--mono);font-size:10px;color:var(--dim);width:13px;flex:0 0 auto}
140
+ .s3spec .rexname{font-size:12px;font-weight:700;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
141
+ .s3spec .rextag{font-size:8px;font-family:var(--mono);text-transform:uppercase;letter-spacing:.5px;color:var(--v-green)}
142
+ .s3spec .rexe1{font-family:var(--mono);font-size:10px;color:var(--dim)}
143
+ .s3spec .rexchev{width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid var(--dim);transition:transform .3s var(--ease)}
144
+ .s3spec .rex.open .rexchev{transform:rotate(180deg)}
145
+ .s3spec .rticks{display:flex;gap:3px;padding-left:19px}
146
+ .s3spec .rtick{flex:1;height:15px;border-radius:3px;background:#22252c;position:relative;overflow:hidden;transition:transform .12s}
147
+ .s3spec .rtick.up{background:none;border:1px dashed #3a3d44;opacity:.5}
148
+ .s3spec .rtick.live{box-shadow:0 0 0 2px var(--brand),0 0 10px rgba(255,121,0,.5);animation:s3tickpulse 1.6s infinite}
149
+ @keyframes s3tickpulse{0%,100%{box-shadow:0 0 0 2px var(--brand),0 0 3px rgba(255,121,0,.3)}50%{box-shadow:0 0 0 2px var(--brand),0 0 14px rgba(255,121,0,.7)}}
150
+ .s3spec .rtick .rf{position:absolute;left:0;right:0;bottom:0;border-radius:3px}
151
+ .s3spec .rtlbl{position:absolute;top:1px;left:0;right:0;text-align:center;font-family:var(--mono);font-size:7px;color:rgba(255,255,255,.6);z-index:2}
152
+ .s3spec .rexbody{overflow:hidden;transition:max-height .4s var(--ease),opacity .3s}
153
+ .s3spec .rsrow{display:flex;align-items:center;gap:7px;padding:6px 9px 6px 22px;font-family:var(--mono);font-size:11px;border-radius:7px}
154
+ .s3spec .rsrow .sn{color:var(--muted);width:30px}
155
+ .s3spec .rsrow .sv{font-weight:800}
156
+ .s3spec .rsrow .sl{margin-left:auto;font-size:10px}
157
+ .s3spec .rsrow .sl.ok{color:var(--v-green)}
158
+ .s3spec .rsrow .sl.warn{color:var(--amber)}
159
+ .s3spec .rsrow .sl.bad{color:#ff4757}
160
+ .s3spec .rsrow.livenow{color:var(--v-green)}
161
+ .s3spec .pace{border-top:1px solid var(--border);padding:11px 13px;background:linear-gradient(180deg,#141519,#101113)}
162
+ .s3spec .pacehead{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
163
+ .s3spec .pacetitle{font-size:10px;font-weight:800;letter-spacing:.6px;text-transform:uppercase;color:var(--brand)}
164
+ .s3spec .pacetoggle{font-size:8px;font-family:var(--mono);color:var(--dim);border:1px solid var(--border-strong);border-radius:5px;padding:2px 5px;cursor:pointer;letter-spacing:.3px}
165
+ .s3spec .pacetime{display:flex;align-items:baseline;gap:6px;font-family:var(--mono)}
166
+ .s3spec .pacetime .big{font-size:20px;font-weight:800;letter-spacing:-.5px}
167
+ .s3spec .pacetime .of{font-size:11px;color:var(--muted)}
168
+ .s3spec .pacebar{height:6px;border-radius:3px;background:#22252c;margin:7px 0 3px;overflow:hidden;position:relative}
169
+ .s3spec .pacebar .pf{position:absolute;left:0;top:0;bottom:0;border-radius:3px}
170
+ .s3spec .pacebar .budget{position:absolute;top:-2px;bottom:-2px;width:2px;background:var(--text);opacity:.7}
171
+ .s3spec .pacemetrics{display:flex;gap:4px;margin:8px 0}
172
+ .s3spec .pm{flex:1;text-align:center;background:var(--raised);border-radius:7px;padding:6px 3px}
173
+ .s3spec .pm .k{font-size:8px;text-transform:uppercase;letter-spacing:.4px;color:var(--dim);font-weight:800}
174
+ .s3spec .pm .v{font-family:var(--mono);font-size:13px;font-weight:800;margin-top:2px}
175
+ .s3spec .pacesuggest{font-size:10.5px;line-height:1.4;border-radius:8px;padding:8px 9px;display:flex;gap:7px;align-items:flex-start}
176
+ .s3spec .pacesuggest.behind{background:rgba(255,176,32,.12);border:1px solid rgba(255,176,32,.35);color:var(--amber)}
177
+ .s3spec .pacesuggest.ahead{background:rgba(46,213,115,.1);border:1px solid rgba(46,213,115,.3);color:var(--v-green)}
178
+ .s3spec .pacesuggest .ic{flex:0 0 auto;font-size:12px}
179
+ .s3spec .pacesuggest b{color:var(--text)}
180
+ .s3spec .speccol{display:flex;flex-direction:column;height:100vh}
181
+ /* Inside a flex-COLUMN the rail flex-basis would set HEIGHT; override to fill height, keep 246 width. */
182
+ .s3spec .speccol > .rail{flex:1 1 auto;min-height:0}
183
+ .s3spec .speclabel{font-family:var(--mono);font-size:10px;color:var(--dim);letter-spacing:.4px;padding:6px 0 8px 2px;flex:0 0 auto}
184
+ `
185
+
186
+ // ----------------------------------------------------------------------------- render pieces
187
+ function TickRow({ ex, live }: { ex: SpecExercise; live: boolean }) {
188
+ const isLiveEx = live && ex.id === LIVE_EX
189
+ return (
190
+ <div className="rticks">
191
+ {ex.sets.map((st, s) => {
192
+ if (isLiveEx && s > LIVE_SET_IDX) return <div key={s} className="rtick up" />
193
+ if (isLiveEx && s === LIVE_SET_IDX && st.live) {
194
+ const pct = Math.round((LIVE_REP / LIVE_TARGET) * 100)
195
+ return (
196
+ <div key={s} className="rtick live">
197
+ <span className="rtlbl">
198
+ {LIVE_REP}/{LIVE_TARGET}
199
+ </span>
200
+ <div className="rf" style={{ height: `${pct}%`, background: 'var(--brand)' }} />
201
+ </div>
202
+ )
203
+ }
204
+ return (
205
+ <div key={s} className="rtick">
206
+ <span className="rtlbl">{f2(st.meanV)}</span>
207
+ <div
208
+ className="rf"
209
+ style={{ height: '100%', background: velColor(st.meanV), opacity: 0.85 }}
210
+ />
211
+ </div>
212
+ )
213
+ })}
214
+ </div>
215
+ )
216
+ }
217
+
218
+ function SetRows({ ex, live }: { ex: SpecExercise; live: boolean }) {
219
+ const isLiveEx = live && ex.id === LIVE_EX
220
+ return (
221
+ <>
222
+ {ex.sets.map((s, i) => {
223
+ if (isLiveEx && i > LIVE_SET_IDX)
224
+ return (
225
+ <div key={i} className="rsrow" style={{ opacity: 0.5 }}>
226
+ <span className="sn">S{i + 1}</span>
227
+ <span>upcoming</span>
228
+ </div>
229
+ )
230
+ if (isLiveEx && i === LIVE_SET_IDX && s.live)
231
+ return (
232
+ <div key={i} className="rsrow livenow">
233
+ <span className="sn">S{i + 1}</span>
234
+ <span className="sv">
235
+ {LIVE_REP}/{LIVE_TARGET}
236
+ </span>
237
+ <span className="sl">live</span>
238
+ </div>
239
+ )
240
+ return (
241
+ <div key={i} className="rsrow">
242
+ <span className="sn">S{i + 1}</span>
243
+ <span className="sv">{f2(s.meanV)}</span>
244
+ <span style={{ color: 'var(--muted)' }}>
245
+ {s.reps}×{ex.wt}
246
+ </span>
247
+ <span className={`sl ${lossClass(s.loss)}`}>−{s.loss}%</span>
248
+ </div>
249
+ )
250
+ })}
251
+ </>
252
+ )
253
+ }
254
+
255
+ function RailItem({
256
+ ex,
257
+ idx,
258
+ live,
259
+ open,
260
+ onToggle,
261
+ }: {
262
+ ex: SpecExercise
263
+ idx: number
264
+ live: boolean
265
+ open: boolean
266
+ onToggle: () => void
267
+ }) {
268
+ const isLiveEx = live && ex.id === LIVE_EX
269
+ const cls = ['rex']
270
+ if (open) cls.push('open')
271
+ if (isLiveEx) cls.push('active')
272
+ return (
273
+ <div className={cls.join(' ')}>
274
+ <div className="rexhead" onClick={onToggle}>
275
+ <div className="rextop">
276
+ <span className="rexidx">{idx + 1}</span>
277
+ <span className="rexname">{ex.name}</span>
278
+ {isLiveEx ? (
279
+ <span className="rextag">live</span>
280
+ ) : (
281
+ <span className="rexe1">{ex.e1rm} e1RM</span>
282
+ )}
283
+ <span className="rexchev" />
284
+ </div>
285
+ <TickRow ex={ex} live={live} />
286
+ </div>
287
+ <div className="rexbody" style={{ maxHeight: open ? 400 : 0, opacity: open ? 1 : 0.4 }}>
288
+ <SetRows ex={ex} live={live} />
289
+ </div>
290
+ </div>
291
+ )
292
+ }
293
+
294
+ function PaceFooter({
295
+ context,
296
+ ahead,
297
+ onToggle,
298
+ }: {
299
+ context: RailContext
300
+ ahead: boolean
301
+ onToggle: () => void
302
+ }) {
303
+ if (context === 'idle') {
304
+ return (
305
+ <div className="pace">
306
+ <div className="pacehead">
307
+ <span className="pacetitle">Next session budget</span>
308
+ </div>
309
+ <div className="pacetime">
310
+ <span className="big">~52:00</span>
311
+ <span className="of">planned · Lower B</span>
312
+ </div>
313
+ <div className="pacesuggest ahead">
314
+ <span className="ic">◷</span>
315
+ <div>6 exercises · ~20 sets. Wall wakes to Live when the first set streams.</div>
316
+ </div>
317
+ </div>
318
+ )
319
+ }
320
+ const behind = !ahead
321
+ return (
322
+ <div className="pace">
323
+ <div className="pacehead">
324
+ <span className="pacetitle">Session Pace</span>
325
+ <span className="pacetoggle" onClick={onToggle}>
326
+ {behind ? 'show ahead ▸' : 'show behind ▸'}
327
+ </span>
328
+ </div>
329
+ <div className="pacetime">
330
+ <span className="big">42:18</span>
331
+ <span className="of">of 60:00 budget</span>
332
+ </div>
333
+ <div className="pacebar">
334
+ <div
335
+ className="pf"
336
+ style={{ width: '70%', background: behind ? 'var(--amber)' : 'var(--v-green)' }}
337
+ />
338
+ <div className="budget" style={{ left: '100%' }} />
339
+ </div>
340
+ <div className="pacemetrics">
341
+ <div className="pm">
342
+ <div className="k">Volume</div>
343
+ <div className="v">76%</div>
344
+ </div>
345
+ <div className="pm">
346
+ <div className="k">Load</div>
347
+ <div className="v">7.3k</div>
348
+ </div>
349
+ <div className="pm">
350
+ <div className="k">Fatigue</div>
351
+ <div className="v" style={{ color: 'var(--amber)' }}>
352
+ MOD
353
+ </div>
354
+ </div>
355
+ </div>
356
+ {behind ? (
357
+ <div className="pacesuggest behind">
358
+ <span className="ic">▲</span>
359
+ <div>
360
+ <b>Behind pace</b> — ~9 min over budget at this rate. Drop <b>Face Pull</b> to 2 sets to
361
+ finish on time.
362
+ </div>
363
+ </div>
364
+ ) : (
365
+ <div className="pacesuggest ahead">
366
+ <span className="ic">+</span>
367
+ <div>
368
+ <b>Ahead of pace</b> — ~7 min of headroom. Room to add a <b>Cable Fly</b> drop-set or a
369
+ back-off set on Row.
370
+ </div>
371
+ </div>
372
+ )}
373
+ </div>
374
+ )
375
+ }
376
+
377
+ // Shared rail chrome header (context dot + title + meta) — reused by both the adapted rail and the
378
+ // "ExerciseCard exactly" comparison so only the item body differs.
379
+ function RailHeader({ context, meta }: { context: RailContext; meta?: string }) {
380
+ const hist = context === 'historical'
381
+ const ctxLabel =
382
+ context === 'historical'
383
+ ? 'Reviewing · session Mar 11'
384
+ : context === 'idle'
385
+ ? 'Last session'
386
+ : 'Live session'
387
+ const title = context === 'idle' ? 'Lower B · Volume (next)' : 'Pull A · Intensification'
388
+ const resolvedMeta =
389
+ meta ??
390
+ (context === 'historical'
391
+ ? '22 sets · 48 min · complete'
392
+ : context === 'idle'
393
+ ? 'ended 3 days ago'
394
+ : 'ex 1/5 · set 4 live')
395
+ return (
396
+ <div className="railhead">
397
+ <div className={`railctx${hist ? ' hist' : ''}`}>
398
+ <span className="d" />
399
+ {ctxLabel}
400
+ </div>
401
+ <div className="railtitle">{title}</div>
402
+ <div className="railmeta">{resolvedMeta}</div>
403
+ </div>
404
+ )
405
+ }
406
+
407
+ // PIECE 1 · Live Session Plan — the context header + expandable exercise skeleton (set-tick progress).
408
+ function SessionPlan({ context = 'live' as RailContext }: { context?: RailContext }) {
409
+ const live = context === 'live'
410
+ const [open, setOpen] = useState<Record<string, boolean>>({ 'cable-row': true })
411
+ return (
412
+ <>
413
+ <RailHeader context={context} />
414
+ <div className="railscroll">
415
+ {SESSION.map((ex, i) => (
416
+ <RailItem
417
+ key={ex.id}
418
+ ex={ex}
419
+ idx={i}
420
+ live={live}
421
+ open={!!open[ex.id]}
422
+ onToggle={() => setOpen((o) => ({ ...o, [ex.id]: !o[ex.id] }))}
423
+ />
424
+ ))}
425
+ </div>
426
+ </>
427
+ )
428
+ }
429
+
430
+ // The assembled rail = Live Session Plan (piece 1) + Session Pace (piece 2).
431
+ function R2Rail({ context = 'live' as RailContext }: { context?: RailContext }) {
432
+ const [ahead, setAhead] = useState(false)
433
+ return (
434
+ <div className="rail">
435
+ <SessionPlan context={context} />
436
+ <PaceFooter context={context} ahead={ahead} onToggle={() => setAhead((a) => !a)} />
437
+ </div>
438
+ )
439
+ }
440
+
441
+ // ----------------------------------------------------------------------------- ExerciseCard rail (state mix)
442
+ const perRepVel = (s: SpecSet): number[] =>
443
+ Array.from({ length: s.reps }, (_, r) => +(s.meanV + 0.04 - r * 0.01).toFixed(2))
444
+
445
+ type CardStatus = 'finished' | 'active' | 'upcoming'
446
+
447
+ // index-aligned to SESSION: 2 finished · 1 active · 2 upcoming
448
+ const CARD_MIX: { ex: SpecExercise; status: CardStatus }[] = SESSION.map((ex, i) => ({
449
+ ex,
450
+ status: (i < 2 ? 'finished' : i === 2 ? 'active' : 'upcoming') as CardStatus,
451
+ }))
452
+
453
+ const completedSet = (ex: SpecExercise, i: number, s: SpecSet): SetRowProps => ({
454
+ state: 'done',
455
+ setNumber: i + 1,
456
+ reps: s.reps,
457
+ weight: ex.wt,
458
+ rpe: 7 + i * 0.5,
459
+ unit: 'lbs',
460
+ velocities: perRepVel(s),
461
+ })
462
+
463
+ // The active exercise's 3-set mix: done · LIVE · todo. The live set is now a
464
+ // first-class SetRow state — the compact velocity-height spotlight strip, brightened.
465
+ const activeSets = (ex: SpecExercise): SetRowProps[] => [
466
+ completedSet(ex, 0, ex.sets[0]),
467
+ {
468
+ state: 'live',
469
+ setNumber: 2,
470
+ unit: 'lbs',
471
+ target: { reps: 8, weight: ex.wt },
472
+ reps: 8,
473
+ weight: ex.wt,
474
+ velocities: [0.58, 0.55, 0.53, 0.5, 0.48],
475
+ },
476
+ {
477
+ state: 'todo',
478
+ setNumber: 3,
479
+ unit: 'lbs',
480
+ target: { reps: 8, weight: ex.wt },
481
+ },
482
+ ]
483
+
484
+ // Real ExerciseCard props for an exercise given its status + whether it's open (onToggle added by caller).
485
+ function cardProps(
486
+ ex: SpecExercise,
487
+ status: CardStatus,
488
+ open: boolean
489
+ ): Omit<ExerciseCardProps, 'onExpandedChange'> {
490
+ const base = { name: ex.name, isPR: ex.pr }
491
+ const summary = {
492
+ sets: status === 'active' ? 3 : ex.sets.length,
493
+ reps: ex.sets[0].reps,
494
+ weight: ex.wt,
495
+ unit: 'lbs' as const,
496
+ }
497
+ if (status === 'upcoming') {
498
+ // Closed → the dimmed upcoming row; open → an expanded card of planned (todo) sets.
499
+ if (!open) {
500
+ return {
501
+ ...base,
502
+ upcoming: true,
503
+ prescription: `${ex.sets.length} × ${ex.sets[0].reps} @ RPE 8`,
504
+ previousBest: `${ex.sets[0].reps} × ${ex.wt} lb`,
505
+ }
506
+ }
507
+ return {
508
+ ...base,
509
+ expanded: true,
510
+ summary,
511
+ tempo: [3, 1, 2, 0],
512
+ sets: ex.sets.map((s, i) => ({
513
+ state: 'todo',
514
+ setNumber: i + 1,
515
+ unit: 'lbs',
516
+ target: { reps: s.reps, weight: ex.wt },
517
+ })),
518
+ }
519
+ }
520
+ if (open) {
521
+ return {
522
+ ...base,
523
+ expanded: true,
524
+ summary,
525
+ tempo: [3, 1, 2, 0],
526
+ sets: status === 'active' ? activeSets(ex) : ex.sets.map((s, i) => completedSet(ex, i, s)),
527
+ }
528
+ }
529
+ const doneSets = status === 'active' ? ex.sets.slice(0, 1) : ex.sets
530
+ return {
531
+ ...base,
532
+ expanded: false,
533
+ summary,
534
+ totalPlannedSets: status === 'active' ? 3 : ex.sets.length,
535
+ setVelocities: doneSets.map(perRepVel),
536
+ }
537
+ }
538
+
539
+ // The session drawn with the REAL (responsive) ExerciseCard — interactive expand/collapse.
540
+ function ExerciseCardPlan() {
541
+ const [open, setOpen] = useState<Record<string, boolean>>({ [SESSION[2].id]: true })
542
+ return (
543
+ <>
544
+ <RailHeader context="live" meta="ex 3/5 · set 2 live" />
545
+ <div className="railscroll" style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
546
+ {CARD_MIX.map(({ ex, status }) => (
547
+ <ExerciseCard
548
+ key={ex.id}
549
+ {...cardProps(ex, status, !!open[ex.id])}
550
+ onExpandedChange={() => setOpen((o) => ({ ...o, [ex.id]: !o[ex.id] }))}
551
+ />
552
+ ))}
553
+ </div>
554
+ </>
555
+ )
556
+ }
557
+
558
+ // A proper, standalone ExerciseCard at native width — reference point (interactive).
559
+ function ReferenceCard() {
560
+ const [open, setOpen] = useState(true)
561
+ return (
562
+ <ExerciseCard
563
+ {...cardProps(SESSION[0], 'finished', open)}
564
+ onExpandedChange={() => setOpen((v) => !v)}
565
+ />
566
+ )
567
+ }
568
+
569
+ // ----------------------------------------------------------------------------- stories
570
+ function Frame({ children }: { children: React.ReactNode }) {
571
+ return (
572
+ <div
573
+ className="s3spec"
574
+ style={{ display: 'flex', height: '100vh', background: 'var(--stage)' }}
575
+ >
576
+ <style>{RAIL_CSS}</style>
577
+ {children}
578
+ </div>
579
+ )
580
+ }
581
+
582
+ const meta: Meta = {
583
+ title: 'Lab/Specimens/Session Rail',
584
+ parameters: { layout: 'fullscreen' },
585
+ }
586
+ export default meta
587
+ type Story = StoryObj
588
+
589
+ /** The converged R2/C3 rail, live-session context — the "version we have", now in React. */
590
+ export const R2Baseline: Story = {
591
+ render: () => (
592
+ <Frame>
593
+ <R2Rail context="live" />
594
+ <div
595
+ style={{
596
+ flex: 1,
597
+ display: 'flex',
598
+ alignItems: 'center',
599
+ justifyContent: 'center',
600
+ color: 'var(--dim)',
601
+ fontFamily: 'var(--mono)',
602
+ fontSize: 12,
603
+ }}
604
+ >
605
+ main stage
606
+ </div>
607
+ </Frame>
608
+ ),
609
+ }
610
+
611
+ /** NAV-D02 context-swap: the same rail across live / historical / idle, side by side. */
612
+ export const ContextSwap: Story = {
613
+ render: () => (
614
+ <Frame>
615
+ <div className="speccol">
616
+ <div className="speclabel">live</div>
617
+ <R2Rail context="live" />
618
+ </div>
619
+ <div className="speccol">
620
+ <div className="speclabel">historical (Review)</div>
621
+ <R2Rail context="historical" />
622
+ </div>
623
+ <div className="speccol">
624
+ <div className="speclabel">idle</div>
625
+ <R2Rail context="idle" />
626
+ </div>
627
+ </Frame>
628
+ ),
629
+ }
630
+
631
+ const RAIL_DELTAS: [string, string][] = [
632
+ [
633
+ 'Width / fit ✓ DONE',
634
+ 'FIXED in the real component: SetRow fixed columns now flexShrink + PREV minWidth → the 5-col table renders correctly at 246px (was: "8×145" mangled per-char). No-op at 390px. This is the whole "make it reactive" ask.',
635
+ ],
636
+ [
637
+ 'Tick row',
638
+ 'REUSE ExerciseCard’s collapsed strip-row (mini VelocityStrips + PlaceholderStrips) AS the set-tick row — already the same concept (see collapsed items in the left column).',
639
+ ],
640
+ [
641
+ 'Live set marker',
642
+ 'ADD an in-progress tick (pulsing + “5/8” rep count). ExerciseCard only knows completed strips + placeholders — no live state.',
643
+ ],
644
+ [
645
+ 'Expanded body',
646
+ 'OPTIONAL: swap the (now-fitting) 5-col input table for a read-only compact list (mean-con · reps×wt · loss%). Rail is read-only — density preference, no longer a fit blocker.',
647
+ ],
648
+ [
649
+ 'States',
650
+ 'Add context-swap (live/historical/idle) + a “live” header tag + leading index; drop input affordances.',
651
+ ],
652
+ ]
653
+
654
+ /**
655
+ * ExerciseCard EXACTLY AS-IS vs ExerciseCard ADAPTED for the rail, both full 246px rails with pace.
656
+ * The middle panel lists the specific deltas. This is the reuse question made concrete.
657
+ */
658
+ export const Directions: Story = {
659
+ name: 'ExerciseCard · exact vs adapted',
660
+ render: () => (
661
+ <Frame>
662
+ <div className="speccol">
663
+ <div className="speclabel">
664
+ ExerciseCard rail — state mix (2 finished · 1 active · 2 upcoming)
665
+ </div>
666
+ <div className="rail">
667
+ <ExerciseCardPlan />
668
+ </div>
669
+ </div>
670
+ <div className="speccol">
671
+ <div className="speclabel">old rail — adapted item (reference)</div>
672
+ <div className="rail">
673
+ <SessionPlan context="live" />
674
+ </div>
675
+ </div>
676
+ <div className="speccol" style={{ width: 340, padding: '6px 4px 0 14px', overflowY: 'auto' }}>
677
+ <div className="speclabel">reference · real ExerciseCard (native width)</div>
678
+ <div style={{ marginBottom: 22 }}>
679
+ <ReferenceCard />
680
+ </div>
681
+ <div className="speclabel">the rail modifications</div>
682
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12, paddingRight: 8 }}>
683
+ {RAIL_DELTAS.map(([k, v], i) => (
684
+ <div key={i} style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
685
+ <span
686
+ style={{
687
+ fontSize: 11,
688
+ fontWeight: 800,
689
+ letterSpacing: 0.4,
690
+ textTransform: 'uppercase',
691
+ color: '#FF7900',
692
+ }}
693
+ >
694
+ {k}
695
+ </span>
696
+ <span style={{ fontSize: 12, lineHeight: 1.45, color: '#B8BEC9' }}>{v}</span>
697
+ </div>
698
+ ))}
699
+ </div>
700
+ </div>
701
+ </Frame>
702
+ ),
703
+ }
704
+
705
+ // ============================================================ decomposed pieces (for review)
706
+
707
+ /**
708
+ * PIECE 1 · LIVE SESSION PLAN — the context header + expandable exercise skeleton with per-set
709
+ * tick progress, reviewed in isolation across live / historical / idle (no pace footer).
710
+ */
711
+ export const SessionPlanPiece: Story = {
712
+ name: 'Piece · Live Session Plan',
713
+ render: () => (
714
+ <Frame>
715
+ {(['live', 'historical', 'idle'] as RailContext[]).map((ctx) => (
716
+ <div className="speccol" key={ctx}>
717
+ <div className="speclabel">{ctx}</div>
718
+ <div className="rail">
719
+ <SessionPlan context={ctx} />
720
+ </div>
721
+ </div>
722
+ ))}
723
+ </Frame>
724
+ ),
725
+ }
726
+
727
+ /**
728
+ * PIECE 2 · SESSION PACE — the net-new pace tile in isolation, all states: behind / ahead / idle.
729
+ * VMCP-primary (NAV-D05): plan-aware trim (behind) / add (ahead); idle → next-session budget.
730
+ */
731
+ export const SessionPacePiece: Story = {
732
+ name: 'Piece · Session Pace',
733
+ render: () => (
734
+ <Frame>
735
+ {(
736
+ [
737
+ ['behind', 'live', false],
738
+ ['ahead', 'live', true],
739
+ ['idle', 'idle', false],
740
+ ] as [string, RailContext, boolean][]
741
+ ).map(([label, ctx, ahead]) => (
742
+ <div className="speccol" key={label} style={{ height: 'auto' }}>
743
+ <div className="speclabel">{label}</div>
744
+ <div
745
+ style={{ width: 246, background: 'var(--spine)', border: '1px solid var(--border)' }}
746
+ >
747
+ <PaceFooter context={ctx} ahead={ahead} onToggle={() => {}} />
748
+ </div>
749
+ </div>
750
+ ))}
751
+ </Frame>
752
+ ),
753
+ }