@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,105 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import { View, StyleSheet, Platform, type ViewProps, type ViewStyle } from 'react-native'
3
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
4
+ import { alpha } from '../../../utils/colors'
5
+
6
+ const t = getSemanticColors('dark')
7
+
8
+ /**
9
+ * Coaching-category flood level for the live surface. Mirrors {@link StatusPill}:
10
+ * - `productive` — quiet, no flood
11
+ * - `threshold` — amber wash at VL20
12
+ * - `stop` — red wash at VL28+ (optionally pulsing)
13
+ */
14
+ export type LiveAuraCategory = 'productive' | 'threshold' | 'stop'
15
+
16
+ export interface LiveAuraFrameProps extends ViewProps {
17
+ category: LiveAuraCategory
18
+ /**
19
+ * Pulse the flood on `stop`. Uses the nativewind `animate-pulse` utility so
20
+ * it stays static-friendly under test (className→style, no live clock).
21
+ * Defaults to true.
22
+ */
23
+ pulse?: boolean
24
+ className?: string
25
+ children?: React.ReactNode
26
+ }
27
+
28
+ const categoryToken: Record<LiveAuraCategory, 'status-warning' | 'status-error' | null> = {
29
+ productive: null,
30
+ threshold: 'status-warning',
31
+ stop: 'status-error',
32
+ }
33
+
34
+ const floodOpacity: Record<LiveAuraCategory, number> = {
35
+ productive: 0,
36
+ threshold: 0.12,
37
+ stop: 0.18,
38
+ }
39
+
40
+ /** Resolved flood color for a category, or `null` when there is no flood. */
41
+ export function liveAuraColor(category: LiveAuraCategory): string | null {
42
+ const token = categoryToken[category]
43
+ return token ? t[token] : null
44
+ }
45
+
46
+ /**
47
+ * Full-surface color-flood frame tied to a coaching category. Wraps `children`
48
+ * on a charcoal base and layers a radial wash (amber at threshold, red at stop)
49
+ * as a decorative, pointer-transparent overlay. The wash uses a solid
50
+ * `backgroundColor` fallback (native) plus a web-only `backgroundImage` radial
51
+ * gradient; both derive from the same semantic token via `alpha()`.
52
+ */
53
+ export function LiveAuraFrame({
54
+ category,
55
+ pulse = true,
56
+ className,
57
+ children,
58
+ style,
59
+ ...props
60
+ }: LiveAuraFrameProps) {
61
+ const color = liveAuraColor(category)
62
+ const tint = color ? alpha(color, floodOpacity[category]) : 'transparent'
63
+
64
+ // Match R2's canonical aura: a top-down radial wash that FADES TO TRANSPARENT
65
+ // by ~62%, so the charcoal base shows through the lower panel — NOT a flat
66
+ // full-surface tint. On web (the target: MCP dashboard + Storybook) render the
67
+ // gradient only; native has no gradient support, so fall back to a solid tint.
68
+ const floodStyle = {
69
+ ...StyleSheet.absoluteFillObject,
70
+ ...(Platform.OS === 'web'
71
+ ? // backgroundImage is a runtime RNW style, not in RN ViewStyle types.
72
+ { backgroundImage: `radial-gradient(130% 95% at 50% 0%, ${tint}, transparent 62%)` }
73
+ : { backgroundColor: tint }),
74
+ } as ViewStyle
75
+
76
+ return (
77
+ <View
78
+ testID="live-aura-frame"
79
+ className={className}
80
+ style={[
81
+ {
82
+ position: 'relative',
83
+ overflow: 'hidden',
84
+ borderRadius: 10,
85
+ backgroundColor: t['background-base'],
86
+ borderWidth: 1,
87
+ borderColor: t['border-default'],
88
+ },
89
+ style,
90
+ ]}
91
+ {...props}
92
+ >
93
+ {color && (
94
+ <View
95
+ testID="live-aura-flood"
96
+ accessibilityElementsHidden
97
+ pointerEvents="none"
98
+ className={category === 'stop' && pulse ? 'animate-pulse' : undefined}
99
+ style={floodStyle}
100
+ />
101
+ )}
102
+ {children}
103
+ </View>
104
+ )
105
+ }
@@ -48,7 +48,7 @@ const weeks: WeekRowProps[] = [
48
48
  ]
49
49
 
