@titan-design/react-ui 0.12.1 → 0.13.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 (509) hide show
  1. package/README.md +46 -47
  2. package/dist/bodymap.js +768 -701
  3. package/dist/bodymap.js.map +1 -1
  4. package/dist/bodymap.mjs +770 -703
  5. package/dist/bodymap.mjs.map +1 -1
  6. package/dist/index.d.mts +1319 -216
  7. package/dist/index.d.ts +1319 -216
  8. package/dist/index.js +5996 -4498
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +5773 -4357
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/{pages-CvsFqNtx.d.mts → pages-CpW3OB3g.d.mts} +21 -3
  13. package/dist/{pages-B1hqnGIC.d.ts → pages-_3Ajpiqu.d.ts} +21 -3
  14. package/dist/pages.d.mts +1 -1
  15. package/dist/pages.d.ts +1 -1
  16. package/dist/pages.js +1435 -1428
  17. package/dist/pages.js.map +1 -1
  18. package/dist/pages.mjs +1443 -1436
  19. package/dist/pages.mjs.map +1 -1
  20. package/dist/theme/index.d.mts +195 -134
  21. package/dist/theme/index.d.ts +195 -134
  22. package/dist/theme/index.js +215 -300
  23. package/dist/theme/index.js.map +1 -1
  24. package/dist/theme/index.mjs +202 -296
  25. package/dist/theme/index.mjs.map +1 -1
  26. package/dist/theme/tokens-css.js +62 -5
  27. package/dist/theme/tokens-css.js.map +1 -1
  28. package/dist/theme/tokens-css.mjs +62 -5
  29. package/dist/theme/tokens-css.mjs.map +1 -1
  30. package/dist/theme/tokens.d.mts +73 -5
  31. package/dist/theme/tokens.d.ts +73 -5
  32. package/dist/theme/tokens.js +55 -7
  33. package/dist/theme/tokens.js.map +1 -1
  34. package/dist/theme/tokens.mjs +55 -8
  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/package.json +1 -1
  40. package/src/arch/arch-graph.freshness.test.ts +52 -0
  41. package/src/arch/arch-graph.json +5021 -1617
  42. package/src/components/custom/ActiveWork/CoChangeChip.stories.tsx +44 -0
  43. package/src/components/custom/ActiveWork/CoChangeChip.test.tsx +29 -0
  44. package/src/components/custom/ActiveWork/CoChangeChip.tsx +48 -0
  45. package/src/components/custom/ActiveWork/Eyebrow.stories.tsx +25 -0
  46. package/src/components/custom/ActiveWork/Eyebrow.test.tsx +16 -0
  47. package/src/components/custom/ActiveWork/Eyebrow.tsx +26 -0
  48. package/src/components/custom/ActiveWork/FileActivityDetail.stories.tsx +50 -0
  49. package/src/components/custom/ActiveWork/FileActivityDetail.test.tsx +66 -0
  50. package/src/components/custom/ActiveWork/FileActivityDetail.tsx +202 -0
  51. package/src/components/custom/ActiveWork/FileActivityRow.stories.tsx +70 -0
  52. package/src/components/custom/ActiveWork/FileActivityRow.test.tsx +67 -0
  53. package/src/components/custom/ActiveWork/FileActivityRow.tsx +130 -0
  54. package/src/components/custom/ActiveWork/FileHistoryExplorer.stories.tsx +84 -0
  55. package/src/components/custom/ActiveWork/FileHistoryExplorer.test.tsx +99 -0
  56. package/src/components/custom/ActiveWork/FileHistoryExplorer.tsx +138 -0
  57. package/src/components/custom/ActiveWork/FilePathLabel.stories.tsx +39 -0
  58. package/src/components/custom/ActiveWork/FilePathLabel.test.tsx +53 -0
  59. package/src/components/custom/ActiveWork/FilePathLabel.tsx +63 -0
  60. package/src/components/custom/ActiveWork/InitiativeBrief.stories.tsx +50 -0
  61. package/src/components/custom/ActiveWork/InitiativeBrief.test.tsx +62 -0
  62. package/src/components/custom/ActiveWork/InitiativeBrief.tsx +181 -0
  63. package/src/components/custom/ActiveWork/InitiativeCard.stories.tsx +73 -0
  64. package/src/components/custom/ActiveWork/InitiativeCard.test.tsx +68 -0
  65. package/src/components/custom/ActiveWork/InitiativeCard.tsx +136 -0
  66. package/src/components/custom/ActiveWork/InitiativeHeader.stories.tsx +59 -0
  67. package/src/components/custom/ActiveWork/InitiativeHeader.test.tsx +40 -0
  68. package/src/components/custom/ActiveWork/InitiativeHeader.tsx +73 -0
  69. package/src/components/custom/ActiveWork/InitiativeReader.stories.tsx +145 -0
  70. package/src/components/custom/ActiveWork/OpenLoops.stories.tsx +58 -0
  71. package/src/components/custom/ActiveWork/OpenLoops.test.tsx +61 -0
  72. package/src/components/custom/ActiveWork/OpenLoops.tsx +107 -0
  73. package/src/components/custom/ActiveWork/PortfolioOverview.stories.tsx +101 -0
  74. package/src/components/custom/ActiveWork/PortfolioOverview.test.tsx +81 -0
  75. package/src/components/custom/ActiveWork/PortfolioOverview.tsx +83 -0
  76. package/src/components/custom/ActiveWork/README.md +305 -0
  77. package/src/components/custom/ActiveWork/SessionDetail.stories.tsx +71 -0
  78. package/src/components/custom/ActiveWork/SessionDetail.test.tsx +84 -0
  79. package/src/components/custom/ActiveWork/SessionDetail.tsx +180 -0
  80. package/src/components/custom/ActiveWork/SessionList.stories.tsx +69 -0
  81. package/src/components/custom/ActiveWork/SessionList.test.tsx +78 -0
  82. package/src/components/custom/ActiveWork/SessionList.tsx +103 -0
  83. package/src/components/custom/ActiveWork/SessionListItem.stories.tsx +64 -0
  84. package/src/components/custom/ActiveWork/SessionListItem.test.tsx +62 -0
  85. package/src/components/custom/ActiveWork/SessionListItem.tsx +152 -0
  86. package/src/components/custom/ActiveWork/SessionReader.stories.tsx +113 -0
  87. package/src/components/custom/ActiveWork/SeverityLabel.stories.tsx +51 -0
  88. package/src/components/custom/ActiveWork/SeverityLabel.test.tsx +68 -0
  89. package/src/components/custom/ActiveWork/SeverityLabel.tsx +97 -0
  90. package/src/components/custom/ActiveWork/TaskRow.stories.tsx +63 -0
  91. package/src/components/custom/ActiveWork/TaskRow.test.tsx +112 -0
  92. package/src/components/custom/ActiveWork/TaskRow.tsx +176 -0
  93. package/src/components/custom/ActiveWork/TaskTable.stories.tsx +123 -0
  94. package/src/components/custom/ActiveWork/TaskTable.test.tsx +233 -0
  95. package/src/components/custom/ActiveWork/TaskTable.tsx +321 -0
  96. package/src/components/custom/ActiveWork/active-work-depth.test.tsx +102 -0
  97. package/src/components/custom/ActiveWork/file-history-fixture.ts +91 -0
  98. package/src/components/custom/ActiveWork/format-time.test.ts +23 -0
  99. package/src/components/custom/ActiveWork/format-time.ts +32 -0
  100. package/src/components/custom/ActiveWork/index.ts +19 -0
  101. package/src/components/custom/ActiveWork/initiative-fixture.ts +104 -0
  102. package/src/components/custom/ActiveWork/session-fixture.ts +267 -0
  103. package/src/components/custom/ActiveWork/session-linkers.ts +56 -0
  104. package/src/components/custom/ActiveWork/task-list-fixture.ts +97 -0
  105. package/src/components/custom/CircularTimer/CircularTimer.stories.tsx +4 -3
  106. package/src/components/custom/DateTime/DateTime.stories.tsx +2 -1
  107. package/src/components/custom/DateTime/DateTime.test.tsx +3 -1
  108. package/src/components/custom/EmptyState/EmptyState.stories.tsx +22 -9
  109. package/src/components/custom/EmptyState/EmptyState.test.tsx +7 -13
  110. package/src/components/custom/EmptyState/EmptyState.tsx +4 -18
  111. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +4 -3
  112. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +2 -2
  113. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +4 -3
  114. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +2 -2
  115. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +8 -4
  116. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +116 -6
  117. package/src/components/custom/Fatigue/LiveFatiguePanel.test.tsx +134 -0
  118. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +54 -83
  119. package/src/components/custom/Fatigue/README.md +40 -3
  120. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +2 -2
  121. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +2 -1
  122. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +4 -3
  123. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +2 -2
  124. package/src/components/custom/Fatigue/fatigue-mock.test.ts +0 -5
  125. package/src/components/custom/Fatigue/fatigue-mock.ts +2 -10
  126. package/src/components/custom/Fatigue/fatigue-tokens.ts +12 -0
  127. package/src/components/custom/Fatigue/index.ts +17 -5
  128. package/src/components/custom/Fatigue/panel-layout.test.ts +149 -0
  129. package/src/components/custom/Fatigue/panel-layout.ts +185 -0
  130. package/src/components/custom/Gauge/Gauge.stories.tsx +2 -2
  131. package/src/components/custom/Gauge/Gauge.test.tsx +1 -1
  132. package/src/components/custom/Gauge/Gauge.tsx +4 -1
  133. package/src/components/custom/Metric/Metric.stories.tsx +10 -1
  134. package/src/components/custom/Metric/Metric.test.tsx +1 -3
  135. package/src/components/custom/Metric/Metric.tsx +5 -20
  136. package/src/components/custom/Prose/MarkdownProse.stories.tsx +88 -0
  137. package/src/components/custom/Prose/MarkdownProse.test.tsx +87 -0
  138. package/src/components/custom/Prose/MarkdownProse.tsx +201 -0
  139. package/src/components/custom/Prose/README.md +58 -0
  140. package/src/components/custom/Prose/index.ts +1 -0
  141. package/src/components/custom/Scatter/Scatter.stories.tsx +46 -8
  142. package/src/components/custom/Scatter/Scatter.test.tsx +2 -2
  143. package/src/components/custom/Scatter/Scatter.tsx +116 -14
  144. package/src/components/custom/Sidebar/Sidebar.stories.tsx +40 -7
  145. package/src/components/custom/Sidebar/Sidebar.test.tsx +5 -1
  146. package/src/components/custom/Sidebar/Sidebar.tsx +16 -33
  147. package/src/components/custom/Table/Table.stories.tsx +98 -15
  148. package/src/components/custom/Table/Table.test.tsx +273 -86
  149. package/src/components/custom/Table/Table.tsx +159 -67
  150. package/src/components/custom/Table/column-fit.test.ts +75 -0
  151. package/src/components/custom/Table/column-fit.ts +116 -0
  152. package/src/components/custom/Table/index.ts +9 -0
  153. package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +8 -6
  154. package/src/components/custom/Treemap/Treemap.stories.tsx +2 -2
  155. package/src/components/custom/Treemap/Treemap.test.tsx +1 -1
  156. package/src/components/custom/Treemap/Treemap.tsx +7 -2
  157. package/src/components/custom/Typography/Typography.stories.tsx +28 -1
  158. package/src/components/custom/Typography/Typography.test.tsx +34 -0
  159. package/src/components/custom/Typography/Typography.tsx +6 -0
  160. package/src/components/custom/Typography/index.ts +1 -8
  161. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +9 -1
  162. package/src/components/custom/Workout/BaseBadge.stories.tsx +19 -7
  163. package/src/components/custom/Workout/BaseBadge.test.tsx +31 -11
  164. package/src/components/custom/Workout/BaseBadge.tsx +3 -0
  165. package/src/components/custom/Workout/BodyMap.stories.tsx +4 -1
  166. package/src/components/custom/Workout/BodyMap.tsx +3 -1
  167. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +16 -4
  168. package/src/components/custom/Workout/BodyMapDetailPanel.test.tsx +11 -15
  169. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +45 -9
  170. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +10 -8
  171. package/src/components/custom/Workout/CapacityBandChart.test.tsx +24 -51
  172. package/src/components/custom/Workout/CapacityBandChart.tsx +139 -108
  173. package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
  174. package/src/components/custom/Workout/DeviationBar.test.tsx +5 -2
  175. package/src/components/custom/Workout/DeviationBar.tsx +21 -16
  176. package/src/components/custom/Workout/DualVelocityStrip.stories.tsx +182 -0
  177. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +3 -2
  178. package/src/components/custom/Workout/ExerciseCard.stories.tsx +8 -7
  179. package/src/components/custom/Workout/ExerciseCard.test.tsx +13 -3
  180. package/src/components/custom/Workout/ExerciseCard.tsx +42 -51
  181. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +7 -7
  182. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +9 -1
  183. package/src/components/custom/Workout/ExerciseHeading.stories.tsx +8 -8
  184. package/src/components/custom/Workout/ExerciseHeading.tsx +8 -5
  185. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +6 -4
  186. package/src/components/custom/Workout/ExerciseIndicator.test.tsx +27 -11
  187. package/src/components/custom/Workout/ExerciseIndicator.tsx +14 -15
  188. package/src/components/custom/Workout/FatigueMeter.stories.tsx +5 -5
  189. package/src/components/custom/Workout/InputBar.stories.tsx +4 -5
  190. package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
  191. package/src/components/custom/Workout/IntensityBar.test.tsx +5 -1
  192. package/src/components/custom/Workout/IntensityBar.tsx +4 -2
  193. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +5 -4
  194. package/src/components/custom/Workout/MesoCard.stories.tsx +4 -1
  195. package/src/components/custom/Workout/MesoCard.test.tsx +7 -18
  196. package/src/components/custom/Workout/MesoCard.tsx +12 -9
  197. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
  198. package/src/components/custom/Workout/MesoProgressBar.test.tsx +18 -11
  199. package/src/components/custom/Workout/MesoProgressBar.tsx +15 -13
  200. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +2 -1
  201. package/src/components/custom/Workout/MesoStatusCard.test.tsx +14 -44
  202. package/src/components/custom/Workout/MesoStatusCard.tsx +9 -23
  203. package/src/components/custom/Workout/MetricTiles.stories.tsx +3 -2
  204. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +2 -8
  205. package/src/components/custom/Workout/MuscleGroupChip.test.tsx +23 -31
  206. package/src/components/custom/Workout/MuscleGroupChip.tsx +28 -67
  207. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
  208. package/src/components/custom/Workout/PlaceholderStrip.test.tsx +17 -10
  209. package/src/components/custom/Workout/PlaceholderStrip.tsx +9 -8
  210. package/src/components/custom/Workout/PrBadge.stories.tsx +2 -1
  211. package/src/components/custom/Workout/PrBadge.test.tsx +3 -9
  212. package/src/components/custom/Workout/PrBadge.tsx +13 -17
  213. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +4 -1
  214. package/src/components/custom/Workout/PrHistoryModal.test.tsx +13 -36
  215. package/src/components/custom/Workout/PrHistoryModal.tsx +8 -4
  216. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +6 -1
  217. package/src/components/custom/Workout/README.md +11 -6
  218. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +3 -2
  219. package/src/components/custom/Workout/ReadinessCheck.test.tsx +12 -18
  220. package/src/components/custom/Workout/ReadinessCheck.tsx +19 -3
  221. package/src/components/custom/Workout/RestTimer.stories.tsx +3 -2
  222. package/src/components/custom/Workout/ScheduleTiles.stories.tsx +3 -2
  223. package/src/components/custom/Workout/ScheduleTiles.test.tsx +3 -4
  224. package/src/components/custom/Workout/ScheduleTiles.tsx +4 -7
  225. package/src/components/custom/Workout/SegmentedBar.stories.tsx +5 -5
  226. package/src/components/custom/Workout/SegmentedBar.tsx +6 -2
  227. package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +5 -5
  228. package/src/components/custom/Workout/SessionHeader.stories.tsx +6 -6
  229. package/src/components/custom/Workout/SessionHeader.tsx +12 -12
  230. package/src/components/custom/Workout/SessionRail.stories.tsx +12 -11
  231. package/src/components/custom/Workout/SessionRail.tsx +7 -9
  232. package/src/components/custom/Workout/SetBar.stories.tsx +10 -6
  233. package/src/components/custom/Workout/SetBar.test.tsx +49 -4
  234. package/src/components/custom/Workout/SetBar.tsx +38 -12
  235. package/src/components/custom/Workout/SetRow.stories.tsx +3 -2
  236. package/src/components/custom/Workout/SetRow.test.tsx +16 -2
  237. package/src/components/custom/Workout/SetRow.tsx +61 -23
  238. package/src/components/custom/Workout/SetStrip.stories.tsx +60 -5
  239. package/src/components/custom/Workout/SetStrip.test.tsx +25 -0
  240. package/src/components/custom/Workout/SetStrip.tsx +24 -4
  241. package/src/components/custom/Workout/SetTableHeader.stories.tsx +6 -6
  242. package/src/components/custom/Workout/SetTableHeader.tsx +9 -12
  243. package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +7 -7
  244. package/src/components/custom/Workout/SetsRepsLoad.tsx +3 -5
  245. package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
  246. package/src/components/custom/Workout/Sparkline.test.tsx +37 -19
  247. package/src/components/custom/Workout/Sparkline.tsx +14 -18
  248. package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
  249. package/src/components/custom/Workout/StatusDot.test.tsx +44 -5
  250. package/src/components/custom/Workout/StatusDot.tsx +93 -63
  251. package/src/components/custom/Workout/StatusPill.stories.tsx +1 -1
  252. package/src/components/custom/Workout/StatusPill.test.tsx +6 -15
  253. package/src/components/custom/Workout/StatusPill.tsx +27 -38
  254. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +9 -3
  255. package/src/components/custom/Workout/StrengthTrendChart.test.tsx +15 -37
  256. package/src/components/custom/Workout/StrengthTrendChart.tsx +31 -30
  257. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +27 -64
  258. package/src/components/custom/Workout/SupersetWrapper.test.tsx +16 -14
  259. package/src/components/custom/Workout/SupersetWrapper.tsx +17 -21
  260. package/src/components/custom/Workout/TempoDisplay.stories.tsx +22 -7
  261. package/src/components/custom/Workout/TempoDisplay.test.tsx +18 -4
  262. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +9 -1
  263. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +4 -1
  264. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +7 -4
  265. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +4 -1
  266. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +4 -1
  267. package/src/components/custom/Workout/VelocityStrip.stories.tsx +12 -6
  268. package/src/components/custom/Workout/VelocityStrip.tsx +3 -0
  269. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +6 -7
  270. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +15 -6
  271. package/src/components/custom/Workout/WeekRow.stories.tsx +4 -1
  272. package/src/components/custom/Workout/WeekRow.test.tsx +18 -15
  273. package/src/components/custom/Workout/WeekRow.tsx +23 -16
  274. package/src/components/custom/Workout/WeightBadge.stories.tsx +2 -1
  275. package/src/components/custom/Workout/WeightBadge.test.tsx +50 -19
  276. package/src/components/custom/Workout/WeightBadge.tsx +28 -27
  277. package/src/components/custom/Workout/WorkoutCard.stories.tsx +6 -1
  278. package/src/components/custom/Workout/WorkoutCard.test.tsx +18 -0
  279. package/src/components/custom/Workout/WorkoutCard.tsx +35 -57
  280. package/src/components/custom/Workout/WorkoutPill.stories.tsx +5 -2
  281. package/src/components/custom/Workout/WorkoutPill.test.tsx +39 -7
  282. package/src/components/custom/Workout/WorkoutPill.tsx +88 -57
  283. package/src/components/custom/Workout/ZoneTrack.stories.tsx +7 -6
  284. package/src/components/custom/Workout/ZoneTrack.tsx +10 -9
  285. package/src/components/custom/Workout/index.ts +5 -0
  286. package/src/components/custom/Workout/metricText.stories.tsx +6 -5
  287. package/src/components/custom/Workout/metricText.tsx +4 -0
  288. package/src/components/custom/Workout/setHeadingKit.tsx +207 -26
  289. package/src/components/custom/charts/README.md +61 -0
  290. package/src/components/custom/charts/SetBarChart.test.tsx +30 -1
  291. package/src/components/custom/charts/SetBarChart.tsx +42 -4
  292. package/src/components/custom/charts/SparkBars.stories.tsx +55 -0
  293. package/src/components/custom/charts/SparkBars.test.tsx +69 -0
  294. package/src/components/custom/charts/SparkBars.tsx +85 -0
  295. package/src/components/custom/charts/index.ts +3 -0
  296. package/src/components/custom/index.ts +3 -0
  297. package/src/components/custom/stepper/Stepper.stories.tsx +13 -10
  298. package/src/components/custom/stepper/Stepper.test.tsx +1 -7
  299. package/src/components/custom/stepper/Stepper.tsx +13 -28
  300. package/src/components/icons/icons.stories.tsx +1 -1
  301. package/src/components/icons/icons.tsx +51 -0
  302. package/src/components/shell/AppShell.stories.tsx +150 -0
  303. package/src/components/shell/AppShell.test.tsx +70 -0
  304. package/src/components/shell/AppShell.tsx +90 -0
  305. package/src/components/shell/BrandLockup.stories.tsx +66 -5
  306. package/src/components/shell/BrandLockup.test.tsx +34 -1
  307. package/src/components/shell/BrandLockup.tsx +27 -11
  308. package/src/components/shell/NavItem.stories.tsx +4 -4
  309. package/src/components/shell/NavItem.test.tsx +17 -0
  310. package/src/components/shell/NavItem.tsx +17 -5
  311. package/src/components/shell/README.md +89 -26
  312. package/src/components/shell/SideNav.stories.tsx +38 -7
  313. package/src/components/shell/SideNav.test.tsx +16 -8
  314. package/src/components/shell/SideNav.tsx +17 -17
  315. package/src/components/shell/TopBar.stories.tsx +44 -19
  316. package/src/components/shell/TopBar.test.tsx +34 -12
  317. package/src/components/shell/TopBar.tsx +61 -40
  318. package/src/components/shell/brands.tsx +66 -0
  319. package/src/components/shell/index.ts +9 -8
  320. package/src/components/shell/{DeviceIndicator.stories.tsx → workout/DeviceIndicator.stories.tsx} +2 -2
  321. package/src/components/shell/{DeviceIndicator.tsx → workout/DeviceIndicator.tsx} +2 -2
  322. package/src/components/shell/{DeviceMenu.stories.tsx → workout/DeviceMenu.stories.tsx} +2 -2
  323. package/src/components/shell/{DeviceMenu.tsx → workout/DeviceMenu.tsx} +3 -3
  324. package/src/components/shell/{DeviceRow.stories.tsx → workout/DeviceRow.stories.tsx} +2 -2
  325. package/src/components/shell/{DeviceRow.tsx → workout/DeviceRow.tsx} +3 -3
  326. package/src/components/shell/{SessionStatePill.stories.tsx → workout/SessionStatePill.stories.tsx} +2 -2
  327. package/src/components/shell/{SessionStatePill.tsx → workout/SessionStatePill.tsx} +5 -3
  328. package/src/components/shell/workout/WorkoutShell.stories.tsx +71 -0
  329. package/src/components/shell/{DashboardShell.test.tsx → workout/WorkoutShell.test.tsx} +19 -8
  330. package/src/components/shell/workout/WorkoutShell.tsx +77 -0
  331. package/src/components/shell/workout/WorkoutTopBar.stories.tsx +58 -0
  332. package/src/components/shell/workout/WorkoutTopBar.test.tsx +39 -0
  333. package/src/components/shell/workout/WorkoutTopBar.tsx +52 -0
  334. package/src/components/shell/workout/index.ts +9 -0
  335. package/src/components/shell/workout/workoutNavItems.tsx +16 -0
  336. package/src/components/ui/README.md +110 -0
  337. package/src/components/ui/alert/Alert.stories.tsx +1 -1
  338. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +11 -13
  339. package/src/components/ui/autocomplete/Autocomplete.test.tsx +7 -25
  340. package/src/components/ui/autocomplete/Autocomplete.tsx +21 -28
  341. package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
  342. package/src/components/ui/avatar/Avatar.tsx +2 -10
  343. package/src/components/ui/badge/Badge.stories.tsx +83 -28
  344. package/src/components/ui/badge/Badge.test.tsx +19 -3
  345. package/src/components/ui/badge/Badge.tsx +48 -55
  346. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  347. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +4 -16
  348. package/src/components/ui/button/Button.stories.tsx +46 -12
  349. package/src/components/ui/button/Button.tsx +32 -25
  350. package/src/components/ui/card/Card.stories.tsx +90 -94
  351. package/src/components/ui/card/Card.test.tsx +80 -6
  352. package/src/components/ui/card/Card.tsx +150 -203
  353. package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
  354. package/src/components/ui/checkbox/Checkbox.tsx +6 -26
  355. package/src/components/ui/chip/Chip.stories.tsx +37 -13
  356. package/src/components/ui/chip/Chip.test.tsx +10 -6
  357. package/src/components/ui/chip/Chip.tsx +76 -91
  358. package/src/components/ui/collapse/Collapse.stories.tsx +23 -23
  359. package/src/components/ui/collapse/Collapse.tsx +6 -11
  360. package/src/components/ui/data-row/DataRow.stories.tsx +9 -2
  361. package/src/components/ui/data-row/DataRow.test.tsx +5 -15
  362. package/src/components/ui/data-row/DataRow.tsx +18 -7
  363. package/src/components/ui/divider/Divider.stories.tsx +1 -1
  364. package/src/components/ui/divider/Divider.tsx +1 -5
  365. package/src/components/ui/drawer/Drawer.stories.tsx +5 -16
  366. package/src/components/ui/drawer/Drawer.tsx +13 -30
  367. package/src/components/ui/floating-overlay-depth.test.tsx +143 -0
  368. package/src/components/ui/form-field/FormField.stories.tsx +6 -20
  369. package/src/components/ui/form-field/FormField.test.tsx +1 -5
  370. package/src/components/ui/form-field/FormField.tsx +10 -46
  371. package/src/components/ui/help-tip/HelpTip.stories.tsx +8 -19
  372. package/src/components/ui/help-tip/HelpTip.test.tsx +1 -3
  373. package/src/components/ui/help-tip/HelpTip.tsx +21 -22
  374. package/src/components/ui/help-tip/index.ts +7 -1
  375. package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
  376. package/src/components/ui/icon-box/IconBox.test.tsx +1 -7
  377. package/src/components/ui/icon-box/IconBox.tsx +2 -12
  378. package/src/components/ui/indicator/Indicator.stories.tsx +1 -1
  379. package/src/components/ui/indicator/Indicator.tsx +20 -14
  380. package/src/components/ui/input/Input.stories.tsx +3 -11
  381. package/src/components/ui/input/Input.test.tsx +2 -10
  382. package/src/components/ui/input/Input.tsx +10 -25
  383. package/src/components/ui/input/index.ts +7 -1
  384. package/src/components/ui/link/Link.stories.tsx +19 -7
  385. package/src/components/ui/list-item/ListItem.stories.tsx +11 -2
  386. package/src/components/ui/list-item/ListItem.tsx +2 -9
  387. package/src/components/ui/menu/Menu.stories.tsx +21 -15
  388. package/src/components/ui/menu/Menu.test.tsx +2 -11
  389. package/src/components/ui/menu/Menu.tsx +12 -14
  390. package/src/components/ui/menu/index.ts +8 -1
  391. package/src/components/ui/modal/Modal.stories.tsx +2 -2
  392. package/src/components/ui/modal/Modal.tsx +29 -19
  393. package/src/components/ui/pill/Pill.stories.tsx +58 -23
  394. package/src/components/ui/pill/Pill.test.tsx +101 -3
  395. package/src/components/ui/pill/Pill.tsx +164 -49
  396. package/src/components/ui/pill/index.ts +1 -1
  397. package/src/components/ui/popover/Popover.stories.tsx +5 -7
  398. package/src/components/ui/popover/Popover.test.tsx +1 -6
  399. package/src/components/ui/popover/Popover.tsx +20 -15
  400. package/src/components/ui/popover/index.ts +7 -1
  401. package/src/components/ui/progress/Progress.stories.tsx +3 -12
  402. package/src/components/ui/progress/Progress.test.tsx +1 -5
  403. package/src/components/ui/radio/Radio.stories.tsx +42 -21
  404. package/src/components/ui/radio/Radio.test.tsx +6 -2
  405. package/src/components/ui/radio/Radio.tsx +23 -22
  406. package/src/components/ui/section/Section.stories.tsx +5 -14
  407. package/src/components/ui/section/Section.test.tsx +1 -6
  408. package/src/components/ui/section/Section.tsx +1 -3
  409. package/src/components/ui/section/index.ts +1 -5
  410. package/src/components/ui/select/Select.stories.tsx +3 -13
  411. package/src/components/ui/select/Select.test.tsx +2 -11
  412. package/src/components/ui/select/Select.tsx +14 -14
  413. package/src/components/ui/skeleton/Skeleton.stories.tsx +3 -11
  414. package/src/components/ui/skeleton/Skeleton.tsx +3 -14
  415. package/src/components/ui/skeleton/index.ts +1 -7
  416. package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
  417. package/src/components/ui/spinner/Spinner.tsx +1 -4
  418. package/src/components/ui/stack/Stack.stories.tsx +1 -1
  419. package/src/components/ui/stack/Stack.tsx +1 -1
  420. package/src/components/ui/surface/Surface.stories.tsx +17 -5
  421. package/src/components/ui/surface/Surface.test.tsx +88 -28
  422. package/src/components/ui/surface/Surface.tsx +37 -87
  423. package/src/components/ui/surface/SurfaceContext.ts +22 -62
  424. package/src/components/ui/surface/index.ts +8 -0
  425. package/src/components/ui/surface/resolveSurface.ts +98 -0
  426. package/src/components/ui/surface/surface.contract.test.ts +6 -1
  427. package/src/components/ui/switch/Switch.stories.tsx +1 -1
  428. package/src/components/ui/switch/Switch.tsx +7 -6
  429. package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
  430. package/src/components/ui/tabs/Tabs.tsx +7 -22
  431. package/src/components/ui/tabs/index.ts +1 -7
  432. package/src/components/ui/tile/Tile.stories.tsx +1 -1
  433. package/src/components/ui/tile/Tile.tsx +3 -0
  434. package/src/components/ui/toast/Toast.stories.tsx +1 -1
  435. package/src/components/ui/toast/Toast.test.tsx +2 -8
  436. package/src/components/ui/toast/Toast.tsx +43 -53
  437. package/src/components/ui/toast/index.ts +0 -1
  438. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +92 -71
  439. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +3 -18
  440. package/src/components/ui/toolbar-button/ToolbarButton.tsx +25 -43
  441. package/src/components/ui/tooltip/Tooltip.stories.tsx +12 -3
  442. package/src/components/ui/tooltip/Tooltip.test.tsx +21 -3
  443. package/src/components/ui/tooltip/Tooltip.tsx +55 -36
  444. package/src/components/ui/tooltip/index.ts +1 -1
  445. package/src/components/ui/trigger/TriggerSurface.test.tsx +215 -0
  446. package/src/components/ui/trigger/TriggerSurface.tsx +81 -0
  447. package/src/components/ui/trigger/index.ts +2 -0
  448. package/src/stories/PresetShowcase.stories.tsx +100 -47
  449. package/src/test/no-device-internals.test.ts +3 -3
  450. package/src/theme/ChoosingTokens.stories.tsx +48 -0
  451. package/src/theme/ColorPalettes.stories.tsx +62 -45
  452. package/src/theme/ColorPrimitives.stories.tsx +35 -35
  453. package/src/theme/Depth.stories.tsx +132 -65
  454. package/src/theme/DepthCalibration.stories.tsx +23 -21
  455. package/src/theme/ThemeProvider.test.tsx +4 -4
  456. package/src/theme/barrel.ts +7 -6
  457. package/src/theme/color-stories.coverage.test.ts +1 -1
  458. package/src/theme/color-utils.ts +41 -29
  459. package/src/theme/config.completeness.test.ts +1 -1
  460. package/src/theme/config.ts +28 -0
  461. package/src/theme/elevation.test.ts +82 -56
  462. package/src/theme/elevation.ts +77 -561
  463. package/src/theme/global.css +30 -2
  464. package/src/theme/grey-ramp.test.ts +24 -8
  465. package/src/theme/lift.test.ts +39 -0
  466. package/src/theme/lift.ts +102 -0
  467. package/src/theme/materials.test.ts +5 -2
  468. package/src/theme/materials.ts +38 -4
  469. package/src/theme/native-theming-guard.test.ts +5 -5
  470. package/src/theme/presets/apply.ts +4 -0
  471. package/src/theme/presets/audiobook.ts +5 -2
  472. package/src/theme/presets/default.ts +4 -0
  473. package/src/theme/presets/index.ts +14 -1
  474. package/src/theme/presets/types.ts +1 -1
  475. package/src/theme/semantic-contrast.test.ts +13 -4
  476. package/src/theme/status-distinctness.test.ts +66 -0
  477. package/src/theme/surface-planes.test.ts +37 -0
  478. package/src/theme/surface-planes.ts +68 -0
  479. package/src/theme/token-export-guard.test.ts +1 -3
  480. package/src/theme/tokens/primitives.test.ts +15 -7
  481. package/src/theme/tokens/primitives.ts +65 -33
  482. package/src/theme/tokens/semantic.ts +71 -37
  483. package/src/theme/wash-ladder.test.ts +58 -0
  484. package/src/theme/workout-tokens.ts +1 -7
  485. package/src/types/jest-axe.d.ts +2 -6
  486. package/src/types/raw-md.d.ts +5 -0
  487. package/src/utils/colors.ts +4 -4
  488. package/src/utils/form.ts +1 -3
  489. package/src/utils/index.ts +2 -7
  490. package/src/utils/number-format.ts +17 -0
  491. package/src/utils/workout-format.ts +15 -6
  492. package/src/web-jsx/jsx-runtime.ts +1 -5
  493. package/tailwind.config.js +5 -0
  494. package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +0 -151
  495. package/src/components/custom/Workout/RowInventory.stories.tsx +0 -408
  496. package/src/components/custom/Workout/S3FullRail.stories.tsx +0 -46
  497. package/src/components/custom/Workout/S3SessionPace.stories.tsx +0 -95
  498. package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +0 -61
  499. package/src/components/custom/Workout/S3SetTypes.stories.tsx +0 -176
  500. package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +0 -240
  501. package/src/components/shell/DashboardShell.stories.tsx +0 -65
  502. package/src/components/shell/DashboardShell.tsx +0 -75
  503. package/src/components/shell/SessionRailSpecimen.stories.tsx +0 -753
  504. package/src/theme/elevation.stories.tsx +0 -346
  505. package/src/utils/useTheme.ts +0 -72
  506. /package/src/components/shell/{DeviceIndicator.test.tsx → workout/DeviceIndicator.test.tsx} +0 -0
  507. /package/src/components/shell/{DeviceMenu.test.tsx → workout/DeviceMenu.test.tsx} +0 -0
  508. /package/src/components/shell/{DeviceRow.test.tsx → workout/DeviceRow.test.tsx} +0 -0
  509. /package/src/components/shell/{SessionStatePill.test.tsx → workout/SessionStatePill.test.tsx} +0 -0
