@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,4 +1,4 @@
1
- import React, { useState } from 'react'
1
+ import { useState } from 'react'
2
2
  import type { Meta, StoryObj } from '@storybook/react-vite'
3
3
  import { View, Text } from 'react-native'
4
4
  import { cn } from '../../../utils/cn'
@@ -15,6 +15,7 @@ import {
15
15
  TableSelectCell,
16
16
  useTable,
17
17
  } from './Table'
18
+ import { useColumnFit, type TableColumnFit } from './column-fit'
18
19
  import { Button, ButtonText } from '../../ui/button/Button'
19
20
 
20
21
  interface User {
@@ -44,7 +45,10 @@ const manyUsers: User[] = Array.from({ length: 50 }, (_, i) => ({
44
45
  const meta: Meta<typeof Table> = {
45
46
  title: 'Components/Organisms/Table',
46
47
  component: Table,
47
- tags: ['autodocs'],
48
+ parameters: {
49
+ docs: { description: { component: 'Composes **Tooltip**.' } },
50
+ },
51
+ tags: ['autodocs', 'status:candidate', '!status:review'],
48
52
  argTypes: {
49
53
  isLoading: {
50
54
  control: 'boolean',
@@ -114,9 +118,7 @@ export const WithSorting: Story = {
114
118
 
115
119
  const handleSort = (column: string) => {
116
120
  if (sortColumn === column) {
117
- setSortDirection((prev) =>
118
- prev === 'asc' ? 'desc' : prev === 'desc' ? null : 'asc'
119
- )
121
+ setSortDirection((prev) => (prev === 'asc' ? 'desc' : prev === 'desc' ? null : 'asc'))
120
122
  } else {
121
123
  setSortColumn(column)
122
124
  setSortDirection('asc')
@@ -313,11 +315,17 @@ export const WithColumnWidths: Story = {
313
315
  <Table>
314
316
  <TableHeader>
315
317
  <TableRow isHoverable={false}>
316
- <TableHeaderCell width={60} align="center">#</TableHeaderCell>
318
+ <TableHeaderCell width={60} align="center">
319
+ #
320
+ </TableHeaderCell>
317
321
  <TableHeaderCell width={200}>Name</TableHeaderCell>
318
322
  <TableHeaderCell>Email</TableHeaderCell>
319
- <TableHeaderCell width={120} align="center">Role</TableHeaderCell>
320
- <TableHeaderCell width={100} align="right">Status</TableHeaderCell>
323
+ <TableHeaderCell width={120} align="center">
324
+ Role
325
+ </TableHeaderCell>
326
+ <TableHeaderCell width={100} align="right">
327
+ Status
328
+ </TableHeaderCell>
321
329
  </TableRow>
322
330
  </TableHeader>
323
331
  <TableBody>
@@ -330,8 +338,12 @@ export const WithColumnWidths: Story = {
330
338
  <Text className="text-sm text-text-primary font-medium">{user.name}</Text>
331
339
  </TableCell>
332
340
  <TableCell>{user.email}</TableCell>
333
- <TableCell width={120} align="center">{user.role}</TableCell>
334
- <TableCell width={100} align="right">{user.status}</TableCell>
341
+ <TableCell width={120} align="center">
342
+ {user.role}
343
+ </TableCell>
344
+ <TableCell width={100} align="right">
345
+ {user.status}
346
+ </TableCell>
335
347
  </TableRow>
336
348
  ))}
337
349
  </TableBody>
@@ -339,6 +351,79 @@ export const WithColumnWidths: Story = {
339
351
  ),
340
352
  }
341
353
 
354
+ /**
355
+ * A column's terms for the fit: what it needs, and when it leaves. `name` has no
356
+ * `dropPriority`, so it never drops — which is what puts a floor under the
357
+ * dropping and hands the overflow to horizontal scroll instead.
358
+ */
359
+ const FIT_COLUMNS: (TableColumnFit & { label: string })[] = [
360
+ { key: 'name', label: 'Name', minWidth: 200 },
361
+ { key: 'email', label: 'Email', minWidth: 220, dropPriority: 3 },
362
+ { key: 'role', label: 'Role', minWidth: 120, dropPriority: 2 },
363
+ { key: 'status', label: 'Status', minWidth: 100, dropPriority: 1 },
364
+ ]
365
+
366
+ /** The hairline frame, which the outer width includes but the columns cannot use. */
367
+ const FRAME_INSET = 2
368
+
369
+ /** The same table at a pinned width, so one story shows every step of the order at once. */
370
+ function FittedTable({ available }: { available: number }) {
371
+ const fit = useColumnFit(FIT_COLUMNS, available - FRAME_INSET)
372
+ const columns = FIT_COLUMNS.filter((column) => fit.isVisible(column.key))
373
+ const width = (key: string) =>
374
+ key === 'name' ? undefined : FIT_COLUMNS.find((c) => c.key === key)!.minWidth
375
+
376
+ return (
377
+ <View className="gap-1" style={{ width: available }}>
378
+ <Text className="text-xs text-text-secondary">
379
+ {`${available}px · ${columns.length} columns${fit.isScrolling ? ' · scrolls' : ''}`}
380
+ </Text>
381
+ <View className="overflow-hidden rounded-lg border border-hairline">
382
+ <Table contentMinWidth={fit.contentMinWidth}>
383
+ <TableHeader>
384
+ <TableRow isHoverable={false}>
385
+ {columns.map((column) => (
386
+ <TableHeaderCell key={column.key} width={width(column.key)}>
387
+ {column.label}
388
+ </TableHeaderCell>
389
+ ))}
390
+ </TableRow>
391
+ </TableHeader>
392
+ <TableBody>
393
+ {sampleUsers.slice(0, 3).map((user) => (
394
+ <TableRow key={user.id}>
395
+ {columns.map((column) => (
396
+ <TableCell key={column.key} width={width(column.key)}>
397
+ {user[column.key as keyof User]}
398
+ </TableCell>
399
+ ))}
400
+ </TableRow>
401
+ ))}
402
+ </TableBody>
403
+ </Table>
404
+ </View>
405
+ </View>
406
+ )
407
+ }
408
+
409
+ /**
410
+ * **Column drop order.** When the measured width cannot hold every column, they
411
+ * drop one at a time in the order the caller declared — `status`, then `role`,
412
+ * then `email`. `name` declares no `dropPriority`, so dropping stops there and
413
+ * the table scrolls horizontally instead: scroll is the floor, not the first
414
+ * resort. Measure with `useMeasuredWidth`, decide with `useColumnFit`, and hand
415
+ * `contentMinWidth` to `Table` so it knows what to scroll to.
416
+ */
417
+ export const ColumnDropOrder: Story = {
418
+ render: () => (
419
+ <View className="gap-6">
420
+ {[660, 560, 440, 300, 150].map((available) => (
421
+ <FittedTable key={available} available={available} />
422
+ ))}
423
+ </View>
424
+ ),
425
+ }
426
+
342
427
  export const UseTableHook: Story = {
343
428
  render: function Render() {
344
429
  const {
@@ -361,8 +446,8 @@ export const UseTableHook: Story = {
361
446
  return (
362
447
  <View style={{ gap: 8 }}>
363
448
  <Text className="text-sm text-text-secondary">
364
- Showing {paginatedData.length} of {totalItems} users, sorted by{' '}
365
- {sortColumn ?? 'none'} ({sortDirection ?? 'none'})
449
+ Showing {paginatedData.length} of {totalItems} users, sorted by {sortColumn ?? 'none'} (
450
+ {sortDirection ?? 'none'})
366
451
  </Text>
367
452
  <Table sortColumn={sortColumn} sortDirection={sortDirection} onSort={handleSort}>
368
453
  <TableHeader>
@@ -434,9 +519,7 @@ export const SortingAndSelection: Story = {
434
519
 
435
520
  return (
436
521
  <View style={{ gap: 8 }}>
437
- <Text className="text-sm text-text-secondary">
438
- Selected: {selectedRows.size} rows
439
- </Text>
522
+ <Text className="text-sm text-text-secondary">Selected: {selectedRows.size} rows</Text>
440
523
  <Table
441
524
  sortColumn={sortColumn}
442
525
  sortDirection={sortDirection}
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi } from 'vitest'
2
- import { render, screen, fireEvent } from '@testing-library/react'
2
+ import { render, screen, fireEvent, renderHook, act } from '@testing-library/react'
3
3
  import { axe } from 'jest-axe'
4
4
  import {
5
5
  Table,
@@ -12,6 +12,7 @@ import {
12
12
  TableEmptyState,
13
13
  TableSelectAllCell,
14
14
  TableSelectCell,
15
+ useTable,
15
16
  } from './Table'
16
17
 
17
18
  function renderBasicTable() {
@@ -321,29 +322,142 @@ describe('Table', () => {
321
322
  })
322
323
  })
323
324
 
324
- describe('TablePagination', () => {
325
- it('renders pagination info', () => {
325
+ describe('sort cycle end state', () => {
326
+ // Regression: after asc -> desc -> unsorted the column is still the sort
327
+ // column, and the header kept showing the descending arrow.
328
+ it('shows the neutral glyph once the cycle returns to unsorted', () => {
326
329
  render(
327
- <TablePagination
328
- page={0}
329
- pageSize={10}
330
- totalItems={50}
331
- onPageChange={vi.fn()}
332
- />
330
+ <Table sortColumn="name" sortDirection={null} onSort={vi.fn()}>
331
+ <TableHeader>
332
+ <TableRow>
333
+ <TableHeaderCell sortKey="name">Name</TableHeaderCell>
334
+ </TableRow>
335
+ </TableHeader>
336
+ </Table>
337
+ )
338
+ expect(screen.getByText('↕')).toBeInTheDocument()
339
+ expect(screen.getByRole('columnheader')).toHaveAttribute('aria-sort', 'none')
340
+ })
341
+ })
342
+
343
+ describe('idle sort glyph', () => {
344
+ it('hides the neutral glyph until the header is hovered, and keeps an active arrow visible', () => {
345
+ render(
346
+ <Table sortColumn="name" sortDirection="asc" onSort={vi.fn()}>
347
+ <TableHeader>
348
+ <TableRow>
349
+ <TableHeaderCell sortKey="name">Name</TableHeaderCell>
350
+ <TableHeaderCell sortKey="email">Email</TableHeaderCell>
351
+ </TableRow>
352
+ </TableHeader>
353
+ </Table>
354
+ )
355
+ expect(screen.getByText('↑')).toHaveStyle({ opacity: '1' })
356
+ const idle = screen.getByText('↕')
357
+ expect(idle).toHaveStyle({ opacity: '0' })
358
+ fireEvent.mouseEnter(screen.getByRole('button', { name: 'Sort by Email' }))
359
+ expect(idle).toHaveStyle({ opacity: '1' })
360
+ fireEvent.mouseLeave(screen.getByRole('button', { name: 'Sort by Email' }))
361
+ expect(idle).toHaveStyle({ opacity: '0' })
362
+ })
363
+ })
364
+
365
+ describe('header tooltip', () => {
366
+ it('uses the full column name as the accessible sort name and shows it on hover', () => {
367
+ render(
368
+ <Table onSort={vi.fn()}>
369
+ <TableHeader>
370
+ <TableRow>
371
+ <TableHeaderCell sortKey="priority" tooltip="Priority">
372
+ Pri
373
+ </TableHeaderCell>
374
+ </TableRow>
375
+ </TableHeader>
376
+ </Table>
377
+ )
378
+ const sortButton = screen.getByRole('button', { name: 'Sort by Priority' })
379
+ fireEvent.mouseEnter(sortButton.parentElement!)
380
+ expect(screen.getByText('Priority')).toBeInTheDocument()
381
+ })
382
+
383
+ it('still sorts when the tooltip-wrapped header is pressed', () => {
384
+ const onSort = vi.fn()
385
+ render(
386
+ <Table onSort={onSort}>
387
+ <TableHeader>
388
+ <TableRow>
389
+ <TableHeaderCell sortKey="priority" tooltip="Priority">
390
+ Pri
391
+ </TableHeaderCell>
392
+ </TableRow>
393
+ </TableHeader>
394
+ </Table>
333
395
  )
396
+ fireEvent.click(screen.getByText('Pri'))
397
+ expect(onSort).toHaveBeenCalledWith('priority')
398
+ })
399
+ })
400
+
401
+ describe('flexible column layout', () => {
402
+ // Regression: a long nowrap title used to size the horizontal scroller to
403
+ // max-content, so the flexible column never shrank and the last fixed
404
+ // column was clipped inside the table's card (AW-22 T2 Gate 2 finding).
405
+ it('fills the scroll viewport instead of sizing to its longest cell', () => {
406
+ renderBasicTable()
407
+ const scrollContent = screen.getByRole('table').parentElement
408
+ expect(scrollContent).toHaveStyle({ width: '100%' })
409
+ })
410
+
411
+ it('lets a cell without a width shrink below its content', () => {
412
+ render(
413
+ <Table>
414
+ <TableBody>
415
+ <TableRow>
416
+ <TableCell width={80}>fixed</TableCell>
417
+ <TableCell>flexible</TableCell>
418
+ </TableRow>
419
+ </TableBody>
420
+ </Table>
421
+ )
422
+ expect(screen.getByText('fixed').closest('[role="cell"]')).toHaveStyle({ width: '80px' })
423
+ expect(screen.getByText('flexible').closest('[role="cell"]')).toHaveStyle({
424
+ flexGrow: '1',
425
+ minWidth: '0px',
426
+ })
427
+ })
428
+
429
+ it('floors the content at the width the columns need, so a too-narrow table scrolls', () => {
430
+ render(
431
+ <Table contentMinWidth={420}>
432
+ <TableBody>
433
+ <TableRow>
434
+ <TableCell>only</TableCell>
435
+ </TableRow>
436
+ </TableBody>
437
+ </Table>
438
+ )
439
+ // width 100% + the floor resolves to max(container, floor): fill, then scroll.
440
+ expect(screen.getByRole('table').parentElement).toHaveStyle({
441
+ width: '100%',
442
+ minWidth: '420px',
443
+ })
444
+ })
445
+
446
+ it('sets no floor when the caller has not measured one', () => {
447
+ renderBasicTable()
448
+ expect(screen.getByRole('table').parentElement).not.toHaveStyle({ minWidth: '420px' })
449
+ })
450
+ })
451
+
452
+ describe('TablePagination', () => {
453
+ it('renders pagination info', () => {
454
+ render(<TablePagination page={0} pageSize={10} totalItems={50} onPageChange={vi.fn()} />)
334
455
  expect(screen.getByText('1-10 of 50')).toBeInTheDocument()
335
456
  })
336
457
 
337
458
  it('calls onPageChange when next is clicked', () => {
338
459
  const onPageChange = vi.fn()
339
- render(
340
- <TablePagination
341
- page={0}
342
- pageSize={10}
343
- totalItems={50}
344
- onPageChange={onPageChange}
345
- />
346
- )
460
+ render(<TablePagination page={0} pageSize={10} totalItems={50} onPageChange={onPageChange} />)
347
461
 
348
462
  fireEvent.click(screen.getByLabelText('Next page'))
349
463
  expect(onPageChange).toHaveBeenCalledWith(1)
@@ -351,41 +465,20 @@ describe('Table', () => {
351
465
 
352
466
  it('calls onPageChange when previous is clicked', () => {
353
467
  const onPageChange = vi.fn()
354
- render(
355
- <TablePagination
356
- page={2}
357
- pageSize={10}
358
- totalItems={50}
359
- onPageChange={onPageChange}
360
- />
361
- )
468
+ render(<TablePagination page={2} pageSize={10} totalItems={50} onPageChange={onPageChange} />)
362
469
 
363
470
  fireEvent.click(screen.getByLabelText('Previous page'))
364
471
  expect(onPageChange).toHaveBeenCalledWith(1)
365
472
  })
366
473
 
367
474
  it('disables previous button on first page', () => {
368
- render(
369
- <TablePagination
370
- page={0}
371
- pageSize={10}
372
- totalItems={50}
373
- onPageChange={vi.fn()}
374
- />
375
- )
475
+ render(<TablePagination page={0} pageSize={10} totalItems={50} onPageChange={vi.fn()} />)
376
476
  const prevButton = screen.getByLabelText('Previous page')
377
477
  expect(prevButton).toHaveAttribute('aria-disabled', 'true')
378
478
  })
379
479
 
380
480
  it('disables next button on last page', () => {
381
- render(
382
- <TablePagination
383
- page={4}
384
- pageSize={10}
385
- totalItems={50}
386
- onPageChange={vi.fn()}
387
- />
388
- )
481
+ render(<TablePagination page={4} pageSize={10} totalItems={50} onPageChange={vi.fn()} />)
389
482
  const nextButton = screen.getByLabelText('Next page')
390
483
  expect(nextButton).toHaveAttribute('aria-disabled', 'true')
391
484
  })
@@ -422,26 +515,12 @@ describe('Table', () => {
422
515
  })
423
516
 
424
517
  it('shows correct range for middle pages', () => {
425
- render(
426
- <TablePagination
427
- page={2}
428
- pageSize={10}
429
- totalItems={50}
430
- onPageChange={vi.fn()}
431
- />
432
- )
518
+ render(<TablePagination page={2} pageSize={10} totalItems={50} onPageChange={vi.fn()} />)
433
519
  expect(screen.getByText('21-30 of 50')).toBeInTheDocument()
434
520
  })
435
521
 
436
522
  it('shows correct range for last partial page', () => {
437
- render(
438
- <TablePagination
439
- page={2}
440
- pageSize={10}
441
- totalItems={25}
442
- onPageChange={vi.fn()}
443
- />
444
- )
523
+ render(<TablePagination page={2} pageSize={10} totalItems={25} onPageChange={vi.fn()} />)
445
524
  expect(screen.getByText('21-25 of 25')).toBeInTheDocument()
446
525
  })
447
526
  })
@@ -454,30 +533,18 @@ describe('Table', () => {
454
533
  })
455
534
 
456
535
  it('renders with custom title and description', () => {
457
- render(
458
- <TableEmptyState title="No users" description="Add users to see them here." />
459
- )
536
+ render(<TableEmptyState title="No users" description="Add users to see them here." />)
460
537
  expect(screen.getByText('No users')).toBeInTheDocument()
461
538
  expect(screen.getByText('Add users to see them here.')).toBeInTheDocument()
462
539
  })
463
540
 
464
541
  it('renders with action element', () => {
465
- render(
466
- <TableEmptyState
467
- title="No data"
468
- action={<button>Add Item</button>}
469
- />
470
- )
542
+ render(<TableEmptyState title="No data" action={<button>Add Item</button>} />)
471
543
  expect(screen.getByText('Add Item')).toBeInTheDocument()
472
544
  })
473
545
 
474
546
  it('renders with custom icon', () => {
475
- render(
476
- <TableEmptyState
477
- title="No data"
478
- icon={<span data-testid="empty-icon">E</span>}
479
- />
480
- )
547
+ render(<TableEmptyState title="No data" icon={<span data-testid="empty-icon">E</span>} />)
481
548
  expect(screen.getByTestId('empty-icon')).toBeInTheDocument()
482
549
  })
483
550
  })
@@ -491,12 +558,7 @@ describe('Table', () => {
491
558
 
492
559
  it('has no accessibility violations for pagination', async () => {
493
560
  const { container } = render(
494
- <TablePagination
495
- page={0}
496
- pageSize={10}
497
- totalItems={50}
498
- onPageChange={vi.fn()}
499
- />
561
+ <TablePagination page={0} pageSize={10} totalItems={50} onPageChange={vi.fn()} />
500
562
  )
501
563
  const results = await axe(container)
502
564
  expect(results).toHaveNoViolations()
@@ -543,16 +605,141 @@ describe('Table', () => {
543
605
  })
544
606
 
545
607
  it('pagination buttons have accessible labels', () => {
546
- render(
547
- <TablePagination
548
- page={1}
549
- pageSize={10}
550
- totalItems={50}
551
- onPageChange={vi.fn()}
552
- />
553
- )
608
+ render(<TablePagination page={1} pageSize={10} totalItems={50} onPageChange={vi.fn()} />)
554
609
  expect(screen.getByLabelText('Previous page')).toBeInTheDocument()
555
610
  expect(screen.getByLabelText('Next page')).toBeInTheDocument()
556
611
  })
557
612
  })
558
613
  })
614
+
615
+ describe('Table density', () => {
616
+ it('defaults to comfortable and accepts dense without changing structure', () => {
617
+ const { rerender } = render(
618
+ <Table>
619
+ <TableBody>
620
+ <TableRow>
621
+ <TableCell>Alice</TableCell>
622
+ </TableRow>
623
+ </TableBody>
624
+ </Table>
625
+ )
626
+ expect(screen.getByRole('cell')).toBeInTheDocument()
627
+
628
+ rerender(
629
+ <Table density="dense">
630
+ <TableBody>
631
+ <TableRow>
632
+ <TableCell>Alice</TableCell>
633
+ </TableRow>
634
+ </TableBody>
635
+ </Table>
636
+ )
637
+ // nativewind compiles className to style, so density is asserted through
638
+ // structure + a11y surviving rather than through class names.
639
+ expect(screen.getByRole('cell')).toBeInTheDocument()
640
+ expect(screen.getByText('Alice')).toBeInTheDocument()
641
+ })
642
+
643
+ it('keeps header cells sortable at dense density', () => {
644
+ const onSort = vi.fn()
645
+ render(
646
+ <Table density="dense" sortColumn="name" sortDirection="asc" onSort={onSort}>
647
+ <TableHeader>
648
+ <TableRow>
649
+ <TableHeaderCell sortKey="name">Name</TableHeaderCell>
650
+ </TableRow>
651
+ </TableHeader>
652
+ </Table>
653
+ )
654
+ fireEvent.click(screen.getByRole('button', { name: 'Sort by Name' }))
655
+ expect(onSort).toHaveBeenCalledWith('name')
656
+ expect(screen.getByRole('columnheader')).toHaveAttribute('aria-sort', 'ascending')
657
+ })
658
+ })
659
+
660
+ describe('useTable', () => {
661
+ // A type alias, not an interface: `useTable` constrains T to an index-signature
662
+ // shape, and only aliases get the implicit index signature that satisfies it.
663
+ type Row = {
664
+ id: string
665
+ rank: number
666
+ size?: number
667
+ }
668
+ const rows: Row[] = [
669
+ { id: 'b', rank: 2, size: 5 },
670
+ { id: 'a', rank: 3 },
671
+ { id: 'c', rank: 1, size: 2 },
672
+ ]
673
+
674
+ const ids = (data: Row[]) => data.map((r) => r.id)
675
+
676
+ it('returns data untouched until a direction is set', () => {
677
+ const { result } = renderHook(() => useTable<Row>({ data: rows, defaultSortColumn: 'rank' }))
678
+ expect(ids(result.current.sortedData)).toEqual(['b', 'a', 'c'])
679
+ })
680
+
681
+ it('sorts by raw field value when no comparator is supplied', () => {
682
+ const { result } = renderHook(() =>
683
+ useTable<Row>({ data: rows, defaultSortColumn: 'rank', defaultSortDirection: 'asc' })
684
+ )
685
+ expect(ids(result.current.sortedData)).toEqual(['c', 'b', 'a'])
686
+ })
687
+
688
+ it('ranks blank values last in BOTH directions', () => {
689
+ const asc = renderHook(() =>
690
+ useTable<Row>({ data: rows, defaultSortColumn: 'size', defaultSortDirection: 'asc' })
691
+ )
692
+ const desc = renderHook(() =>
693
+ useTable<Row>({ data: rows, defaultSortColumn: 'size', defaultSortDirection: 'desc' })
694
+ )
695
+ // `a` has no size; flipping direction must not promote it to the top.
696
+ expect(ids(asc.result.current.sortedData).at(-1)).toBe('a')
697
+ expect(ids(desc.result.current.sortedData).at(-1)).toBe('a')
698
+ })
699
+
700
+ it('uses a per-column comparator when one is supplied', () => {
701
+ const { result } = renderHook(() =>
702
+ useTable<Row>({
703
+ data: rows,
704
+ defaultSortColumn: 'id',
705
+ defaultSortDirection: 'asc',
706
+ // Reverse-alphabetical, to prove the comparator beats the field compare.
707
+ comparators: { id: (x, y) => y.id.localeCompare(x.id) },
708
+ })
709
+ )
710
+ expect(ids(result.current.sortedData)).toEqual(['c', 'b', 'a'])
711
+ })
712
+
713
+ it('inverts a comparator for desc rather than reversing the array', () => {
714
+ const { result } = renderHook(() =>
715
+ useTable<Row>({
716
+ data: rows,
717
+ defaultSortColumn: 'id',
718
+ defaultSortDirection: 'desc',
719
+ comparators: { id: (x, y) => y.id.localeCompare(x.id) },
720
+ })
721
+ )
722
+ expect(ids(result.current.sortedData)).toEqual(['a', 'b', 'c'])
723
+ })
724
+
725
+ it('leaves columns without a comparator on the default compare', () => {
726
+ const { result } = renderHook(() =>
727
+ useTable<Row>({
728
+ data: rows,
729
+ defaultSortColumn: 'rank',
730
+ defaultSortDirection: 'asc',
731
+ comparators: { id: (x, y) => y.id.localeCompare(x.id) },
732
+ })
733
+ )
734
+ expect(ids(result.current.sortedData)).toEqual(['c', 'b', 'a'])
735
+ })
736
+
737
+ it('toggles direction through handleSort on the same column', () => {
738
+ const { result } = renderHook(() =>
739
+ useTable<Row>({ data: rows, defaultSortColumn: 'rank', defaultSortDirection: 'asc' })
740
+ )
741
+ act(() => result.current.handleSort('rank'))
742
+ expect(result.current.sortDirection).toBe('desc')
743
+ expect(ids(result.current.sortedData)).toEqual(['a', 'b', 'c'])
744
+ })
745
+ })