@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
@@ -1,9 +1,36 @@
1
1
  import React, { createContext, useContext, useState, useMemo } from 'react'
2
- import { View, Text, Pressable, ScrollView, type ViewProps, type PressableProps } from 'react-native'
2
+ import {
3
+ View,
4
+ Text,
5
+ Pressable,
6
+ ScrollView,
7
+ type ViewProps,
8
+ type PressableProps,
9
+ } from 'react-native'
3
10
  import { cn } from '../../../utils/cn'
11
+ import { Tooltip } from '../../ui/tooltip'
4
12
 
5
13
  export type SortDirection = 'asc' | 'desc' | null
6
14
 
15
+ /**
16
+ * Row height / cell padding axis. `comfortable` is the default reading density;
17
+ * `dense` is the scannable-grid density for tables whose job is to fit many rows
18
+ * on screen at once (a backlog, a log, an audit list).
19
+ */
20
+ export type TableDensity = 'comfortable' | 'dense'
21
+
22
+ /** Cell padding per density, shared by header cells and data cells so a row's two halves cannot drift. */
23
+ const CELL_PADDING: Record<TableDensity, string> = {
24
+ comfortable: 'px-4 py-3',
25
+ dense: 'px-2 py-1',
26
+ }
27
+
28
+ // minWidth 0 lets a flexible cell shrink below its nowrap text, so long content truncates instead of pushing the row past the table.
29
+ const FLEX_CELL = { flex: 1, minWidth: 0 } as const
30
+
31
+ // A horizontal ScrollView sizes its content to max-content; a definite width makes the table fill the viewport and flexible cells shrink.
32
+ const SCROLL_CONTENT = { width: '100%' } as const
33
+
7
34
  interface TableContextType {
8
35
  sortColumn?: string
9
36
  sortDirection: SortDirection
@@ -13,6 +40,7 @@ interface TableContextType {
13
40
  onSelectAll?: (selected: boolean) => void
14
41
  selectable: boolean
15
42
  allRowIds: string[]
43
+ density: TableDensity
16
44
  }
17
45
 
18
46
  const TableContext = createContext<TableContextType>({
@@ -20,6 +48,7 @@ const TableContext = createContext<TableContextType>({
20
48
  selectedRows: new Set(),
21
49
  selectable: false,
22
50
  allRowIds: [],
51
+ density: 'comfortable',
23
52
  })
24
53
 
25
54
  export interface TableProps extends ViewProps {
@@ -43,6 +72,14 @@ export interface TableProps extends ViewProps {
43
72
  isLoading?: boolean
44
73
  /** Number of skeleton rows to show when loading */
45
74
  loadingRowCount?: number
75
+ /** Row height / cell padding. Defaults to `comfortable`. */
76
+ density?: TableDensity
77
+ /**
78
+ * Width the columns need, below which the table scrolls horizontally instead
79
+ * of squeezing them into overlap. Pair with {@link useColumnFit}, which drops
80
+ * columns first and reports what the survivors need.
81
+ */
82
+ contentMinWidth?: number
46
83
  /** Additional className */
47
84
  className?: string
48
85
  children?: React.ReactNode
@@ -82,10 +119,19 @@ export function Table({
82
119
  rowIds = [],
83
120
  isLoading = false,
84
121
  loadingRowCount = 5,
122
+ density = 'comfortable',
123
+ contentMinWidth,
85
124
  className,
86
125
  children,
87
126
  ...props
88
127
  }: TableProps) {
128
+ // width 100% + a minWidth floor resolves to max(container, floor): the table fills its
129
+ // container, and once the columns need more than that it overflows and scrolls.
130
+ const contentStyle =
131
+ contentMinWidth === undefined
132
+ ? SCROLL_CONTENT
133
+ : { ...SCROLL_CONTENT, minWidth: contentMinWidth }
134
+
89
135
  return (
90
136
  <TableContext.Provider
91
137
  value={{
@@ -97,16 +143,17 @@ export function Table({
97
143
  onSelectAll,
98
144
  selectable,
99
145
  allRowIds: rowIds,
146
+ density,
100
147
  }}
101
148
  >
102
149
  <View className={cn('w-full', className)} {...props}>
103
- <ScrollView horizontal showsHorizontalScrollIndicator={false}>
150
+ <ScrollView
151
+ horizontal
152
+ showsHorizontalScrollIndicator={contentMinWidth !== undefined}
153
+ contentContainerStyle={contentStyle}
154
+ >
104
155
  <View role="table" className="w-full min-w-full">
105
- {isLoading ? (
106
- <TableLoadingSkeleton rowCount={loadingRowCount} />
107
- ) : (
108
- children
109
- )}
156
+ {isLoading ? <TableLoadingSkeleton rowCount={loadingRowCount} /> : children}
110
157
  </View>
111
158
  </ScrollView>
112
159
  </View>
@@ -124,10 +171,7 @@ export interface TableHeaderProps {
124
171
  */
125
172
  export function TableHeader({ children, className }: TableHeaderProps) {
126
173
  return (
127
- <View
128
- role="rowgroup"
129
- className={cn('bg-background-subtle rounded-t-lg', className)}
130
- >
174
+ <View role="rowgroup" className={cn('bg-background-subtle rounded-t-lg', className)}>
131
175
  {children}
132
176
  </View>
133
177
  )
@@ -188,6 +232,8 @@ export function TableRow({
188
232
  export interface TableHeaderCellProps extends Omit<PressableProps, 'children'> {
189
233
  /** Sort key for this column */
190
234
  sortKey?: string
235
+ /** Full column name behind an abbreviated label: shown on hover and used as the accessible sort name. */
236
+ tooltip?: string
191
237
  /** Text alignment */
192
238
  align?: 'left' | 'center' | 'right'
193
239
  /** Cell width in pixels */
@@ -202,6 +248,7 @@ export interface TableHeaderCellProps extends Omit<PressableProps, 'children'> {
202
248
  */
203
249
  export function TableHeaderCell({
204
250
  sortKey,
251
+ tooltip,
205
252
  align = 'left',
206
253
  width,
207
254
  className,
@@ -209,9 +256,11 @@ export function TableHeaderCell({
209
256
  onPress,
210
257
  ...props
211
258
  }: TableHeaderCellProps) {
212
- const { sortColumn, sortDirection, onSort } = useContext(TableContext)
213
- const isSorted = sortKey && sortColumn === sortKey
259
+ const { sortColumn, sortDirection, onSort, density } = useContext(TableContext)
260
+ // The sort cycle ends at direction null with the column still set; that is unsorted, not "still descending".
261
+ const isSorted = !!sortKey && sortColumn === sortKey && sortDirection != null
214
262
  const isSortable = !!sortKey && !!onSort
263
+ const [hovered, setHovered] = useState(false)
215
264
  const ariaSort = isSorted
216
265
  ? sortDirection === 'asc'
217
266
  ? 'ascending'
@@ -233,18 +282,36 @@ export function TableHeaderCell({
233
282
  right: 'justify-end',
234
283
  }
235
284
 
285
+ const label = (
286
+ <Text
287
+ className={cn(
288
+ 'text-xs font-semibold uppercase tracking-wider text-text-secondary',
289
+ isSorted && 'text-text-primary'
290
+ )}
291
+ >
292
+ {children}
293
+ </Text>
294
+ )
295
+
296
+ // The tooltip wraps the sort button rather than sitting inside it: a nested Pressable would take the press.
297
+ const withTooltip = (node: React.ReactNode) =>
298
+ tooltip ? (
299
+ <Tooltip label={tooltip} usePortal>
300
+ {node}
301
+ </Tooltip>
302
+ ) : (
303
+ node
304
+ )
305
+
236
306
  const content = (
237
307
  <>
238
- <Text
239
- className={cn(
240
- 'text-xs font-semibold uppercase tracking-wider text-text-secondary',
241
- isSorted && 'text-text-primary'
242
- )}
243
- >
244
- {children}
245
- </Text>
308
+ {label}
246
309
  {isSortable && (
247
- <Text className={cn('ml-1 text-xs', isSorted ? 'text-text-primary' : 'text-text-tertiary')}>
310
+ // Idle glyphs stay in layout but invisible, so the header reads quiet and nothing shifts on hover.
311
+ <Text
312
+ className={cn('ml-1 text-xs', isSorted ? 'text-text-primary' : 'text-text-tertiary')}
313
+ style={{ opacity: isSorted || hovered ? 1 : 0 }}
314
+ >
248
315
  {isSorted ? (sortDirection === 'asc' ? '↑' : '↓') : '↕'}
249
316
  </Text>
250
317
  )}
@@ -258,22 +325,29 @@ export function TableHeaderCell({
258
325
  <View
259
326
  role="columnheader"
260
327
  aria-sort={ariaSort}
261
- style={width ? { width } : { flex: 1 }}
328
+ // A label and its sort glyph belong to one column: clipped is recoverable, painted over the neighbour is not.
329
+ className="overflow-hidden"
330
+ style={width ? { width } : FLEX_CELL}
262
331
  >
263
- <Pressable
264
- accessibilityRole="button"
265
- accessibilityLabel={`Sort by ${children}`}
266
- onPress={handlePress}
267
- className={cn(
268
- 'flex-row items-center px-4 py-3',
269
- alignStyles[align],
270
- 'web:hover:bg-interactive-hover active:bg-interactive-active',
271
- className
272
- )}
273
- {...props}
274
- >
275
- {content}
276
- </Pressable>
332
+ {withTooltip(
333
+ <Pressable
334
+ accessibilityRole="button"
335
+ accessibilityLabel={`Sort by ${tooltip ?? children}`}
336
+ onPress={handlePress}
337
+ onHoverIn={() => setHovered(true)}
338
+ onHoverOut={() => setHovered(false)}
339
+ className={cn(
340
+ 'flex-row items-center',
341
+ CELL_PADDING[density],
342
+ alignStyles[align],
343
+ 'web:hover:bg-interactive-hover active:bg-interactive-active',
344
+ className
345
+ )}
346
+ {...props}
347
+ >
348
+ {content}
349
+ </Pressable>
350
+ )}
277
351
  </View>
278
352
  )
279
353
  }
@@ -281,14 +355,15 @@ export function TableHeaderCell({
281
355
  return (
282
356
  <View
283
357
  role="columnheader"
284
- style={width ? { width } : { flex: 1 }}
358
+ style={width ? { width } : FLEX_CELL}
285
359
  className={cn(
286
- 'flex-row items-center px-4 py-3',
360
+ 'flex-row items-center overflow-hidden',
361
+ CELL_PADDING[density],
287
362
  alignStyles[align],
288
363
  className
289
364
  )}
290
365
  >
291
- {content}
366
+ {withTooltip(content)}
292
367
  </View>
293
368
  )
294
369
  }
@@ -313,6 +388,7 @@ export function TableCell({
313
388
  children,
314
389
  ...props
315
390
  }: TableCellProps) {
391
+ const { density } = useContext(TableContext)
316
392
  const alignStyles = {
317
393
  left: 'items-start',
318
394
  center: 'items-center',
@@ -322,16 +398,14 @@ export function TableCell({
322
398
  return (
323
399
  <View
324
400
  role="cell"
325
- style={width ? { width } : { flex: 1 }}
326
- className={cn(
327
- 'justify-center px-4 py-3.5',
328
- alignStyles[align],
329
- className
330
- )}
401
+ style={width ? { width } : FLEX_CELL}
402
+ className={cn('justify-center', CELL_PADDING[density], alignStyles[align], className)}
331
403
  {...props}
332
404
  >
333
405
  {typeof children === 'string' ? (
334
- <Text className="text-sm text-text-primary">{children}</Text>
406
+ <Text className={cn(density === 'dense' ? 'text-xs' : 'text-sm', 'text-text-primary')}>
407
+ {children}
408
+ </Text>
335
409
  ) : (
336
410
  children
337
411
  )}
@@ -394,9 +468,7 @@ export function TablePagination({
394
468
  )}
395
469
  >
396
470
  <View className="flex-row items-center gap-2">
397
- <Text className="text-sm text-text-secondary">
398
- Rows per page:
399
- </Text>
471
+ <Text className="text-sm text-text-secondary">Rows per page:</Text>
400
472
  <View className="flex-row gap-1">
401
473
  {pageSizeOptions.map((size) => (
402
474
  <Pressable
@@ -463,11 +535,28 @@ export function TablePagination({
463
535
 
464
536
  // Helper hook for managing table state
465
537
 
538
+ /**
539
+ * An ascending comparator for one column. Returning 0 lets the caller express a
540
+ * tie-break inside the same function; `useTable` inverts the result for `desc`
541
+ * rather than reversing the array, so ties keep their relative order both ways.
542
+ */
543
+ export type TableComparator<T> = (a: T, b: T) => number
544
+
545
+ const isBlank = (v: unknown): boolean => v === null || v === undefined
546
+
466
547
  export interface UseTableOptions<T> {
467
548
  data: T[]
468
549
  defaultPageSize?: number
469
550
  defaultSortColumn?: string
470
551
  defaultSortDirection?: SortDirection
552
+ /**
553
+ * Per-column ascending comparators, for columns whose order is not their raw
554
+ * field order — a severity ranked critical→low rather than alphabetically, a
555
+ * numeric field that should sort blanks last, a date read newest-first.
556
+ * Columns absent from the map fall back to the default field compare.
557
+ */
558
+ comparators?: Partial<Record<keyof T & string, TableComparator<T>>> &
559
+ Record<string, TableComparator<T> | undefined>
471
560
  }
472
561
 
473
562
  export interface UseTableReturn<T> {
@@ -502,6 +591,7 @@ export function useTable<T extends Record<string, any>>({
502
591
  defaultPageSize = 10,
503
592
  defaultSortColumn,
504
593
  defaultSortDirection = null,
594
+ comparators,
505
595
  }: UseTableOptions<T>): UseTableReturn<T> {
506
596
  const [page, setPage] = useState(0)
507
597
  const [pageSize, setPageSize] = useState(defaultPageSize)
@@ -511,18 +601,26 @@ export function useTable<T extends Record<string, any>>({
511
601
  const sortedData = useMemo(() => {
512
602
  if (!sortColumn || !sortDirection) return data
513
603
 
604
+ const custom = comparators?.[sortColumn]
605
+ // Invert rather than reverse: reversing an already-sorted array also flips
606
+ // tied rows, so equal values would shuffle every time direction changed.
607
+ const sign = sortDirection === 'asc' ? 1 : -1
608
+
609
+ if (custom) return [...data].sort((a, b) => sign * custom(a, b))
610
+
514
611
  return [...data].sort((a, b) => {
515
612
  const aVal = a[sortColumn]
516
613
  const bVal = b[sortColumn]
517
614
 
518
- if (aVal === bVal) return 0
519
- if (aVal === null || aVal === undefined) return 1
520
- if (bVal === null || bVal === undefined) return -1
615
+ // Blanks rank last in BOTH directions — outside the sign, so a missing
616
+ // value never masquerades as the smallest one when the column flips.
617
+ const blanks = isBlank(aVal) ? (isBlank(bVal) ? 0 : 1) : isBlank(bVal) ? -1 : 0
618
+ if (blanks !== 0) return blanks
521
619
 
522
- const comparison = aVal < bVal ? -1 : 1
523
- return sortDirection === 'asc' ? comparison : -comparison
620
+ if (aVal === bVal) return 0
621
+ return sign * (aVal < bVal ? -1 : 1)
524
622
  })
525
- }, [data, sortColumn, sortDirection])
623
+ }, [data, sortColumn, sortDirection, comparators])
526
624
 
527
625
  const paginatedData = useMemo(() => {
528
626
  const start = page * pageSize
@@ -532,9 +630,7 @@ export function useTable<T extends Record<string, any>>({
532
630
  const handleSort = (column: string) => {
533
631
  if (sortColumn === column) {
534
632
  // Cycle: asc -> desc -> null
535
- setSortDirection((prev) =>
536
- prev === 'asc' ? 'desc' : prev === 'desc' ? null : 'asc'
537
- )
633
+ setSortDirection((prev) => (prev === 'asc' ? 'desc' : prev === 'desc' ? null : 'asc'))
538
634
  } else {
539
635
  setSortColumn(column)
540
636
  setSortDirection('asc')
@@ -589,14 +685,12 @@ export function TableSelectAllCell({ className }: TableSelectAllCellProps) {
589
685
  allSelected
590
686
  ? 'bg-brand-primary border-brand-primary'
591
687
  : someSelected
592
- ? 'bg-brand-primary-subtle border-brand-primary'
593
- : 'border-hairline-strong bg-transparent'
688
+ ? 'bg-brand-primary-subtle border-brand-primary'
689
+ : 'border-hairline-strong bg-transparent'
594
690
  )}
595
691
  >
596
692
  {(allSelected || someSelected) && (
597
- <Text className="text-white text-xs font-bold">
598
- {allSelected ? '✓' : '−'}
599
- </Text>
693
+ <Text className="text-white text-xs font-bold">{allSelected ? '✓' : '−'}</Text>
600
694
  )}
601
695
  </Pressable>
602
696
  </View>
@@ -632,9 +726,7 @@ export function TableSelectCell({ rowId, className }: TableSelectCellProps) {
632
726
  : 'border-hairline-strong bg-transparent web:hover:border-brand-primary'
633
727
  )}
634
728
  >
635
- {isSelected && (
636
- <Text className="text-white text-xs font-bold">✓</Text>
637
- )}
729
+ {isSelected && <Text className="text-white text-xs font-bold">✓</Text>}
638
730
  </Pressable>
639
731
  </View>
640
732
  )
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { fitColumns, type TableColumnFit } from './column-fit'
3
+
4
+ /**
5
+ * Five 100px columns: two that must survive, three that drop in a declared
6
+ * order. Every width below is picked to land on one step of that order.
7
+ */
8
+ const COLUMNS: TableColumnFit[] = [
9
+ { key: 'id', minWidth: 100 },
10
+ { key: 'title', minWidth: 100 },
11
+ { key: 'sev', minWidth: 100, dropPriority: 1 },
12
+ { key: 'pri', minWidth: 100, dropPriority: 2 },
13
+ { key: 'tags', minWidth: 100, dropPriority: 3 },
14
+ ]
15
+
16
+ const fitAt = (available: number | null) => fitColumns(COLUMNS, available)
17
+
18
+ describe('fitColumns', () => {
19
+ it('keeps every column until one has been measured', () => {
20
+ expect(fitAt(null).visibleKeys).toEqual(['id', 'title', 'sev', 'pri', 'tags'])
21
+ expect(fitAt(null).isScrolling).toBe(false)
22
+ })
23
+
24
+ it('keeps all five columns at a width that fits them', () => {
25
+ expect(fitAt(500).visibleKeys).toEqual(['id', 'title', 'sev', 'pri', 'tags'])
26
+ expect(fitAt(500).contentMinWidth).toBe(500)
27
+ expect(fitAt(500).isScrolling).toBe(false)
28
+ })
29
+
30
+ it('drops the first column in the order when four fit', () => {
31
+ expect(fitAt(450).visibleKeys).toEqual(['id', 'title', 'pri', 'tags'])
32
+ expect(fitAt(450).contentMinWidth).toBe(400)
33
+ })
34
+
35
+ it('drops the second when three fit', () => {
36
+ expect(fitAt(350).visibleKeys).toEqual(['id', 'title', 'tags'])
37
+ expect(fitAt(350).contentMinWidth).toBe(300)
38
+ })
39
+
40
+ it('drops the third when only two fit', () => {
41
+ expect(fitAt(250).visibleKeys).toEqual(['id', 'title'])
42
+ expect(fitAt(250).contentMinWidth).toBe(200)
43
+ expect(fitAt(250).isScrolling).toBe(false)
44
+ })
45
+
46
+ it('stops dropping at the undroppable columns and scrolls instead', () => {
47
+ const fit = fitAt(150)
48
+ expect(fit.visibleKeys).toEqual(['id', 'title'])
49
+ expect(fit.contentMinWidth).toBe(200)
50
+ expect(fit.isScrolling).toBe(true)
51
+ })
52
+
53
+ it('drops one column at a time rather than everything droppable at once', () => {
54
+ // 450 fits four columns; a fit that dropped by category would leave two.
55
+ expect(fitAt(450).visibleKeys).toHaveLength(4)
56
+ expect(fitAt(350).visibleKeys).toHaveLength(3)
57
+ })
58
+
59
+ it('breaks a tied priority by declaration order', () => {
60
+ const tied: TableColumnFit[] = [
61
+ { key: 'keep', minWidth: 100 },
62
+ { key: 'left', minWidth: 100, dropPriority: 1 },
63
+ { key: 'right', minWidth: 100, dropPriority: 1 },
64
+ ]
65
+ expect(fitColumns(tied, 200).visibleKeys).toEqual(['keep', 'right'])
66
+ })
67
+
68
+ it('reports what the survivors need, which is what the table scrolls to', () => {
69
+ expect(fitColumns([{ key: 'only', minWidth: 320 }], 100)).toEqual({
70
+ visibleKeys: ['only'],
71
+ contentMinWidth: 320,
72
+ isScrolling: true,
73
+ })
74
+ })
75
+ })
@@ -0,0 +1,116 @@
1
+ import { useCallback, useMemo, useState } from 'react'
2
+ import type { LayoutChangeEvent } from 'react-native'
3
+
4
+ /**
5
+ * One column's terms for the fit calculation: how much room it needs, and where
6
+ * it sits in the drop order.
7
+ */
8
+ export interface TableColumnFit {
9
+ /** Matches the key the caller renders the column under. */
10
+ key: string
11
+ /**
12
+ * Narrowest width in px at which the column still reads — its header label
13
+ * plus the sort affordance, or its content, whichever is wider. Real numbers
14
+ * matter here: the fit is arithmetic over these, not a guess at a breakpoint.
15
+ */
16
+ minWidth: number
17
+ /**
18
+ * Drop order: the lowest number leaves first. A column with no `dropPriority`
19
+ * never drops, which is what keeps the identifying columns on screen when the
20
+ * table runs out of room.
21
+ */
22
+ dropPriority?: number
23
+ }
24
+
25
+ export interface ColumnFitResult {
26
+ /** The keys still rendered, in declaration order. */
27
+ visibleKeys: string[]
28
+ /** What the survivors need. Hand to `Table`'s `contentMinWidth`. */
29
+ contentMinWidth: number
30
+ /** Nothing droppable is left and the survivors still overflow: the table scrolls. */
31
+ isScrolling: boolean
32
+ }
33
+
34
+ // Ties keep declaration order, so a duplicated priority drops left to right rather than at random.
35
+ function dropOrder(columns: TableColumnFit[]): TableColumnFit[] {
36
+ return columns
37
+ .map((column, index) => ({ column, index }))
38
+ .filter((entry) => entry.column.dropPriority !== undefined)
39
+ .sort((a, b) => a.column.dropPriority! - b.column.dropPriority! || a.index - b.index)
40
+ .map((entry) => entry.column)
41
+ }
42
+
43
+ /**
44
+ * Drops columns one at a time in the declared order until the rest fit, then
45
+ * stops. Dropping cannot take an undroppable column, so once those are all that
46
+ * is left the table overflows on purpose and scrolls horizontally — scroll is
47
+ * the floor under dropping, not an alternative to it.
48
+ *
49
+ * `available` of `null` means "not measured yet": everything renders.
50
+ */
51
+ export function fitColumns(columns: TableColumnFit[], available: number | null): ColumnFitResult {
52
+ const dropped = new Set<string>()
53
+ let needed = columns.reduce((total, column) => total + column.minWidth, 0)
54
+
55
+ if (available !== null) {
56
+ for (const column of dropOrder(columns)) {
57
+ if (needed <= available) break
58
+ dropped.add(column.key)
59
+ needed -= column.minWidth
60
+ }
61
+ }
62
+
63
+ return {
64
+ visibleKeys: columns.filter((column) => !dropped.has(column.key)).map((column) => column.key),
65
+ contentMinWidth: needed,
66
+ isScrolling: available !== null && needed > available,
67
+ }
68
+ }
69
+
70
+ export interface UseMeasuredWidthResult {
71
+ /** The measured width, or `null` until the first layout pass. */
72
+ width: number | null
73
+ onLayout: (event: LayoutChangeEvent) => void
74
+ }
75
+
76
+ /**
77
+ * The width of whatever it is attached to, from the `onLayout` pass every other
78
+ * width-aware table here already uses — no resize observer, and no window
79
+ * width standing in for a column that lives inside a much narrower card.
80
+ *
81
+ * `override` pins the width instead of measuring, so a story or a test can hold
82
+ * one breakpoint still.
83
+ */
84
+ export function useMeasuredWidth(override?: number): UseMeasuredWidthResult {
85
+ const [measured, setMeasured] = useState<number | null>(null)
86
+ const onLayout = useCallback(
87
+ (event: LayoutChangeEvent) => setMeasured(event.nativeEvent.layout.width),
88
+ []
89
+ )
90
+ return { width: override ?? measured, onLayout }
91
+ }
92
+
93
+ export interface UseColumnFitResult extends ColumnFitResult {
94
+ isVisible: (key: string) => boolean
95
+ }
96
+
97
+ /**
98
+ * {@link fitColumns} as a hook, with a membership test for the render pass.
99
+ *
100
+ * @example
101
+ * const { width, onLayout } = useMeasuredWidth()
102
+ * const fit = useColumnFit(COLUMNS, width)
103
+ * <View onLayout={onLayout}>
104
+ * <Table contentMinWidth={fit.contentMinWidth}>
105
+ * {COLUMNS.filter((c) => fit.isVisible(c.key)).map(renderColumn)}
106
+ * </Table>
107
+ * </View>
108
+ */
109
+ export function useColumnFit(
110
+ columns: TableColumnFit[],
111
+ available: number | null
112
+ ): UseColumnFitResult {
113
+ const fit = useMemo(() => fitColumns(columns, available), [columns, available])
114
+ const visible = useMemo(() => new Set(fit.visibleKeys), [fit])
115
+ return { ...fit, isVisible: (key: string) => visible.has(key) }
116
+ }
@@ -19,4 +19,13 @@ export type {
19
19
  UseTableOptions,
20
20
  UseTableReturn,
21
21
  SortDirection,
22
+ TableDensity,
23
+ TableComparator,
22
24
  } from './Table'
25
+ export { fitColumns, useColumnFit, useMeasuredWidth } from './column-fit'
26
+ export type {
27
+ TableColumnFit,
28
+ ColumnFitResult,
29
+ UseColumnFitResult,
30
+ UseMeasuredWidthResult,
31
+ } from './column-fit'
@@ -1,6 +1,6 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
- import { View } from 'react-native'
3
2
  import { TimerReadout } from './TimerReadout'
3
+ import { Surface } from '../../ui/surface'
4
4
 
5
5
  /**
6
6
  * `TimerReadout` — a small textual timer (⏱ + mono, right-justified) built on the
@@ -8,21 +8,23 @@ import { TimerReadout } from './TimerReadout'
8
8
  * `running`; the ` / total` suffix stays dim tertiary.
9
9
  */
10
10
  const meta: Meta<typeof TimerReadout> = {
11
- title: 'Custom/TimerReadout',
11
+ title: 'Components/Molecules/TimerReadout',
12
12
  component: TimerReadout,
13
- tags: ['autodocs'],
13
+ tags: ['autodocs', 'status:candidate', '!status:review'],
14
14
  parameters: {
15
15
  docs: {
16
16
  description: {
17
- component: '**Atom.** A small textual timer on [useTimer]. Used-by ↑ SessionHeader.',
17
+ component:
18
+ '**Molecule.** A small textual timer on [useTimer]. Used-by ↑ SessionHeader.' +
19
+ ' Composes **Typography**.',
18
20
  },
19
21
  },
20
22
  },
21
23
  decorators: [
22
24
  (Story) => (
23
- <View style={{ padding: 16, backgroundColor: '#131313', alignItems: 'flex-end', width: 200 }}>
25
+ <Surface level="base" style={{ padding: 16, alignItems: 'flex-end', width: 200 }}>
24
26
  <Story />
25
- </View>
27
+ </Surface>
26
28
  ),
27
29
  ],
28
30
  }
@@ -2,9 +2,9 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import { Treemap, type TreemapDatum } from './Treemap'
3
3
 
4
4
  const meta: Meta<typeof Treemap> = {
5
- title: 'Components/DataViz/Treemap',
5
+ title: 'Custom/Charts/Treemap',
6
6
  component: Treemap,
7
- tags: ['autodocs'],
7
+ tags: ['autodocs', 'status:candidate', '!status:review'],
8
8
  argTypes: {
9
9
  width: { control: { type: 'range', min: 200, max: 900, step: 20 } },
10
10
  height: { control: { type: 'range', min: 120, max: 600, step: 20 } },
@@ -47,7 +47,7 @@ describe('Treemap', () => {
47
47
 
48
48
  it('assigns fallback palette colors when omitted', () => {
49
49
  const { container } = render(
50
- <Treemap data={[{ id: 'x', value: 1 }]} width={100} height={100} />,
50
+ <Treemap data={[{ id: 'x', value: 1 }]} width={100} height={100} />
51
51
  )
52
52
  expect(container.firstChild).toBeInTheDocument()
53
53
  })