@@ -33,7 +33,8 @@ export const RED = '#D14343' // red 600
33
33
  export const ORANGE = '#FF7900' // orange 400
34
34
  export const AMBER = '#F9B415' // amber 300
35
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)
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 = `
@@ -42,7 +43,16 @@ export const PULSE_CSS = `
42
43
  @keyframes s3pAmber{0%,100%{background:#FFD352}50%{background:#E08C00}}
43
44
  @keyframes s3pGreen{0%,100%{background:#58F69E}50%{background:#21C05D}}`
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
 
@@ -50,9 +60,23 @@ export const pulseAnim = (c: string): string | undefined => {
50
60
  const INTER = 'Inter, sans-serif'
51
61
  const SRL_SEP = '#6B7280'
52
62
  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>
63
+ return (
64
+ <span style={{ fontFamily: INTER, fontSize: 11, fontWeight: 600, letterSpacing: 1, color }}>
65
+ {children}
66
+ </span>
67
+ )
54
68
  }
55
- export function SetsRepsLoad({ sets, reps, load, unit = 'lb' }: { sets: number; reps: number | string; load: number; unit?: string }) {
69
+ export function SetsRepsLoad({
70
+ sets,
71
+ reps,
72
+ load,
73
+ unit = 'lb',
74
+ }: {
75
+ sets: number
76
+ reps: number | string
77
+ load: number
78
+ unit?: string
79
+ }) {
56
80
  return (
57
81
  <span style={{ display: 'inline-flex', alignItems: 'baseline', whiteSpace: 'nowrap' }}>
58
82
  <SRLCell>{sets}</SRLCell>
@@ -66,7 +90,10 @@ export function SetsRepsLoad({ sets, reps, load, unit = 'lb' }: { sets: number;
66
90
  }
67
91
 
68
92
  // ---- 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 }
93
+ export type SetState =
94
+ | { status: 'done'; reps: number[] }
95
+ | { status: 'active'; reps: number[]; planned: number }
96
+ | { status: 'todo'; planned: number }
70
97
  export function setColors(s: SetState): string[] {
71
98
  if (s.status === 'todo') return [GREY]
72
99
  if (s.status === 'done') return s.reps.map(velColor)
@@ -74,9 +101,26 @@ export function setColors(s: SetState): string[] {
74
101
  }
75
102
  function SetBar({ colors, h, pulse }: { colors: string[]; h: number; pulse?: boolean }) {
76
103
  return (
77
- <div style={{ display: 'flex', flex: 1, minWidth: 0, height: h, borderRadius: 2, overflow: 'hidden' }}>
104
+ <div
105
+ style={{
106
+ display: 'flex',
107
+ flex: 1,
108
+ minWidth: 0,
109
+ height: h,
110
+ borderRadius: 2,
111
+ overflow: 'hidden',
112
+ }}
113
+ >
78
114
  {colors.map((c, i) => (
79
- <div key={i} style={{ flex: 1, height: '100%', background: c, animation: pulse && c !== GREY ? pulseAnim(c) : undefined }} />
115
+ <div
116
+ key={i}
117
+ style={{
118
+ flex: 1,
119
+ height: '100%',
120
+ background: c,
121
+ animation: pulse && c !== GREY ? pulseAnim(c) : undefined,
122
+ }}
123
+ />
80
124
  ))}
81
125
  </div>
82
126
  )
@@ -95,17 +139,48 @@ export function Indicator({ kind }: { kind: 'pr' | 'issue' | 'info' }) {
95
139
  const map = { pr: ['★', ORANGE], issue: ['!', RED], info: ['i', '#2196F3'] } as const
96
140
  const [glyph, color] = map[kind]
97
141
  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>
142
+ <span
143
+ style={{
144
+ width: 16,
145
+ height: 16,
146
+ borderRadius: 8,
147
+ border: `1px solid ${color}`,
148
+ color,
149
+ fontSize: 10,
150
+ fontWeight: 800,
151
+ display: 'inline-flex',
152
+ alignItems: 'center',
153
+ justifyContent: 'center',
154
+ }}
155
+ >
156
+ {glyph}
157
+ </span>
99
158
  )
100
159
  }
101
160
 
102
161
  const labelStyle = { fontSize: 14, fontWeight: 800, color: T_PRIMARY } as const
103
162
 
104
163
  // ---- 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 }
164
+ export type Exercise = {
165
+ name: string
166
+ sets: number
167
+ reps: number | string
168
+ load: number
169
+ tempo: [number, number, number, number]
170
+ indicator?: 'pr' | 'issue' | 'info'
171
+ setStates: SetState[]
172
+ upcoming?: boolean
173
+ }
106
174
  export function Heading({ ex, stripH }: { ex: Exercise; stripH: number }) {
107
175
  return (
108
- <div style={{ padding: '9px 12px', display: 'flex', flexDirection: 'column', opacity: ex.upcoming ? 0.55 : 1 }}>
176
+ <div
177
+ style={{
178
+ padding: '9px 12px',
179
+ display: 'flex',
180
+ flexDirection: 'column',
181
+ opacity: ex.upcoming ? 0.55 : 1,
182
+ }}
183
+ >
109
184
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
110
185
  <span style={labelStyle}>{ex.name}</span>
111
186
  <div style={{ flex: 1 }} />
@@ -124,21 +199,80 @@ export function Heading({ ex, stripH }: { ex: Exercise; stripH: number }) {
124
199
  }
125
200
 
126
201
  // ---- 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))
202
+ export const reps = (n: number, start: number, span = 0.4): number[] =>
203
+ Array.from({ length: n }, (_, r) => +(start - (span * r) / Math.max(1, n - 1)).toFixed(3))
128
204
  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 })) },
205
+ {
206
+ name: 'Seated Cable Row',
207
+ sets: 5,
208
+ reps: 8,
209
+ load: 145,
210
+ tempo: [3, 1, 2, 0],
211
+ indicator: 'pr',
212
+ setStates: [1.0, 0.9, 0.8, 0.7, 0.6].map((s) => ({ status: 'done', reps: reps(8, s) })),
213
+ },
214
+ {
215
+ name: 'Incline DB Press',
216
+ sets: 3,
217
+ reps: 8,
218
+ load: 70,
219
+ tempo: [3, 0, 3, 0],
220
+ setStates: [0.72, 0.62, 0.54].map((s) => ({ status: 'done', reps: reps(8, s) })),
221
+ },
222
+ {
223
+ name: 'Cable Chest Press',
224
+ sets: 3,
225
+ reps: 10,
226
+ load: 90,
227
+ tempo: [2, 1, 2, 0],
228
+ indicator: 'info',
229
+ setStates: [
230
+ { status: 'done', reps: reps(10, 0.72) },
231
+ { status: 'active', reps: reps(5, 0.62), planned: 10 },
232
+ { status: 'todo', planned: 10 },
233
+ ],
234
+ },
235
+ {
236
+ name: 'Standing Calf Raise',
237
+ sets: 5,
238
+ reps: 20,
239
+ load: 25,
240
+ tempo: [2, 1, 2, 0],
241
+ upcoming: true,
242
+ setStates: [1, 2, 3, 4, 5].map(() => ({ status: 'todo', planned: 20 })),
243
+ },
244
+ {
245
+ name: 'Face Pull',
246
+ sets: 3,
247
+ reps: '15-20',
248
+ load: 40,
249
+ tempo: [2, 1, 2, 0],
250
+ upcoming: true,
251
+ setStates: [1, 2, 3].map(() => ({ status: 'todo', planned: 18 })),
252
+ },
134
253
  ]
135
254
 
136
255
  // ---- shell chrome (neumorphic): flat raised nav + heading plane; sunk inset exercise list
137
256
  export function NavStub() {
138
257
  return (
139
- <div style={{ width: 60, alignSelf: 'stretch', position: 'relative', zIndex: 2, background: RAISED, display: 'flex', flexDirection: 'column', alignItems: 'center', paddingTop: 12, gap: 8 }}>
258
+ <div
259
+ style={{
260
+ width: 60,
261
+ alignSelf: 'stretch',
262
+ position: 'relative',
263
+ zIndex: 2,
264
+ background: RAISED,
265
+ display: 'flex',
266
+ flexDirection: 'column',
267
+ alignItems: 'center',
268
+ paddingTop: 12,
269
+ gap: 8,
270
+ }}
271
+ >
140
272
  {['Live', 'Plan', 'Body'].map((t) => (
141
- <span key={t} style={{ fontSize: 8, color: T_TERTIARY, fontWeight: 700 }}>{t}</span>
273
+ <span key={t} style={{ fontSize: 8, color: T_TERTIARY, fontWeight: 700 }}>
274
+ {t}
275
+ </span>
142
276
  ))}
143
277
  </div>
144
278
  )
@@ -146,17 +280,39 @@ export function NavStub() {
146
280
  export function Rail({ stripH, footer }: { stripH: number; footer?: ReactNode }) {
147
281
  return (
148
282
  <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 }}>
283
+ <div
284
+ style={{ position: 'relative', zIndex: 2, padding: '11px 12px 10px', background: RAISED }}
285
+ >
286
+ <div
287
+ style={{
288
+ fontSize: 9,
289
+ fontWeight: 800,
290
+ letterSpacing: 1,
291
+ textTransform: 'uppercase',
292
+ color: T_TERTIARY,
293
+ display: 'flex',
294
+ alignItems: 'center',
295
+ gap: 5,
296
+ }}
297
+ >
151
298
  <span style={{ width: 6, height: 6, borderRadius: 3, background: GREEN }} />
152
299
  Live session
153
300
  </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>
301
+ <div style={{ fontSize: 14, fontWeight: 800, marginTop: 3, color: T_PRIMARY }}>
302
+ Pull A · Intensification
303
+ </div>
304
+ <div style={{ fontSize: 10, color: T_SECONDARY, marginTop: 2, fontFamily: 'monospace' }}>
305
+ ex 3/5 · set 2 live
306
+ </div>
156
307
  </div>
157
308
  <div style={{ flex: 1, background: INSET, boxShadow: INSET_SHADOW }}>
158
309
  {SESSION.map((ex, i) => (
159
- <div key={i} style={{ borderBottom: i < SESSION.length - 1 ? `1px solid ${BORDER_SUBTLE}` : undefined }}>
310
+ <div
311
+ key={i}
312
+ style={{
313
+ borderBottom: i < SESSION.length - 1 ? `1px solid ${BORDER_SUBTLE}` : undefined,
314
+ }}
315
+ >
160
316
  <Heading ex={ex} stripH={stripH} />
161
317
  </div>
162
318
  ))}
@@ -167,9 +323,28 @@ export function Rail({ stripH, footer }: { stripH: number; footer?: ReactNode })
167
323
  }
168
324
 
169
325
  // ---- page scaffold shared by the sheets
170
- export function Page({ title, note, children }: { title: string; note: string; children: ReactNode }) {
326
+ export function Page({
327
+ title,
328
+ note,
329
+ children,
330
+ }: {
331
+ title: string
332
+ note: string
333
+ children: ReactNode
334
+ }) {
171
335
  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' }}>
336
+ <div
337
+ style={{
338
+ minHeight: '100vh',
339
+ background: '#0A0A0A',
340
+ color: T_PRIMARY,
341
+ padding: 28,
342
+ display: 'flex',
343
+ flexDirection: 'column',
344
+ gap: 22,
345
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
346
+ }}
347
+ >
173
348
  <style>{PULSE_CSS}</style>
174
349
  <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
175
350
  <span style={{ fontSize: 20, fontWeight: 800 }}>{title}</span>
@@ -179,5 +354,11 @@ export function Page({ title, note, children }: { title: string; note: string; c
179
354
  </div>
180
355
  )
181
356
  }
182
- export const sectionTitle = { fontSize: 11, fontWeight: 800, letterSpacing: 1.2, textTransform: 'uppercase', color: ORANGE } as const
357
+ export const sectionTitle = {
358
+ fontSize: 11,
359
+ fontWeight: 800,
360
+ letterSpacing: 1.2,
361
+ textTransform: 'uppercase',
362
+ color: ORANGE,
363
+ } as const
183
364
  export const monoTag = { fontFamily: 'monospace', fontSize: 11, color: T_SECONDARY } as const
@@ -0,0 +1,61 @@
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
+
20
+ `SetBarChart` and `live-rep-growth` are deliberately absent from `index.ts`: they
21
+ are workout-internal and imported by path, so the public barrel stays one mark
22
+ wide. Adding them to the barrel is a decision, not a tidy-up.
23
+
24
+ ## What each owns
25
+
26
+ **`SetBarChart`** owns the chart _geometry_ — value→height scaling, the shared
27
+ `scaleMax` override, plot-width-driven adaptive bar width and gap stepping, value
28
+ label thinning — plus the shared bar _language_: paper treatment, the
29
+ grow-from-bottom live rep, up/down orientation, the set-type slot vocabulary. Bar
30
+ colour and the reference overlay (velocity VL bands, ROM working/short lines) are
31
+ props. A spacing or entrance improvement therefore lands once for both consumers.
32
+ Value-height only: `VelocityStrip`'s flat 3 px `mini` strip stays its own mark.
33
+
34
+ **`SparkBars`** is a signed-series sparkline: bars over a baseline, negatives in
35
+ `result-degrade`, a `MIN_BAR_HEIGHT` floor so an almost-empty series still reads
36
+ as a series, and `maxBars` because a sparkline is a recent-history glance.
37
+
38
+ **`live-rep-growth`** is the newest-rep entrance, promoted out of `VelocityStrip`
39
+ so every value-height family animates the live rep identically. It carries the
40
+ 12% `PEAK_OVERSHOOT` for a new-peak bar and honours `prefers-reduced-motion`.
41
+
42
+ ## Reuse audit
43
+
44
+ | Leaf | Should compose | Status |
45
+ | --------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
46
+ | Bar geometry | `SetBarChart` | **Closed** for Fatigue and VelocityStrip's expanded charts; `VelocityStrip` mini is a deliberate exception. |
47
+ | Live-rep entrance | `live-rep-growth` | **Closed.** Both consumers use the hook. |
48
+ | 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. |
49
+ | Surface-relative fill | `SurfaceContext` | **Closed.** `SetBarChart` reads `useSurface`/`useOnSurfaceColor` rather than assuming a plane. |
50
+ | Inline colour | `resolveColor(token)` | **Closed** for `SparkBars`; the family is in the token-pure eslint error block. |
51
+
52
+ ## Watch list
53
+
54
+ - **`mixHex` duplication** (above) is the family's only open token debt. Until
55
+ decision 8 lands, do not add a third copy — import the one in `SetBarChart`.
56
+ - **`SetBarChart` has no story.** `SparkBars` does; the chart is only seen through
57
+ its consumers' stories (`Custom/Fatigue/*`, `Custom/Workout/VelocityStrip`).
58
+ That is why it reads `status:candidate` — see [`MATURITY.md`](../../../../MATURITY.md).
59
+ - **Both marks are `keep-internal`** in the arch graph: two in-repo consumers
60
+ each, zero external. Neither is public API yet; treat prop changes as cheap
61
+ while that holds.
@@ -1,7 +1,14 @@
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
+ type SetSlot,
11
+ } from './SetBarChart'
5
12
 
6
13
  const reps = (values: number[]): SetSlot[] => values.map((v) => ({ kind: 'rep', value: v }))
7
14
  const silver = () => '#C7CBD1'
@@ -18,6 +25,28 @@ describe('valueLabelFontSize', () => {
18
25
  })
19
26
  })
20
27
 
28
+ describe('shouldFlipEdgeLabel (TD-07.10)', () => {
29
+ it.each([
30
+ // [roomPx, labelWidthPx, expected]
31
+ [30, 20, false], // half-width (10) comfortably clears the room (30)
32
+ [30, 70, true], // half-width (35) exceeds the room (30)
33
+ [10, 20, false], // half-width (10) exactly equals the room — still a fit (not flipped)
34
+ [9, 20, true], // half-width (10) just exceeds the room (9)
35
+ ])('roomPx=%d labelWidthPx=%d -> flip=%s', (roomPx, labelWidthPx, expected) => {
36
+ expect(shouldFlipEdgeLabel(roomPx, labelWidthPx)).toBe(expected)
37
+ })
38
+ })
39
+
40
+ describe('estimateValueLabelWidth', () => {
41
+ it('scales with both character count and font size', () => {
42
+ expect(estimateValueLabelWidth('1.02', 12)).toBeCloseTo(4 * 12 * 0.62)
43
+ expect(estimateValueLabelWidth('1.02', 8)).toBeLessThan(estimateValueLabelWidth('1.02', 12))
44
+ expect(estimateValueLabelWidth('10.02', 12)).toBeGreaterThan(
45
+ estimateValueLabelWidth('1.02', 12)
46
+ )
47
+ })
48
+ })
49
+
21
50
  describe('sideLabelText', () => {
22
51
  it('keeps the full name when the vertical extent has room', () => {
23
52
  expect(sideLabelText('Left Arm', 200)).toBe('Left Arm')
@@ -133,6 +133,14 @@ export interface SetBarChartProps {
133
133
  showValueLabels?: boolean
134
134
  /** Format a rep value for its label. Default `String`. */
135
135
  formatValue?: (value: number) => string
136
+ /**
137
+ * The LAST column has no sibling to its right to absorb an overflowing value label, so a wide
138
+ * label there can bleed past the plot's right edge and crowd the reference line that also runs
139
+ * flush to it (TD-07.10). Default false (unchanged for every existing consumer — the single
140
+ * hero and ROM chart). When true, that column's label flips from centered to right-anchored
141
+ * (grows left) once {@link shouldFlipEdgeLabel} says it doesn't fit.
142
+ */
143
+ flipEdgeLabel?: boolean
136
144
  /**
137
145
  * To-do placeholder treatment. `solid` (default) — a solid surface-relative section (the plane
138
146
  * blended toward the on-surface neutral). `dashed` — a dashed outline stub. Exists so ROM can keep
@@ -198,6 +206,25 @@ export function valueLabelFontSize(height: number): number {
198
206
  return Math.round(Math.max(8, Math.min(12, height * 0.11)))
199
207
  }
200
208
 
209
+ /** Average glyph width for the bold numeric value-label font, as a fraction of its font size — a
210
+ * measurement-free estimate (RN text can't be synchronously measured) good enough to catch overflow. */
211
+ const LABEL_GLYPH_WIDTH_RATIO = 0.62
212
+
213
+ /** Estimate a value label's rendered width (px) from its formatted text and font size. */
214
+ export function estimateValueLabelWidth(text: string, fontSize: number): number {
215
+ return text.length * fontSize * LABEL_GLYPH_WIDTH_RATIO
216
+ }
217
+
218
+ /**
219
+ * Whether a value label needs to flip from its default centered anchor to a right-anchored
220
+ * (left-growing) placement to clear the plot's right edge (TD-07.10). `roomPx` is the space
221
+ * available to the right of the label's centered position; `labelWidthPx` is its measured or
222
+ * estimated rendered width.
223
+ */
224
+ export function shouldFlipEdgeLabel(roomPx: number, labelWidthPx: number): boolean {
225
+ return labelWidthPx / 2 > roomPx
226
+ }
227
+
201
228
  // --- Hero geometry -----------------------------------------------------------
202
229
  /** Default plot height (px) — tall enough to read a set's shape across a room. */
203
230
  export const SET_BAR_DEFAULT_HEIGHT = 220
@@ -281,6 +308,7 @@ export function SetBarChart({
281
308
  flat = false,
282
309
  showValueLabels: showValueLabelsProp = false,
283
310
  formatValue = String,
311
+ flipEdgeLabel = false,
284
312
  todoVariant = 'solid',
285
313
  minColumns,
286
314
  renderReference,
@@ -462,6 +490,17 @@ export function SetBarChart({
462
490
  // duplicate them); its own label shows otherwise, fading in with `expandProgress`.
463
491
  const ownLabel = showValueLabels && !renderBarOverlay && showBarLabel(repIndex)
464
492
  const labelBottom = (expandProgress ? valueBarHeight(best) : barHeight(best)) + LABEL_GAP
493
+ // Only the LAST cell has no sibling to its right to absorb an overflowing label
494
+ // (TD-07.10) — flip it right-anchored (grows left) when it doesn't fit.
495
+ const isLastCell = i === cells.length - 1
496
+ const labelText = formatValue(value)
497
+ const flipLabel =
498
+ flipEdgeLabel &&
499
+ isLastCell &&
500
+ shouldFlipEdgeLabel(
501
+ barWidth / 2,
502
+ estimateValueLabelWidth(labelText, valueLabelFontSize(height))
503
+ )
465
504
  return (
466
505
  <View key={i} accessibilityElementsHidden={renderBarOverlay == null} style={column}>
467
506
  {ownLabel && (
@@ -471,9 +510,8 @@ export function SetBarChart({
471
510
  style={{
472
511
  position: 'absolute',
473
512
  bottom: labelBottom,
474
- left: 0,
475
- right: 0,
476
- alignItems: 'center',
513
+ ...(flipLabel ? { right: 0 } : { left: 0, right: 0 }),
514
+ alignItems: flipLabel ? 'flex-end' : 'center',
477
515
  ...(expandProgress ? { opacity: expandProgress } : null),
478
516
  }}
479
517
  pointerEvents="none"
@@ -483,7 +521,7 @@ export function SetBarChart({
483
521
  style={[{ fontSize: valueLabelFontSize(height), fontWeight: '700' }, flipStyle]}
484
522
  testID={`${testIDPrefix}-label-${repIndex}`}
485
523
  >
486
- {formatValue(value)}
524
+ {labelText}
487
525
  </Text>
488
526
  </Animated.View>
489
527
  )}
@@ -0,0 +1,55 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { SparkBars } from './SparkBars'
3
+
4
+ const meta: Meta<typeof SparkBars> = {
5
+ title: 'Custom/Charts/SparkBars',
6
+ component: SparkBars,
7
+ tags: ['autodocs', 'status:candidate', '!status:review'],
8
+ parameters: {
9
+ docs: {
10
+ description: {
11
+ component:
12
+ '**Atom.** A tiny bar-mark sparkline for a signed series — the counterpart to ' +
13
+ '[Sparkline](?path=/docs/custom-workout-dataviz-sparkline--docs). Where a line reads a *trajectory*, ' +
14
+ 'bars read *discrete per-period magnitude*. Bars scale on absolute value so a negative ' +
15
+ 'period is as tall as an equally large positive one, with the sign carried by fill colour.',
16
+ },
17
+ },
18
+ },
19
+ args: {
20
+ values: [120, 340, -80, 910, 420, 0, 1580, 260, -310, 740, 1120, 90],
21
+ height: 22,
22
+ barWidth: 3,
23
+ gap: 2,
24
+ maxBars: 24,
25
+ },
26
+ }
27
+ export default meta
28
+ type Story = StoryObj<typeof SparkBars>
29
+
30
+ export const Default: Story = {}
31
+
32
+ /** All-positive — the plain growth case. */
33
+ export const AllPositive: Story = {
34
+ args: { values: [40, 120, 260, 310, 480, 520, 900] },
35
+ }
36
+
37
+ /** All-negative — every bar takes the error token. */
38
+ export const AllNegative: Story = {
39
+ args: { values: [-40, -120, -260, -310, -480] },
40
+ }
41
+
42
+ /** Taller field, as used in the file-history growth block. */
43
+ export const Tall: Story = {
44
+ args: { height: 34, barWidth: 5, gap: 3 },
45
+ }
46
+
47
+ /** `maxBars` keeps only the most recent values — a sparkline is a glance, not an archive. */
48
+ export const Truncated: Story = {
49
+ args: { values: Array.from({ length: 60 }, (_, i) => (i % 7) * 30 - 40), maxBars: 12 },
50
+ }
51
+
52
+ /** An empty series renders an empty field rather than collapsing the layout. */
53
+ export const Empty: Story = {
54
+ args: { values: [] },
55
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { SparkBars } from './SparkBars'
5
+
6
+ describe('SparkBars', () => {
7
+ it('renders one bar per value', () => {
8
+ render(<SparkBars values={[1, 2, 3, 4]} />)
9
+ expect(screen.getAllByTestId(/^spark-bars-bar-/)).toHaveLength(4)
10
+ })
11
+
12
+ it('keeps only the last maxBars values', () => {
13
+ render(<SparkBars values={[1, 2, 3, 4, 5, 6]} maxBars={2} />)
14
+ expect(screen.getAllByTestId(/^spark-bars-bar-/)).toHaveLength(2)
15
+ })
16
+
17
+ it('renders no bars for an empty series but keeps the field', () => {
18
+ render(<SparkBars values={[]} />)
19
+ expect(screen.queryByTestId(/^spark-bars-bar-/)).not.toBeInTheDocument()
20
+ expect(screen.getByTestId('spark-bars')).toBeInTheDocument()
21
+ })
22
+
23
+ it('tints bars by sign with the result-* tokens', () => {
24
+ // Asserts the token, not its current value: resolveColor emits the CSS var on
25
+ // web, which is what keeps the chart theme-correct.
26
+ render(<SparkBars values={[10, -10]} />)
27
+ expect(screen.getByTestId('spark-bars-bar-0')).toHaveStyle({
28
+ backgroundColor: 'var(--color-result-improve)',
29
+ })
30
+ expect(screen.getByTestId('spark-bars-bar-1')).toHaveStyle({
31
+ backgroundColor: 'var(--color-result-degrade)',
32
+ })
33
+ })
34
+
35
+ it('honours explicit color overrides', () => {
36
+ render(<SparkBars values={[10, -10]} color="#00ff00" negativeColor="#ff00ff" />)
37
+ expect(screen.getByTestId('spark-bars-bar-0')).toHaveStyle({
38
+ backgroundColor: 'rgb(0, 255, 0)',
39
+ })
40
+ expect(screen.getByTestId('spark-bars-bar-1')).toHaveStyle({
41
+ backgroundColor: 'rgb(255, 0, 255)',
42
+ })
43
+ })
44
+
45
+ it('scales bars on magnitude, so equal-magnitude values are equally tall', () => {
46
+ render(<SparkBars values={[100, -100, 50]} height={40} />)
47
+ const [a, b, c] = ['0', '1', '2'].map((i) => screen.getByTestId(`spark-bars-bar-${i}`))
48
+ expect(a).toHaveStyle({ height: '40px' })
49
+ expect(b).toHaveStyle({ height: '40px' })
50
+ expect(c).toHaveStyle({ height: '20px' })
51
+ })
52
+
53
+ it('gives a zero value the minimum bar height rather than collapsing it', () => {
54
+ render(<SparkBars values={[100, 0]} height={40} />)
55
+ expect(screen.getByTestId('spark-bars-bar-1')).toHaveStyle({ height: '2px' })
56
+ })
57
+
58
+ it('exposes a default accessible label and accepts an override', () => {
59
+ const { rerender } = render(<SparkBars values={[1, 2]} />)
60
+ expect(screen.getByLabelText('Bar sparkline with 2 values')).toBeInTheDocument()
61
+ rerender(<SparkBars values={[1, 2]} label="Growth for open.ts" />)
62
+ expect(screen.getByLabelText('Growth for open.ts')).toBeInTheDocument()
63
+ })
64
+
65
+ it('has no a11y violations', async () => {
66
+ const { container } = render(<SparkBars values={[3, -1, 4]} />)
67
+ expect(await axe(container)).toHaveNoViolations()
68
+ })
69
+ })