@titan-design/react-ui 0.12.1 → 0.14.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 (531) hide show
  1. package/README.md +60 -47
  2. package/dist/bodymap.js +856 -736
  3. package/dist/bodymap.js.map +1 -1
  4. package/dist/bodymap.mjs +858 -738
  5. package/dist/bodymap.mjs.map +1 -1
  6. package/dist/index.d.mts +1431 -237
  7. package/dist/index.d.ts +1431 -237
  8. package/dist/index.js +6506 -4781
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +6280 -4642
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/{pages-B1hqnGIC.d.ts → pages-CsjsUn0G.d.ts} +51 -16
  13. package/dist/{pages-CvsFqNtx.d.mts → pages-ZbT_pD_U.d.mts} +51 -16
  14. package/dist/pages.d.mts +1 -1
  15. package/dist/pages.d.ts +1 -1
  16. package/dist/pages.js +1919 -1937
  17. package/dist/pages.js.map +1 -1
  18. package/dist/pages.mjs +1927 -1945
  19. package/dist/pages.mjs.map +1 -1
  20. package/dist/theme/index.d.mts +225 -140
  21. package/dist/theme/index.d.ts +225 -140
  22. package/dist/theme/index.js +298 -319
  23. package/dist/theme/index.js.map +1 -1
  24. package/dist/theme/index.mjs +283 -315
  25. package/dist/theme/index.mjs.map +1 -1
  26. package/dist/theme/tokens-css.js +145 -24
  27. package/dist/theme/tokens-css.js.map +1 -1
  28. package/dist/theme/tokens-css.mjs +145 -24
  29. package/dist/theme/tokens-css.mjs.map +1 -1
  30. package/dist/theme/tokens.d.mts +128 -11
  31. package/dist/theme/tokens.d.ts +128 -11
  32. package/dist/theme/tokens.js +126 -26
  33. package/dist/theme/tokens.js.map +1 -1
  34. package/dist/theme/tokens.mjs +124 -27
  35. package/dist/theme/tokens.mjs.map +1 -1
  36. package/docs/audits/2026-09-08-critique-addenda.md +51 -0
  37. package/docs/audits/2026-09-08-library-critique.md +212 -0
  38. package/docs/library-roadmap.md +99 -0
  39. package/docs/render-testing-pattern.md +130 -0
  40. package/package.json +1 -1
  41. package/src/arch/Architecture.stories.tsx +3 -1
  42. package/src/arch/arch-graph.freshness.test.ts +52 -0
  43. package/src/arch/arch-graph.json +4989 -1583
  44. package/src/components/custom/ActiveWork/CoChangeChip.stories.tsx +44 -0
  45. package/src/components/custom/ActiveWork/CoChangeChip.test.tsx +29 -0
  46. package/src/components/custom/ActiveWork/CoChangeChip.tsx +48 -0
  47. package/src/components/custom/ActiveWork/Eyebrow.stories.tsx +25 -0
  48. package/src/components/custom/ActiveWork/Eyebrow.test.tsx +16 -0
  49. package/src/components/custom/ActiveWork/Eyebrow.tsx +26 -0
  50. package/src/components/custom/ActiveWork/FileActivityDetail.stories.tsx +50 -0
  51. package/src/components/custom/ActiveWork/FileActivityDetail.test.tsx +66 -0
  52. package/src/components/custom/ActiveWork/FileActivityDetail.tsx +202 -0
  53. package/src/components/custom/ActiveWork/FileActivityRow.stories.tsx +70 -0
  54. package/src/components/custom/ActiveWork/FileActivityRow.test.tsx +67 -0
  55. package/src/components/custom/ActiveWork/FileActivityRow.tsx +130 -0
  56. package/src/components/custom/ActiveWork/FileHistoryExplorer.stories.tsx +84 -0
  57. package/src/components/custom/ActiveWork/FileHistoryExplorer.test.tsx +99 -0
  58. package/src/components/custom/ActiveWork/FileHistoryExplorer.tsx +138 -0
  59. package/src/components/custom/ActiveWork/FilePathLabel.stories.tsx +39 -0
  60. package/src/components/custom/ActiveWork/FilePathLabel.test.tsx +53 -0
  61. package/src/components/custom/ActiveWork/FilePathLabel.tsx +63 -0
  62. package/src/components/custom/ActiveWork/InitiativeBrief.stories.tsx +50 -0
  63. package/src/components/custom/ActiveWork/InitiativeBrief.test.tsx +62 -0
  64. package/src/components/custom/ActiveWork/InitiativeBrief.tsx +182 -0
  65. package/src/components/custom/ActiveWork/InitiativeCard.stories.tsx +73 -0
  66. package/src/components/custom/ActiveWork/InitiativeCard.test.tsx +68 -0
  67. package/src/components/custom/ActiveWork/InitiativeCard.tsx +136 -0
  68. package/src/components/custom/ActiveWork/InitiativeHeader.stories.tsx +59 -0
  69. package/src/components/custom/ActiveWork/InitiativeHeader.test.tsx +40 -0
  70. package/src/components/custom/ActiveWork/InitiativeHeader.tsx +73 -0
  71. package/src/components/custom/ActiveWork/InitiativeReader.stories.tsx +145 -0
  72. package/src/components/custom/ActiveWork/OpenLoops.stories.tsx +58 -0
  73. package/src/components/custom/ActiveWork/OpenLoops.test.tsx +61 -0
  74. package/src/components/custom/ActiveWork/OpenLoops.tsx +107 -0
  75. package/src/components/custom/ActiveWork/PortfolioOverview.stories.tsx +101 -0
  76. package/src/components/custom/ActiveWork/PortfolioOverview.test.tsx +81 -0
  77. package/src/components/custom/ActiveWork/PortfolioOverview.tsx +83 -0
  78. package/src/components/custom/ActiveWork/README.md +305 -0
  79. package/src/components/custom/ActiveWork/SessionDetail.stories.tsx +71 -0
  80. package/src/components/custom/ActiveWork/SessionDetail.test.tsx +84 -0
  81. package/src/components/custom/ActiveWork/SessionDetail.tsx +180 -0
  82. package/src/components/custom/ActiveWork/SessionList.stories.tsx +69 -0
  83. package/src/components/custom/ActiveWork/SessionList.test.tsx +78 -0
  84. package/src/components/custom/ActiveWork/SessionList.tsx +103 -0
  85. package/src/components/custom/ActiveWork/SessionListItem.stories.tsx +64 -0
  86. package/src/components/custom/ActiveWork/SessionListItem.test.tsx +62 -0
  87. package/src/components/custom/ActiveWork/SessionListItem.tsx +152 -0
  88. package/src/components/custom/ActiveWork/SessionReader.stories.tsx +113 -0
  89. package/src/components/custom/ActiveWork/SeverityLabel.stories.tsx +51 -0
  90. package/src/components/custom/ActiveWork/SeverityLabel.test.tsx +68 -0
  91. package/src/components/custom/ActiveWork/SeverityLabel.tsx +97 -0
  92. package/src/components/custom/ActiveWork/TaskRow.stories.tsx +63 -0
  93. package/src/components/custom/ActiveWork/TaskRow.test.tsx +112 -0
  94. package/src/components/custom/ActiveWork/TaskRow.tsx +176 -0
  95. package/src/components/custom/ActiveWork/TaskTable.stories.tsx +123 -0
  96. package/src/components/custom/ActiveWork/TaskTable.test.tsx +233 -0
  97. package/src/components/custom/ActiveWork/TaskTable.tsx +321 -0
  98. package/src/components/custom/ActiveWork/active-work-depth.test.tsx +102 -0
  99. package/src/components/custom/ActiveWork/file-history-fixture.ts +91 -0
  100. package/src/components/custom/ActiveWork/format-time.test.ts +23 -0
  101. package/src/components/custom/ActiveWork/format-time.ts +32 -0
  102. package/src/components/custom/ActiveWork/index.ts +19 -0
  103. package/src/components/custom/ActiveWork/initiative-fixture.ts +104 -0
  104. package/src/components/custom/ActiveWork/session-fixture.ts +267 -0
  105. package/src/components/custom/ActiveWork/session-linkers.ts +56 -0
  106. package/src/components/custom/ActiveWork/task-list-fixture.ts +97 -0
  107. package/src/components/custom/CircularTimer/CircularTimer.stories.tsx +12 -6
  108. package/src/components/custom/DateTime/DateTime.stories.tsx +2 -1
  109. package/src/components/custom/DateTime/DateTime.test.tsx +3 -1
  110. package/src/components/custom/EmptyState/EmptyState.stories.tsx +22 -9
  111. package/src/components/custom/EmptyState/EmptyState.test.tsx +7 -13
  112. package/src/components/custom/EmptyState/EmptyState.tsx +4 -18
  113. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +4 -3
  114. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +2 -2
  115. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +4 -3
  116. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +2 -2
  117. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +8 -4
  118. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +116 -6
  119. package/src/components/custom/Fatigue/LiveFatiguePanel.test.tsx +134 -0
  120. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +54 -83
  121. package/src/components/custom/Fatigue/README.md +40 -3
  122. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +2 -2
  123. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +2 -1
  124. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +4 -3
  125. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +2 -2
  126. package/src/components/custom/Fatigue/fatigue-mock.test.ts +0 -5
  127. package/src/components/custom/Fatigue/fatigue-mock.ts +2 -10
  128. package/src/components/custom/Fatigue/fatigue-tokens.ts +12 -0
  129. package/src/components/custom/Fatigue/index.ts +17 -5
  130. package/src/components/custom/Fatigue/panel-layout.test.ts +149 -0
  131. package/src/components/custom/Fatigue/panel-layout.ts +185 -0
  132. package/src/components/custom/Gauge/Gauge.stories.tsx +6 -3
  133. package/src/components/custom/Gauge/Gauge.test.tsx +1 -1
  134. package/src/components/custom/Gauge/Gauge.tsx +7 -2
  135. package/src/components/custom/Metric/Metric.stories.tsx +10 -1
  136. package/src/components/custom/Metric/Metric.test.tsx +1 -3
  137. package/src/components/custom/Metric/Metric.tsx +5 -20
  138. package/src/components/custom/Prose/MarkdownProse.stories.tsx +88 -0
  139. package/src/components/custom/Prose/MarkdownProse.test.tsx +87 -0
  140. package/src/components/custom/Prose/MarkdownProse.tsx +201 -0
  141. package/src/components/custom/Prose/README.md +58 -0
  142. package/src/components/custom/Prose/index.ts +1 -0
  143. package/src/components/custom/Scatter/Scatter.stories.tsx +55 -14
  144. package/src/components/custom/Scatter/Scatter.test.tsx +2 -2
  145. package/src/components/custom/Scatter/Scatter.tsx +122 -18
  146. package/src/components/custom/Sidebar/Sidebar.stories.tsx +41 -8
  147. package/src/components/custom/Sidebar/Sidebar.test.tsx +5 -1
  148. package/src/components/custom/Sidebar/Sidebar.tsx +16 -33
  149. package/src/components/custom/Table/Table.stories.tsx +99 -16
  150. package/src/components/custom/Table/Table.test.tsx +273 -86
  151. package/src/components/custom/Table/Table.tsx +159 -67
  152. package/src/components/custom/Table/column-fit.test.ts +75 -0
  153. package/src/components/custom/Table/column-fit.ts +116 -0
  154. package/src/components/custom/Table/index.ts +9 -0
  155. package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +8 -6
  156. package/src/components/custom/Treemap/Treemap.stories.tsx +15 -12
  157. package/src/components/custom/Treemap/Treemap.test.tsx +1 -1
  158. package/src/components/custom/Treemap/Treemap.tsx +11 -3
  159. package/src/components/custom/Typography/Typography.stories.tsx +28 -1
  160. package/src/components/custom/Typography/Typography.test.tsx +34 -0
  161. package/src/components/custom/Typography/Typography.tsx +6 -0
  162. package/src/components/custom/Typography/index.ts +1 -8
  163. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +11 -2
  164. package/src/components/custom/Workout/BaseBadge.stories.tsx +27 -11
  165. package/src/components/custom/Workout/BaseBadge.test.tsx +31 -11
  166. package/src/components/custom/Workout/BaseBadge.tsx +6 -2
  167. package/src/components/custom/Workout/BodyMap.stories.tsx +4 -1
  168. package/src/components/custom/Workout/BodyMap.tsx +8 -4
  169. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +28 -6
  170. package/src/components/custom/Workout/BodyMapDetailPanel.test.tsx +11 -15
  171. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +50 -12
  172. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +12 -9
  173. package/src/components/custom/Workout/CapacityBandChart.test.tsx +24 -51
  174. package/src/components/custom/Workout/CapacityBandChart.tsx +144 -111
  175. package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
  176. package/src/components/custom/Workout/DeviationBar.test.tsx +10 -4
  177. package/src/components/custom/Workout/DeviationBar.tsx +24 -16
  178. package/src/components/custom/Workout/DualVelocityStrip.stories.tsx +183 -0
  179. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +32 -18
  180. package/src/components/custom/Workout/ExerciseCard.stories.tsx +12 -11
  181. package/src/components/custom/Workout/ExerciseCard.test.tsx +52 -22
  182. package/src/components/custom/Workout/ExerciseCard.tsx +99 -183
  183. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +95 -12
  184. package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +189 -1
  185. package/src/components/custom/Workout/ExerciseCardHeading.tsx +167 -18
  186. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +9 -1
  187. package/src/components/custom/Workout/ExerciseDetailPage.tsx +3 -2
  188. package/src/components/custom/Workout/ExerciseHeading.stories.tsx +8 -8
  189. package/src/components/custom/Workout/ExerciseHeading.tsx +194 -46
  190. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +12 -9
  191. package/src/components/custom/Workout/ExerciseIndicator.test.tsx +27 -11
  192. package/src/components/custom/Workout/ExerciseIndicator.tsx +14 -15
  193. package/src/components/custom/Workout/FatigueMeter.stories.tsx +5 -5
  194. package/src/components/custom/Workout/InputBar.stories.tsx +4 -5
  195. package/src/components/custom/Workout/InputBar.tsx +4 -2
  196. package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
  197. package/src/components/custom/Workout/IntensityBar.test.tsx +5 -1
  198. package/src/components/custom/Workout/IntensityBar.tsx +8 -3
  199. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +5 -4
  200. package/src/components/custom/Workout/MesoCard.stories.tsx +4 -1
  201. package/src/components/custom/Workout/MesoCard.test.tsx +7 -18
  202. package/src/components/custom/Workout/MesoCard.tsx +14 -10
  203. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
  204. package/src/components/custom/Workout/MesoProgressBar.test.tsx +18 -11
  205. package/src/components/custom/Workout/MesoProgressBar.tsx +15 -13
  206. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +2 -1
  207. package/src/components/custom/Workout/MesoStatusCard.test.tsx +14 -44
  208. package/src/components/custom/Workout/MesoStatusCard.tsx +20 -33
  209. package/src/components/custom/Workout/MetricTiles.stories.tsx +3 -2
  210. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +2 -8
  211. package/src/components/custom/Workout/MuscleGroupChip.test.tsx +23 -31
  212. package/src/components/custom/Workout/MuscleGroupChip.tsx +28 -67
  213. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +9 -4
  214. package/src/components/custom/Workout/PlaceholderStrip.test.tsx +17 -10
  215. package/src/components/custom/Workout/PlaceholderStrip.tsx +9 -8
  216. package/src/components/custom/Workout/PrBadge.stories.tsx +2 -1
  217. package/src/components/custom/Workout/PrBadge.test.tsx +3 -9
  218. package/src/components/custom/Workout/PrBadge.tsx +13 -17
  219. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +16 -3
  220. package/src/components/custom/Workout/PrHistoryModal.test.tsx +13 -36
  221. package/src/components/custom/Workout/PrHistoryModal.tsx +8 -4
  222. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +6 -1
  223. package/src/components/custom/Workout/README.md +68 -17
  224. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +3 -2
  225. package/src/components/custom/Workout/ReadinessCheck.test.tsx +12 -18
  226. package/src/components/custom/Workout/ReadinessCheck.tsx +22 -4
  227. package/src/components/custom/Workout/RestTimer.stories.tsx +16 -4
  228. package/src/components/custom/Workout/RestTimer.tsx +4 -2
  229. package/src/components/custom/Workout/ScheduleTiles.stories.tsx +3 -2
  230. package/src/components/custom/Workout/ScheduleTiles.test.tsx +3 -4
  231. package/src/components/custom/Workout/ScheduleTiles.tsx +4 -7
  232. package/src/components/custom/Workout/SegmentedBar.stories.tsx +21 -17
  233. package/src/components/custom/Workout/SegmentedBar.tsx +10 -5
  234. package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +5 -5
  235. package/src/components/custom/Workout/SessionHeader.stories.tsx +6 -6
  236. package/src/components/custom/Workout/SessionHeader.tsx +12 -12
  237. package/src/components/custom/Workout/SessionRail.stories.tsx +12 -11
  238. package/src/components/custom/Workout/SessionRail.tsx +7 -9
  239. package/src/components/custom/Workout/SetBar.stories.tsx +10 -6
  240. package/src/components/custom/Workout/SetBar.test.tsx +49 -4
  241. package/src/components/custom/Workout/SetBar.tsx +38 -12
  242. package/src/components/custom/Workout/SetRow.stories.tsx +3 -2
  243. package/src/components/custom/Workout/SetRow.test.tsx +16 -2
  244. package/src/components/custom/Workout/SetRow.tsx +61 -23
  245. package/src/components/custom/Workout/SetStrip.stories.tsx +63 -7
  246. package/src/components/custom/Workout/SetStrip.test.tsx +25 -0
  247. package/src/components/custom/Workout/SetStrip.tsx +24 -4
  248. package/src/components/custom/Workout/SetTableHeader.stories.tsx +6 -6
  249. package/src/components/custom/Workout/SetTableHeader.tsx +9 -12
  250. package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +7 -7
  251. package/src/components/custom/Workout/SetsRepsLoad.tsx +24 -12
  252. package/src/components/custom/Workout/Sparkline.stories.tsx +14 -11
  253. package/src/components/custom/Workout/Sparkline.test.tsx +37 -19
  254. package/src/components/custom/Workout/Sparkline.tsx +14 -18
  255. package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
  256. package/src/components/custom/Workout/StatusDot.test.tsx +44 -5
  257. package/src/components/custom/Workout/StatusDot.tsx +93 -63
  258. package/src/components/custom/Workout/StatusPill.stories.tsx +1 -1
  259. package/src/components/custom/Workout/StatusPill.test.tsx +6 -15
  260. package/src/components/custom/Workout/StatusPill.tsx +27 -38
  261. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +11 -4
  262. package/src/components/custom/Workout/StrengthTrendChart.test.tsx +15 -37
  263. package/src/components/custom/Workout/StrengthTrendChart.tsx +39 -36
  264. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +27 -64
  265. package/src/components/custom/Workout/SupersetWrapper.test.tsx +16 -14
  266. package/src/components/custom/Workout/SupersetWrapper.tsx +17 -21
  267. package/src/components/custom/Workout/TempoDisplay.stories.tsx +26 -10
  268. package/src/components/custom/Workout/TempoDisplay.test.tsx +18 -4
  269. package/src/components/custom/Workout/TempoDisplay.tsx +2 -0
  270. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +9 -1
  271. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +4 -1
  272. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +10 -6
  273. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +5 -2
  274. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +44 -1
  275. package/src/components/custom/Workout/VelocityStrip.stories.tsx +12 -6
  276. package/src/components/custom/Workout/VelocityStrip.test.tsx +48 -6
  277. package/src/components/custom/Workout/VelocityStrip.tsx +26 -14
  278. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +6 -7
  279. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +15 -6
  280. package/src/components/custom/Workout/WeekRow.stories.tsx +4 -1
  281. package/src/components/custom/Workout/WeekRow.test.tsx +18 -15
  282. package/src/components/custom/Workout/WeekRow.tsx +23 -16
  283. package/src/components/custom/Workout/WeightBadge.stories.tsx +2 -1
  284. package/src/components/custom/Workout/WeightBadge.test.tsx +50 -19
  285. package/src/components/custom/Workout/WeightBadge.tsx +28 -27
  286. package/src/components/custom/Workout/WorkoutCard.stories.tsx +6 -1
  287. package/src/components/custom/Workout/WorkoutCard.test.tsx +18 -0
  288. package/src/components/custom/Workout/WorkoutCard.tsx +35 -57
  289. package/src/components/custom/Workout/WorkoutPill.stories.tsx +5 -2
  290. package/src/components/custom/Workout/WorkoutPill.test.tsx +39 -7
  291. package/src/components/custom/Workout/WorkoutPill.tsx +88 -57
  292. package/src/components/custom/Workout/ZoneTrack.stories.tsx +10 -7
  293. package/src/components/custom/Workout/ZoneTrack.tsx +10 -9
  294. package/src/components/custom/Workout/exerciseRowState.ts +29 -0
  295. package/src/components/custom/Workout/index.ts +17 -2
  296. package/src/components/custom/Workout/metricText.stories.tsx +15 -13
  297. package/src/components/custom/Workout/metricText.tsx +8 -2
  298. package/src/components/custom/Workout/setHeadingKit.tsx +219 -36
  299. package/src/components/custom/Workout/velocity-story-kit.tsx +2 -2
  300. package/src/components/custom/charts/README.md +66 -0
  301. package/src/components/custom/charts/SetBarChart.test.tsx +46 -1
  302. package/src/components/custom/charts/SetBarChart.tsx +56 -9
  303. package/src/components/custom/charts/SparkBars.stories.tsx +55 -0
  304. package/src/components/custom/charts/SparkBars.test.tsx +69 -0
  305. package/src/components/custom/charts/SparkBars.tsx +85 -0
  306. package/src/components/custom/charts/flatBarGeometry.test.ts +41 -0
  307. package/src/components/custom/charts/flatBarGeometry.ts +32 -0
  308. package/src/components/custom/charts/index.ts +3 -0
  309. package/src/components/custom/index.ts +3 -0
  310. package/src/components/custom/stepper/Stepper.stories.tsx +13 -10
  311. package/src/components/custom/stepper/Stepper.test.tsx +1 -7
  312. package/src/components/custom/stepper/Stepper.tsx +21 -34
  313. package/src/components/icons/icons.stories.tsx +1 -1
  314. package/src/components/icons/icons.tsx +51 -0
  315. package/src/components/shell/AppShell.stories.tsx +150 -0
  316. package/src/components/shell/AppShell.test.tsx +70 -0
  317. package/src/components/shell/AppShell.tsx +90 -0
  318. package/src/components/shell/BrandLockup.stories.tsx +66 -5
  319. package/src/components/shell/BrandLockup.test.tsx +34 -1
  320. package/src/components/shell/BrandLockup.tsx +27 -11
  321. package/src/components/shell/NavItem.stories.tsx +4 -4
  322. package/src/components/shell/NavItem.test.tsx +17 -0
  323. package/src/components/shell/NavItem.tsx +17 -5
  324. package/src/components/shell/README.md +89 -26
  325. package/src/components/shell/SideNav.stories.tsx +38 -7
  326. package/src/components/shell/SideNav.test.tsx +16 -8
  327. package/src/components/shell/SideNav.tsx +17 -17
  328. package/src/components/shell/TopBar.stories.tsx +44 -19
  329. package/src/components/shell/TopBar.test.tsx +34 -12
  330. package/src/components/shell/TopBar.tsx +61 -40
  331. package/src/components/shell/brands.tsx +66 -0
  332. package/src/components/shell/index.ts +9 -8
  333. package/src/components/shell/{DeviceIndicator.stories.tsx → workout/DeviceIndicator.stories.tsx} +2 -2
  334. package/src/components/shell/{DeviceIndicator.tsx → workout/DeviceIndicator.tsx} +2 -2
  335. package/src/components/shell/{DeviceMenu.stories.tsx → workout/DeviceMenu.stories.tsx} +2 -2
  336. package/src/components/shell/{DeviceMenu.tsx → workout/DeviceMenu.tsx} +3 -3
  337. package/src/components/shell/{DeviceRow.stories.tsx → workout/DeviceRow.stories.tsx} +2 -2
  338. package/src/components/shell/{DeviceRow.tsx → workout/DeviceRow.tsx} +3 -3
  339. package/src/components/shell/{SessionStatePill.stories.tsx → workout/SessionStatePill.stories.tsx} +2 -2
  340. package/src/components/shell/{SessionStatePill.tsx → workout/SessionStatePill.tsx} +5 -3
  341. package/src/components/shell/workout/WorkoutShell.stories.tsx +71 -0
  342. package/src/components/shell/{DashboardShell.test.tsx → workout/WorkoutShell.test.tsx} +19 -8
  343. package/src/components/shell/workout/WorkoutShell.tsx +77 -0
  344. package/src/components/shell/workout/WorkoutTopBar.stories.tsx +58 -0
  345. package/src/components/shell/workout/WorkoutTopBar.test.tsx +39 -0
  346. package/src/components/shell/workout/WorkoutTopBar.tsx +52 -0
  347. package/src/components/shell/workout/index.ts +9 -0
  348. package/src/components/shell/workout/workoutNavItems.tsx +16 -0
  349. package/src/components/ui/README.md +110 -0
  350. package/src/components/ui/alert/Alert.stories.tsx +15 -5
  351. package/src/components/ui/alert/Alert.tsx +24 -3
  352. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +11 -13
  353. package/src/components/ui/autocomplete/Autocomplete.test.tsx +7 -25
  354. package/src/components/ui/autocomplete/Autocomplete.tsx +21 -28
  355. package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
  356. package/src/components/ui/avatar/Avatar.tsx +2 -10
  357. package/src/components/ui/badge/Badge.stories.tsx +83 -28
  358. package/src/components/ui/badge/Badge.test.tsx +19 -3
  359. package/src/components/ui/badge/Badge.tsx +48 -55
  360. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  361. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +4 -16
  362. package/src/components/ui/button/Button.stories.tsx +46 -12
  363. package/src/components/ui/button/Button.tsx +44 -37
  364. package/src/components/ui/card/Card.stories.tsx +90 -94
  365. package/src/components/ui/card/Card.test.tsx +80 -6
  366. package/src/components/ui/card/Card.tsx +150 -203
  367. package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
  368. package/src/components/ui/checkbox/Checkbox.tsx +7 -27
  369. package/src/components/ui/chip/Chip.stories.tsx +37 -13
  370. package/src/components/ui/chip/Chip.test.tsx +10 -6
  371. package/src/components/ui/chip/Chip.tsx +76 -91
  372. package/src/components/ui/collapse/Collapse.stories.tsx +23 -23
  373. package/src/components/ui/collapse/Collapse.tsx +6 -11
  374. package/src/components/ui/data-row/DataRow.stories.tsx +14 -3
  375. package/src/components/ui/data-row/DataRow.test.tsx +5 -15
  376. package/src/components/ui/data-row/DataRow.tsx +18 -7
  377. package/src/components/ui/divider/Divider.stories.tsx +1 -1
  378. package/src/components/ui/divider/Divider.tsx +1 -5
  379. package/src/components/ui/drawer/Drawer.stories.tsx +5 -16
  380. package/src/components/ui/drawer/Drawer.tsx +13 -27
  381. package/src/components/ui/floating-overlay-depth.test.tsx +143 -0
  382. package/src/components/ui/form-field/FormField.stories.tsx +6 -20
  383. package/src/components/ui/form-field/FormField.test.tsx +1 -5
  384. package/src/components/ui/form-field/FormField.tsx +10 -46
  385. package/src/components/ui/help-tip/HelpTip.stories.tsx +8 -19
  386. package/src/components/ui/help-tip/HelpTip.test.tsx +1 -3
  387. package/src/components/ui/help-tip/HelpTip.tsx +21 -22
  388. package/src/components/ui/help-tip/index.ts +7 -1
  389. package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
  390. package/src/components/ui/icon-box/IconBox.test.tsx +1 -7
  391. package/src/components/ui/icon-box/IconBox.tsx +10 -18
  392. package/src/components/ui/indicator/Indicator.stories.tsx +5 -4
  393. package/src/components/ui/indicator/Indicator.tsx +20 -14
  394. package/src/components/ui/input/Input.stories.tsx +3 -11
  395. package/src/components/ui/input/Input.test.tsx +2 -10
  396. package/src/components/ui/input/Input.tsx +10 -25
  397. package/src/components/ui/input/index.ts +7 -1
  398. package/src/components/ui/link/Link.stories.tsx +19 -7
  399. package/src/components/ui/list-item/ListItem.stories.tsx +17 -5
  400. package/src/components/ui/list-item/ListItem.tsx +2 -9
  401. package/src/components/ui/menu/Menu.stories.tsx +21 -15
  402. package/src/components/ui/menu/Menu.test.tsx +2 -11
  403. package/src/components/ui/menu/Menu.tsx +12 -14
  404. package/src/components/ui/menu/index.ts +8 -1
  405. package/src/components/ui/modal/Modal.stories.tsx +2 -2
  406. package/src/components/ui/modal/Modal.tsx +31 -19
  407. package/src/components/ui/pill/Pill.stories.tsx +111 -23
  408. package/src/components/ui/pill/Pill.test.tsx +101 -3
  409. package/src/components/ui/pill/Pill.tsx +168 -49
  410. package/src/components/ui/pill/index.ts +1 -1
  411. package/src/components/ui/popover/Popover.stories.tsx +6 -8
  412. package/src/components/ui/popover/Popover.test.tsx +1 -6
  413. package/src/components/ui/popover/Popover.tsx +20 -15
  414. package/src/components/ui/popover/index.ts +7 -1
  415. package/src/components/ui/progress/Progress.stories.tsx +3 -12
  416. package/src/components/ui/progress/Progress.test.tsx +1 -5
  417. package/src/components/ui/progress/Progress.tsx +9 -6
  418. package/src/components/ui/radio/Radio.stories.tsx +42 -21
  419. package/src/components/ui/radio/Radio.test.tsx +6 -2
  420. package/src/components/ui/radio/Radio.tsx +23 -22
  421. package/src/components/ui/section/Section.stories.tsx +12 -21
  422. package/src/components/ui/section/Section.test.tsx +1 -6
  423. package/src/components/ui/section/Section.tsx +1 -3
  424. package/src/components/ui/section/index.ts +1 -5
  425. package/src/components/ui/select/Select.stories.tsx +3 -13
  426. package/src/components/ui/select/Select.test.tsx +2 -11
  427. package/src/components/ui/select/Select.tsx +16 -15
  428. package/src/components/ui/skeleton/Skeleton.stories.tsx +3 -11
  429. package/src/components/ui/skeleton/Skeleton.tsx +3 -14
  430. package/src/components/ui/skeleton/index.ts +1 -7
  431. package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
  432. package/src/components/ui/spinner/Spinner.tsx +4 -5
  433. package/src/components/ui/stack/Stack.stories.tsx +8 -8
  434. package/src/components/ui/stack/Stack.tsx +1 -1
  435. package/src/components/ui/surface/Surface.stories.tsx +92 -33
  436. package/src/components/ui/surface/Surface.test.tsx +88 -28
  437. package/src/components/ui/surface/Surface.tsx +37 -87
  438. package/src/components/ui/surface/SurfaceContext.ts +22 -62
  439. package/src/components/ui/surface/index.ts +8 -0
  440. package/src/components/ui/surface/resolveSurface.ts +98 -0
  441. package/src/components/ui/surface/surface.contract.test.ts +6 -1
  442. package/src/components/ui/switch/Switch.stories.tsx +1 -1
  443. package/src/components/ui/switch/Switch.tsx +7 -6
  444. package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
  445. package/src/components/ui/tabs/Tabs.tsx +8 -21
  446. package/src/components/ui/tabs/index.ts +1 -7
  447. package/src/components/ui/tile/Tile.stories.tsx +1 -1
  448. package/src/components/ui/tile/Tile.tsx +3 -0
  449. package/src/components/ui/toast/Toast.stories.tsx +1 -1
  450. package/src/components/ui/toast/Toast.test.tsx +2 -8
  451. package/src/components/ui/toast/Toast.tsx +49 -57
  452. package/src/components/ui/toast/index.ts +0 -1
  453. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +92 -71
  454. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +3 -18
  455. package/src/components/ui/toolbar-button/ToolbarButton.tsx +38 -49
  456. package/src/components/ui/tooltip/Tooltip.stories.tsx +12 -3
  457. package/src/components/ui/tooltip/Tooltip.test.tsx +21 -3
  458. package/src/components/ui/tooltip/Tooltip.tsx +55 -36
  459. package/src/components/ui/tooltip/index.ts +1 -1
  460. package/src/components/ui/trigger/TriggerSurface.test.tsx +215 -0
  461. package/src/components/ui/trigger/TriggerSurface.tsx +81 -0
  462. package/src/components/ui/trigger/index.ts +2 -0
  463. package/src/stories/PresetShowcase.stories.tsx +101 -48
  464. package/src/test/no-device-internals.test.ts +3 -3
  465. package/src/test/no-raw-color.test.ts +39 -0
  466. package/src/theme/Alpha.stories.tsx +103 -0
  467. package/src/theme/ChoosingTokens.stories.tsx +48 -0
  468. package/src/theme/ColorPalettes.stories.tsx +62 -45
  469. package/src/theme/ColorPrimitives.stories.tsx +35 -35
  470. package/src/theme/Depth.stories.tsx +132 -65
  471. package/src/theme/DepthCalibration.stories.tsx +23 -21
  472. package/src/theme/ProposedTokensVW82.stories.tsx +414 -0
  473. package/src/theme/ThemeProvider.test.tsx +4 -4
  474. package/src/theme/barrel.ts +7 -6
  475. package/src/theme/color-stories.coverage.test.ts +1 -1
  476. package/src/theme/color-utils.ts +41 -29
  477. package/src/theme/config.completeness.test.ts +1 -1
  478. package/src/theme/config.ts +40 -0
  479. package/src/theme/elevation.test.ts +82 -56
  480. package/src/theme/elevation.ts +77 -561
  481. package/src/theme/global.css +45 -5
  482. package/src/theme/grey-ramp.test.ts +24 -8
  483. package/src/theme/lift.test.ts +39 -0
  484. package/src/theme/lift.ts +102 -0
  485. package/src/theme/materials.test.ts +5 -2
  486. package/src/theme/materials.ts +38 -4
  487. package/src/theme/native-theming-guard.test.ts +5 -5
  488. package/src/theme/presets/apply.ts +4 -0
  489. package/src/theme/presets/audiobook.ts +5 -2
  490. package/src/theme/presets/default.ts +4 -0
  491. package/src/theme/presets/index.ts +14 -1
  492. package/src/theme/presets/types.ts +1 -1
  493. package/src/theme/semantic-contrast.test.ts +13 -4
  494. package/src/theme/status-distinctness.test.ts +66 -0
  495. package/src/theme/subtle-tone-weight.test.ts +163 -0
  496. package/src/theme/surface-planes.test.ts +37 -0
  497. package/src/theme/surface-planes.ts +68 -0
  498. package/src/theme/tailwind-var-opacity.test.ts +163 -0
  499. package/src/theme/token-export-guard.test.ts +1 -3
  500. package/src/theme/tokens/primitives.test.ts +15 -7
  501. package/src/theme/tokens/primitives.ts +98 -33
  502. package/src/theme/tokens/semantic-hex-guard.test.ts +322 -0
  503. package/src/theme/tokens/semantic.ts +124 -54
  504. package/src/theme/wash-ladder.test.ts +58 -0
  505. package/src/theme/workout-tokens.ts +1 -7
  506. package/src/types/jest-axe.d.ts +2 -6
  507. package/src/types/raw-md.d.ts +5 -0
  508. package/src/utils/colors.alpha-adoption.test.ts +408 -0
  509. package/src/utils/colors.ts +4 -4
  510. package/src/utils/form.ts +1 -3
  511. package/src/utils/index.ts +2 -7
  512. package/src/utils/number-format.ts +17 -0
  513. package/src/utils/workout-format.ts +15 -6
  514. package/src/web-jsx/jsx-runtime.ts +1 -5
  515. package/tailwind.config.js +55 -9
  516. package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +0 -151
  517. package/src/components/custom/Workout/RowInventory.stories.tsx +0 -408
  518. package/src/components/custom/Workout/S3FullRail.stories.tsx +0 -46
  519. package/src/components/custom/Workout/S3SessionPace.stories.tsx +0 -95
  520. package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +0 -61
  521. package/src/components/custom/Workout/S3SetTypes.stories.tsx +0 -176
  522. package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +0 -240
  523. package/src/components/shell/DashboardShell.stories.tsx +0 -65
  524. package/src/components/shell/DashboardShell.tsx +0 -75
  525. package/src/components/shell/SessionRailSpecimen.stories.tsx +0 -753
  526. package/src/theme/elevation.stories.tsx +0 -346
  527. package/src/utils/useTheme.ts +0 -72
  528. /package/src/components/shell/{DeviceIndicator.test.tsx → workout/DeviceIndicator.test.tsx} +0 -0
  529. /package/src/components/shell/{DeviceMenu.test.tsx → workout/DeviceMenu.test.tsx} +0 -0
  530. /package/src/components/shell/{DeviceRow.test.tsx → workout/DeviceRow.test.tsx} +0 -0
  531. /package/src/components/shell/{SessionStatePill.test.tsx → workout/SessionStatePill.test.tsx} +0 -0
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import type { ReactNode } from 'react'
7
7
  import { TempoDisplay } from './TempoDisplay'
