@titan-design/react-ui 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (509) hide show
  1. package/README.md +46 -47
  2. package/dist/bodymap.js +768 -701
  3. package/dist/bodymap.js.map +1 -1
  4. package/dist/bodymap.mjs +770 -703
  5. package/dist/bodymap.mjs.map +1 -1
  6. package/dist/index.d.mts +1319 -216
  7. package/dist/index.d.ts +1319 -216
  8. package/dist/index.js +5996 -4498
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +5773 -4357
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/{pages-CvsFqNtx.d.mts → pages-CpW3OB3g.d.mts} +21 -3
  13. package/dist/{pages-B1hqnGIC.d.ts → pages-_3Ajpiqu.d.ts} +21 -3
  14. package/dist/pages.d.mts +1 -1
  15. package/dist/pages.d.ts +1 -1
  16. package/dist/pages.js +1435 -1428
  17. package/dist/pages.js.map +1 -1
  18. package/dist/pages.mjs +1443 -1436
  19. package/dist/pages.mjs.map +1 -1
  20. package/dist/theme/index.d.mts +195 -134
  21. package/dist/theme/index.d.ts +195 -134
  22. package/dist/theme/index.js +215 -300
  23. package/dist/theme/index.js.map +1 -1
  24. package/dist/theme/index.mjs +202 -296
  25. package/dist/theme/index.mjs.map +1 -1
  26. package/dist/theme/tokens-css.js +62 -5
  27. package/dist/theme/tokens-css.js.map +1 -1
  28. package/dist/theme/tokens-css.mjs +62 -5
  29. package/dist/theme/tokens-css.mjs.map +1 -1
  30. package/dist/theme/tokens.d.mts +73 -5
  31. package/dist/theme/tokens.d.ts +73 -5
  32. package/dist/theme/tokens.js +55 -7
  33. package/dist/theme/tokens.js.map +1 -1
  34. package/dist/theme/tokens.mjs +55 -8
  35. package/dist/theme/tokens.mjs.map +1 -1
  36. package/docs/audits/2026-09-08-critique-addenda.md +51 -0
  37. package/docs/audits/2026-09-08-library-critique.md +212 -0
  38. package/docs/library-roadmap.md +99 -0
  39. package/package.json +1 -1
  40. package/src/arch/arch-graph.freshness.test.ts +52 -0
  41. package/src/arch/arch-graph.json +5021 -1617
  42. package/src/components/custom/ActiveWork/CoChangeChip.stories.tsx +44 -0
  43. package/src/components/custom/ActiveWork/CoChangeChip.test.tsx +29 -0
  44. package/src/components/custom/ActiveWork/CoChangeChip.tsx +48 -0
  45. package/src/components/custom/ActiveWork/Eyebrow.stories.tsx +25 -0
  46. package/src/components/custom/ActiveWork/Eyebrow.test.tsx +16 -0
  47. package/src/components/custom/ActiveWork/Eyebrow.tsx +26 -0
  48. package/src/components/custom/ActiveWork/FileActivityDetail.stories.tsx +50 -0
  49. package/src/components/custom/ActiveWork/FileActivityDetail.test.tsx +66 -0
  50. package/src/components/custom/ActiveWork/FileActivityDetail.tsx +202 -0
  51. package/src/components/custom/ActiveWork/FileActivityRow.stories.tsx +70 -0
  52. package/src/components/custom/ActiveWork/FileActivityRow.test.tsx +67 -0
  53. package/src/components/custom/ActiveWork/FileActivityRow.tsx +130 -0
  54. package/src/components/custom/ActiveWork/FileHistoryExplorer.stories.tsx +84 -0
  55. package/src/components/custom/ActiveWork/FileHistoryExplorer.test.tsx +99 -0
  56. package/src/components/custom/ActiveWork/FileHistoryExplorer.tsx +138 -0
  57. package/src/components/custom/ActiveWork/FilePathLabel.stories.tsx +39 -0
  58. package/src/components/custom/ActiveWork/FilePathLabel.test.tsx +53 -0
  59. package/src/components/custom/ActiveWork/FilePathLabel.tsx +63 -0
  60. package/src/components/custom/ActiveWork/InitiativeBrief.stories.tsx +50 -0
  61. package/src/components/custom/ActiveWork/InitiativeBrief.test.tsx +62 -0
  62. package/src/components/custom/ActiveWork/InitiativeBrief.tsx +181 -0
  63. package/src/components/custom/ActiveWork/InitiativeCard.stories.tsx +73 -0
  64. package/src/components/custom/ActiveWork/InitiativeCard.test.tsx +68 -0
  65. package/src/components/custom/ActiveWork/InitiativeCard.tsx +136 -0
  66. package/src/components/custom/ActiveWork/InitiativeHeader.stories.tsx +59 -0
  67. package/src/components/custom/ActiveWork/InitiativeHeader.test.tsx +40 -0
  68. package/src/components/custom/ActiveWork/InitiativeHeader.tsx +73 -0
  69. package/src/components/custom/ActiveWork/InitiativeReader.stories.tsx +145 -0
  70. package/src/components/custom/ActiveWork/OpenLoops.stories.tsx +58 -0
  71. package/src/components/custom/ActiveWork/OpenLoops.test.tsx +61 -0
  72. package/src/components/custom/ActiveWork/OpenLoops.tsx +107 -0
  73. package/src/components/custom/ActiveWork/PortfolioOverview.stories.tsx +101 -0
  74. package/src/components/custom/ActiveWork/PortfolioOverview.test.tsx +81 -0
  75. package/src/components/custom/ActiveWork/PortfolioOverview.tsx +83 -0
  76. package/src/components/custom/ActiveWork/README.md +305 -0
  77. package/src/components/custom/ActiveWork/SessionDetail.stories.tsx +71 -0
  78. package/src/components/custom/ActiveWork/SessionDetail.test.tsx +84 -0
  79. package/src/components/custom/ActiveWork/SessionDetail.tsx +180 -0
  80. package/src/components/custom/ActiveWork/SessionList.stories.tsx +69 -0
  81. package/src/components/custom/ActiveWork/SessionList.test.tsx +78 -0
  82. package/src/components/custom/ActiveWork/SessionList.tsx +103 -0
  83. package/src/components/custom/ActiveWork/SessionListItem.stories.tsx +64 -0
  84. package/src/components/custom/ActiveWork/SessionListItem.test.tsx +62 -0
  85. package/src/components/custom/ActiveWork/SessionListItem.tsx +152 -0
  86. package/src/components/custom/ActiveWork/SessionReader.stories.tsx +113 -0
  87. package/src/components/custom/ActiveWork/SeverityLabel.stories.tsx +51 -0
  88. package/src/components/custom/ActiveWork/SeverityLabel.test.tsx +68 -0
  89. package/src/components/custom/ActiveWork/SeverityLabel.tsx +97 -0
  90. package/src/components/custom/ActiveWork/TaskRow.stories.tsx +63 -0
  91. package/src/components/custom/ActiveWork/TaskRow.test.tsx +112 -0
  92. package/src/components/custom/ActiveWork/TaskRow.tsx +176 -0
  93. package/src/components/custom/ActiveWork/TaskTable.stories.tsx +123 -0
  94. package/src/components/custom/ActiveWork/TaskTable.test.tsx +233 -0
  95. package/src/components/custom/ActiveWork/TaskTable.tsx +321 -0
  96. package/src/components/custom/ActiveWork/active-work-depth.test.tsx +102 -0
  97. package/src/components/custom/ActiveWork/file-history-fixture.ts +91 -0
  98. package/src/components/custom/ActiveWork/format-time.test.ts +23 -0
  99. package/src/components/custom/ActiveWork/format-time.ts +32 -0
  100. package/src/components/custom/ActiveWork/index.ts +19 -0
  101. package/src/components/custom/ActiveWork/initiative-fixture.ts +104 -0
  102. package/src/components/custom/ActiveWork/session-fixture.ts +267 -0
  103. package/src/components/custom/ActiveWork/session-linkers.ts +56 -0
  104. package/src/components/custom/ActiveWork/task-list-fixture.ts +97 -0
  105. package/src/components/custom/CircularTimer/CircularTimer.stories.tsx +4 -3
  106. package/src/components/custom/DateTime/DateTime.stories.tsx +2 -1
  107. package/src/components/custom/DateTime/DateTime.test.tsx +3 -1
  108. package/src/components/custom/EmptyState/EmptyState.stories.tsx +22 -9
  109. package/src/components/custom/EmptyState/EmptyState.test.tsx +7 -13
  110. package/src/components/custom/EmptyState/EmptyState.tsx +4 -18
  111. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +4 -3
  112. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +2 -2
  113. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +4 -3
  114. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +2 -2
  115. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +8 -4
  116. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +116 -6
  117. package/src/components/custom/Fatigue/LiveFatiguePanel.test.tsx +134 -0
  118. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +54 -83
  119. package/src/components/custom/Fatigue/README.md +40 -3
  120. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +2 -2
  121. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +2 -1
  122. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +4 -3
  123. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +2 -2
  124. package/src/components/custom/Fatigue/fatigue-mock.test.ts +0 -5
  125. package/src/components/custom/Fatigue/fatigue-mock.ts +2 -10
  126. package/src/components/custom/Fatigue/fatigue-tokens.ts +12 -0
  127. package/src/components/custom/Fatigue/index.ts +17 -5
  128. package/src/components/custom/Fatigue/panel-layout.test.ts +149 -0
  129. package/src/components/custom/Fatigue/panel-layout.ts +185 -0
  130. package/src/components/custom/Gauge/Gauge.stories.tsx +2 -2
  131. package/src/components/custom/Gauge/Gauge.test.tsx +1 -1
  132. package/src/components/custom/Gauge/Gauge.tsx +4 -1
  133. package/src/components/custom/Metric/Metric.stories.tsx +10 -1
  134. package/src/components/custom/Metric/Metric.test.tsx +1 -3
  135. package/src/components/custom/Metric/Metric.tsx +5 -20
  136. package/src/components/custom/Prose/MarkdownProse.stories.tsx +88 -0
  137. package/src/components/custom/Prose/MarkdownProse.test.tsx +87 -0
  138. package/src/components/custom/Prose/MarkdownProse.tsx +201 -0
  139. package/src/components/custom/Prose/README.md +58 -0
  140. package/src/components/custom/Prose/index.ts +1 -0
  141. package/src/components/custom/Scatter/Scatter.stories.tsx +46 -8
  142. package/src/components/custom/Scatter/Scatter.test.tsx +2 -2
  143. package/src/components/custom/Scatter/Scatter.tsx +116 -14
  144. package/src/components/custom/Sidebar/Sidebar.stories.tsx +40 -7
  145. package/src/components/custom/Sidebar/Sidebar.test.tsx +5 -1
  146. package/src/components/custom/Sidebar/Sidebar.tsx +16 -33
  147. package/src/components/custom/Table/Table.stories.tsx +98 -15
  148. package/src/components/custom/Table/Table.test.tsx +273 -86
  149. package/src/components/custom/Table/Table.tsx +159 -67
  150. package/src/components/custom/Table/column-fit.test.ts +75 -0
  151. package/src/components/custom/Table/column-fit.ts +116 -0
  152. package/src/components/custom/Table/index.ts +9 -0
  153. package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +8 -6
  154. package/src/components/custom/Treemap/Treemap.stories.tsx +2 -2
  155. package/src/components/custom/Treemap/Treemap.test.tsx +1 -1
  156. package/src/components/custom/Treemap/Treemap.tsx +7 -2
  157. package/src/components/custom/Typography/Typography.stories.tsx +28 -1
  158. package/src/components/custom/Typography/Typography.test.tsx +34 -0
  159. package/src/components/custom/Typography/Typography.tsx +6 -0
  160. package/src/components/custom/Typography/index.ts +1 -8
  161. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +9 -1
  162. package/src/components/custom/Workout/BaseBadge.stories.tsx +19 -7
  163. package/src/components/custom/Workout/BaseBadge.test.tsx +31 -11
  164. package/src/components/custom/Workout/BaseBadge.tsx +3 -0
  165. package/src/components/custom/Workout/BodyMap.stories.tsx +4 -1
  166. package/src/components/custom/Workout/BodyMap.tsx +3 -1
  167. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +16 -4
  168. package/src/components/custom/Workout/BodyMapDetailPanel.test.tsx +11 -15
  169. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +45 -9
  170. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +10 -8
  171. package/src/components/custom/Workout/CapacityBandChart.test.tsx +24 -51
  172. package/src/components/custom/Workout/CapacityBandChart.tsx +139 -108
  173. package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
  174. package/src/components/custom/Workout/DeviationBar.test.tsx +5 -2
  175. package/src/components/custom/Workout/DeviationBar.tsx +21 -16
  176. package/src/components/custom/Workout/DualVelocityStrip.stories.tsx +182 -0
  177. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +3 -2
  178. package/src/components/custom/Workout/ExerciseCard.stories.tsx +8 -7
  179. package/src/components/custom/Workout/ExerciseCard.test.tsx +13 -3
  180. package/src/components/custom/Workout/ExerciseCard.tsx +42 -51
  181. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +7 -7
  182. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +9 -1
  183. package/src/components/custom/Workout/ExerciseHeading.stories.tsx +8 -8
  184. package/src/components/custom/Workout/ExerciseHeading.tsx +8 -5
  185. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +6 -4
  186. package/src/components/custom/Workout/ExerciseIndicator.test.tsx +27 -11
  187. package/src/components/custom/Workout/ExerciseIndicator.tsx +14 -15
  188. package/src/components/custom/Workout/FatigueMeter.stories.tsx +5 -5
  189. package/src/components/custom/Workout/InputBar.stories.tsx +4 -5
  190. package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
  191. package/src/components/custom/Workout/IntensityBar.test.tsx +5 -1
  192. package/src/components/custom/Workout/IntensityBar.tsx +4 -2
  193. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +5 -4
  194. package/src/components/custom/Workout/MesoCard.stories.tsx +4 -1
  195. package/src/components/custom/Workout/MesoCard.test.tsx +7 -18
  196. package/src/components/custom/Workout/MesoCard.tsx +12 -9
  197. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
  198. package/src/components/custom/Workout/MesoProgressBar.test.tsx +18 -11
  199. package/src/components/custom/Workout/MesoProgressBar.tsx +15 -13
  200. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +2 -1
  201. package/src/components/custom/Workout/MesoStatusCard.test.tsx +14 -44
  202. package/src/components/custom/Workout/MesoStatusCard.tsx +9 -23
  203. package/src/components/custom/Workout/MetricTiles.stories.tsx +3 -2
  204. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +2 -8
  205. package/src/components/custom/Workout/MuscleGroupChip.test.tsx +23 -31
  206. package/src/components/custom/Workout/MuscleGroupChip.tsx +28 -67
  207. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
  208. package/src/components/custom/Workout/PlaceholderStrip.test.tsx +17 -10
  209. package/src/components/custom/Workout/PlaceholderStrip.tsx +9 -8
  210. package/src/components/custom/Workout/PrBadge.stories.tsx +2 -1
  211. package/src/components/custom/Workout/PrBadge.test.tsx +3 -9
  212. package/src/components/custom/Workout/PrBadge.tsx +13 -17
  213. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +4 -1
  214. package/src/components/custom/Workout/PrHistoryModal.test.tsx +13 -36
  215. package/src/components/custom/Workout/PrHistoryModal.tsx +8 -4
  216. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +6 -1
  217. package/src/components/custom/Workout/README.md +11 -6
  218. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +3 -2
  219. package/src/components/custom/Workout/ReadinessCheck.test.tsx +12 -18
  220. package/src/components/custom/Workout/ReadinessCheck.tsx +19 -3
  221. package/src/components/custom/Workout/RestTimer.stories.tsx +3 -2
  222. package/src/components/custom/Workout/ScheduleTiles.stories.tsx +3 -2
  223. package/src/components/custom/Workout/ScheduleTiles.test.tsx +3 -4
  224. package/src/components/custom/Workout/ScheduleTiles.tsx +4 -7
  225. package/src/components/custom/Workout/SegmentedBar.stories.tsx +5 -5
  226. package/src/components/custom/Workout/SegmentedBar.tsx +6 -2
  227. package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +5 -5
  228. package/src/components/custom/Workout/SessionHeader.stories.tsx +6 -6
  229. package/src/components/custom/Workout/SessionHeader.tsx +12 -12
  230. package/src/components/custom/Workout/SessionRail.stories.tsx +12 -11
  231. package/src/components/custom/Workout/SessionRail.tsx +7 -9
  232. package/src/components/custom/Workout/SetBar.stories.tsx +10 -6
  233. package/src/components/custom/Workout/SetBar.test.tsx +49 -4
  234. package/src/components/custom/Workout/SetBar.tsx +38 -12
  235. package/src/components/custom/Workout/SetRow.stories.tsx +3 -2
  236. package/src/components/custom/Workout/SetRow.test.tsx +16 -2
  237. package/src/components/custom/Workout/SetRow.tsx +61 -23
  238. package/src/components/custom/Workout/SetStrip.stories.tsx +60 -5
  239. package/src/components/custom/Workout/SetStrip.test.tsx +25 -0
  240. package/src/components/custom/Workout/SetStrip.tsx +24 -4
  241. package/src/components/custom/Workout/SetTableHeader.stories.tsx +6 -6
  242. package/src/components/custom/Workout/SetTableHeader.tsx +9 -12
  243. package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +7 -7
  244. package/src/components/custom/Workout/SetsRepsLoad.tsx +3 -5
  245. package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
  246. package/src/components/custom/Workout/Sparkline.test.tsx +37 -19
  247. package/src/components/custom/Workout/Sparkline.tsx +14 -18
  248. package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
  249. package/src/components/custom/Workout/StatusDot.test.tsx +44 -5
  250. package/src/components/custom/Workout/StatusDot.tsx +93 -63
  251. package/src/components/custom/Workout/StatusPill.stories.tsx +1 -1
  252. package/src/components/custom/Workout/StatusPill.test.tsx +6 -15
  253. package/src/components/custom/Workout/StatusPill.tsx +27 -38
  254. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +9 -3
  255. package/src/components/custom/Workout/StrengthTrendChart.test.tsx +15 -37
  256. package/src/components/custom/Workout/StrengthTrendChart.tsx +31 -30
  257. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +27 -64
  258. package/src/components/custom/Workout/SupersetWrapper.test.tsx +16 -14
  259. package/src/components/custom/Workout/SupersetWrapper.tsx +17 -21
  260. package/src/components/custom/Workout/TempoDisplay.stories.tsx +22 -7
  261. package/src/components/custom/Workout/TempoDisplay.test.tsx +18 -4
  262. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +9 -1
  263. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +4 -1
  264. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +7 -4
  265. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +4 -1
  266. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +4 -1
  267. package/src/components/custom/Workout/VelocityStrip.stories.tsx +12 -6
  268. package/src/components/custom/Workout/VelocityStrip.tsx +3 -0
  269. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +6 -7
  270. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +15 -6
  271. package/src/components/custom/Workout/WeekRow.stories.tsx +4 -1
  272. package/src/components/custom/Workout/WeekRow.test.tsx +18 -15
  273. package/src/components/custom/Workout/WeekRow.tsx +23 -16
  274. package/src/components/custom/Workout/WeightBadge.stories.tsx +2 -1
  275. package/src/components/custom/Workout/WeightBadge.test.tsx +50 -19
  276. package/src/components/custom/Workout/WeightBadge.tsx +28 -27
  277. package/src/components/custom/Workout/WorkoutCard.stories.tsx +6 -1
  278. package/src/components/custom/Workout/WorkoutCard.test.tsx +18 -0
  279. package/src/components/custom/Workout/WorkoutCard.tsx +35 -57
  280. package/src/components/custom/Workout/WorkoutPill.stories.tsx +5 -2
  281. package/src/components/custom/Workout/WorkoutPill.test.tsx +39 -7
  282. package/src/components/custom/Workout/WorkoutPill.tsx +88 -57
  283. package/src/components/custom/Workout/ZoneTrack.stories.tsx +7 -6
  284. package/src/components/custom/Workout/ZoneTrack.tsx +10 -9
  285. package/src/components/custom/Workout/index.ts +5 -0
  286. package/src/components/custom/Workout/metricText.stories.tsx +6 -5
  287. package/src/components/custom/Workout/metricText.tsx +4 -0
  288. package/src/components/custom/Workout/setHeadingKit.tsx +207 -26
  289. package/src/components/custom/charts/README.md +61 -0
  290. package/src/components/custom/charts/SetBarChart.test.tsx +30 -1
  291. package/src/components/custom/charts/SetBarChart.tsx +42 -4
  292. package/src/components/custom/charts/SparkBars.stories.tsx +55 -0
  293. package/src/components/custom/charts/SparkBars.test.tsx +69 -0
  294. package/src/components/custom/charts/SparkBars.tsx +85 -0
  295. package/src/components/custom/charts/index.ts +3 -0
  296. package/src/components/custom/index.ts +3 -0
  297. package/src/components/custom/stepper/Stepper.stories.tsx +13 -10
  298. package/src/components/custom/stepper/Stepper.test.tsx +1 -7
  299. package/src/components/custom/stepper/Stepper.tsx +13 -28
  300. package/src/components/icons/icons.stories.tsx +1 -1
  301. package/src/components/icons/icons.tsx +51 -0
  302. package/src/components/shell/AppShell.stories.tsx +150 -0
  303. package/src/components/shell/AppShell.test.tsx +70 -0
  304. package/src/components/shell/AppShell.tsx +90 -0
  305. package/src/components/shell/BrandLockup.stories.tsx +66 -5
  306. package/src/components/shell/BrandLockup.test.tsx +34 -1
  307. package/src/components/shell/BrandLockup.tsx +27 -11
  308. package/src/components/shell/NavItem.stories.tsx +4 -4
  309. package/src/components/shell/NavItem.test.tsx +17 -0
  310. package/src/components/shell/NavItem.tsx +17 -5
  311. package/src/components/shell/README.md +89 -26
  312. package/src/components/shell/SideNav.stories.tsx +38 -7
  313. package/src/components/shell/SideNav.test.tsx +16 -8
  314. package/src/components/shell/SideNav.tsx +17 -17
  315. package/src/components/shell/TopBar.stories.tsx +44 -19
  316. package/src/components/shell/TopBar.test.tsx +34 -12
  317. package/src/components/shell/TopBar.tsx +61 -40
  318. package/src/components/shell/brands.tsx +66 -0
  319. package/src/components/shell/index.ts +9 -8
  320. package/src/components/shell/{DeviceIndicator.stories.tsx → workout/DeviceIndicator.stories.tsx} +2 -2
  321. package/src/components/shell/{DeviceIndicator.tsx → workout/DeviceIndicator.tsx} +2 -2
  322. package/src/components/shell/{DeviceMenu.stories.tsx → workout/DeviceMenu.stories.tsx} +2 -2
  323. package/src/components/shell/{DeviceMenu.tsx → workout/DeviceMenu.tsx} +3 -3
  324. package/src/components/shell/{DeviceRow.stories.tsx → workout/DeviceRow.stories.tsx} +2 -2
  325. package/src/components/shell/{DeviceRow.tsx → workout/DeviceRow.tsx} +3 -3
  326. package/src/components/shell/{SessionStatePill.stories.tsx → workout/SessionStatePill.stories.tsx} +2 -2
  327. package/src/components/shell/{SessionStatePill.tsx → workout/SessionStatePill.tsx} +5 -3
  328. package/src/components/shell/workout/WorkoutShell.stories.tsx +71 -0
  329. package/src/components/shell/{DashboardShell.test.tsx → workout/WorkoutShell.test.tsx} +19 -8
  330. package/src/components/shell/workout/WorkoutShell.tsx +77 -0
  331. package/src/components/shell/workout/WorkoutTopBar.stories.tsx +58 -0
  332. package/src/components/shell/workout/WorkoutTopBar.test.tsx +39 -0
  333. package/src/components/shell/workout/WorkoutTopBar.tsx +52 -0
  334. package/src/components/shell/workout/index.ts +9 -0
  335. package/src/components/shell/workout/workoutNavItems.tsx +16 -0
  336. package/src/components/ui/README.md +110 -0
  337. package/src/components/ui/alert/Alert.stories.tsx +1 -1
  338. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +11 -13
  339. package/src/components/ui/autocomplete/Autocomplete.test.tsx +7 -25
  340. package/src/components/ui/autocomplete/Autocomplete.tsx +21 -28
  341. package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
  342. package/src/components/ui/avatar/Avatar.tsx +2 -10
  343. package/src/components/ui/badge/Badge.stories.tsx +83 -28
  344. package/src/components/ui/badge/Badge.test.tsx +19 -3
  345. package/src/components/ui/badge/Badge.tsx +48 -55
  346. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  347. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +4 -16
  348. package/src/components/ui/button/Button.stories.tsx +46 -12
  349. package/src/components/ui/button/Button.tsx +32 -25
  350. package/src/components/ui/card/Card.stories.tsx +90 -94
  351. package/src/components/ui/card/Card.test.tsx +80 -6
  352. package/src/components/ui/card/Card.tsx +150 -203
  353. package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
  354. package/src/components/ui/checkbox/Checkbox.tsx +6 -26
  355. package/src/components/ui/chip/Chip.stories.tsx +37 -13
  356. package/src/components/ui/chip/Chip.test.tsx +10 -6
  357. package/src/components/ui/chip/Chip.tsx +76 -91
  358. package/src/components/ui/collapse/Collapse.stories.tsx +23 -23
  359. package/src/components/ui/collapse/Collapse.tsx +6 -11
  360. package/src/components/ui/data-row/DataRow.stories.tsx +9 -2
  361. package/src/components/ui/data-row/DataRow.test.tsx +5 -15
  362. package/src/components/ui/data-row/DataRow.tsx +18 -7
  363. package/src/components/ui/divider/Divider.stories.tsx +1 -1
  364. package/src/components/ui/divider/Divider.tsx +1 -5
  365. package/src/components/ui/drawer/Drawer.stories.tsx +5 -16
  366. package/src/components/ui/drawer/Drawer.tsx +13 -30
  367. package/src/components/ui/floating-overlay-depth.test.tsx +143 -0
  368. package/src/components/ui/form-field/FormField.stories.tsx +6 -20
  369. package/src/components/ui/form-field/FormField.test.tsx +1 -5
  370. package/src/components/ui/form-field/FormField.tsx +10 -46
  371. package/src/components/ui/help-tip/HelpTip.stories.tsx +8 -19
  372. package/src/components/ui/help-tip/HelpTip.test.tsx +1 -3
  373. package/src/components/ui/help-tip/HelpTip.tsx +21 -22
  374. package/src/components/ui/help-tip/index.ts +7 -1
  375. package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
  376. package/src/components/ui/icon-box/IconBox.test.tsx +1 -7
  377. package/src/components/ui/icon-box/IconBox.tsx +2 -12
  378. package/src/components/ui/indicator/Indicator.stories.tsx +1 -1
  379. package/src/components/ui/indicator/Indicator.tsx +20 -14
  380. package/src/components/ui/input/Input.stories.tsx +3 -11
  381. package/src/components/ui/input/Input.test.tsx +2 -10
  382. package/src/components/ui/input/Input.tsx +10 -25
  383. package/src/components/ui/input/index.ts +7 -1
  384. package/src/components/ui/link/Link.stories.tsx +19 -7
  385. package/src/components/ui/list-item/ListItem.stories.tsx +11 -2
  386. package/src/components/ui/list-item/ListItem.tsx +2 -9
  387. package/src/components/ui/menu/Menu.stories.tsx +21 -15
  388. package/src/components/ui/menu/Menu.test.tsx +2 -11
  389. package/src/components/ui/menu/Menu.tsx +12 -14
  390. package/src/components/ui/menu/index.ts +8 -1
  391. package/src/components/ui/modal/Modal.stories.tsx +2 -2
  392. package/src/components/ui/modal/Modal.tsx +29 -19
  393. package/src/components/ui/pill/Pill.stories.tsx +58 -23
  394. package/src/components/ui/pill/Pill.test.tsx +101 -3
  395. package/src/components/ui/pill/Pill.tsx +164 -49
  396. package/src/components/ui/pill/index.ts +1 -1
  397. package/src/components/ui/popover/Popover.stories.tsx +5 -7
  398. package/src/components/ui/popover/Popover.test.tsx +1 -6
  399. package/src/components/ui/popover/Popover.tsx +20 -15
  400. package/src/components/ui/popover/index.ts +7 -1
  401. package/src/components/ui/progress/Progress.stories.tsx +3 -12
  402. package/src/components/ui/progress/Progress.test.tsx +1 -5
  403. package/src/components/ui/radio/Radio.stories.tsx +42 -21
  404. package/src/components/ui/radio/Radio.test.tsx +6 -2
  405. package/src/components/ui/radio/Radio.tsx +23 -22
  406. package/src/components/ui/section/Section.stories.tsx +5 -14
  407. package/src/components/ui/section/Section.test.tsx +1 -6
  408. package/src/components/ui/section/Section.tsx +1 -3
  409. package/src/components/ui/section/index.ts +1 -5
  410. package/src/components/ui/select/Select.stories.tsx +3 -13
  411. package/src/components/ui/select/Select.test.tsx +2 -11
  412. package/src/components/ui/select/Select.tsx +14 -14
  413. package/src/components/ui/skeleton/Skeleton.stories.tsx +3 -11
  414. package/src/components/ui/skeleton/Skeleton.tsx +3 -14
  415. package/src/components/ui/skeleton/index.ts +1 -7
  416. package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
  417. package/src/components/ui/spinner/Spinner.tsx +1 -4
  418. package/src/components/ui/stack/Stack.stories.tsx +1 -1
  419. package/src/components/ui/stack/Stack.tsx +1 -1
  420. package/src/components/ui/surface/Surface.stories.tsx +17 -5
  421. package/src/components/ui/surface/Surface.test.tsx +88 -28
  422. package/src/components/ui/surface/Surface.tsx +37 -87
  423. package/src/components/ui/surface/SurfaceContext.ts +22 -62
  424. package/src/components/ui/surface/index.ts +8 -0
  425. package/src/components/ui/surface/resolveSurface.ts +98 -0
  426. package/src/components/ui/surface/surface.contract.test.ts +6 -1
  427. package/src/components/ui/switch/Switch.stories.tsx +1 -1
  428. package/src/components/ui/switch/Switch.tsx +7 -6
  429. package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
  430. package/src/components/ui/tabs/Tabs.tsx +7 -22
  431. package/src/components/ui/tabs/index.ts +1 -7
  432. package/src/components/ui/tile/Tile.stories.tsx +1 -1
  433. package/src/components/ui/tile/Tile.tsx +3 -0
  434. package/src/components/ui/toast/Toast.stories.tsx +1 -1
  435. package/src/components/ui/toast/Toast.test.tsx +2 -8
  436. package/src/components/ui/toast/Toast.tsx +43 -53
  437. package/src/components/ui/toast/index.ts +0 -1
  438. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +92 -71
  439. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +3 -18
  440. package/src/components/ui/toolbar-button/ToolbarButton.tsx +25 -43
  441. package/src/components/ui/tooltip/Tooltip.stories.tsx +12 -3
  442. package/src/components/ui/tooltip/Tooltip.test.tsx +21 -3
  443. package/src/components/ui/tooltip/Tooltip.tsx +55 -36
  444. package/src/components/ui/tooltip/index.ts +1 -1
  445. package/src/components/ui/trigger/TriggerSurface.test.tsx +215 -0
  446. package/src/components/ui/trigger/TriggerSurface.tsx +81 -0
  447. package/src/components/ui/trigger/index.ts +2 -0
  448. package/src/stories/PresetShowcase.stories.tsx +100 -47
  449. package/src/test/no-device-internals.test.ts +3 -3
  450. package/src/theme/ChoosingTokens.stories.tsx +48 -0
  451. package/src/theme/ColorPalettes.stories.tsx +62 -45
  452. package/src/theme/ColorPrimitives.stories.tsx +35 -35
  453. package/src/theme/Depth.stories.tsx +132 -65
  454. package/src/theme/DepthCalibration.stories.tsx +23 -21
  455. package/src/theme/ThemeProvider.test.tsx +4 -4
  456. package/src/theme/barrel.ts +7 -6
  457. package/src/theme/color-stories.coverage.test.ts +1 -1
  458. package/src/theme/color-utils.ts +41 -29
  459. package/src/theme/config.completeness.test.ts +1 -1
  460. package/src/theme/config.ts +28 -0
  461. package/src/theme/elevation.test.ts +82 -56
  462. package/src/theme/elevation.ts +77 -561
  463. package/src/theme/global.css +30 -2
  464. package/src/theme/grey-ramp.test.ts +24 -8
  465. package/src/theme/lift.test.ts +39 -0
  466. package/src/theme/lift.ts +102 -0
  467. package/src/theme/materials.test.ts +5 -2
  468. package/src/theme/materials.ts +38 -4
  469. package/src/theme/native-theming-guard.test.ts +5 -5
  470. package/src/theme/presets/apply.ts +4 -0
  471. package/src/theme/presets/audiobook.ts +5 -2
  472. package/src/theme/presets/default.ts +4 -0
  473. package/src/theme/presets/index.ts +14 -1
  474. package/src/theme/presets/types.ts +1 -1
  475. package/src/theme/semantic-contrast.test.ts +13 -4
  476. package/src/theme/status-distinctness.test.ts +66 -0
  477. package/src/theme/surface-planes.test.ts +37 -0
  478. package/src/theme/surface-planes.ts +68 -0
  479. package/src/theme/token-export-guard.test.ts +1 -3
  480. package/src/theme/tokens/primitives.test.ts +15 -7
  481. package/src/theme/tokens/primitives.ts +65 -33
  482. package/src/theme/tokens/semantic.ts +71 -37
  483. package/src/theme/wash-ladder.test.ts +58 -0
  484. package/src/theme/workout-tokens.ts +1 -7
  485. package/src/types/jest-axe.d.ts +2 -6
  486. package/src/types/raw-md.d.ts +5 -0
  487. package/src/utils/colors.ts +4 -4
  488. package/src/utils/form.ts +1 -3
  489. package/src/utils/index.ts +2 -7
  490. package/src/utils/number-format.ts +17 -0
  491. package/src/utils/workout-format.ts +15 -6
  492. package/src/web-jsx/jsx-runtime.ts +1 -5
  493. package/tailwind.config.js +5 -0
  494. package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +0 -151
  495. package/src/components/custom/Workout/RowInventory.stories.tsx +0 -408
  496. package/src/components/custom/Workout/S3FullRail.stories.tsx +0 -46
  497. package/src/components/custom/Workout/S3SessionPace.stories.tsx +0 -95
  498. package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +0 -61
  499. package/src/components/custom/Workout/S3SetTypes.stories.tsx +0 -176
  500. package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +0 -240
  501. package/src/components/shell/DashboardShell.stories.tsx +0 -65
  502. package/src/components/shell/DashboardShell.tsx +0 -75
  503. package/src/components/shell/SessionRailSpecimen.stories.tsx +0 -753
  504. package/src/theme/elevation.stories.tsx +0 -346
  505. package/src/utils/useTheme.ts +0 -72
  506. /package/src/components/shell/{DeviceIndicator.test.tsx → workout/DeviceIndicator.test.tsx} +0 -0
  507. /package/src/components/shell/{DeviceMenu.test.tsx → workout/DeviceMenu.test.tsx} +0 -0
  508. /package/src/components/shell/{DeviceRow.test.tsx → workout/DeviceRow.test.tsx} +0 -0
  509. /package/src/components/shell/{SessionStatePill.test.tsx → workout/SessionStatePill.test.tsx} +0 -0