50
50
  const meta: Meta<typeof MesoCard> = {
51
- title: 'Custom/Workout/MesoCard',
51
+ title: 'Workout/MesoCard',
52
52
  component: MesoCard,
53
53
  tags: ['autodocs'],
54
54
  argTypes: {
@@ -4,7 +4,7 @@ import { View } from 'react-native'
4
4
  import { MesoProgressBar, type Meso } from './MesoProgressBar'
5
5
 
6
6
  const meta: Meta<typeof MesoProgressBar> = {
7
- title: 'Custom/Workout/MesoProgressBar',
7
+ title: 'Workout/MesoProgressBar',
8
8
  component: MesoProgressBar,
9
9
  tags: ['autodocs'],
10
10
  argTypes: {
@@ -3,7 +3,7 @@ import { View } from 'react-native'
3
3
  import { MesoStatusCard } from './MesoStatusCard'
4
4
 
5
5
  const meta: Meta<typeof MesoStatusCard> = {
6
- title: 'Custom/Workout/MesoStatusCard',
6
+ title: 'Workout/MesoStatusCard',
7
7
  component: MesoStatusCard,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -0,0 +1,60 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { MetricTiles } from './MetricTiles'
4
+ import { getSemanticColors } from '../../../theme'
5
+
6
+ const AMBER = getSemanticColors('dark')['status-warning']
7
+
8
+ const meta: Meta<typeof MetricTiles> = {
9
+ title: 'Workout/MetricTiles',
10
+ component: MetricTiles,
11
+ tags: ['autodocs'],
12
+ parameters: {
13
+ docs: {
14
+ description: {
15
+ component:
16
+ '**Molecule.** An HStack of [Tile](?path=/docs/components-atoms-tile--docs). Used-by ↑ SessionHeader.',
17
+ },
18
+ },
19
+ },
20
+ decorators: [
21
+ (Story) => (
22
+ <View style={{ width: 246 }}>
23
+ <Story />
24
+ </View>
25
+ ),
26
+ ],
27
+ }
28
+
29
+ export default meta
30
+ type Story = StoryObj<typeof MetricTiles>
31
+
32
+ export const SessionStats: Story = {
33
+ args: {
34
+ metrics: [
35
+ { label: 'Volume', value: '76%' },
36
+ { label: 'Load', value: '7.3k' },
37
+ { label: 'Fatigue', value: 'MOD', valueColor: AMBER },
38
+ ],
39
+ },
40
+ }
41
+
42
+ export const TwoTiles: Story = {
43
+ args: {
44
+ metrics: [
45
+ { label: 'Time', value: '6:30 PM' },
46
+ { label: 'Until', value: '5h', valueColor: AMBER },
47
+ ],
48
+ },
49
+ }
50
+
51
+ export const FourTiles: Story = {
52
+ args: {
53
+ metrics: [
54
+ { label: 'Sets', value: '7/12' },
55
+ { label: 'Volume', value: '76%' },
56
+ { label: 'Load', value: '7.3k' },
57
+ { label: 'Fatigue', value: 'MOD', valueColor: AMBER },
58
+ ],
59
+ },
60
+ }
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { MetricTiles } from './MetricTiles'
5
+
6
+ const STATS = [
7
+ { label: 'Volume', value: '76%' },
8
+ { label: 'Load', value: '7.3k' },
9
+ { label: 'Fatigue', value: 'MOD', valueColor: '#F5A623' },
10
+ ]
11
+
12
+ describe('MetricTiles', () => {
13
+ it('renders a tile per metric', () => {
14
+ render(<MetricTiles metrics={STATS} />)
15
+ expect(screen.getByText('Volume')).toBeInTheDocument()
16
+ expect(screen.getByText('76%')).toBeInTheDocument()
17
+ expect(screen.getByText('Load')).toBeInTheDocument()
18
+ expect(screen.getByText('7.3k')).toBeInTheDocument()
19
+ expect(screen.getByText('Fatigue')).toBeInTheDocument()
20
+ expect(screen.getByText('MOD')).toBeInTheDocument()
21
+ })
22
+
23
+ it('passes valueColor through to the tile value', () => {
24
+ render(<MetricTiles metrics={STATS} />)
25
+ expect(screen.getByText('MOD')).toHaveStyle({ color: '#F5A623' })
26
+ })
27
+
28
+ it('does not tint values without a valueColor', () => {
29
+ render(<MetricTiles metrics={STATS} />)
30
+ expect(screen.getByText('76%').style.color).toBe('')
31
+ })
32
+
33
+ it('reflows an arbitrary count of metrics', () => {
34
+ render(
35
+ <MetricTiles
36
+ metrics={[
37
+ { label: 'Time', value: '6:30 PM' },
38
+ { label: 'Until', value: '5h' },
39
+ ]}
40
+ />
41
+ )
42
+ expect(screen.getByText('6:30 PM')).toBeInTheDocument()
43
+ expect(screen.getByText('5h')).toBeInTheDocument()
44
+ })
45
+
46
+ it('passes additional props through', () => {
47
+ render(<MetricTiles metrics={STATS} testID="metric-tiles" />)
48
+ expect(screen.getByText('Volume')).toBeInTheDocument()
49
+ })
50
+
51
+ describe('accessibility', () => {
52
+ it('has no accessibility violations', async () => {
53
+ const { container } = render(<MetricTiles metrics={STATS} />)
54
+ const results = await axe(container)
55
+ expect(results).toHaveNoViolations()
56
+ })
57
+ })
58
+ })
@@ -0,0 +1,48 @@
1
+ import { type ViewProps } from 'react-native'
2
+ import { HStack } from '../../ui/stack'
3
+ import { Tile } from '../../ui/tile'
4
+
5
+ export interface MetricTileData {
6
+ /** Uppercase micro-label shown above the value */
7
+ label: string
8
+ /** Primary value */
9
+ value: string
10
+ /** Tints the value (status/accent hex); defaults to primary text */
11
+ valueColor?: string
12
+ }
13
+
14
+ export interface MetricTilesProps extends ViewProps {
15
+ /** Stat cells rendered left-to-right; typically three */
16
+ metrics: MetricTileData[]
17
+ /** Space between tiles (Stack gap scale, default 1 ≈ 4px) */
18
+ gap?: 0 | 1 | 2 | 3 | 4
19
+ }
20
+
21
+ /**
22
+ * MetricTiles — a row of equal-width stat tiles.
23
+ *
24
+ * A thin, presentational HStack of {@link Tile}. Each tile flexes to fill its
25
+ * share so the row reflows gracefully in a responsive rail. Used by SessionHeader
26
+ * for Volume / Load / Fatigue (and Date / Time / Until when upcoming).
27
+ *
28
+ * @example
29
+ * <MetricTiles metrics={[
30
+ * { label: 'Volume', value: '76%' },
31
+ * { label: 'Load', value: '7.3k' },
32
+ * { label: 'Fatigue', value: 'MOD', valueColor: amber },
33
+ * ]} />
34
+ */
35
+ export function MetricTiles({ metrics, gap = 1, ...props }: MetricTilesProps) {
36
+ return (
37
+ <HStack gap={gap} align="stretch" {...props}>
38
+ {metrics.map((metric) => (
39
+ <Tile
40
+ key={metric.label}
41
+ label={metric.label}
42
+ value={metric.value}
43
+ valueColor={metric.valueColor}
44
+ />
45
+ ))}
46
+ </HStack>
47
+ )
48
+ }
@@ -3,7 +3,7 @@ import { View } from 'react-native'
3
3
  import { MuscleGroupChip, type VolumeStatus } from './MuscleGroupChip'
4
4
 
5
5
  const meta: Meta<typeof MuscleGroupChip> = {
6
- title: 'Custom/Workout/MuscleGroupChip',
6
+ title: 'Components/Atoms/MuscleGroupChip',
7
7
  component: MuscleGroupChip,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -3,7 +3,7 @@ import { View } from 'react-native'
3
3
  import { PlaceholderStrip } from './PlaceholderStrip'
4
4
 
5
5
  const meta: Meta<typeof PlaceholderStrip> = {
6
- title: 'Custom/Workout/PlaceholderStrip',
6
+ title: 'Workout/PlaceholderStrip',
7
7
  component: PlaceholderStrip,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -3,7 +3,7 @@ import { View } from 'react-native'
3
3
  import { PrBadge } from './PrBadge'
4
4
 
5
5
  const meta: Meta<typeof PrBadge> = {
6
- title: 'Custom/Workout/PrBadge',
6
+ title: 'Components/Atoms/PrBadge',
7
7
  component: PrBadge,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -12,7 +12,7 @@ const records: PrRecord[] = [
12
12
  ]
13
13
 
14
14
  const meta: Meta<typeof PrHistoryModal> = {
15
- title: 'Custom/Workout/PrHistoryModal',
15
+ title: 'Workout/PrHistoryModal',
16
16
  component: PrHistoryModal,
17
17
  tags: ['autodocs'],
18
18
  argTypes: {
@@ -18,10 +18,7 @@ const lowerMuscles: WorkoutMuscleGroup[] = [
18
18
  const upperExercises: ExerciseCardProps[] = [
19
19
  {
20
20
  name: 'Barbell Bench Press',
21
- state: 'collapsed',
22
- onToggle: () => {},
23
21
  summary: { sets: 4, reps: 8, weight: 185, unit: 'lbs' },
24
- e1rm: { value: 235, unit: 'lbs' },
25
22
  isPR: true,
26
23
  setVelocities: [
27
24
  [1.05, 0.94, 0.82, 0.74],
@@ -31,16 +28,12 @@ const upperExercises: ExerciseCardProps[] = [
31
28
  },
32
29
  {
33
30
  name: 'Incline Dumbbell Press',
34
- state: 'collapsed',
35
- onToggle: () => {},
36
31
  summary: { sets: 3, reps: 10, weight: 70, unit: 'lbs' },
37
- e1rm: { value: 92, unit: 'lbs' },
38
32
  totalPlannedSets: 3,
39
33
  },
40
34
  {
41
35
  name: 'Cable Triceps Pushdown',
42
- state: 'upcoming',
43
- onToggle: () => {},
36
+ upcoming: true,
44
37
  prescription: '3×12-15 @ RPE 8',
45
38
  previousBest: '50 lbs × 14',
46
39
  },
@@ -49,23 +42,18 @@ const upperExercises: ExerciseCardProps[] = [
49
42
  const lowerExercises: ExerciseCardProps[] = [
50
43
  {
51
44
  name: 'Back Squat',
52
- state: 'collapsed',
53
- onToggle: () => {},
54
45
  summary: { sets: 5, reps: 5, weight: 275, unit: 'lbs' },
55
- e1rm: { value: 335, unit: 'lbs' },
56
46
  totalPlannedSets: 5,
57
47
  },
58
48
  {
59
49
  name: 'Romanian Deadlift',
60
- state: 'upcoming',
61
- onToggle: () => {},
50
+ upcoming: true,
62
51
  prescription: '3×8-10 @ RPE 8',
63
52
  previousBest: '225 lbs × 9',
64
53
  },
65
54
  {
66
55
  name: 'Leg Press',
67
- state: 'upcoming',
68
- onToggle: () => {},
56
+ upcoming: true,
69
57
  prescription: '3×12 @ RPE 9',
70
58
  previousBest: '450 lbs × 12',
71
59
  },
@@ -240,7 +228,7 @@ const accumulationDone: PlanMeso = {
240
228
  const mesos: PlanMeso[] = [accumulationDone, accumulation, intensification, peak]
241
229
 
242
230
  const meta: Meta<typeof ProgramPlanningPage> = {
243
- title: 'Custom/Workout/ProgramPlanningPage',
231
+ title: 'Pages/Program Planning',
244
232
  component: ProgramPlanningPage,
245
233
  parameters: { layout: 'fullscreen' },
246
234
  tags: ['autodocs'],
@@ -154,7 +154,11 @@ function Breadcrumbs({ crumbs, onNavigate }: BreadcrumbsProps) {
154
154
  const isLast = index === crumbs.length - 1
155
155
  return (
156
156
  <View key={crumb.key} className="flex-row items-center" style={{ gap: 4 }}>
157
- {index > 0 && <Text className="text-text-tertiary" style={{ fontSize: 12 }}>{'›'}</Text>}
157
+ {index > 0 && (
158
+ <Text className="text-text-tertiary" style={{ fontSize: 12 }}>
159
+ {'›'}
160
+ </Text>
161
+ )}
158
162
  <Pressable
159
163
  onPress={() => onNavigate(crumb.level)}
160
164
  disabled={isLast}
@@ -270,13 +274,11 @@ interface WorkoutLevelProps {
270
274
  }
271
275
 
272
276
  function WorkoutLevel({ workout, expandedExercise, onToggleExercise }: WorkoutLevelProps) {
277
+ // `upcoming` cards ignore `expanded`, so it's safe to wire the toggle uniformly.
273
278
  const exercises = workout.exercises.map((exercise, index) => ({
274
279
  ...exercise,
275
- state:
276
- exercise.state === 'upcoming'
277
- ? exercise.state
278
- : ((index === expandedExercise ? 'expanded' : 'collapsed') as ExerciseCardProps['state']),
279
- onToggle: () => onToggleExercise(index),
280
+ expanded: index === expandedExercise,
281
+ onExpandedChange: () => onToggleExercise(index),
280
282
  }))
281
283
  return (
282
284
  <View testID="program-planning-page-workout-level">
@@ -10,15 +10,15 @@ component is pure import churn with no file moves.
10
10
  **Atoms** — single-purpose, no cross-component state:
11
11
  BaseBadge · WeightBadge · PrBadge · StatusDot · PlaceholderStrip · TempoBar ·
12
12
  DeviationBar · IntensityBar · WorkoutPill · MuscleGroupChip · Sparkline ·
13
- SupersetWrapper · InputBar
13
+ SupersetWrapper · InputBar · MetricCell · SetsRepsLoad · ExerciseIndicator · SetBar
14
14
 
15
15
  **Molecules** — compose atoms, own a little local state:
16
16
  VelocityStrip · SetRow · TempoDisplay · RestTimer · MesoProgressBar · WeekRow ·
17
- WorkoutCard
17
+ WorkoutCard · SetStrip · ExerciseHeading · ExerciseCardHeading
18
18
 
19
19
  **Organisms** — full features, often with their own data contract:
20
- ExerciseCard · MesoCard · MesoStatusCard · PrHistoryModal · ReadinessCheck ·
21
- StrengthTrendChart · CapacityBandChart · BodyMap · BodyMapDetailPanel
20
+ ExerciseCard · SessionRail · MesoCard · MesoStatusCard · PrHistoryModal ·
21
+ ReadinessCheck · StrengthTrendChart · CapacityBandChart · BodyMap · BodyMapDetailPanel
22
22
 
23
23
  **Pages** — phone-shaped reference screens (whole-screen compositions):
24
24
  ActiveWorkoutPage · ExerciseDetailPage · ProgramPlanningPage · TrainingStatusPage
@@ -44,7 +44,196 @@ type props without pulling the dependency.
44
44
  - **BaseBadge is an internal composition primitive** — the shared shell that
45
45
  WeightBadge and PrBadge build on. It is exempt from orphan accounting; it is not
46
46
  meant to be consumed directly even though it is exported for composition.
47
+ - **MetricCell is an internal composition primitive** — the shared Inter · 600 ·
48
+ letter-spacing-1 value/separator cell. `TempoDisplay` (tempo digits) and
49
+ `SetsRepsLoad` (sets × reps @ load) both compose it so the two read as one
50
+ visual language. Exported for composition, not for direct app use.
51
+ - **S3 session-rail family** — `SessionRail` (organism) composes the standalone
52
+ `ExerciseCardHeading` molecule per exercise, which composes an `ExerciseHeading`
53
+ info block + a `SetStrip`:
54
+
55
+ ```
56
+ SessionRail
57
+ └─ ExerciseCardHeading (complete standalone heading)
58
+ ├─ ExerciseHeading (name/indicator + metrics, no strip)
59
+ │ ├─ ExerciseIndicator
60
+ │ ├─ SetsRepsLoad → MetricCell
61
+ │ └─ TempoDisplay (showLabel={false}) → MetricCell
62
+ └─ SetStrip
63
+ └─ SetBar × N (one set's per-rep colour bar)
64
+
65
+ ExerciseCard state="rail" ──delegates to──▶ ExerciseCardHeading
66
+ ```
67
+
68
+ `ExerciseCard`'s `rail` state is now a thin adapter that delegates to
69
+ `ExerciseCardHeading` (so the heading is reusable without the card). `SetBar`
70
+ owns the per-set colour/pulse logic; `SetStrip` lays several side by side.
71
+ Beyond the flat `done`/`active`/`todo` sets, `SetStripSet` models set-type
72
+ prescriptions: `range` (variable rep-range — range-max segments, committed-todo grey
73
+ vs. variable-todo cyan), `drop` (one set, sub-loads split by 2px notches), `myo`
74
+ (done rest-pause — activation + clusters split by 3px cluster gaps), and
75
+ `myo-upcoming` (planned myo, length unknown — grey activation + a fading, right-open
76
+ cyan "clusters-to-failure" trail). The chunk-size *pattern* carries set-type identity:
77
+ **butted reps (0) < notch (2px) < cluster gap (3px) < set gap (5px)**. `cyan-900`
78
+ (`SET_STRIP_VARIABLE_COLOR`) is the shared "variable / unknown / opportunity" pin.
79
+ `SegmentedBar` carries these via additive `leadingGap` (per-segment left margin) and
80
+ static `opacity` props — the flat sets stay byte-identical.
81
+ `SetStrip`/`SetBar` colors are the real titan ramp pins (`primitiveRamps` red-600 /
82
+ orange-400 / amber-300 / green-300); the rail surfaces bind to the charcoal ramp +
83
+ a subtle neumorphic inset (`neumorphicShadows.charcoal.pressed.subtle`). The heading
84
+ design is locked in `coordination/.../S3-sessionrail/DECISIONS-ExerciseRow.md`; the
85
+ exploration specimens live under `Custom/Workout/Explorations/*` (do not repoint yet).
86
+ In Storybook the family nests by composition under **`Shell/SessionRail/…`**
87
+ (organism → `ExerciseCardHeading` → `ExerciseHeading` → its atoms/molecules →
88
+ `MetricCell`, and `SetStrip` → `SetBar`; `ExerciseCard` sits as a leaf whose
89
+ Composes link points at `ExerciseCardHeading`), each node's autodocs carrying a
90
+ **Composes** link down the tree — matching the S1/S2 shell families. (The component
91
+ files stay flat on disk in `custom/Workout/`; only the story `title`s build the tree.)
92
+
93
+ - **VelocityStrip set-type modes (`set` prop)** — beyond the flat `velocities`
94
+ array (unchanged, still the source of truth for `SetRow` / `ExerciseCard`), the
95
+ strip accepts an optional structured `VelocitySet` descriptor and renders the
96
+ strength set-type vocabulary as a typed slot list. Slots are
97
+ `rep` (velocity-coloured) · `todo` (charcoal-300 grey) · `variable` /
98
+ `continue` (`SET_STRIP_VARIABLE_COLOR` cyan-900; `continue` adds a cyan-800
99
+ outline to read as "keep going"). Types: `straight` (done + grey todo to
100
+ `planned`), `range` (committed grey + a cyan variable window `floor..max`),
101
+ `amrap` (reps + a trailing cyan continue), `drop` (sub-loads split by a wide
102
+ notch), `myo` (activation + clusters split by wide gaps, `open` adds a continue),
103
+ `cluster` (fixed count grouped by wide intra-rest gaps). The gap hierarchy is
104
+ **butted reps (`REP_GAP` 2px) < chunk boundary (`WIDE_GAP` 7px)**, applied as
105
+ per-slot `marginLeft` (the mini container drops its uniform `gap` so wide gaps
106
+ render; a no-`set` strip is `[0, 2, 2, …]`, i.e. byte-identical to before). The
107
+ done-velocity array (`velocities`, or subloads/activation+clusters flattened) is
108
+ derived from the `set` and drives the mean · loss · zone summary either way. The
109
+ `mini` variant is the primary set-type surface; `expanded` gives `straight` the
110
+ active-set spotlight (velocity-height done reps + short grey stubs) and renders
111
+ the advanced types as a short mini-style encoding. Every modality is documented
112
+ with a copy-paste `set` config in **`Workout/DataViz/VelocityStrip/Set
113
+ Modalities`** (each card carries a `Collapse` accordion; promoted from the
114
+ now-deleted `S3SetModalities` Lab specimen).
115
+ - **VelocityStrip `hero` variant** — the across-the-room, single-set **wall**
116
+ treatment (the north-star live page's velocity hero). Tall bars (default 220px
117
+ plot) with a per-bar velocity value label, a dashed **running-best reference
118
+ line** (unlabeled — the tallest bar already shows the number, and the container
119
+ a11y label carries it), and dashed **placeholders for the reps still to come**
120
+ driven by a new `targetReps` prop. Absorbs the R2 `HeroVelocityBars` candidate
121
+ into the atom rather than shipping a parallel component. Reuses the shared zone
122
+ scale (`barColorFor`) and the extracted `useLiveRepPop` entrance (now shared with
123
+ the framed `expanded` chart). **Layout:** width-fluid (flex bars, capped at 52px,
124
+ left-packed) with a caller-set fixed height; the eyebrow/section title is
125
+ organism chrome, not part of the primitive. **Live-update model:** the plan's
126
+ slots are pre-allocated (`max(done, target)` columns), so a landing rep converts
127
+ placeholder→bar in the *same* slot with a pop — no reflow within the plan; pair
128
+ with `scale="fixed"` so heights never rescale either. The one reflow case is
129
+ set-expansion **beyond** `targetReps` (AMRAP overflow adds a column and flex-
130
+ narrows the rest — currently snaps; smooth overflow reflow is a deferred
131
+ follow-up). Documented by the `HeroPlayground` / `Hero*` stories on the wall
132
+ background.
133
+ - **RestTimer `ring` variant** — the across-the-room **wall** rest treatment (the
134
+ north-star rest page). Built as a **three-tier decomposition** (not a one-off):
135
+ `CircularProgress` (atom, gained a **`children`** center slot + a **`fill`**
136
+ responsive mode) → **`CircularTimer`** (new molecule, `custom/CircularTimer/` — a
137
+ batteries-included circular countdown/countup; owns `useTimer`, renders the mm:ss
138
+ readout into the ring center, flips a completed `down` timer to a full `doneColor`
139
+ ring reading its `doneLabel`, and takes an optional `controls` slot) → **RestTimer
140
+ `ring`** which composes `CircularTimer` (`doneLabel="GO"`, `doneColor="success"`,
141
+ `controls={<RestActions/>}`) and adds the rest-specific next-set footer caption.
142
+ No third copy of the arc math and no `react-native-svg` — it rides
143
+ `CircularProgress`'s web-`<svg>` + free `stroke-dashoffset` transition. Absorbs
144
+ the R2 `RestRing` candidate into the atom; the mobile `CircularTimer` countup fork
145
+ is the natural second consumer of the new molecule (via `mode="up"`). `displayOnly`
146
+ hides the shared `RestActions` (+30s/Skip, extracted, used by both variants);
147
+ `size` (default 180) sets the diameter; the eyebrow/section title is organism
148
+ chrome. Web/RNW-only (like `CircularProgress`) — the wall variant; mobile keeps
149
+ `bar`. `Ring*` (RestTimer) + `Custom/CircularTimer` stories on the wall background.
150
+ - **`size="wall"` density (TempoBar · FatigueMeter · ZoneTrack)** — the across-the-room
151
+ dashboard scale, added as the idiomatic titan `size` union (a JS number-map per
152
+ component; **`default` values are byte-identical** to before, so existing consumers are
153
+ untouched). **TempoBar** scales its bar/labels/durations and at wall spells out the full
154
+ phase words (Concentric / Hold / Eccentric); its completed segments are **colour-coded**
155
+ (a missed target reads red, not just a ✗), and the **active** segment shows a **delta
156
+ countdown** (`activeDisplay`) — remaining time to target counting to `0.0` then negative
157
+ (red) once over — **tap to toggle** delta ↔ elapsed/target. **ZoneTrack** (the shared
158
+ gauge primitive) gained `size` that scales track, needle, tick lines and tick labels
159
+ together; its *other* consumers (TrainingLoadGauge, RpeCalibration) default to `default`
160
+ and are unaffected. **FatigueMeter** passes `size` through and lets `trackHeight` flow
161
+ from it. `Wall*` / `WallDensity` stories on the wall background.
47
162
  - Badge icons (WeightBadge's dumbbell, PrBadge / PrHistoryModal's star) are inline
48
163
  SVGs (`./icons.tsx`), not `lucide-react` — that dependency was dropped in 0.5.0
49
164
  to keep the root barrel light. The SVG paths mirror lucide's glyphs so rendering
50
165
  is unchanged.
166
+ - **`ExerciseIndicator` taxonomy (LOCKED, TD-03.51)** — one precedence-ranked slot
167
+ per exercise, six distinct kinds over four tier colors (bound to titan status
168
+ tokens, read as literal hex via `getSemanticColors('dark')` — the TempoBar
169
+ pattern). Glyph = the specific signal; color = the severity tier. Chips are
170
+ outlined (border + glyph, no fill) so they never collide with the *filled*
171
+ velocity strip, and static (no pulse — they are chrome). Precedence, high → low:
172
+
173
+ | # | kind | tier | glyph (mirrors lucide) |
174
+ |---|------|------|------------------------|
175
+ | 1 | `imbalance` | danger (`status-error`) | `Scale` |
176
+ | 2 | `overshoot` | danger (`status-error`) | `AlertTriangle` |
177
+ | 3 | `velocity-loss` | warning (`status-warning`) | `TrendingDown` |
178
+ | 4 | `missed-reps` | warning (`status-warning`) | `CircleSlash` |
179
+ | 5 | `pr` | success (`status-success`) | `Award` |
180
+ | 6 | `info` | info (`status-info`) | `Info` |
181
+
182
+ `resolveIndicator(candidates)` collapses the active signals to the single
183
+ highest-precedence kind to show (`undefined` when none). Alerts outrank `pr`. The
184
+ glyphs are new inline SVGs in the shared `components/icons` set (not the local
185
+ `./icons.tsx` badge glyphs). The old generic `issue` kind is superseded by the
186
+ specific danger/warning kinds and was removed.
187
+
188
+ ## Shell/SessionRail feature area
189
+
190
+ The live-workout rail is a **feature assembly**: the genuinely rail-specific parts
191
+ (header / pace / expand drawer / set-table header) nest under `Shell/SessionRail/*`,
192
+ while reused leaves stay in their `Workout/*` tier and are **referenced via Composes
193
+ links, not re-homed** (`SetRow`, `ExerciseCardHeading`). This is the one deliberate
194
+ exception to the flat `Shell` taxonomy — the NEW components below are the only
195
+ ones that nest.
196
+
197
+ ```
198
+ Shell/SessionRail (organism — SessionRail.tsx)
199
+ ├─ SessionHeader (NEW · real — title, stat tiles, chunked pace bar, ⏱ readout)
200
+ └─ ExerciseCardHeading × N (Workout/ · linked, NOT re-homed)
201
+
202
+ Shell/SessionRail/ExpandedDrawer (NEW · 🚧 WIP — opens when an exercise expands)
203
+ ├─ TableHeader (NEW · 🚧 WIP — exported as `SetTableHeader`; the generic
204
+ │ `TableHeader` name is owned by the DataViz `Table`)
205
+ └─ SetRow × N (Workout/ · linked, NOT re-homed)
206
+ ```
207
+
208
+ - `SessionHeader` is the rail's session glance: the title over a stat row (Volume/Load/
209
+ Fatigue live, Date/Time/Until upcoming) and a per-exercise chunked pace bar with a
210
+ mono sets label + ⏱ readout. It composes the merged primitives (`MetricTiles` /
211
+ `ScheduleTiles` + `SegmentedProgressBar` + `TimerReadout`); the old footer pace tile is
212
+ gone — the header carries the glance. It is the REAL (non-WIP) new component.
213
+ - `SetTableHeader` (story leaf `.../ExpandedDrawer/TableHeader`) is the expanded-set
214
+ column-header row extracted from `ExerciseCard`'s `expanded` state; `ExerciseCard`
215
+ now consumes it too (backward-compatible — it passes its own
216
+ `testID="exercise-card-column-headers"`).
217
+ - Each new node's autodocs carry a `**Tier.** Composes […] / Used-by ↑ […]` line; the
218
+ WIP two (`ExpandedDrawer`, `TableHeader`) are prefixed `**🚧 WIP / placeholder.**`.
219
+
220
+ ## Dead candidates (do NOT remove yet — pending TD-03.56 responsive unification)
221
+
222
+ Superseded by the target Shell/SessionRail scaffold; flagged now, removed only when the
223
+ responsive level views land (removal is out of scope for this scaffold ticket):
224
+
225
+ - **`setHeadingKit.tsx`** — the throwaway CSS R&D kit (raw HTML `<div>`s at the rail
226
+ width). Its own header says it moves into real components "when these decisions
227
+ harden" — which has now happened (`SessionRail` / `SessionHeader` /
228
+ `ExerciseCardHeading` / `ExerciseHeading` / `SetStrip` / `SetBar` /
229
+ `SetTableHeader`).
230
+ - **The five `Lab/Explorations` specimens that import it** — `S3FullRail`
231
+ (Full Rail), `S3SessionRailHeading` (Session Rail Heading), `S3WorkoutExpansion`
232
+ (Workout Expansion), `S3SetTypes` (Set Types), `S3SessionPace` (Session Pace).
233
+ Each is superseded by a real Shell/SessionRail component + its Storybook stories;
234
+ `S3SessionPace` in particular is superseded by the `SessionHeader` pace glance.
235
+ - **`ExerciseCard`'s `collapsed` / `upcoming` state representations** — the rail now
236
+ owns the live-list heading (`rail` state → `ExerciseCardHeading`) and the expanded
237
+ view is moving to `ExpandedDrawer`. Once the responsive unification (TD-03.56)
238
+ lands a single responsive card, the `collapsed`/`upcoming` branches of
239
+ `ExerciseCard` are the superseded representations to retire.
@@ -16,7 +16,7 @@ const staticFactors: ReadinessFactor[] = FACTOR_SEED.map((f) => ({
16
16
  }))
17
17
 
18
18
  const meta: Meta<typeof ReadinessCheck> = {
19
- title: 'Custom/Workout/ReadinessCheck',
19
+ title: 'Workout/ReadinessCheck',
20
20
  component: ReadinessCheck,
21
21
  tags: ['autodocs'],
22
22
  argTypes: {