8
- import { greyRamp } from '../../../theme/tokens/primitives'
8
+ import { greyRamp, primitiveRamps } from '../../../theme/tokens/primitives'
9
9
  import { getSemanticColors } from '../../../theme/tokens/semantic'
10
10
 
11
11
  // ---- geometry
@@ -29,30 +29,55 @@ export const INSET_SHADOW =
29
29
  'inset 0 9px 12px -6px rgba(0,0,0,0.85), inset 7px 0 10px -6px rgba(0,0,0,0.7), inset 0 -1px 0 rgba(255,255,255,0.02)'
30
30
 
31
31
  // ---- zone colors = real titan ramp pins (primitiveRamps)
32
- export const RED = '#D14343' // red 600
33
- export const ORANGE = '#FF7900' // orange 400
34
- export const AMBER = '#F9B415' // amber 300
35
- export const GREEN = '#2ED573' // green 300
36
- export const velColor = (v: number): string => (v < 0.5 ? RED : v < 0.75 ? ORANGE : v < 1.0 ? AMBER : GREEN)
32
+ export const RED = primitiveRamps.red[600]
33
+ export const ORANGE = primitiveRamps.orange[400]
34
+ export const AMBER = primitiveRamps.amber[300]
35
+ export const GREEN = primitiveRamps.green[300]
36
+ export const velColor = (v: number): string =>
37
+ v < 0.5 ? RED : v < 0.75 ? ORANGE : v < 1.0 ? AMBER : GREEN
37
38
 
