@titan-design/react-ui 0.12.0 → 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 +1320 -215
  7. package/dist/index.d.ts +1320 -215
  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 +202 -1042
  21. package/dist/theme/index.d.ts +202 -1042
  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 +972 -0
  31. package/dist/theme/tokens.d.ts +972 -0
  32. package/dist/theme/tokens.js +904 -0
  33. package/dist/theme/tokens.js.map +1 -0
  34. package/dist/theme/tokens.mjs +880 -0
  35. package/dist/theme/tokens.mjs.map +1 -0
  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 +7 -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 +12 -7
  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 +90 -27
  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 +24 -22
  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 +102 -0
  480. package/src/theme/tokens/primitives.test.ts +15 -7
  481. package/src/theme/tokens/primitives.ts +67 -35
  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
@@ -0,0 +1,180 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import { useMemo } from 'react'
3
+ import { View } from 'react-native'
4
+ import { cn } from '../../../utils/cn'
5
+ import { Card } from '../../ui/card'
6
+ import { Collapse, CollapseButton, CollapseContent } from '../../ui/collapse'
7
+ import { Divider } from '../../ui/divider'
8
+ import { Pill } from '../../ui/pill'
9
+ import { Tooltip, useHoverState } from '../../ui/tooltip'
10
+ import { DateTime } from '../DateTime'
11
+ import { MarkdownProse } from '../Prose'
12
+ import { Typography } from '../Typography'
13
+ import { formatSessionDuration, formatTaskAge } from './format-time'
14
+ import { extractTaskRefs, sessionLinkers, type SessionLinkHandlers } from './session-linkers'
15
+ import { ExactTime, type SessionSummary } from './SessionListItem'
16
+ import { TaskTable, type TaskColumnKey } from './TaskTable'
17
+ import type { TaskListItem } from './TaskRow'
18
+
19
+ /** Task pills beyond this collapse into a `+N`, so a sprawling session cannot push the log off screen. */
20
+ const MAX_TASK_PILLS = 14
21
+
22
+ // The card already names the initiative context, and tags would crowd the title; the id column keeps the prefix.
23
+ const EMBEDDED_HIDDEN_COLUMNS: TaskColumnKey[] = ['slug', 'tags']
24
+
25
+ export interface SessionDetailProps extends SessionLinkHandlers {
26
+ session: SessionSummary
27
+ /** Reference instant for the age label, injected so renders are deterministic. */
28
+ now: number
29
+ /**
30
+ * Task rows for the ids the log mentions, resolved by the host from its task
31
+ * store. When given, the tasks-touched strip can expand into a table.
32
+ */
33
+ tasks?: TaskListItem[]
34
+ className?: string
35
+ }
36
+
37
+ /** The body without its leading h1: the card header carries the title. */
38
+ export function stripLeadingHeading(body: string): string {
39
+ const lines = body.split('\n')
40
+ const first = lines.findIndex((line) => line.trim() !== '')
41
+ if (first === -1 || !/^#\s/.test(lines[first]!.trim())) return body
42
+ return lines.slice(first + 1).join('\n')
43
+ }
44
+
45
+ function AgeHover({ session, now }: { session: SessionSummary; now: number }) {
46
+ const { hovered, hoverProps } = useHoverState()
47
+ return (
48
+ <Tooltip usePortal isOpen={hovered} content={<ExactTime session={session} />}>
49
+ <View {...hoverProps} className="shrink-0" testID="session-age">
50
+ <Typography variant="caption" className="leading-none text-text-tertiary">
51
+ {formatTaskAge(session.ended, now)}
52
+ </Typography>
53
+ </View>
54
+ </Tooltip>
55
+ )
56
+ }
57
+
58
+ function SessionMeta({ session, now }: { session: SessionSummary; now: number }) {
59
+ const duration = formatSessionDuration(session.started, session.ended)
60
+ return (
61
+ <View className="flex-row items-center gap-2.5">
62
+ <DateTime
63
+ value={session.ended}
64
+ format="medium"
65
+ isUTC
66
+ variant="mono"
67
+ className="shrink-0 text-text-secondary"
68
+ />
69
+ <AgeHover session={session} now={now} />
70
+ {duration ? (
71
+ <Typography variant="caption" className="shrink-0 leading-none text-text-tertiary">
72
+ {`· ${duration}`}
73
+ </Typography>
74
+ ) : null}
75
+ <Typography variant="mono" numberOfLines={1} className="shrink text-text-tertiary">
76
+ {session.id}
77
+ </Typography>
78
+ </View>
79
+ )
80
+ }
81
+
82
+ interface TasksTouchedProps {
83
+ refs: string[]
84
+ tasks: TaskListItem[]
85
+ now: number
86
+ onPressTask?: (id: string) => void
87
+ }
88
+
89
+ /** The strip of task pills; with task rows supplied it expands into a table. */
90
+ function TasksTouched({ refs, tasks, now, onPressTask }: TasksTouchedProps) {
91
+ if (refs.length === 0) return null
92
+ const shown = refs.slice(0, MAX_TASK_PILLS)
93
+ const hidden = refs.length - shown.length
94
+ return (
95
+ <Collapse>
96
+ <View className="flex-row flex-wrap items-center gap-1.5" testID="session-tasks-touched">
97
+ <Typography variant="caption" className="leading-none text-text-tertiary">
98
+ tasks touched
99
+ </Typography>
100
+ {shown.map((id) => (
101
+ <Pill
102
+ key={id}
103
+ variant="subtle"
104
+ color="primary"
105
+ size="xs"
106
+ onPress={onPressTask ? () => onPressTask(id) : undefined}
107
+ >
108
+ {id}
109
+ </Pill>
110
+ ))}
111
+ {hidden > 0 ? (
112
+ <Typography variant="caption" className="leading-none text-text-tertiary">
113
+ {`+${hidden}`}
114
+ </Typography>
115
+ ) : null}
116
+ {tasks.length > 0 ? (
117
+ <CollapseButton
118
+ className="ml-auto px-1 py-0"
119
+ accessibilityLabel={`Show the ${tasks.length} tasks touched as a table`}
120
+ />
121
+ ) : null}
122
+ </View>
123
+ {tasks.length > 0 ? (
124
+ <CollapseContent className="pt-2">
125
+ <TaskTable
126
+ tasks={tasks}
127
+ now={now}
128
+ hideLegend
129
+ hideColumns={EMBEDDED_HIDDEN_COLUMNS}
130
+ label={`${tasks.length} ${tasks.length === 1 ? 'task' : 'tasks'} touched`}
131
+ />
132
+ </CollapseContent>
133
+ ) : null}
134
+ </Collapse>
135
+ )
136
+ }
137
+
138
+ /**
139
+ * SessionDetail — one session log, readable: its title, when it ran and for
140
+ * how long, the tasks it touched (as pills, or as a table when the host
141
+ * supplies the rows), then the markdown body with task ids, `[[name]]` links
142
+ * and PR numbers auto-linked.
143
+ *
144
+ * Holds no selection state; pair it with {@link SessionList} in the host.
145
+ * Composes {@link Card}, {@link Divider}, {@link Pill}, {@link Collapse},
146
+ * {@link DateTime}, {@link TaskTable} and {@link MarkdownProse}.
147
+ */
148
+ export function SessionDetail({
149
+ session,
150
+ now,
151
+ tasks = [],
152
+ onPressTask,
153
+ onPressLink,
154
+ onPressPr,
155
+ className,
156
+ }: SessionDetailProps) {
157
+ const refs = useMemo(() => extractTaskRefs(session.body), [session.body])
158
+ const touched = useMemo(
159
+ () => refs.map((id) => tasks.find((t) => t.id === id)).filter((t): t is TaskListItem => !!t),
160
+ [refs, tasks]
161
+ )
162
+ const body = useMemo(() => stripLeadingHeading(session.body), [session.body])
163
+ const linkers = useMemo(
164
+ () => sessionLinkers({ onPressTask, onPressLink, onPressPr }),
165
+ [onPressTask, onPressLink, onPressPr]
166
+ )
167
+ return (
168
+ <Card className={cn('gap-3 p-5', className)} testID="session-detail">
169
+ <View className="gap-2">
170
+ <Typography variant="h5" className="text-text-primary">
171
+ {session.title}
172
+ </Typography>
173
+ <SessionMeta session={session} now={now} />
174
+ <TasksTouched refs={refs} tasks={touched} now={now} onPressTask={onPressTask} />
175
+ </View>
176
+ <Divider />
177
+ <MarkdownProse body={body} linkers={linkers} testID="session-body" />
178
+ </Card>
179
+ )
180
+ }
@@ -0,0 +1,69 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useArgs } from 'storybook/preview-api'
3
+ import { Surface } from '../../ui/surface'
4
+ import { SessionList } from './SessionList'
5
+ import { SESSION_FIXTURE, SESSION_NOW } from './session-fixture'
6
+
7
+ /**
8
+ * **SessionList** — the selectable list half of the session reader. The host
9
+ * holds `selectedId`; this story keeps it in Storybook args so clicking a row
10
+ * updates the Controls panel.
11
+ *
12
+ * Composes `Eyebrow` · `SessionListItem`.
13
+ */
14
+ const meta: Meta<typeof SessionList> = {
15
+ title: 'Custom/ActiveWork/SessionList',
16
+ component: SessionList,
17
+ tags: ['autodocs', 'status:candidate', '!status:review'],
18
+ args: {
19
+ sessions: SESSION_FIXTURE,
20
+ now: SESSION_NOW,
21
+ selectedId: SESSION_FIXTURE[0]!.id,
22
+ },
23
+ argTypes: {
24
+ sessions: { table: { disable: true } },
25
+ now: { table: { disable: true } },
26
+ selectedId: { control: 'select', options: SESSION_FIXTURE.map((s) => s.id) },
27
+ },
28
+ render: function Render(args) {
29
+ const [, updateArgs] = useArgs()
30
+ return <SessionList {...args} onSelect={(s) => updateArgs({ selectedId: s.id })} />
31
+ },
32
+ decorators: [
33
+ (Story) => (
34
+ <Surface level="base" className="min-h-screen w-72 p-6" testID="page-surface">
35
+ <Story />
36
+ </Surface>
37
+ ),
38
+ ],
39
+ parameters: {
40
+ layout: 'fullscreen',
41
+ docs: {
42
+ description: {
43
+ component:
44
+ 'Composes **Eyebrow** · **Divider** · **SessionListItem**. Used-by ↑ the Session Reader composition. Selection is controlled by the host; sessions group under a month divider once the list spans more than one.',
45
+ },
46
+ },
47
+ },
48
+ }
49
+ export default meta
50
+
51
+ type Story = StoryObj<typeof SessionList>
52
+
53
+ /** Six sessions across two months, newest selected. */
54
+ export const Default: Story = {}
55
+
56
+ /** One month only: no period dividers. */
57
+ export const SingleMonth: Story = {
58
+ args: { sessions: SESSION_FIXTURE.filter((s) => s.ended.startsWith('2026-07')) },
59
+ }
60
+
61
+ /** Nothing selected yet. */
62
+ export const NoSelection: Story = {
63
+ args: { selectedId: undefined },
64
+ }
65
+
66
+ /** A custom heading in place of the count. */
67
+ export const CustomLabel: Story = {
68
+ args: { label: 'Recent sessions' },
69
+ }
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { fireEvent, render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { SessionList, groupByPeriod, sessionPeriod } from './SessionList'
5
+ import { SESSION_FIXTURE, SESSION_NOW } from './session-fixture'
6
+
7
+ const july = SESSION_FIXTURE.filter((s) => s.ended.startsWith('2026-07'))
8
+
9
+ describe('SessionList', () => {
10
+ it('renders a row per session under a count heading, inside a listbox', () => {
11
+ render(<SessionList sessions={SESSION_FIXTURE} now={SESSION_NOW} />)
12
+ expect(screen.getByText('6 sessions')).toBeInTheDocument()
13
+ expect(screen.getByRole('listbox', { name: '6 sessions' })).toBeInTheDocument()
14
+ expect(screen.getAllByRole('option')).toHaveLength(SESSION_FIXTURE.length)
15
+ })
16
+
17
+ it('divides the list by month once it spans more than one, and not before', () => {
18
+ const { rerender } = render(<SessionList sessions={SESSION_FIXTURE} now={SESSION_NOW} />)
19
+ expect(screen.getAllByTestId('session-period').map((p) => p.textContent)).toEqual([
20
+ 'July 2026',
21
+ 'May 2026',
22
+ ])
23
+ rerender(<SessionList sessions={july} now={SESSION_NOW} />)
24
+ expect(screen.queryByTestId('session-period')).not.toBeInTheDocument()
25
+ })
26
+
27
+ it('marks only the selected id and reports the pressed session to the host', () => {
28
+ const onSelect = vi.fn()
29
+ render(
30
+ <SessionList
31
+ sessions={SESSION_FIXTURE}
32
+ now={SESSION_NOW}
33
+ selectedId={SESSION_FIXTURE[1]!.id}
34
+ onSelect={onSelect}
35
+ />
36
+ )
37
+ const options = screen.getAllByRole('option')
38
+ expect(options.filter((o) => o.getAttribute('aria-selected') === 'true')).toHaveLength(1)
39
+ expect(options[1]).toHaveAttribute('aria-selected', 'true')
40
+ fireEvent.click(options[3]!)
41
+ expect(onSelect).toHaveBeenCalledWith(SESSION_FIXTURE[3])
42
+ })
43
+
44
+ it('singularises the heading and accepts a custom label', () => {
45
+ const { rerender } = render(
46
+ <SessionList sessions={SESSION_FIXTURE.slice(0, 1)} now={SESSION_NOW} />
47
+ )
48
+ expect(screen.getByText('1 session')).toBeInTheDocument()
49
+ rerender(<SessionList sessions={SESSION_FIXTURE} now={SESSION_NOW} label="Recent" />)
50
+ expect(screen.getByRole('listbox', { name: 'Recent' })).toBeInTheDocument()
51
+ })
52
+
53
+ it('has no a11y violations', async () => {
54
+ const { container } = render(
55
+ <SessionList
56
+ sessions={SESSION_FIXTURE}
57
+ now={SESSION_NOW}
58
+ selectedId={SESSION_FIXTURE[0]!.id}
59
+ />
60
+ )
61
+ expect(await axe(container)).toHaveNoViolations()
62
+ })
63
+ })
64
+
65
+ describe('period grouping', () => {
66
+ it('labels a session by the calendar month it ended in, in UTC', () => {
67
+ expect(sessionPeriod('2026-07-01T00:30:00Z')).toBe('July 2026')
68
+ expect(sessionPeriod('nope')).toBe('Undated')
69
+ })
70
+
71
+ it('groups consecutive sessions and keeps the given order', () => {
72
+ const groups = groupByPeriod(SESSION_FIXTURE)
73
+ expect(groups.map((g) => [g.label, g.sessions.length])).toEqual([
74
+ ['July 2026', 5],
75
+ ['May 2026', 1],
76
+ ])
77
+ })
78
+ })
@@ -0,0 +1,103 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import { useMemo } from 'react'
3
+ import { View, type ViewProps } from 'react-native'
4
+ import { cn } from '../../../utils/cn'
5
+ import { Divider } from '../../ui/divider'
6
+ import { Typography } from '../Typography'
7
+ import { Eyebrow } from './Eyebrow'
8
+ import { SessionListItem, type SessionSummary } from './SessionListItem'
9
+
10
+ // React Native's `Role` union omits `'listbox'` and `'group'`, even though RNW
11
+ // passes both straight through to the DOM.
12
+ const LISTBOX_ROLE = 'listbox' as ViewProps['role']
13
+ const GROUP_ROLE = 'group' as ViewProps['role']
14
+
15
+ export interface SessionListProps {
16
+ /** Newest first is the expected order; the list renders what it is given. */
17
+ sessions: SessionSummary[]
18
+ /** Reference instant for the age labels, injected so renders are deterministic. */
19
+ now: number
20
+ selectedId?: string
21
+ onSelect?: (session: SessionSummary) => void
22
+ /** Heading over the list. Defaults to the session count. */
23
+ label?: string
24
+ className?: string
25
+ }
26
+
27
+ interface Period {
28
+ label: string
29
+ sessions: SessionSummary[]
30
+ }
31
+
32
+ /** The calendar month a session ended in, as the list's period label. */
33
+ export function sessionPeriod(iso: string): string {
34
+ const date = new Date(iso)
35
+ if (Number.isNaN(date.getTime())) return 'Undated'
36
+ return date.toLocaleDateString('en-US', { month: 'long', year: 'numeric', timeZone: 'UTC' })
37
+ }
38
+
39
+ /** Consecutive sessions in the same month, in the order given. */
40
+ export function groupByPeriod(sessions: SessionSummary[]): Period[] {
41
+ const periods: Period[] = []
42
+ for (const session of sessions) {
43
+ const label = sessionPeriod(session.ended)
44
+ const last = periods[periods.length - 1]
45
+ if (last && last.label === label) last.sessions.push(session)
46
+ else periods.push({ label, sessions: [session] })
47
+ }
48
+ return periods
49
+ }
50
+
51
+ function PeriodDivider({ label }: { label: string }) {
52
+ return (
53
+ <View className="flex-row items-center gap-2 pt-2" testID="session-period">
54
+ <Typography variant="caption" className="leading-none text-text-tertiary">
55
+ {label}
56
+ </Typography>
57
+ <Divider className="flex-1" />
58
+ </View>
59
+ )
60
+ }
61
+
62
+ /**
63
+ * SessionList — the selectable list half of the session reader. Owns no
64
+ * selection state: the host holds `selectedId` and pairs the list with a
65
+ * `SessionDetail`, so the two can be laid out however the surface needs.
66
+ * Sessions are grouped under a hairline divider per calendar month once the
67
+ * list spans more than one.
68
+ *
69
+ * Composes {@link Eyebrow}, {@link Divider} and {@link SessionListItem}.
70
+ */
71
+ export function SessionList({
72
+ sessions,
73
+ now,
74
+ selectedId,
75
+ onSelect,
76
+ label,
77
+ className,
78
+ }: SessionListProps) {
79
+ const heading = label ?? `${sessions.length} ${sessions.length === 1 ? 'session' : 'sessions'}`
80
+ const periods = useMemo(() => groupByPeriod(sessions), [sessions])
81
+ const showPeriods = periods.length > 1
82
+ return (
83
+ <View className={cn('gap-2', className)}>
84
+ <Eyebrow>{heading}</Eyebrow>
85
+ <View className="gap-1" role={LISTBOX_ROLE} aria-label={heading}>
86
+ {periods.map((period) => (
87
+ <View key={period.label} className="gap-1" role={GROUP_ROLE} aria-label={period.label}>
88
+ {showPeriods ? <PeriodDivider label={period.label} /> : null}
89
+ {period.sessions.map((session) => (
90
+ <SessionListItem
91
+ key={session.id}
92
+ session={session}
93
+ now={now}
94
+ selected={session.id === selectedId}
95
+ onSelect={onSelect ? () => onSelect(session) : undefined}
96
+ />
97
+ ))}
98
+ </View>
99
+ ))}
100
+ </View>
101
+ </View>
102
+ )
103
+ }
@@ -0,0 +1,64 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { Surface } from '../../ui/surface'
3
+ import { SessionListItem } from './SessionListItem'
4
+ import { SESSION_FIXTURE, SESSION_NOW } from './session-fixture'
5
+
6
+ /**
7
+ * **SessionListItem** — one session in the reader's list, led by its title,
8
+ * then age · duration · tasks touched · track. Hover for the full title, the
9
+ * exact end time and the task ids.
10
+ *
11
+ * Composes `Tooltip` · `DateTime` · `Typography`.
12
+ */
13
+ const meta: Meta<typeof SessionListItem> = {
14
+ title: 'Custom/ActiveWork/SessionListItem',
15
+ component: SessionListItem,
16
+ tags: ['autodocs', 'status:candidate', '!status:review'],
17
+ args: {
18
+ session: SESSION_FIXTURE[0],
19
+ now: SESSION_NOW,
20
+ selected: false,
21
+ },
22
+ argTypes: {
23
+ session: { table: { disable: true } },
24
+ now: { table: { disable: true } },
25
+ onSelect: { action: 'onSelect' },
26
+ },
27
+ decorators: [
28
+ (Story) => (
29
+ <Surface level="base" className="min-h-screen w-72 p-6" testID="page-surface">
30
+ <Story />
31
+ </Surface>
32
+ ),
33
+ ],
34
+ parameters: {
35
+ layout: 'fullscreen',
36
+ docs: {
37
+ description: {
38
+ component:
39
+ 'Composes **Tooltip** · **DateTime** · **Typography**. Used-by ↑ SessionList. Selection is a prop; the row renders the raised fill and accent bar. Hover a row for the full title, exact end time and task ids.',
40
+ },
41
+ },
42
+ },
43
+ }
44
+ export default meta
45
+
46
+ type Story = StoryObj<typeof SessionListItem>
47
+
48
+ /** A canonical session touching six tasks. */
49
+ export const Default: Story = {}
50
+
51
+ /** The selected treatment: raised fill and a leading accent bar. */
52
+ export const Selected: Story = {
53
+ args: { selected: true },
54
+ }
55
+
56
+ /** An ad-hoc session: the track reads in the footer. */
57
+ export const AdHoc: Story = {
58
+ args: { session: SESSION_FIXTURE[1] },
59
+ }
60
+
61
+ /** A six-minute session mentioning one task. */
62
+ export const Short: Story = {
63
+ args: { session: SESSION_FIXTURE[5] },
64
+ }
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { fireEvent, render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { SessionListItem, sessionRowMeta } from './SessionListItem'
5
+ import { SESSION_FIXTURE, SESSION_NOW } from './session-fixture'
6
+
7
+ const session = SESSION_FIXTURE[0]!
8
+
9
+ describe('SessionListItem', () => {
10
+ it('leads with the title and reads age · duration · tasks · track beneath it', () => {
11
+ render(<SessionListItem session={session} now={SESSION_NOW} />)
12
+ expect(screen.getByText(session.title)).toBeInTheDocument()
13
+ expect(screen.getByRole('option')).toHaveAccessibleName(/1d ago · 1h 4m · 6 tasks · canonical$/)
14
+ expect(screen.getByText('6 tasks')).toBeInTheDocument()
15
+ expect(screen.queryByText('Jul 12')).not.toBeInTheDocument()
16
+ })
17
+
18
+ it('shows the exact end time when the age is hovered', () => {
19
+ render(<SessionListItem session={session} now={SESSION_NOW} />)
20
+ expect(screen.queryByText('07/12/2026, 02:52 PM')).not.toBeInTheDocument()
21
+ fireEvent.pointerEnter(screen.getByTestId('session-age'))
22
+ expect(screen.getByText('07/12/2026, 02:52 PM')).toBeInTheDocument()
23
+ fireEvent.pointerLeave(screen.getByTestId('session-age'))
24
+ expect(screen.queryByText('07/12/2026, 02:52 PM')).not.toBeInTheDocument()
25
+ })
26
+
27
+ it('lists the task ids when the count is hovered', () => {
28
+ render(<SessionListItem session={session} now={SESSION_NOW} />)
29
+ fireEvent.pointerEnter(screen.getByTestId('session-task-count'))
30
+ expect(screen.getByText(/^AW-17 AW-18/)).toBeInTheDocument()
31
+ })
32
+
33
+ it('exposes selection to assistive tech and shows the accent bar only when selected', () => {
34
+ const { rerender } = render(<SessionListItem session={session} now={SESSION_NOW} />)
35
+ expect(screen.getByRole('option')).toHaveAttribute('aria-selected', 'false')
36
+ expect(screen.queryByTestId('session-list-item-accent')).not.toBeInTheDocument()
37
+ rerender(<SessionListItem session={session} now={SESSION_NOW} selected />)
38
+ expect(screen.getByRole('option')).toHaveAttribute('aria-selected', 'true')
39
+ expect(screen.getByTestId('session-list-item-accent')).toBeInTheDocument()
40
+ })
41
+
42
+ it('selects on press, including a press on a hover field', () => {
43
+ const onSelect = vi.fn()
44
+ render(<SessionListItem session={session} now={SESSION_NOW} onSelect={onSelect} />)
45
+ fireEvent.click(screen.getByText('6 tasks'))
46
+ expect(onSelect).toHaveBeenCalledTimes(1)
47
+ })
48
+
49
+ it('singularises one task and drops the count when no task is mentioned', () => {
50
+ expect(sessionRowMeta(session, SESSION_NOW, 1)).toBe('1d ago · 1h 4m · 1 task · canonical')
51
+ expect(sessionRowMeta(session, SESSION_NOW, 0)).toBe('1d ago · 1h 4m · canonical')
52
+ })
53
+
54
+ it('has no a11y violations', async () => {
55
+ const { container } = render(
56
+ <div role="listbox" aria-label="Sessions">
57
+ <SessionListItem session={session} now={SESSION_NOW} selected />
58
+ </div>
59
+ )
60
+ expect(await axe(container)).toHaveNoViolations()
61
+ })
62
+ })