@@ -0,0 +1,176 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import { View } from 'react-native'
3
+ import { Pill } from '../../ui/pill'
4
+ import { Tooltip } from '../../ui/tooltip'
5
+ import { formatDateTime } from '../DateTime'
6
+ import { TableCell, TableRow } from '../Table'
7
+ import { Typography } from '../Typography'
8
+ import { SEVERITY_META, SeverityLabel, type TaskSeverity } from './SeverityLabel'
9
+ import type { TaskColumnKey } from './TaskTable'
10
+
11
+ /** One open task, as the task list renders it. */
12
+ export interface TaskListItem {
13
+ /** Initiative slug the task belongs to. */
14
+ slug: string
15
+ /** Per-initiative task id, e.g. `AW-22`. */
16
+ id: string
17
+ title: string
18
+ severity?: TaskSeverity
19
+ /** Lower sorts first; unique within an initiative, not globally. */
20
+ priority: number
21
+ /** Rough size. Absent means unestimated, which sorts last. */
22
+ estimate?: number
23
+ tags?: string[]
24
+ /** ISO date the task last changed, used for the age column. */
25
+ updated: string
26
+ }
27
+
28
+ /**
29
+ * Fixed column widths, shared by {@link TaskRow} and {@link TaskTable}'s header
30
+ * so the two cannot drift. `title` is the flexible column: it renders with no
31
+ * width and only declares the floor it may be squeezed to.
32
+ *
33
+ * Every fixed width holds its own header — the uppercase label, the 4px gap and
34
+ * the sort glyph, inside the dense cell's 16px of padding. A column narrower
35
+ * than its own header is what let `SEV` and `PRI` paint over each other.
36
+ */
37
+ export const TASK_COLUMN_WIDTHS = {
38
+ slug: 132,
39
+ id: 66,
40
+ /** The narrowest the flexible title column may be squeezed before a column drops instead. */
41
+ titleMin: 160,
42
+ severity: 112,
43
+ /** The severity column once it has collapsed to its dot. */
44
+ severityCompact: 56,
45
+ priority: 56,
46
+ estimate: 56,
47
+ tags: 150,
48
+ age: 74,
49
+ } as const
50
+
51
+ /** How many tags fit before the row starts eliding them. */
52
+ const MAX_VISIBLE_TAGS = 2
53
+
54
+ /** The tags a row elided, shown when the `+N` count is hovered. */
55
+ function HiddenTags({ tags }: { tags: string[] }) {
56
+ return (
57
+ <View className="flex-row flex-wrap gap-1" testID="hidden-tags">
58
+ {tags.map((tag) => (
59
+ <Pill key={tag} variant="subtle" color="default" size="xs">
60
+ {tag}
61
+ </Pill>
62
+ ))}
63
+ </View>
64
+ )
65
+ }
66
+
67
+ export interface TaskRowProps {
68
+ task: TaskListItem
69
+ /** Pre-formatted age label (e.g. "3d ago"). Passed in so rows stay pure and deterministic. */
70
+ ageLabel: string
71
+ /** Collapse severity to its dot (word on hover); the table decides this from its width. */
72
+ severityDotOnly?: boolean
73
+ /** Columns the table left out; the row skips their cells so header and body stay aligned. */
74
+ hideColumns?: TaskColumnKey[]
75
+ }
76
+
77
+ /**
78
+ * TaskRow — one task as a dense grid row: initiative, id, title, severity,
79
+ * priority, estimate, tags and age.
80
+ *
81
+ * Composes {@link TableRow} / {@link TableCell} for row semantics and density,
82
+ * {@link SeverityLabel} for the severity dot, and {@link Pill} for tags. Used by
83
+ * {@link TaskTable}.
84
+ */
85
+ export function TaskRow({ task, ageLabel, severityDotOnly = false, hideColumns }: TaskRowProps) {
86
+ const show = (column: TaskColumnKey) => !hideColumns?.includes(column)
87
+ const tags = task.tags ?? []
88
+ const hiddenTags = tags.slice(MAX_VISIBLE_TAGS)
89
+ const severityWidth = severityDotOnly
90
+ ? TASK_COLUMN_WIDTHS.severityCompact
91
+ : TASK_COLUMN_WIDTHS.severity
92
+
93
+ return (
94
+ <TableRow testID="task-row">
95
+ {show('slug') ? (
96
+ <TableCell width={TASK_COLUMN_WIDTHS.slug}>
97
+ <Typography variant="mono" numberOfLines={1} className="text-xs text-text-tertiary">
98
+ {task.slug}
99
+ </Typography>
100
+ </TableCell>
101
+ ) : null}
102
+
103
+ {show('id') ? (
104
+ <TableCell width={TASK_COLUMN_WIDTHS.id}>
105
+ <Typography variant="mono" className="text-xs text-brand-primary">
106
+ {task.id}
107
+ </Typography>
108
+ </TableCell>
109
+ ) : null}
110
+
111
+ <TableCell>
112
+ <Typography variant="body2" numberOfLines={1} className="text-xs text-text-primary">
113
+ {task.title}
114
+ </Typography>
115
+ </TableCell>
116
+
117
+ {show('severity') ? (
118
+ <TableCell width={severityWidth}>
119
+ {severityDotOnly && task.severity ? (
120
+ <Tooltip usePortal label={SEVERITY_META[task.severity].label}>
121
+ <SeverityLabel severity={task.severity} dotOnly />
122
+ </Tooltip>
123
+ ) : (
124
+ <SeverityLabel severity={task.severity} />
125
+ )}
126
+ </TableCell>
127
+ ) : null}
128
+
129
+ {show('priority') ? (
130
+ <TableCell width={TASK_COLUMN_WIDTHS.priority} align="right">
131
+ <Typography variant="mono" className="text-xs text-text-secondary">
132
+ {String(task.priority)}
133
+ </Typography>
134
+ </TableCell>
135
+ ) : null}
136
+
137
+ {show('estimate') ? (
138
+ <TableCell width={TASK_COLUMN_WIDTHS.estimate} align="right">
139
+ <Typography variant="mono" className="text-xs text-text-secondary">
140
+ {task.estimate === undefined ? '—' : String(task.estimate)}
141
+ </Typography>
142
+ </TableCell>
143
+ ) : null}
144
+
145
+ {show('tags') ? (
146
+ <TableCell width={TASK_COLUMN_WIDTHS.tags}>
147
+ <View className="flex-row items-center gap-1">
148
+ {tags.slice(0, MAX_VISIBLE_TAGS).map((tag) => (
149
+ <Pill key={tag} variant="subtle" color="default" size="xs">
150
+ {tag}
151
+ </Pill>
152
+ ))}
153
+ {hiddenTags.length > 0 ? (
154
+ <Tooltip usePortal content={<HiddenTags tags={hiddenTags} />}>
155
+ {/* leading-none: the caption's loose line box would otherwise float the pills above centre. */}
156
+ <Typography variant="caption" className="leading-none text-text-tertiary">
157
+ {`+${hiddenTags.length}`}
158
+ </Typography>
159
+ </Tooltip>
160
+ ) : null}
161
+ </View>
162
+ </TableCell>
163
+ ) : null}
164
+
165
+ {show('updated') ? (
166
+ <TableCell width={TASK_COLUMN_WIDTHS.age} align="right">
167
+ <Tooltip usePortal label={formatDateTime(task.updated, 'medium', true)}>
168
+ <Typography variant="caption" className="text-text-tertiary">
169
+ {ageLabel}
170
+ </Typography>
171
+ </Tooltip>
172
+ </TableCell>
173
+ ) : null}
174
+ </TableRow>
175
+ )
176
+ }
@@ -0,0 +1,123 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { Surface } from '../../ui/surface'
4
+ import { TaskTable } from './TaskTable'
5
+ import { TASK_LIST_FIXTURE, TASK_LIST_NOW } from './task-list-fixture'
6
+
7
+ /**
8
+ * **T2 · Task table** — every open task across initiatives in one dense,
9
+ * sortable grid.
10
+ *
11
+ * Composes `Table` at `density="dense"`, driven by `useTable` with per-column
12
+ * comparators. Rows are `TaskRow`; the legend tallies `SeverityLabel`.
13
+ */
14
+ const meta: Meta<typeof TaskTable> = {
15
+ title: 'Custom/ActiveWork/TaskTable',
16
+ component: TaskTable,
17
+ tags: ['autodocs', 'status:candidate', '!status:review'],
18
+ args: {
19
+ tasks: TASK_LIST_FIXTURE,
20
+ now: TASK_LIST_NOW,
21
+ },
22
+ argTypes: {
23
+ defaultSortKey: {
24
+ control: 'select',
25
+ options: ['priority', 'severity', 'updated', 'slug', 'id', 'title', 'estimate'],
26
+ },
27
+ hideLegend: { control: 'boolean' },
28
+ severityDisplay: { control: 'inline-radio', options: ['auto', 'full', 'dot'] },
29
+ fitWidth: { control: { type: 'number', min: 120, step: 20 } },
30
+ now: { table: { disable: true } },
31
+ tasks: { table: { disable: true } },
32
+ },
33
+ decorators: [
34
+ (Story) => (
35
+ <Surface level="base" className="min-h-screen p-6" testID="page-surface">
36
+ <View className="w-full max-w-[1100px]">
37
+ <Story />
38
+ </View>
39
+ </Surface>
40
+ ),
41
+ ],
42
+ parameters: {
43
+ layout: 'fullscreen',
44
+ docs: {
45
+ description: {
46
+ component:
47
+ 'Composes **Table** (density="dense") · **useTable** (comparators) · **TaskRow** · **SeverityLabel** · **Eyebrow**.',
48
+ },
49
+ },
50
+ },
51
+ }
52
+ export default meta
53
+
54
+ type Story = StoryObj<typeof TaskTable>
55
+
56
+ /** Default view: sorted by priority, lowest number first. */
57
+ export const Default: Story = {}
58
+
59
+ /**
60
+ * Under 840px the severity column collapses to its dot (word on hover) so the
61
+ * title keeps a scannable width. Driven by the table's own measured width, so
62
+ * resize the canvas to watch it switch. Narrower still and whole columns start
63
+ * to drop — see **Embedded Narrow**.
64
+ */
65
+ export const Narrow: Story = {
66
+ decorators: [
67
+ (Story) => (
68
+ <View className="w-full max-w-[760px]">
69
+ <Story />
70
+ </View>
71
+ ),
72
+ ],
73
+ }
74
+
75
+ /**
76
+ * The table inside a card half a page wide, which is where the reader puts it.
77
+ * Columns drop in the declared order — severity, priority, estimate, tags, age,
78
+ * initiative — and `title` and `id` never drop, because a task with no name is
79
+ * not a narrower row, it is a useless one. Below the last droppable column the
80
+ * table scrolls horizontally rather than squeezing the survivors into overlap.
81
+ */
82
+ export const EmbeddedNarrow: Story = {
83
+ args: { hideLegend: true, hideColumns: ['slug'] },
84
+ decorators: [
85
+ (Story) => (
86
+ <View className="w-full max-w-[400px]">
87
+ <Story />
88
+ </View>
89
+ ),
90
+ ],
91
+ }
92
+
93
+ /**
94
+ * Sorted by severity. The comparator ranks critical→low by meaning, then breaks
95
+ * ties on priority — an alphabetical sort would bury `low` between `high` and
96
+ * `medium`.
97
+ */
98
+ export const SortedBySeverity: Story = {
99
+ args: { defaultSortKey: 'severity' },
100
+ }
101
+
102
+ /** Sorted by age, newest first — the direction that makes an age column useful. */
103
+ export const SortedByAge: Story = {
104
+ args: { defaultSortKey: 'updated' },
105
+ }
106
+
107
+ /**
108
+ * Sorted by estimate. `TP-16` and `C-6` are estimated; `AW-86` is not, and
109
+ * unestimated rows sort last in both directions rather than reading as zero.
110
+ */
111
+ export const SortedByEstimate: Story = {
112
+ args: { defaultSortKey: 'estimate' },
113
+ }
114
+
115
+ /** Without the severity legend, for embedding under an existing header. */
116
+ export const NoLegend: Story = {
117
+ args: { hideLegend: true },
118
+ }
119
+
120
+ /** Empty backlog — the grid keeps its header so the columns stay readable. */
121
+ export const Empty: Story = {
122
+ args: { tasks: [] },
123
+ }
@@ -0,0 +1,233 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen, fireEvent, within } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { TaskTable, formatTaskAge, type TaskColumnKey } from './TaskTable'
5
+ import { TASK_LIST_FIXTURE, TASK_LIST_NOW } from './task-list-fixture'
6
+
7
+ /** Task ids in the order they currently appear, read off the rendered rows. */
8
+ function renderedIds(): string[] {
9
+ return screen
10
+ .getAllByTestId('task-row')
11
+ .map((row) => within(row).getByText(/^[A-Z]+-\d+$/).textContent ?? '')
12
+ }
13
+
14
+ describe('TaskTable', () => {
15
+ it('renders a row per task with its id and title', () => {
16
+ render(<TaskTable tasks={TASK_LIST_FIXTURE} now={TASK_LIST_NOW} />)
17
+ expect(screen.getAllByTestId('task-row')).toHaveLength(TASK_LIST_FIXTURE.length)
18
+ expect(screen.getByText('AW-22')).toBeInTheDocument()
19
+ expect(screen.getByText('npm publish the packaged distribution')).toBeInTheDocument()
20
+ })
21
+
22
+ it('collapses severity to its dot when told to, keeping the full name for hover and a11y', () => {
23
+ render(
24
+ <TaskTable tasks={TASK_LIST_FIXTURE} now={TASK_LIST_NOW} severityDisplay="dot" hideLegend />
25
+ )
26
+ expect(screen.getByText('Sev')).toBeInTheDocument()
27
+ expect(screen.getByRole('button', { name: 'Sort by Severity' })).toBeInTheDocument()
28
+ expect(screen.queryByText('Critical')).not.toBeInTheDocument()
29
+ expect(screen.getByTestId('severity-dot-critical')).toBeInTheDocument()
30
+ })
31
+
32
+ it('keeps the severity word until it has measured a narrow width', () => {
33
+ render(<TaskTable tasks={TASK_LIST_FIXTURE} now={TASK_LIST_NOW} hideLegend />)
34
+ expect(screen.getByText('Critical')).toBeInTheDocument()
35
+ })
36
+
37
+ it('leaves out hidden columns in both the header and the rows', () => {
38
+ render(
39
+ <TaskTable
40
+ tasks={TASK_LIST_FIXTURE}
41
+ now={TASK_LIST_NOW}
42
+ hideLegend
43
+ hideColumns={['slug', 'tags']}
44
+ />
45
+ )
46
+ const headers = screen.getAllByRole('columnheader').map((h) => h.textContent)
47
+ expect(headers.some((h) => h?.startsWith('Initiative'))).toBe(false)
48
+ expect(headers.some((h) => h?.startsWith('Tags'))).toBe(false)
49
+ expect(headers.some((h) => h?.startsWith('Title'))).toBe(true)
50
+ const firstRow = screen.getAllByTestId('task-row')[0]!
51
+ expect(within(firstRow).getAllByRole('cell')).toHaveLength(headers.length)
52
+ })
53
+
54
+ it('sorts by priority ascending by default', () => {
55
+ render(<TaskTable tasks={TASK_LIST_FIXTURE} now={TASK_LIST_NOW} />)
56
+ // RL-14 is priority 3, the lowest number in the fixture.
57
+ expect(renderedIds()[0]).toBe('RL-14')
58
+ })
59
+
60
+ it('ranks severity by meaning rather than alphabetically', () => {
61
+ render(<TaskTable tasks={TASK_LIST_FIXTURE} now={TASK_LIST_NOW} defaultSortKey="severity" />)
62
+ const ids = renderedIds()
63
+ // critical first; `low` must land after `medium`, which an alphabetical
64
+ // string sort would get wrong.
65
+ expect(ids[0]).toBe('RL-14')
66
+ expect(ids.indexOf('AW-22')).toBeGreaterThan(ids.indexOf('HA-3'))
67
+ })
68
+
69
+ it('sorts unestimated tasks last, not as zero', () => {
70
+ render(<TaskTable tasks={TASK_LIST_FIXTURE} now={TASK_LIST_NOW} defaultSortKey="estimate" />)
71
+ // AW-86 is the only fixture task without an estimate, so it must land last.
72
+ expect(renderedIds().at(-1)).toBe('AW-86')
73
+ })
74
+
75
+ it('keeps blanks last after flipping direction', () => {
76
+ render(<TaskTable tasks={TASK_LIST_FIXTURE} now={TASK_LIST_NOW} defaultSortKey="estimate" />)
77
+ fireEvent.click(screen.getByRole('button', { name: 'Sort by Estimate' }))
78
+ expect(renderedIds().at(-1)).toBe('AW-86')
79
+ })
80
+
81
+ it('reverses order when a sorted column header is pressed', () => {
82
+ render(<TaskTable tasks={TASK_LIST_FIXTURE} now={TASK_LIST_NOW} />)
83
+ const first = renderedIds()[0]
84
+ fireEvent.click(screen.getByRole('button', { name: 'Sort by Priority' }))
85
+ expect(renderedIds()[0]).not.toBe(first)
86
+ })
87
+
88
+ it('sorts age newest-first when ascending', () => {
89
+ render(<TaskTable tasks={TASK_LIST_FIXTURE} now={TASK_LIST_NOW} defaultSortKey="updated" />)
90
+ // RL-14 was updated 2026-08-30, the most recent in the fixture.
91
+ expect(renderedIds()[0]).toBe('RL-14')
92
+ })
93
+
94
+ it('exposes sort state to assistive tech on the column header', () => {
95
+ render(<TaskTable tasks={TASK_LIST_FIXTURE} now={TASK_LIST_NOW} />)
96
+ const headers = screen.getAllByRole('columnheader')
97
+ const sorted = headers.filter((h) => h.getAttribute('aria-sort') === 'ascending')
98
+ expect(sorted).toHaveLength(1)
99
+ })
100
+
101
+ it('tallies open tasks by severity in the legend', () => {
102
+ render(<TaskTable tasks={TASK_LIST_FIXTURE} now={TASK_LIST_NOW} />)
103
+ // The legend renders one dot per severity on top of the per-row dots.
104
+ expect(screen.getAllByTestId('severity-dot-critical').length).toBeGreaterThan(1)
105
+ })
106
+
107
+ it('hides the legend when asked', () => {
108
+ render(<TaskTable tasks={TASK_LIST_FIXTURE} now={TASK_LIST_NOW} hideLegend />)
109
+ // Only the single critical row keeps a critical dot once the legend is gone.
110
+ expect(screen.getAllByTestId('severity-dot-critical')).toHaveLength(1)
111
+ })
112
+
113
+ it('renders the header with no rows for an empty backlog', () => {
114
+ render(<TaskTable tasks={[]} now={TASK_LIST_NOW} />)
115
+ expect(screen.queryAllByTestId('task-row')).toHaveLength(0)
116
+ expect(screen.getByRole('button', { name: 'Sort by Title' })).toBeInTheDocument()
117
+ })
118
+
119
+ it('has no a11y violations', async () => {
120
+ const { container } = render(<TaskTable tasks={TASK_LIST_FIXTURE} now={TASK_LIST_NOW} />)
121
+ expect(await axe(container)).toHaveNoViolations()
122
+ })
123
+ })
124
+
125
+ /** Header labels in render order, with the sort glyph stripped off. */
126
+ function headerLabels(): string[] {
127
+ return screen
128
+ .getAllByRole('columnheader')
129
+ .map((header) => (header.textContent ?? '').replace(/[↑↓↕]/g, '').trim())
130
+ }
131
+
132
+ describe('TaskTable column drop order', () => {
133
+ const at = (fitWidth: number, hideColumns?: TaskColumnKey[]) =>
134
+ render(
135
+ <TaskTable
136
+ tasks={TASK_LIST_FIXTURE}
137
+ now={TASK_LIST_NOW}
138
+ hideLegend
139
+ fitWidth={fitWidth}
140
+ hideColumns={hideColumns}
141
+ />
142
+ )
143
+
144
+ // Each width is one step of the declared order: severity, priority, estimate, tags, age, initiative.
145
+ it.each([
146
+ [1100, ['Initiative', 'ID', 'Title', 'Severity', 'Pri', 'Est', 'Tags', 'Age']],
147
+ [700, ['Initiative', 'ID', 'Title', 'Pri', 'Est', 'Tags', 'Age']],
148
+ [640, ['Initiative', 'ID', 'Title', 'Est', 'Tags', 'Age']],
149
+ [590, ['Initiative', 'ID', 'Title', 'Tags', 'Age']],
150
+ [440, ['Initiative', 'ID', 'Title', 'Age']],
151
+ [380, ['Initiative', 'ID', 'Title']],
152
+ [200, ['ID', 'Title']],
153
+ ])('renders %ipx as %j', (fitWidth, expected) => {
154
+ at(fitWidth as number)
155
+ expect(headerLabels()).toEqual(expected)
156
+ })
157
+
158
+ it('keeps the title and the id at any width, however narrow', () => {
159
+ at(80)
160
+ expect(headerLabels()).toEqual(['ID', 'Title'])
161
+ // Below the floor the table scrolls rather than dropping what names the task.
162
+ expect(screen.getAllByText(TASK_LIST_FIXTURE[0]!.title)[0]).toBeInTheDocument()
163
+ })
164
+
165
+ it('drops one column at a time rather than collapsing to the floor', () => {
166
+ at(700)
167
+ expect(headerLabels()).toHaveLength(7)
168
+ })
169
+
170
+ it('gives every header label a column of its own', () => {
171
+ for (const width of [1100, 700, 440, 200]) {
172
+ const { unmount } = at(width)
173
+ const labels = headerLabels()
174
+ expect(new Set(labels).size).toBe(labels.length)
175
+ expect(labels.every((label) => label.split(/\s+/).length === 1)).toBe(true)
176
+ unmount()
177
+ }
178
+ })
179
+
180
+ it('keeps the rows in step with the header it dropped columns from', () => {
181
+ at(440)
182
+ const firstRow = screen.getAllByTestId('task-row')[0]!
183
+ expect(within(firstRow).getAllByRole('cell')).toHaveLength(headerLabels().length)
184
+ })
185
+
186
+ it("shows id, title and age in the reader's embedded card", () => {
187
+ // 369px is what the reader's right-hand card measured at an 865px viewport.
188
+ at(369, ['slug'])
189
+ expect(headerLabels()).toEqual(['ID', 'Title', 'Age'])
190
+ })
191
+
192
+ it('has no a11y violations once columns have dropped', async () => {
193
+ const { container } = at(440)
194
+ expect(await axe(container)).toHaveNoViolations()
195
+ })
196
+
197
+ it('has no a11y violations at the horizontal-scroll floor', async () => {
198
+ const { container } = at(200)
199
+ expect(await axe(container)).toHaveNoViolations()
200
+ })
201
+ })
202
+
203
+ describe('formatTaskAge', () => {
204
+ const now = new Date('2026-08-30T12:00:00Z').getTime()
205
+
206
+ it('reads today for the same day', () => {
207
+ expect(formatTaskAge('2026-08-30T06:00:00Z', now)).toBe('today')
208
+ })
209
+
210
+ it('singularises one day', () => {
211
+ expect(formatTaskAge('2026-08-29T06:00:00Z', now)).toBe('1d ago')
212
+ })
213
+
214
+ it('counts days below a month', () => {
215
+ expect(formatTaskAge('2026-08-18T12:00:00Z', now)).toBe('12d ago')
216
+ })
217
+
218
+ it('switches to months at thirty days', () => {
219
+ expect(formatTaskAge('2026-07-01T12:00:00Z', now)).toBe('2mo ago')
220
+ })
221
+
222
+ it('renders an em-dash for a missing or unparseable date', () => {
223
+ expect(formatTaskAge(null, now)).toBe('—')
224
+ expect(formatTaskAge(undefined, now)).toBe('—')
225
+ expect(formatTaskAge('not-a-date', now)).toBe('—')
226
+ })
227
+
228
+ it('does not read the clock — the same inputs always give the same label', () => {
229
+ expect(formatTaskAge('2026-08-18T12:00:00Z', now)).toBe(
230
+ formatTaskAge('2026-08-18T12:00:00Z', now)
231
+ )
232
+ })
233
+ })