38
39
  // active-set pulse: each segment eases along its own ramp (adjacent lighter/darker steps) through the pin.
39
40
  export const PULSE_CSS = `
40
- @keyframes s3pRed{0%,100%{background:#E05254}50%{background:#A4221C}}
41
- @keyframes s3pOrange{0%,100%{background:#FFA063}50%{background:#DA5F00}}
42
- @keyframes s3pAmber{0%,100%{background:#FFD352}50%{background:#E08C00}}
43
- @keyframes s3pGreen{0%,100%{background:#58F69E}50%{background:#21C05D}}`
41
+ @keyframes s3pRed{0%,100%{background:${primitiveRamps.red[500]}}50%{background:${primitiveRamps.red[700]}}}
42
+ @keyframes s3pOrange{0%,100%{background:${primitiveRamps.orange[300]}}50%{background:${primitiveRamps.orange[500]}}}
43
+ @keyframes s3pAmber{0%,100%{background:${primitiveRamps.amber[200]}}50%{background:${primitiveRamps.amber[400]}}}
44
+ @keyframes s3pGreen{0%,100%{background:${primitiveRamps.green[200]}}50%{background:${primitiveRamps.green[400]}}}`
44
45
  export const pulseAnim = (c: string): string | undefined => {
45
- const name = c === RED ? 's3pRed' : c === ORANGE ? 's3pOrange' : c === AMBER ? 's3pAmber' : c === GREEN ? 's3pGreen' : null
46
+ const name =
47
+ c === RED
48
+ ? 's3pRed'
49
+ : c === ORANGE
50
+ ? 's3pOrange'
51
+ : c === AMBER
52
+ ? 's3pAmber'
53
+ : c === GREEN
54
+ ? 's3pGreen'
55
+ : null
46
56
  return name ? `${name} 1.9s ease-in-out infinite` : undefined
47
57
  }
48
58
 
49
59
  // ---- sets/reps/load in the TempoDisplay visual language (Inter · 600 · letter-spacing 1 · gray separators)
50
60
  const INTER = 'Inter, sans-serif'
61
+ // VW-82: Tailwind gray-500; no ramp step matches. Left raw, proposed in the PR.
51
62
  const SRL_SEP = '#6B7280'
52
63
  function SRLCell({ children, color = T_PRIMARY }: { children: ReactNode; color?: string }) {
53
- return <span style={{ fontFamily: INTER, fontSize: 11, fontWeight: 600, letterSpacing: 1, color }}>{children}</span>
64
+ return (
65
+ <span style={{ fontFamily: INTER, fontSize: 11, fontWeight: 600, letterSpacing: 1, color }}>
66
+ {children}
67
+ </span>
68
+ )
54
69
  }
55
- export function SetsRepsLoad({ sets, reps, load, unit = 'lb' }: { sets: number; reps: number | string; load: number; unit?: string }) {
70
+ export function SetsRepsLoad({
71
+ sets,
72
+ reps,
73
+ load,
74
+ unit = 'lb',
75
+ }: {
76
+ sets: number
77
+ reps: number | string
78
+ load: number
79
+ unit?: string
80
+ }) {
56
81
  return (
57
82
  <span style={{ display: 'inline-flex', alignItems: 'baseline', whiteSpace: 'nowrap' }}>
58
83
  <SRLCell>{sets}</SRLCell>
@@ -66,7 +91,10 @@ export function SetsRepsLoad({ sets, reps, load, unit = 'lb' }: { sets: number;
66
91
  }
67
92
 
68
93
  // ---- per-set bar: one continuous bar, rep intensities as butted segments (no gaps); active segments pulse
69
- export type SetState = { status: 'done'; reps: number[] } | { status: 'active'; reps: number[]; planned: number } | { status: 'todo'; planned: number }
94
+ export type SetState =
95
+ | { status: 'done'; reps: number[] }
96
+ | { status: 'active'; reps: number[]; planned: number }
97
+ | { status: 'todo'; planned: number }
70
98
  export function setColors(s: SetState): string[] {
71
99
  if (s.status === 'todo') return [GREY]
72
100
  if (s.status === 'done') return s.reps.map(velColor)
@@ -74,9 +102,26 @@ export function setColors(s: SetState): string[] {
74
102
  }
75
103
  function SetBar({ colors, h, pulse }: { colors: string[]; h: number; pulse?: boolean }) {
76
104
  return (
77
- <div style={{ display: 'flex', flex: 1, minWidth: 0, height: h, borderRadius: 2, overflow: 'hidden' }}>
105
+ <div
106
+ style={{
107
+ display: 'flex',
108
+ flex: 1,
109
+ minWidth: 0,
110
+ height: h,
111
+ borderRadius: 2,
112
+ overflow: 'hidden',
113
+ }}
114
+ >
78
115
  {colors.map((c, i) => (
79
- <div key={i} style={{ flex: 1, height: '100%', background: c, animation: pulse && c !== GREY ? pulseAnim(c) : undefined }} />
116
+ <div
117
+ key={i}
118
+ style={{
119
+ flex: 1,
120
+ height: '100%',
121
+ background: c,
122
+ animation: pulse && c !== GREY ? pulseAnim(c) : undefined,
123
+ }}
124
+ />
80
125
  ))}
81
126
  </div>
82
127
  )
@@ -92,20 +137,51 @@ export function SetStrip({ sets, h }: { sets: SetState[]; h: number }) {
92
137
  }
93
138
 
94
139
  export function Indicator({ kind }: { kind: 'pr' | 'issue' | 'info' }) {
95
- const map = { pr: ['★', ORANGE], issue: ['!', RED], info: ['i', '#2196F3'] } as const
140
+ const map = { pr: ['★', ORANGE], issue: ['!', RED], info: ['i', T['status-info']] } as const
96
141
  const [glyph, color] = map[kind]
97
142
  return (
98
- <span style={{ width: 16, height: 16, borderRadius: 8, border: `1px solid ${color}`, color, fontSize: 10, fontWeight: 800, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>{glyph}</span>
143
+ <span
144
+ style={{
145
+ width: 16,
146
+ height: 16,
147
+ borderRadius: 8,
148
+ border: `1px solid ${color}`,
149
+ color,
150
+ fontSize: 10,
151
+ fontWeight: 800,
152
+ display: 'inline-flex',
153
+ alignItems: 'center',
154
+ justifyContent: 'center',
155
+ }}
156
+ >
157
+ {glyph}
158
+ </span>
99
159
  )
100
160
  }
101
161
 
102
162
  const labelStyle = { fontSize: 14, fontWeight: 800, color: T_PRIMARY } as const
103
163
 
104
164
  // ---- the finalized heading unit
105
- export type Exercise = { name: string; sets: number; reps: number | string; load: number; tempo: [number, number, number, number]; indicator?: 'pr' | 'issue' | 'info'; setStates: SetState[]; upcoming?: boolean }
165
+ export type Exercise = {
166
+ name: string
167
+ sets: number
168
+ reps: number | string
169
+ load: number
170
+ tempo: [number, number, number, number]
171
+ indicator?: 'pr' | 'issue' | 'info'
172
+ setStates: SetState[]
173
+ upcoming?: boolean
174
+ }
106
175
  export function Heading({ ex, stripH }: { ex: Exercise; stripH: number }) {
107
176
  return (
108
- <div style={{ padding: '9px 12px', display: 'flex', flexDirection: 'column', opacity: ex.upcoming ? 0.55 : 1 }}>
177
+ <div
178
+ style={{
179
+ padding: '9px 12px',
180
+ display: 'flex',
181
+ flexDirection: 'column',
182
+ opacity: ex.upcoming ? 0.55 : 1,
183
+ }}
184
+ >
109
185
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
110
186
  <span style={labelStyle}>{ex.name}</span>
111
187
  <div style={{ flex: 1 }} />
@@ -124,21 +200,80 @@ export function Heading({ ex, stripH }: { ex: Exercise; stripH: number }) {
124
200
  }
125
201
 
126
202
  // ---- sample session (per-rep mean velocities; sets fatigue across the workout, reps decay in-set)
127
- export const reps = (n: number, start: number, span = 0.4): number[] => Array.from({ length: n }, (_, r) => +(start - (span * r) / Math.max(1, n - 1)).toFixed(3))
203
+ export const reps = (n: number, start: number, span = 0.4): number[] =>
204
+ Array.from({ length: n }, (_, r) => +(start - (span * r) / Math.max(1, n - 1)).toFixed(3))
128
205
  export const SESSION: Exercise[] = [
129
- { name: 'Seated Cable Row', sets: 5, reps: 8, load: 145, tempo: [3, 1, 2, 0], indicator: 'pr', setStates: [1.0, 0.9, 0.8, 0.7, 0.6].map((s) => ({ status: 'done', reps: reps(8, s) })) },
130
- { name: 'Incline DB Press', sets: 3, reps: 8, load: 70, tempo: [3, 0, 3, 0], setStates: [0.72, 0.62, 0.54].map((s) => ({ status: 'done', reps: reps(8, s) })) },
131
- { name: 'Cable Chest Press', sets: 3, reps: 10, load: 90, tempo: [2, 1, 2, 0], indicator: 'info', setStates: [{ status: 'done', reps: reps(10, 0.72) }, { status: 'active', reps: reps(5, 0.62), planned: 10 }, { status: 'todo', planned: 10 }] },
132
- { name: 'Standing Calf Raise', sets: 5, reps: 20, load: 25, tempo: [2, 1, 2, 0], upcoming: true, setStates: [1, 2, 3, 4, 5].map(() => ({ status: 'todo', planned: 20 })) },
133
- { name: 'Face Pull', sets: 3, reps: '15-20', load: 40, tempo: [2, 1, 2, 0], upcoming: true, setStates: [1, 2, 3].map(() => ({ status: 'todo', planned: 18 })) },
206
+ {
207
+ name: 'Seated Cable Row',
208
+ sets: 5,
209
+ reps: 8,
210
+ load: 145,
211
+ tempo: [3, 1, 2, 0],
212
+ indicator: 'pr',
213
+ setStates: [1.0, 0.9, 0.8, 0.7, 0.6].map((s) => ({ status: 'done', reps: reps(8, s) })),
214
+ },
215
+ {
216
+ name: 'Incline DB Press',
217
+ sets: 3,
218
+ reps: 8,
219
+ load: 70,
220
+ tempo: [3, 0, 3, 0],
221
+ setStates: [0.72, 0.62, 0.54].map((s) => ({ status: 'done', reps: reps(8, s) })),
222
+ },
223
+ {
224
+ name: 'Cable Chest Press',
225
+ sets: 3,
226
+ reps: 10,
227
+ load: 90,
228
+ tempo: [2, 1, 2, 0],
229
+ indicator: 'info',
230
+ setStates: [
231
+ { status: 'done', reps: reps(10, 0.72) },
232
+ { status: 'active', reps: reps(5, 0.62), planned: 10 },
233
+ { status: 'todo', planned: 10 },
234
+ ],
235
+ },
236
+ {
237
+ name: 'Standing Calf Raise',
238
+ sets: 5,
239
+ reps: 20,
240
+ load: 25,
241
+ tempo: [2, 1, 2, 0],
242
+ upcoming: true,
243
+ setStates: [1, 2, 3, 4, 5].map(() => ({ status: 'todo', planned: 20 })),
244
+ },
245
+ {
246
+ name: 'Face Pull',
247
+ sets: 3,
248
+ reps: '15-20',
249
+ load: 40,
250
+ tempo: [2, 1, 2, 0],
251
+ upcoming: true,
252
+ setStates: [1, 2, 3].map(() => ({ status: 'todo', planned: 18 })),
253
+ },
134
254
  ]
135
255
 
136
256
  // ---- shell chrome (neumorphic): flat raised nav + heading plane; sunk inset exercise list
137
257
  export function NavStub() {
138
258
  return (
139
- <div style={{ width: 60, alignSelf: 'stretch', position: 'relative', zIndex: 2, background: RAISED, display: 'flex', flexDirection: 'column', alignItems: 'center', paddingTop: 12, gap: 8 }}>
259
+ <div
260
+ style={{
261
+ width: 60,
262
+ alignSelf: 'stretch',
263
+ position: 'relative',
264
+ zIndex: 2,
265
+ background: RAISED,
266
+ display: 'flex',
267
+ flexDirection: 'column',
268
+ alignItems: 'center',
269
+ paddingTop: 12,
270
+ gap: 8,
271
+ }}
272
+ >
140
273
  {['Live', 'Plan', 'Body'].map((t) => (
141
- <span key={t} style={{ fontSize: 8, color: T_TERTIARY, fontWeight: 700 }}>{t}</span>
274
+ <span key={t} style={{ fontSize: 8, color: T_TERTIARY, fontWeight: 700 }}>
275
+ {t}
276
+ </span>
142
277
  ))}
143
278
  </div>
144
279
  )
@@ -146,17 +281,39 @@ export function NavStub() {
146
281
  export function Rail({ stripH, footer }: { stripH: number; footer?: ReactNode }) {
147
282
  return (
148
283
  <div style={{ width: 246, background: INSET, display: 'flex', flexDirection: 'column' }}>
149
- <div style={{ position: 'relative', zIndex: 2, padding: '11px 12px 10px', background: RAISED }}>
150
- <div style={{ fontSize: 9, fontWeight: 800, letterSpacing: 1, textTransform: 'uppercase', color: T_TERTIARY, display: 'flex', alignItems: 'center', gap: 5 }}>
284
+ <div
285
+ style={{ position: 'relative', zIndex: 2, padding: '11px 12px 10px', background: RAISED }}
286
+ >
287
+ <div
288
+ style={{
289
+ fontSize: 9,
290
+ fontWeight: 800,
291
+ letterSpacing: 1,
292
+ textTransform: 'uppercase',
293
+ color: T_TERTIARY,
294
+ display: 'flex',
295
+ alignItems: 'center',
296
+ gap: 5,
297
+ }}
298
+ >
151
299
  <span style={{ width: 6, height: 6, borderRadius: 3, background: GREEN }} />
152
300
  Live session
153
301
  </div>
154
- <div style={{ fontSize: 14, fontWeight: 800, marginTop: 3, color: T_PRIMARY }}>Pull A · Intensification</div>
155
- <div style={{ fontSize: 10, color: T_SECONDARY, marginTop: 2, fontFamily: 'monospace' }}>ex 3/5 · set 2 live</div>
302
+ <div style={{ fontSize: 14, fontWeight: 800, marginTop: 3, color: T_PRIMARY }}>
303
+ Pull A · Intensification
304
+ </div>
305
+ <div style={{ fontSize: 10, color: T_SECONDARY, marginTop: 2, fontFamily: 'monospace' }}>
306
+ ex 3/5 · set 2 live
307
+ </div>
156
308
  </div>
157
309
  <div style={{ flex: 1, background: INSET, boxShadow: INSET_SHADOW }}>
158
310
  {SESSION.map((ex, i) => (
159
- <div key={i} style={{ borderBottom: i < SESSION.length - 1 ? `1px solid ${BORDER_SUBTLE}` : undefined }}>
311
+ <div
312
+ key={i}
313
+ style={{
314
+ borderBottom: i < SESSION.length - 1 ? `1px solid ${BORDER_SUBTLE}` : undefined,
315
+ }}
316
+ >
160
317
  <Heading ex={ex} stripH={stripH} />
161
318
  </div>
162
319
  ))}
@@ -167,9 +324,29 @@ export function Rail({ stripH, footer }: { stripH: number; footer?: ReactNode })
167
324
  }
168
325
 
169
326
  // ---- page scaffold shared by the sheets
170
- export function Page({ title, note, children }: { title: string; note: string; children: ReactNode }) {
327
+ export function Page({
328
+ title,
329
+ note,
330
+ children,
331
+ }: {
332
+ title: string
333
+ note: string
334
+ children: ReactNode
335
+ }) {
171
336
  return (
172
- <div style={{ minHeight: '100vh', background: '#0A0A0A', color: T_PRIMARY, padding: 28, display: 'flex', flexDirection: 'column', gap: 22, fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' }}>
337
+ <div
338
+ style={{
339
+ minHeight: '100vh',
340
+ // VW-82: no token at this value (nearest background-frame #100D0A).
341
+ background: '#0A0A0A',
342
+ color: T_PRIMARY,
343
+ padding: 28,
344
+ display: 'flex',
345
+ flexDirection: 'column',
346
+ gap: 22,
347
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
348
+ }}
349
+ >
173
350
  <style>{PULSE_CSS}</style>
174
351
  <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
175
352
  <span style={{ fontSize: 20, fontWeight: 800 }}>{title}</span>
@@ -179,5 +356,11 @@ export function Page({ title, note, children }: { title: string; note: string; c
179
356
  </div>
180
357
  )
181
358
  }
182
- export const sectionTitle = { fontSize: 11, fontWeight: 800, letterSpacing: 1.2, textTransform: 'uppercase', color: ORANGE } as const
359
+ export const sectionTitle = {
360
+ fontSize: 11,
361
+ fontWeight: 800,
362
+ letterSpacing: 1.2,
363
+ textTransform: 'uppercase',
364
+ color: ORANGE,
365
+ } as const
183
366
  export const monoTag = { fontFamily: 'monospace', fontSize: 11, color: T_SECONDARY } as const
@@ -116,8 +116,8 @@ export const VIEW_HEIGHT: Record<StripView, { single: number; dual: number }> =
116
116
 
117
117
  export type StripView = 'compact' | 'expanded' | 'hero'
118
118
 
119
- /** The dual renderer for a view. `expanded`'s dual is the lean `rail` (a verified misnomer). */
120
- export const dualVariantFor = (view: StripView) => (view === 'expanded' ? 'rail' : view)
119
+ /** The dual renderer for a view. `expanded`'s dual is the lean `dual-expanded` renderer. */
120
+ export const dualVariantFor = (view: StripView) => (view === 'expanded' ? 'dual-expanded' : view)
121
121
 
122
122
  /**
123
123
  * One scenario, rendered single above dual on the same dataset.
@@ -0,0 +1,66 @@
1
+ # `custom/charts` — shared chart substrate
2
+
3
+ Two marks and one hook, extracted so every value-height bar family in the library
4
+ draws the same bar rather than each re-rolling geometry and entrance animation.
5
+ This is a substrate family: it holds no domain knowledge, and colour, reference
6
+ overlays and labels are passed in by the consumer.
7
+
8
+ This README is the **index**: **composes ↓** and **used-by ↑** for each member,
9
+ so the tree navigates both ways and a hand-rolled bar shows up as a gap. Counts
10
+ come from [`src/arch/arch-graph.json`](../../../arch/arch-graph.json).
11
+
12
+ ## Dependency map
13
+
14
+ | Member | Kind | Composes ↓ | Used-by ↑ | Exported |
15
+ | ----------------- | ---- | ----------------------------------------------- | ------------------------------------------------------ | ------------ |
16
+ | `SparkBars` | atom | `resolveColor`, `cn` | FileActivityDetail, FileActivityRow (ActiveWork) | yes |
17
+ | `SetBarChart` | atom | `barPaper`, `SurfaceContext`, `live-rep-growth` | RomProgressionChart (Fatigue), VelocityStrip (Workout) | no — by path |
18
+ | `live-rep-growth` | hook | `Animated`, `Easing` | SetBarChart, VelocityStrip | no — by path |
19
+ | `flatBarGeometry` | module | — | SegmentedBar (Workout), SetBarChart | no — by path |
20
+
21
+ `SetBarChart` and `live-rep-growth` are deliberately absent from `index.ts`: they
22
+ are workout-internal and imported by path, so the public barrel stays one mark
23
+ wide. Adding them to the barrel is a decision, not a tidy-up.
24
+
25
+ ## What each owns
26
+
27
+ **`SetBarChart`** owns the chart _geometry_ — value→height scaling, the shared
28
+ `scaleMax` override, plot-width-driven adaptive bar width and gap stepping, value
29
+ label thinning — plus the shared bar _language_: paper treatment, the
30
+ grow-from-bottom live rep, up/down orientation, the set-type slot vocabulary. Bar
31
+ colour and the reference overlay (velocity VL bands, ROM working/short lines) are
32
+ props. A spacing or entrance improvement therefore lands once for both consumers.
33
+ Value-height only: `VelocityStrip`'s flat 3 px `mini` strip stays its own mark.
34
+
35
+ **`SparkBars`** is a signed-series sparkline: bars over a baseline, negatives in
36
+ `result-degrade`, a `MIN_BAR_HEIGHT` floor so an almost-empty series still reads
37
+ as a series, and `maxBars` because a sparkline is a recent-history glance.
38
+
39
+ **`live-rep-growth`** is the newest-rep entrance, promoted out of `VelocityStrip`
40
+ so every value-height family animates the live rep identically. It carries the
41
+ 12% `PEAK_OVERSHOOT` for a new-peak bar and honours `prefers-reduced-motion`.
42
+
43
+ **`flatBarGeometry`** is the single source for the default height/gap/radius
44
+ `SegmentedBar` (set-level) and `SetBarChart` (rep-level) each declare, documented
45
+ as intentionally different sizes rather than left to drift apart unnoticed (VW-86).
46
+
47
+ ## Reuse audit
48
+
49
+ | Leaf | Should compose | Status |
50
+ | --------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | Bar geometry | `SetBarChart` | **Closed** for Fatigue and VelocityStrip's expanded charts; `VelocityStrip` mini is a deliberate exception. |
52
+ | Live-rep entrance | `live-rep-growth` | **Closed.** Both consumers use the hook. |
53
+ | Colour blending | `theme/color-math` | **Open.** `SetBarChart.tsx` carries a local `mixHex`. Roadmap decision 8 collapses the four copies into one module; this is one of them. |
54
+ | Surface-relative fill | `SurfaceContext` | **Closed.** `SetBarChart` reads `useSurface`/`useOnSurfaceColor` rather than assuming a plane. |
55
+ | Inline colour | `resolveColor(token)` | **Closed** for `SparkBars`; the family is in the token-pure eslint error block. |
56
+
57
+ ## Watch list
58
+
59
+ - **`mixHex` duplication** (above) is the family's only open token debt. Until
60
+ decision 8 lands, do not add a third copy — import the one in `SetBarChart`.
61
+ - **`SetBarChart` has no story.** `SparkBars` does; the chart is only seen through
62
+ its consumers' stories (`Custom/Fatigue/*`, `Custom/Workout/VelocityStrip`).
63
+ That is why it reads `status:candidate` — see [`MATURITY.md`](../../../../MATURITY.md).
64
+ - **Both marks are `keep-internal`** in the arch graph: two in-repo consumers
65
+ each, zero external. Neither is public API yet; treat prop changes as cheap
66
+ while that holds.
@@ -1,11 +1,34 @@
1
1
  import { describe, it, expect } from 'vitest'
2
2
  import { render, screen } from '@testing-library/react'
3
3
  import { axe } from 'jest-axe'
4
- import { SetBarChart, sideLabelText, valueLabelFontSize, type SetSlot } from './SetBarChart'
4
+ import {
5
+ SetBarChart,
6
+ sideLabelText,
7
+ valueLabelFontSize,
8
+ estimateValueLabelWidth,
9
+ shouldFlipEdgeLabel,
10
+ scaleDenominator,
11
+ FIXED_MAX_VALUE,
12
+ PEAK_HEADROOM,
13
+ type SetSlot,
14
+ } from './SetBarChart'
5
15
 
6
16
  const reps = (values: number[]): SetSlot[] => values.map((v) => ({ kind: 'rep', value: v }))
7
17
  const silver = () => '#C7CBD1'
8
18
 
19
+ describe('scaleDenominator', () => {
20
+ it('gives `fixed` the same headroom band above its ceiling as `peak` gets above its max', () => {
21
+ // Pinned: without this headroom, a rep AT the fixed ceiling fills the plot with zero margin
22
+ // above it, unlike an equally-tall `peak` bar (which always keeps PEAK_HEADROOM's margin).
23
+ expect(scaleDenominator('fixed', 0)).toBeCloseTo(FIXED_MAX_VALUE * PEAK_HEADROOM)
24
+ expect(scaleDenominator('peak', 1)).toBeCloseTo(1 * PEAK_HEADROOM)
25
+ })
26
+
27
+ it('ignores the performed max for `fixed` — the ceiling stays constant across sets', () => {
28
+ expect(scaleDenominator('fixed', 0.5)).toBe(scaleDenominator('fixed', 100))
29
+ })
30
+ })
31
+
9
32
  describe('valueLabelFontSize', () => {
10
33
  it('reads the full 12px at the standard single-hero (220) and dual-wing (110) scales', () => {
11
34
  expect(valueLabelFontSize(220)).toBe(12)
@@ -18,6 +41,28 @@ describe('valueLabelFontSize', () => {
18
41
  })
19
42
  })
20
43
 
44
+ describe('shouldFlipEdgeLabel (TD-07.10)', () => {
45
+ it.each([
46
+ // [roomPx, labelWidthPx, expected]
47
+ [30, 20, false], // half-width (10) comfortably clears the room (30)
48
+ [30, 70, true], // half-width (35) exceeds the room (30)
49
+ [10, 20, false], // half-width (10) exactly equals the room — still a fit (not flipped)
50
+ [9, 20, true], // half-width (10) just exceeds the room (9)
51
+ ])('roomPx=%d labelWidthPx=%d -> flip=%s', (roomPx, labelWidthPx, expected) => {
52
+ expect(shouldFlipEdgeLabel(roomPx, labelWidthPx)).toBe(expected)
53
+ })
54
+ })
55
+
56
+ describe('estimateValueLabelWidth', () => {
57
+ it('scales with both character count and font size', () => {
58
+ expect(estimateValueLabelWidth('1.02', 12)).toBeCloseTo(4 * 12 * 0.62)
59
+ expect(estimateValueLabelWidth('1.02', 8)).toBeLessThan(estimateValueLabelWidth('1.02', 12))
60
+ expect(estimateValueLabelWidth('10.02', 12)).toBeGreaterThan(
61
+ estimateValueLabelWidth('1.02', 12)
62
+ )
63
+ })
64
+ })
65
+
21
66
  describe('sideLabelText', () => {
22
67
  it('keeps the full name when the vertical extent has room', () => {
23
68
  expect(sideLabelText('Left Arm', 200)).toBe('Left Arm')
@@ -23,6 +23,7 @@ import { primitiveRamps } from '../../../theme/tokens/primitives'
23
23
  import { barPaper } from '../../../theme/materials'
24
24
  import { useOnSurfaceColor, useSurface, surfaceBackground } from '../../ui/surface/SurfaceContext'
25
25
  import { useLiveRepGrowth } from './live-rep-growth'
26
+ import { REP_LEVEL_FLAT_BAR } from './flatBarGeometry'
26
27
 
27
28
  /** Linear-blend two #RRGGBB hexes (`t`=0 → a, 1 → b) — the surface-relative solid to-do tone. */
