@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,83 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import { View, type ViewProps } from 'react-native'
3
+ import { Card } from '../../ui/card'
4
+ import { Metric } from '../Metric'
5
+ import { Typography } from '../Typography'
6
+ import { Eyebrow } from './Eyebrow'
7
+ import { InitiativeCard, type InitiativeCardProps } from './InitiativeCard'
8
+
9
+ export interface PortfolioOverviewStat {
10
+ value: string
11
+ label: string
12
+ }
13
+
14
+ export interface PortfolioOverviewSection {
15
+ /** Section eyebrow, e.g. "Focused · by rank" or "Backburner". */
16
+ heading: string
17
+ items: InitiativeCardProps[]
18
+ }
19
+
20
+ export interface PortfolioOverviewProps extends ViewProps {
21
+ title: string
22
+ subtitle?: string
23
+ /** KPI tiles rendered above the sections, e.g. Focused / Open tasks / Initiatives. */
24
+ stats: PortfolioOverviewStat[]
25
+ /** Initiative groups, rendered top-to-bottom in the order given. */
26
+ sections: PortfolioOverviewSection[]
27
+ className?: string
28
+ }
29
+
30
+ /**
31
+ * PortfolioOverview — at-a-glance status across every tracked initiative: a
32
+ * KPI row followed by initiative groups (typically Focused, then Backburner).
33
+ * Composes Card / Metric / Eyebrow / {@link InitiativeCard} — presentational
34
+ * only, all data supplied by props.
35
+ *
36
+ * Data plan: the caller is responsible for computing `stats` and `sections`
37
+ * from its own source of truth (e.g. the active-work session-mining export).
38
+ * This component has no fetch/store dependency, so wiring it to a live data
39
+ * source is a caller-side concern — flagged here as the integration's only
40
+ * open gap.
41
+ */
42
+ export function PortfolioOverview({
43
+ title,
44
+ subtitle,
45
+ stats,
46
+ sections,
47
+ className,
48
+ ...props
49
+ }: PortfolioOverviewProps) {
50
+ return (
51
+ <View className={`gap-5 ${className ?? ''}`} {...props}>
52
+ <View>
53
+ <Typography variant="h4" className="text-text-primary">
54
+ {title}
55
+ </Typography>
56
+ {subtitle ? (
57
+ <Typography variant="body2" className="text-text-tertiary">
58
+ {subtitle}
59
+ </Typography>
60
+ ) : null}
61
+ </View>
62
+
63
+ <View className="flex-row flex-wrap gap-3">
64
+ {stats.map((stat) => (
65
+ <Card key={stat.label} className="min-w-[150px] flex-1 p-4" testID="portfolio-stat">
66
+ <Metric value={stat.value} label={stat.label} />
67
+ </Card>
68
+ ))}
69
+ </View>
70
+
71
+ {sections.map((section) => (
72
+ <View key={section.heading} className="gap-2.5">
73
+ <Eyebrow>{section.heading}</Eyebrow>
74
+ <View className="flex-row flex-wrap gap-3">
75
+ {section.items.map((item) => (
76
+ <InitiativeCard key={item.slug} {...item} />
77
+ ))}
78
+ </View>
79
+ </View>
80
+ ))}
81
+ </View>
82
+ )
83
+ }
@@ -0,0 +1,305 @@
1
+ # ActiveWork — component family
2
+
3
+ Presentational components for surfacing `active-work` data: initiative/task rollups (portfolio status),
4
+ the cross-initiative task table, session logs (the session reader) and per-file mined history (the
5
+ file-history explorer). Read-only: every component takes plain data as props and
6
+ has no fetch/store dependency of its own — wiring to a live source (the active-work session-mining export, or
7
+ a future API) is entirely the caller's concern. Components live flat on disk; the tiering below is a
8
+ documentation contract, not a directory layout.
9
+
10
+ ## Composition trees
11
+
12
+ ```
13
+ PortfolioOverview ................ organism
14
+ ├─ Eyebrow ....................... molecule → Typography (overline)
15
+ ├─ Card + Metric .................. (existing primitives, used inline for the KPI row)
16
+ └─ InitiativeCard ................ card
17
+ ├─ Card ........................ (existing primitive)
18
+ ├─ Pill ........................ (existing primitive)
19
+ ├─ StatusDot .................... (Workout family primitive)
20
+ └─ SegmentedBar ................. (Workout family primitive)
21
+
22
+ FileHistoryExplorer .............. organism
23
+ ├─ Tile .......................... (existing primitive, KPI strip)
24
+ ├─ Card + Divider ................ (existing primitives)
25
+ ├─ Eyebrow ....................... molecule
26
+ ├─ FileActivityRow ............... row (listbox `option`)
27
+ │ ├─ FilePathLabel .............. molecule → Typography (mono)
28
+ │ └─ SparkBars .................. atom (Custom/Charts — new shared primitive)
29
+ ├─ FileActivityDetail ............ card
30
+ │ ├─ Tile / Pill / DataRow / DateTime .... (existing primitives)
31
+ │ ├─ SparkBars .................. atom
32
+ │ ├─ FilePathLabel .............. molecule
33
+ │ └─ Eyebrow .................... molecule
34
+ └─ CoChangeChip .................. molecule
35
+ ├─ Card + Pill ................ (existing primitives)
36
+ └─ FilePathLabel .............. molecule
37
+
38
+ TaskTable ........................ organism
39
+ ├─ Eyebrow ....................... molecule
40
+ ├─ Table (density="dense") ....... (existing primitive, Table family)
41
+ │ └─ useTable ({ comparators }) . (existing hook, Table family)
42
+ ├─ SeverityLabel ................. molecule (legend tallies)
43
+ └─ TaskRow ....................... row
44
+ ├─ TableRow + TableCell ....... (existing primitives, Table family)
45
+ ├─ SeverityLabel .............. molecule → Indicator
46
+ └─ Pill ....................... (existing primitive)
47
+
48
+ Session reader ................... (no organism: the host composes the two halves)
49
+ ├─ SessionList ................... list
50
+ │ ├─ Eyebrow .................... molecule
51
+ │ └─ SessionListItem ............ row (listbox `option`)
52
+ │ ├─ DateTime / Pill ......... (existing primitives)
53
+ │ └─ Typography .............. (existing primitive)
54
+ └─ SessionDetail ................. card
55
+ ├─ Card / Divider / Pill / DateTime .... (existing primitives)
56
+ └─ MarkdownProse .............. molecule (Custom/Prose — new shared primitive)
57
+ └─ Typography .............. (existing primitive)
58
+
59
+ Initiative reader (no organism: the host composes the pieces)
60
+ ├─ InitiativeHeader .............. molecule
61
+ │ ├─ StatusDot / Pill / DateTime ...... (existing primitives; INITIATIVE_STATE_META shared with InitiativeCard)
62
+ │ └─ Typography ................. (existing primitive)
63
+ ├─ OpenLoops ..................... list
64
+ │ ├─ Eyebrow / Pill / Divider ... (existing primitives)
65
+ │ └─ MarkdownProse .............. molecule
66
+ ├─ InitiativeBrief ............... card (single-open `##` accordion + prev/next stepper)
67
+ │ ├─ Eyebrow / Typography ....... (existing primitives)
68
+ │ └─ MarkdownProse .............. molecule
69
+ ├─ TaskTable ..................... organism (embedded, hideColumns)
70
+ └─ SessionList ................... list
71
+ ```
72
+
73
+ ## Dependency map
74
+
75
+ | Component | Tier | Composes ↓ | Used-by ↑ |
76
+ | --------------------- | -------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
77
+ | `PortfolioOverview` | organism | Card, Metric, Eyebrow, InitiativeCard | app root (`Custom/ActiveWork/PortfolioOverview`) |
78
+ | `InitiativeCard` | card | Card, Pill, StatusDot, SegmentedBar, Typography | PortfolioOverview |
79
+ | `FileHistoryExplorer` | organism | Card, Tile, Divider, Eyebrow, FileActivityRow/Detail, CoChangeChip | app root (`Custom/ActiveWork/FileHistoryExplorer`) |
80
+ | `FileActivityDetail` | card | Card, Tile, Pill, DataRow, DateTime, SparkBars, FilePathLabel, Eyebrow | FileHistoryExplorer |
81
+ | `FileActivityRow` | row | FilePathLabel, SparkBars, Typography | FileHistoryExplorer |
82
+ | `CoChangeChip` | molecule | Card, Pill, FilePathLabel, Typography | FileHistoryExplorer |
83
+ | `FilePathLabel` | molecule | Typography (`mono`) | FileActivityRow, FileActivityDetail, CoChangeChip |
84
+ | `TaskTable` | organism | Table, useTable, TableHeader/Row/HeaderCell, TaskRow, SeverityLabel, Eyebrow | app root (`Custom/ActiveWork/TaskTable`) |
85
+ | `TaskRow` | row | TableRow, TableCell, SeverityLabel, Pill, Typography | TaskTable |
86
+ | `SeverityLabel` | molecule | Indicator, Typography (`caption`) | TaskRow, TaskTable (legend), InitiativeCard (vocabulary) |
87
+ | `Eyebrow` | molecule | Typography (`overline`) | PortfolioOverview, FileHistoryExplorer, FileActivityDetail, TaskTable, SessionList |
88
+ | `SessionList` | list | Eyebrow, SessionListItem | host composition (`Custom/ActiveWork/SessionReader` story) |
89
+ | `SessionListItem` | row | DateTime, Pill, Typography | SessionList |
90
+ | `SessionDetail` | card | Card, Divider, Pill, DateTime, MarkdownProse (+ `sessionLinkers`) | host composition (`Custom/ActiveWork/SessionReader` story) |
91
+ | `InitiativeHeader` | molecule | StatusDot, Pill, DateTime, Typography (+ `INITIATIVE_STATE_META`) | host composition (`Custom/ActiveWork/InitiativeReader` story) |
92
+ | `OpenLoops` | list | Eyebrow, Pill, Divider, MarkdownProse | host composition (`Custom/ActiveWork/InitiativeReader` story) |
93
+ | `InitiativeBrief` | card | Eyebrow, Typography, MarkdownProse | host composition (`Custom/ActiveWork/InitiativeReader` story) |
94
+
95
+ ## Surfaces and depth
96
+
97
+ The page is a `<Surface level="base">` (greyRamp[925], `#252321`). Every content container on it is a
98
+ **default `<Card>`** — no variant, no `bg-*` className — which lifts two planes to greyRamp[875]
99
+ (`#31302F`) and wears the lift: a 1px top rim-light at 0.12 plus an ambient shadow. Depth here is tone AND
100
+ lift together, not tone alone.
101
+
102
+ Inside a lifted card, nothing lifts again. Organise with `CardInset` (a pressed well one plane down,
103
+ greyRamp[900] `#2C2A28`, with the `insetWell` recess), a tone-only `filled` tile, or a `Divider`.
104
+ `variant="outline"` is reserved for a status edge; it is not how you make a box.
105
+
106
+ `Card` writes its plane into `style`, so a `bg-surface-*` className on a `Card` is **discarded**. `bgColor`
107
+ is the only override. The M2 reader shipped with `className="… bg-surface-raised" variant="subtle"` on its
108
+ three cards and rendered flat on greyRamp[900] with a hairline ring — that combination is the bug, not the
109
+ model.
110
+
111
+ ## Shared substrates introduced here (reusable beyond this family)
112
+
113
+ - **`Eyebrow`** — a generic uppercase micro-label. Not active-work-specific; exported at
114
+ `Components/Molecules/Eyebrow` (not nested under `ActiveWork/`) so any family can reach for it instead of
115
+ hand-rolling `Typography` + tracking/uppercase classes again.
116
+ - **`SparkBars`** (`components/custom/charts`, `Custom/Charts/SparkBars`) — a bar-mark sparkline for a
117
+ signed series, the counterpart to `Sparkline`'s line mark. Domain-neutral and exported top-level, not
118
+ nested under `ActiveWork/`. `custom/charts` gained an `index.ts` that deliberately exports **only**
119
+ `SparkBars`: `SetBarChart` and `live-rep-growth` stay workout-internal and imported by path.
120
+ - **`formatCompact` / `formatSignedCompact`** (`utils/number-format`) — `1234 → "1.2k"`, `+514689 → "+514.7k"`.
121
+ Generic dense-readout formatting; no equivalent existed (`workout-format` is domain-specific).
122
+ - **`MarkdownProse`** (`components/custom/Prose`, `Custom/Prose/MarkdownProse`) — a small markdown subset
123
+ (headings, bullets, paragraphs, bold, code) rendered as themed prose, with caller-supplied `linkers` that
124
+ auto-link references in a `brand` / `link` / `muted` tone and, when a linker carries `onPress`, make them
125
+ pressable `link`-role spans. Deliberately not a markdown engine. Domain-neutral; the active-work patterns
126
+ (`taskRefLinker`, `wikiLinkLinker`, `prRefLinker`, bundled as `sessionLinkers`) live in this family's
127
+ `session-linkers.ts`. Second consumer is the initiative reader (M2: brief and handoff prose). Inline refs
128
+ are `Text` with `onPress`, not `Link`: `Link` wraps a `Pressable` view, which cannot sit inline in prose.
129
+ - **`formatTaskAge` / `formatSessionDuration`** (`format-time.ts`) — the compact age label moved out of
130
+ `TaskTable` so the session reader shares it (`TaskTable` still re-exports it), plus `1h 4m` / `42m` for a
131
+ session's wall-clock length. `hooks/useTimer`'s `formatDuration` is `mm:ss` for timers, a different job.
132
+
133
+ ## Changes to existing primitives
134
+
135
+ - **`DataRow.label` widened from `string` to `ReactNode`** (plus a new `labelClassName`), mirroring how
136
+ `value` already worked. The co-change rows need a rich `FilePathLabel` on the left; the alternative was
137
+ passing `label=""` and hand-rolling the row, which defeats the primitive. Backwards compatible — `string`
138
+ is a `ReactNode` and the string branch is unchanged.
139
+ - **`Table` gained a `density` axis** (`comfortable` | `dense`, default `comfortable`). Cell padding is
140
+ chosen once in `CELL_PADDING` and read from context by both `TableHeaderCell` and `TableCell`, so a row's
141
+ header and body halves cannot drift. `TaskTable` is a scannable backlog grid, not a reading table; without
142
+ this it would have had to hand-roll the whole grid to get 34px rows.
143
+ - **`useTable` gained per-column `comparators`.** It previously sorted on the raw field value only, which
144
+ cannot express "rank severity critical→low" (alphabetically `low` lands between `high` and `medium`),
145
+ "tie-break on priority", or "read this date newest-first". Columns absent from the map keep the default
146
+ compare, so this is additive. Two ordering rules were also made explicit while in there: `desc` **inverts**
147
+ the comparator rather than reversing the array (reversing also flips tied rows), and blank values rank last
148
+ in **both** directions rather than reading as the smallest value when the column flips.
149
+ - **`status-error-vivid` fixed from `#D14343` to `#FF4757`** (both themes). It pointed at `ramp.red[600]`,
150
+ the exact value of `status-error`, so the two rendered as one colour — Critical and High were
151
+ indistinguishable, and so were `DeviceRow`/`DeviceIndicator`'s `lost` state and a plain error. Three other
152
+ places already encoded the correct vivid red (`--color-status-error-vivid-rgb`, the `-subtle` rgba, and
153
+ `Indicator`'s `glow` shadow); only the value anyone rendered was wrong. Found 2026-08-31 by measuring the
154
+ Storybook render, not by reading tokens. Guarded by `theme/status-distinctness.test.ts`, which asks whether
155
+ tokens LOOK different — a question the existing presence/parity tests structurally cannot ask, since a
156
+ collapsed token is present and parity-matched.
157
+
158
+ - **`Tooltip` gained a controlled `isOpen` and a `useHoverState` hook.** RNW's `Pressable` hovers with
159
+ `contain: true`, which locks the pointer to the innermost Pressable and ends any ancestor's hover, so a
160
+ `Tooltip` wrapped around a pressable row never shows in a real browser (jsdom's synthetic `mouseEnter`
161
+ hides this). In controlled mode the Tooltip renders no Pressable of its own, so it can sit _inside_ a
162
+ pressable row without taking its press; the field owns hover through `useHoverState`'s pointer
163
+ enter/leave props. The session row's age and task count use this. Same trap as the T2 header tooltip, seen
164
+ from the other side.
165
+ - **`CollapseButton` gained `accessibilityLabel`**, so a bare chevron toggle (the tasks-touched table) has an
166
+ accessible name.
167
+ - **`TaskTable` gained `hideColumns`** (mirrored by `TaskRow`), so an embedded table can drop columns its
168
+ context already implies: the session detail hides `slug` and `tags` to give the title room.
169
+ - **`Table` gained a column drop order** (`useMeasuredWidth` + `useColumnFit` + `Table.contentMinWidth`), and
170
+ `TaskTable` declares one: severity, priority, estimate, tags, age, initiative. `title` and `id` declare no
171
+ `dropPriority`, so they never drop and the table scrolls horizontally instead. Found in the initiative
172
+ reader at an 865px viewport: the card measured 369px against 418px of fixed columns, the flexible title
173
+ collapsed to 0 and painted its header over the severity column, and the age column was clipped off the
174
+ right edge. `priority`, `estimate` and `severityCompact` also widened (42/42/44 → 56/56/56) because each
175
+ was narrower than its own uppercase header plus the sort glyph — the arithmetic behind `SEV`/`PRI`
176
+ overlapping at every width, not just narrow ones.
177
+ - **Subtle coloured `Pill`s are borderless with a stronger tint** (`bg-*/15`, `border-transparent`; the
178
+ neutral `default` keeps its hairline). The 25%-alpha coloured border read as a dark outline on dark
179
+ surfaces. System-wide; visual baselines for coloured subtle pills move.
180
+
181
+ ## Severity vocabulary has one owner
182
+
183
+ `SeverityLabel.tsx` owns `TaskSeverity`, `SEVERITY_ORDER`, `SEVERITY_RANK`/`severityRank`, `SEVERITY_META`
184
+ (dot colours) and `SEVERITY_BAR_COLOR` (area-mark fills). `InitiativeCard` previously declared its own copy
185
+ of the type plus private `SEVERITY_ORDER`/`SEVERITY_COLOR` constants; it now imports them. The two colour
186
+ maps stay separate on purpose — `low` is `status-info` as a dot (it must stay legible among four) and
187
+ `text-tertiary` as a bar segment (it should recede) — but the _set_ of severities is defined once.
188
+
189
+ ## Reuse audit
190
+
191
+ | Concern | Uses | Not |
192
+ | ------------------ | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
193
+ | status dot + label | `StatusDot` (Workout family) | the original Lab specimen's hand-rolled `DotLabel` (deleted) |
194
+ | severity mix bar | `SegmentedBar` (Workout family) | the original Lab specimen's hand-rolled `SeverityBar` (deleted) |
195
+ | bar sparkline | `SparkBars` (new, `Custom/Charts`) | the specimen's hand-rolled `MiniBars` (deleted); `Sparkline` is a _line_ mark, `SetBar`/`SetStrip` are workout-domain |
196
+ | KPI stat boxes | `Tile` (bare on the page; one plane up inside a card) | the specimen's redundant `Card variant="filled"` wrapper around `Tile` |
197
+ | label ↔ value rows | `DataRow` (label widened to `ReactNode`) | a hand-rolled `flex-row justify-between` |
198
+ | short dates | `DateTime` `format="short"` + `fallback` | the specimen's hand-rolled `shortDate` (deleted) |
199
+ | compact numbers | `formatCompact` / `formatSignedCompact` (new util) | the specimen's inline `compact` / `signedCompact` (deleted) |
200
+ | count badges | `Pill` `variant="subtle"` | ad-hoc bordered `View` |
201
+ | card chrome | `Card` (default; `accent` for focused state) | ad-hoc bordered `View`; `subtle`/`outline` used to make a box |
202
+ | colors | `getSemanticColors` / `greyRamp` tokens | magic hex |
203
+ | session prose | `MarkdownProse` (new, `Custom/Prose`) | the specimen's inline `parseBlocks` / `renderInline` / `BlockView` (deleted); no markdown renderer existed |
204
+ | session durations | `formatSessionDuration` (new, `format-time.ts`) | the specimen's inline `duration`; `useTimer.formatDuration` is `mm:ss` |
205
+ | selectable rows | `Pressable` + `role="option"` (the F1 pattern) | the specimen's bordered `Card`-per-row; `ListItem` has no selected state |
206
+
207
+ ### Colour vocabularies
208
+
209
+ Two live here and must not be conflated:
210
+
211
+ - **`FILE_EVENT_COLOR`** — read / write / edit, i.e. _what kind of event_. Two variants ship:
212
+ `FILE_EVENT_COLOR_CATEGORICAL` (**the default** — the canonical CVD-safe palette taken in order, on the
213
+ grounds that the three kinds are peer categories) and `FILE_EVENT_COLOR_SEMANTIC` (the alternative —
214
+ colours by importance, greying reads out). Overridable per component via `eventColors`. Note the default
215
+ spends three of `CATEGORICAL_CVD_SAFE_MAX`: a view that also plots series must start from index 3.
216
+ - **`GROWTH_COLOR`** — added / removed / neutral, i.e. _char deltas_. Uses `result-*`, because a file that
217
+ net-shrank was refactored, not broken. `status-error` would say something is wrong. See TOKENS.md §1.
218
+
219
+ All colours resolve through `resolveColor(token)`, never `getSemanticColors('dark')` — the latter freezes
220
+ to the dark hex and silently breaks light theme.
221
+
222
+ ### Surfaces and depth
223
+
224
+ Follows _Foundations → Depth_ as corrected on 2026-09-08. Every container plane is a step on the grey ramp,
225
+ and a plane that sits above its host wears the **lift** — a 1px top rim-light at 0.12 plus an ambient
226
+ shadow. The earlier claim on this page that levels 1–3 separate by tone alone, and that "nothing here
227
+ floats, so nothing casts a shadow", was reversed: the upper ramp steps are only ΔL\* 2.5–3 apart, too tight
228
+ for tone to carry the separation on its own.
229
+
230
+ Measured planes (dark mode; light mode is a separate pass):
231
+
232
+ | Role | Component | Plane | Colour | Treatment |
233
+ | ----------------------------------------- | --------------------------------------- | --------------------- | --------- | ----------------------------- |
234
+ | Page | `<Surface level="base">` | `base` / grey-925 | `#252321` | flat, no lift |
235
+ | List pane, detail pane, reader cards, KPI | `<Card>` (default, `elevation={2}`) | `raised` / grey-875 | `#31302F` | rim `inset 0 1px 0` + ambient |
236
+ | Focused initiative | `<Card variant="accent">` | `raised` / grey-875 | `#31302F` | lift + brand left stripe |
237
+ | Growth block (grouped stats) | `<CardInset>` | `elevated` / grey-900 | `#2C2A28` | `insetWell` recess |
238
+ | Co-change chips | `<Card variant="filled">` | `raised` / grey-875 | `#31302F` | tone only, no lift |
239
+ | Activity tiles inside the detail pane | `<Tile className="bg-surface-overlay">` | `overlay` / grey-850 | `#373635` | tone only, no lift |
240
+ | Table frame, month rules, section rules | `border-hairline` / `Divider` | host plane | — | hairline edge only |
241
+
242
+ Three rules the family now holds:
243
+
244
+ 1. **Lift sparingly.** One lift per page-level container. A card inside a card does not lift again; it
245
+ becomes a `CardInset`, a `filled` tile, or a `Divider`.
246
+ 2. **Never pass a `bg-*` className to a `Card`.** The plane is written into `style`, so the class is
247
+ silently dropped. `bgColor` is the one override.
248
+ 3. **A hairline is an edge, not a plane.** Dividers, table rules and the `outline` variant's status edge
249
+ are the only places it belongs. `variant="subtle"`/`"outline"` used to make a box is the anti-pattern
250
+ this migration removed.
251
+
252
+ `Tile` still hand-rolls `bg-surface-raised`, which _is_ the card plane. At page level that is right; inside
253
+ a lifted card the call site steps it up one (`className="bg-surface-overlay"`). Teaching `Tile` to resolve
254
+ its plane from the surface context is a separate epic.
255
+
256
+ ### Token discipline
257
+
258
+ This family is on the **token-pure** eslint list (`eslint.config.js`, error-level): no raw hex, no
259
+ arbitrary spacing/radius/type values, no hardcoded inline `fontSize`, no `getSemanticColors` in a
260
+ component. It was brought to zero violations when F1 was hardened — including retrofitting T1, which had
261
+ carried 19 arbitrary values in from its specimen. Adding a family to that list is the last step of
262
+ hardening it; see TOKENS.md §6.
263
+
264
+ **Watch-list (known gaps):**
265
+
266
+ - `InitiativeCard`'s state→`StatusDot` variant mapping (`focused → on-track`, `backburner → future`,
267
+ `paused → deviation`, `done → success`) is a first-pass color choice, not yet operator-confirmed against a
268
+ render — flagged for Gate 2 review rather than blocking hardening on a separate round-trip.
269
+ - The live data wiring (active-work → `PortfolioOverview` / `FileHistoryExplorer` props) still lives only in
270
+ the `titan-aw-dashboard` Lab specimen story files, not in a shared adapter. Promoting that mapping out of
271
+ the specimens is a follow-up, not part of this unit.
272
+ - `FilePathLabel` is domain-neutral (a file path is not an active-work concept) but every consumer today is
273
+ in this family, so it stays here. Promote it top-level the moment a second family needs it.
274
+ - The KPI strip now uses bare `Tile` while `PortfolioOverview` still uses `Card` + `Metric`. The family is
275
+ internally inconsistent until T1 is revisited — deliberate, since changing T1 is out of this unit's scope.
276
+
277
+ ## Accessibility notes
278
+
279
+ - The ranked list is a real **`listbox`** and each `FileActivityRow` a real **`option`** carrying
280
+ `aria-selected`. This uses raw `role` / `aria-selected` props rather than `accessibilityState={{ selected }}`,
281
+ because **RNW silently drops the latter** — the gap `components/shell/README.md` flags as unresolved for
282
+ `NavItem`, where active state is asserted via an accent-bar testID instead. The same fix applies there.
283
+ - React Native's `Role` union has `'option'` but omits `'listbox'`, so `FileHistoryExplorer` casts once
284
+ through a named `LISTBOX_ROLE` constant rather than dropping the ARIA parent. `SessionList` does the same;
285
+ each `SessionListItem` is an `option` with `aria-selected` and an accessible name of title plus footer.
286
+ - `MarkdownProse` references with a handler carry `accessibilityRole="link"`; inert ones carry no role, so
287
+ a screen reader does not announce a link that goes nowhere.
288
+
289
+ ## Testing
290
+
291
+ - Unit: `*.test.tsx` per component (render/behavior/a11y via `jest-axe`). Branch coverage on state/rank/topTask
292
+ presence and the all-zero-severity case (T1); on controlled-vs-uncontrolled selection, unmatched
293
+ `selectedPath`, empty `files`, empty `coEdges`, missing touch dates, no-co-change, net-negative growth, and
294
+ `maxRows` / `maxCoEdges` capping (F1).
295
+ - Fixtures: `file-history-fixture.ts` — a small hand-trimmed slice of a real mine with fixed values (no
296
+ `Date.now()`, no randomness) so visual baselines stay deterministic. `session-fixture.ts` likewise: five
297
+ real session logs (their real markdown, so the linkers meet real references) plus one synthesized ad-hoc
298
+ session, paired with `SESSION_NOW`.
299
+ - Visual: not yet added to `tests/visual/stories.spec.ts` — see follow-up in the PR description.
300
+ - Lint guardrails: the token-pure rule set above; no
301
+ re-implemented status dot, segmented bar, or sparkline.
302
+
303
+ ```
304
+
305
+ ```
@@ -0,0 +1,71 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { Surface } from '../../ui/surface'
3
+ import { SessionDetail } from './SessionDetail'
4
+ import { SESSION_FIXTURE, SESSION_NOW, SESSION_TASK_FIXTURE } from './session-fixture'
5
+
6
+ /**
7
+ * **SessionDetail** — one session log: when it ran and for how long, the
8
+ * tasks it touched, then the body as auto-linked prose.
9
+ *
10
+ * Composes `Card` · `Divider` · `Pill` · `DateTime` · `MarkdownProse`.
11
+ */
12
+ const meta: Meta<typeof SessionDetail> = {
13
+ title: 'Custom/ActiveWork/SessionDetail',
14
+ component: SessionDetail,
15
+ tags: ['autodocs', 'status:candidate', '!status:review'],
16
+ args: {
17
+ session: SESSION_FIXTURE[0],
18
+ tasks: SESSION_TASK_FIXTURE,
19
+ now: SESSION_NOW,
20
+ },
21
+ argTypes: {
22
+ session: { table: { disable: true } },
23
+ tasks: { table: { disable: true } },
24
+ now: { table: { disable: true } },
25
+ onPressTask: { action: 'onPressTask' },
26
+ onPressLink: { action: 'onPressLink' },
27
+ onPressPr: { action: 'onPressPr' },
28
+ },
29
+ decorators: [
30
+ (Story) => (
31
+ <Surface level="base" className="min-h-screen w-full max-w-[760px] p-6" testID="page-surface">
32
+ <Story />
33
+ </Surface>
34
+ ),
35
+ ],
36
+ parameters: {
37
+ layout: 'fullscreen',
38
+ docs: {
39
+ description: {
40
+ component:
41
+ 'Composes **Card** · **Divider** · **Pill** · **DateTime** · **MarkdownProse** (with the session linkers). Used-by ↑ the Session Reader composition.',
42
+ },
43
+ },
44
+ },
45
+ }
46
+ export default meta
47
+
48
+ type Story = StoryObj<typeof SessionDetail>
49
+
50
+ /** A long canonical session touching six tasks. */
51
+ export const Default: Story = {}
52
+
53
+ /** A six-minute session with a short log. */
54
+ export const Short: Story = {
55
+ args: { session: SESSION_FIXTURE[5] },
56
+ }
57
+
58
+ /** An ad-hoc session: `[[name]]` link and a PR number, one task mentioned. */
59
+ export const AdHoc: Story = {
60
+ args: { session: SESSION_FIXTURE[1] },
61
+ }
62
+
63
+ /** No task rows supplied: the strip is pills only, with no table toggle. */
64
+ export const WithoutTasks: Story = {
65
+ args: { tasks: [] },
66
+ }
67
+
68
+ /** No handlers: references stay highlighted but are not pressable. */
69
+ export const Inert: Story = {
70
+ args: { onPressTask: undefined, onPressLink: undefined, onPressPr: undefined },
71
+ }
@@ -0,0 +1,84 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { fireEvent, render, screen, within } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { SessionDetail, stripLeadingHeading } from './SessionDetail'
5
+ import { SESSION_FIXTURE, SESSION_NOW, SESSION_TASK_FIXTURE } from './session-fixture'
6
+ import { extractTaskRefs } from './session-linkers'
7
+
8
+ const session = SESSION_FIXTURE[0]!
9
+ const adhoc = SESSION_FIXTURE[1]!
10
+
11
+ describe('SessionDetail', () => {
12
+ it('renders the header: title, date, age, duration and session id', () => {
13
+ render(<SessionDetail session={session} now={SESSION_NOW} />)
14
+ expect(screen.getByText(session.title)).toBeInTheDocument()
15
+ expect(screen.getByText('Jul 12, 2026')).toBeInTheDocument()
16
+ expect(screen.getByText('1d ago')).toBeInTheDocument()
17
+ expect(screen.getByText('· 1h 4m')).toBeInTheDocument()
18
+ expect(screen.getByText(session.id)).toBeInTheDocument()
19
+ })
20
+
21
+ it('lists every distinct task the log touches, once each, in first-seen order', () => {
22
+ render(<SessionDetail session={session} now={SESSION_NOW} />)
23
+ const strip = within(screen.getByTestId('session-tasks-touched'))
24
+ const refs = extractTaskRefs(session.body)
25
+ expect(refs.length).toBeGreaterThan(1)
26
+ for (const id of refs) expect(strip.getByText(id)).toBeInTheDocument()
27
+ })
28
+
29
+ it('routes presses on task pills and on in-body references to the host', () => {
30
+ const onPressTask = vi.fn()
31
+ const onPressLink = vi.fn()
32
+ const onPressPr = vi.fn()
33
+ render(
34
+ <SessionDetail
35
+ session={adhoc}
36
+ now={SESSION_NOW}
37
+ onPressTask={onPressTask}
38
+ onPressLink={onPressLink}
39
+ onPressPr={onPressPr}
40
+ />
41
+ )
42
+ fireEvent.click(within(screen.getByTestId('session-tasks-touched')).getByText('AW-17'))
43
+ expect(onPressTask).toHaveBeenCalledWith('AW-17')
44
+ fireEvent.click(screen.getByTestId('prose-ref-wiki'))
45
+ expect(onPressLink).toHaveBeenCalledWith('setup-walkthrough')
46
+ fireEvent.click(screen.getByTestId('prose-ref-pr'))
47
+ expect(onPressPr).toHaveBeenCalledWith(42)
48
+ })
49
+
50
+ it('leaves the leading h1 to the header and renders the rest as prose', () => {
51
+ render(<SessionDetail session={adhoc} now={SESSION_NOW} />)
52
+ const body = within(screen.getByTestId('session-body'))
53
+ expect(body.queryByText(adhoc.title)).not.toBeInTheDocument()
54
+ expect(body.getByText(/Tightened the install section/)).toBeInTheDocument()
55
+ })
56
+
57
+ it('expands the tasks-touched strip into a table only when task rows are supplied', () => {
58
+ const { rerender } = render(<SessionDetail session={adhoc} now={SESSION_NOW} />)
59
+ expect(screen.queryByRole('button', { name: /table/ })).not.toBeInTheDocument()
60
+ rerender(<SessionDetail session={adhoc} now={SESSION_NOW} tasks={SESSION_TASK_FIXTURE} />)
61
+ expect(screen.queryByTestId('task-row')).not.toBeInTheDocument()
62
+ fireEvent.click(screen.getByRole('button', { name: /table/ }))
63
+ expect(screen.getByText('1 task touched')).toBeInTheDocument()
64
+ const rows = screen.getAllByTestId('task-row')
65
+ expect(rows).toHaveLength(1)
66
+ expect(within(rows[0]!).getByText('AW-17')).toBeInTheDocument()
67
+ })
68
+
69
+ it('has no a11y violations', async () => {
70
+ const { container } = render(
71
+ <SessionDetail session={adhoc} now={SESSION_NOW} onPressTask={() => {}} />
72
+ )
73
+ expect(await axe(container)).toHaveNoViolations()
74
+ })
75
+ })
76
+
77
+ describe('stripLeadingHeading', () => {
78
+ it('drops a first-line h1 and nothing else', () => {
79
+ expect(stripLeadingHeading('# Title\n\nBody')).toBe('\nBody')
80
+ expect(stripLeadingHeading('\n# Title\nBody')).toBe('Body')
81
+ expect(stripLeadingHeading('## Section\nBody')).toBe('## Section\nBody')
82
+ expect(stripLeadingHeading('Body only')).toBe('Body only')
83
+ })
84
+ })