28
29
  function mixHex(a: string, b: string, t: number): string {
@@ -133,6 +134,14 @@ export interface SetBarChartProps {
133
134
  showValueLabels?: boolean
134
135
  /** Format a rep value for its label. Default `String`. */
135
136
  formatValue?: (value: number) => string
137
+ /**
138
+ * The LAST column has no sibling to its right to absorb an overflowing value label, so a wide
139
+ * label there can bleed past the plot's right edge and crowd the reference line that also runs
140
+ * flush to it (TD-07.10). Default false (unchanged for every existing consumer — the single
141
+ * hero and ROM chart). When true, that column's label flips from centered to right-anchored
142
+ * (grows left) once {@link shouldFlipEdgeLabel} says it doesn't fit.
143
+ */
144
+ flipEdgeLabel?: boolean
136
145
  /**
137
146
  * To-do placeholder treatment. `solid` (default) — a solid surface-relative section (the plane
138
147
  * blended toward the on-surface neutral). `dashed` — a dashed outline stub. Exists so ROM can keep
@@ -180,13 +189,21 @@ export interface SetBarChartProps {
180
189
  }
181
190
 
182
191
  /** The `scale="fixed"` velocity ceiling (m/s) — a bar height reads the same absolute value across sets. */
183
- const FIXED_MAX_VALUE = 1.15
184
- /** Headroom above the peak bar: just enough to seat its value label without a big empty band on top. */
192
+ export const FIXED_MAX_VALUE = 1.15
193
+ /**
194
+ * Headroom above the scaling ceiling: just enough to seat the peak bar's value label without a big
195
+ * empty band on top. Applied to BOTH scales — `peak` already multiplies the performed max by this,
196
+ * but `fixed` used the bare ceiling with none: a rep AT or PAST `FIXED_MAX_VALUE` (a fast lift on a
197
+ * live wall, not just a theoretical edge) filled the plot to its very top with no room left for that
198
+ * bar's own label, while an equally-tall `peak` bar always kept this margin. Multiplying `fixed`'s
199
+ * denominator by the same constant gives both scales the identical breathing room above their tallest
200
+ * bar, so a lift at the ceiling reads the same on either scale.
201
+ */
185
202
  export const PEAK_HEADROOM = 1.03
186
203
 
187
204
  /** The height-scaling denominator for `scale` at the given performed max (guarded ≥ 0 by callers). */
188
205
  export function scaleDenominator(scale: 'peak' | 'fixed', maxValue: number): number {
189
- return scale === 'fixed' ? FIXED_MAX_VALUE : maxValue * PEAK_HEADROOM
206
+ return (scale === 'fixed' ? FIXED_MAX_VALUE : maxValue) * PEAK_HEADROOM
190
207
  }
191
208
 
192
209
  /**
@@ -198,6 +215,25 @@ export function valueLabelFontSize(height: number): number {
198
215
  return Math.round(Math.max(8, Math.min(12, height * 0.11)))
199
216
  }
200
217
 
218
+ /** Average glyph width for the bold numeric value-label font, as a fraction of its font size — a
219
+ * measurement-free estimate (RN text can't be synchronously measured) good enough to catch overflow. */
220
+ const LABEL_GLYPH_WIDTH_RATIO = 0.62
221
+
222
+ /** Estimate a value label's rendered width (px) from its formatted text and font size. */
223
+ export function estimateValueLabelWidth(text: string, fontSize: number): number {
224
+ return text.length * fontSize * LABEL_GLYPH_WIDTH_RATIO
225
+ }
226
+
227
+ /**
228
+ * Whether a value label needs to flip from its default centered anchor to a right-anchored
229
+ * (left-growing) placement to clear the plot's right edge (TD-07.10). `roomPx` is the space
230
+ * available to the right of the label's centered position; `labelWidthPx` is its measured or
231
+ * estimated rendered width.
232
+ */
233
+ export function shouldFlipEdgeLabel(roomPx: number, labelWidthPx: number): boolean {
234
+ return labelWidthPx / 2 > roomPx
235
+ }
236
+
201
237
  // --- Hero geometry -----------------------------------------------------------
202
238
  /** Default plot height (px) — tall enough to read a set's shape across a room. */
203
239
  export const SET_BAR_DEFAULT_HEIGHT = 220
@@ -208,7 +244,7 @@ const LABEL_GAP = 3
208
244
  /** Inter-bar gap as a fraction of bar width — the locked dense default (near expanded density). */
209
245
  export const GAP_RATIO = 0.08
210
246
  /** Floor on the proportional inter-bar gap (px) — narrow plots tighten to ~expanded density. */
211
- const MIN_BAR_GAP = 2
247
+ const MIN_BAR_GAP = REP_LEVEL_FLAT_BAR.gapFloor
212
248
  /**
213
249
  * The EXTRA margin at a chunk boundary (drop sub-load / myo cluster / cluster intra-rest), as a
214
250
  * fraction of bar width — proportional so it scales with the dense spacing and reads as a clear
@@ -223,7 +259,7 @@ export const BAR_MAX_WIDTH = 120
223
259
  /** Below this per-bar width the value labels collide, so all but the peak + live rep are dropped. */
224
260
  const LABEL_MIN_BAR_WIDTH = 30
225
261
  /** Default top-corner radius on bars (px). */
226
- const DEFAULT_BAR_RADIUS = 5
262
+ const DEFAULT_BAR_RADIUS = REP_LEVEL_FLAT_BAR.radius
227
263
  /** Minimum drawn height of a performed bar (px) — a near-zero rep still reads as a rep. */
228
264
  const MIN_BAR_HEIGHT = 4
229
265
  /** In `flat` (compact) mode every rep bar is this fraction of the plot height — a uniform short bar. */
@@ -281,6 +317,7 @@ export function SetBarChart({
281
317
  flat = false,
282
318
  showValueLabels: showValueLabelsProp = false,
283
319
  formatValue = String,
320
+ flipEdgeLabel = false,
284
321
  todoVariant = 'solid',
285
322
  minColumns,
286
323
  renderReference,
@@ -462,6 +499,17 @@ export function SetBarChart({
462
499
  // duplicate them); its own label shows otherwise, fading in with `expandProgress`.
463
500
  const ownLabel = showValueLabels && !renderBarOverlay && showBarLabel(repIndex)
464
501
  const labelBottom = (expandProgress ? valueBarHeight(best) : barHeight(best)) + LABEL_GAP
502
+ // Only the LAST cell has no sibling to its right to absorb an overflowing label
503
+ // (TD-07.10) — flip it right-anchored (grows left) when it doesn't fit.
504
+ const isLastCell = i === cells.length - 1
505
+ const labelText = formatValue(value)
506
+ const flipLabel =
507
+ flipEdgeLabel &&
508
+ isLastCell &&
509
+ shouldFlipEdgeLabel(
510
+ barWidth / 2,
511
+ estimateValueLabelWidth(labelText, valueLabelFontSize(height))
512
+ )
465
513
  return (
466
514
  <View key={i} accessibilityElementsHidden={renderBarOverlay == null} style={column}>
467
515
  {ownLabel && (
@@ -471,9 +519,8 @@ export function SetBarChart({
471
519
  style={{
472
520
  position: 'absolute',
473
521
  bottom: labelBottom,
474
- left: 0,
475
- right: 0,
476
- alignItems: 'center',
522
+ ...(flipLabel ? { right: 0 } : { left: 0, right: 0 }),
523
+ alignItems: flipLabel ? 'flex-end' : 'center',
477
524
  ...(expandProgress ? { opacity: expandProgress } : null),
478
525
  }}
479
526
  pointerEvents="none"
@@ -483,7 +530,7 @@ export function SetBarChart({
483
530
  style={[{ fontSize: valueLabelFontSize(height), fontWeight: '700' }, flipStyle]}
484
531
  testID={`${testIDPrefix}-label-${repIndex}`}
485
532
  >
486
- {formatValue(value)}
533
+ {labelText}
487
534
  </Text>
488
535
  </Animated.View>
489
536
  )}