@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
@@ -11,12 +11,7 @@ describe('ToolbarButton', () => {
11
11
  })
12
12
 
13
13
  it('renders icon when provided', () => {
14
- render(
15
- <ToolbarButton
16
- label="Settings"
17
- icon={<span data-testid="icon">gear</span>}
18
- />
19
- )
14
+ render(<ToolbarButton label="Settings" icon={<span data-testid="icon">gear</span>} />)
20
15
  expect(screen.getByTestId('icon')).toBeInTheDocument()
21
16
  })
22
17
 
@@ -108,24 +103,14 @@ describe('ToolbarButton', () => {
108
103
 
109
104
  describe('menu content', () => {
110
105
  it('shows menu when pressed with menuContent', () => {
111
- render(
112
- <ToolbarButton
113
- label="Options"
114
- menuContent={<span>Menu item</span>}
115
- />
116
- )
106
+ render(<ToolbarButton label="Options" menuContent={<span>Menu item</span>} />)
117
107
 
118
108
  fireEvent.click(screen.getByRole('button', { name: 'Options' }))
119
109
  expect(screen.getByText('Menu item')).toBeInTheDocument()
120
110
  })
121
111
 
122
112
  it('hides menu when pressed again', () => {
123
- render(
124
- <ToolbarButton
125
- label="Options"
126
- menuContent={<span>Menu item</span>}
127
- />
128
- )
113
+ render(<ToolbarButton label="Options" menuContent={<span>Menu item</span>} />)
129
114
 
130
115
  const button = screen.getByRole('button', { name: 'Options' })
131
116
  fireEvent.click(button)
@@ -3,8 +3,10 @@ import { View, Text, Pressable, type ViewProps, StyleSheet, Platform } from 'rea
3
3
  import { cn } from '../../../utils/cn'
4
4
  import { getHoverColors } from '../../../theme'
5
5
  import { greyRamp } from '../../../theme/tokens/primitives'
6
- import { getSemanticColors } from '../../../theme/tokens/semantic'
7
6
  import { resolveColor } from '../../../theme/resolve-color'
7
+ import { getPressedRecessShadow } from '../../../theme/elevation'
8
+ import { liftStyle } from '../../../theme/lift'
9
+ import { Surface, useSurfaceMode } from '../surface'
8
10
 
9
11
  export type ToolbarButtonVariant = 'default' | 'raised'
10
12
  export type ToolbarButtonSize = 'sm' | 'md' | 'lg'
@@ -24,7 +26,7 @@ export interface ToolbarButtonProps extends ViewProps {
24
26
  label: string
25
27
  /** Icon component */
26
28
  icon?: React.ReactNode
27
- /**
29
+ /**
28
30
  * Whether the button is in active/selected state.
29
31
  * - `true`: pressed/active appearance (darker, white text, orange icon)
30
32
  * - `undefined`: pressed appearance (darker, white text/icon)
@@ -49,19 +51,10 @@ export interface ToolbarButtonProps extends ViewProps {
49
51
  className?: string
50
52
  }
51
53
 
52
- const SEMANTIC = getSemanticColors('dark')
53
-
54
54
  // Base button colours. These were raw `#3C3C3C`/`#2C2C2C` literals — old cold
55
55
  // charcoal steps that survived the grey migration only because they were plain
56
56
  // strings rather than scale references.
57
57
  const BUTTON_BG = greyRamp[800]
58
- const MENU_BG = greyRamp[900]
59
-
60
- // Inline depth is a HAIRLINE, not a shadow (TD-07.16). On a ~10% lightness
61
- // surface a dual-opposing neumorphic shadow has no room below to read; alpha
62
- // composites by the same amount over any plane, so the edge holds everywhere.
63
- const EDGE = { borderWidth: 1, borderColor: SEMANTIC['hairline-default'] } as const
64
- const EDGE_PRESSED = { borderWidth: 1, borderColor: SEMANTIC['hairline-strong'] } as const
65
58
 
66
59
  // Calculate hover colors using color math
67
60
  const hoverColors = getHoverColors(BUTTON_BG, 'medium')
@@ -83,11 +76,11 @@ const textSizeStyles: Record<ToolbarButtonSize, string> = {
83
76
  * ToolbarButton component for toolbar actions with toggle state support.
84
77
  *
85
78
  * Features:
86
- * - Neumorphic styling with inset shadows for pressed/raised 3D effect
79
+ * - Depth from the fill plus the lift (raised) or the inset recess (active)
87
80
  * - Active (pressed) state is the default visual appearance
88
81
  * - Set isActive={false} explicitly for the raised/inactive appearance
89
82
  * - Orange accent color on icon when isActive={true}
90
- * - Hover states that lighten background and remove shadows
83
+ * - Hover states that lighten the background
91
84
  *
92
85
  * @example
93
86
  * // Toggle button - orange icon when active
@@ -123,6 +116,7 @@ export function ToolbarButton({
123
116
  }: ToolbarButtonProps) {
124
117
  const [isOpen, setIsOpen] = useState(false)
125
118
  const [isHovered, setIsHovered] = useState(false)
119
+ const mode = useSurfaceMode()
126
120
 
127
121
  const handlePress = useCallback(() => {
128
122
  if (isDisabled) return
@@ -146,28 +140,21 @@ export function ToolbarButton({
146
140
  return showActive ? '#FFFFFF' : '#D1D1D1'
147
141
  }
148
142
 
149
- // Get shadow and background styles for neumorphic variant
150
143
  /**
151
- * Raised/pressed treatment. Depth comes from the FILL plus a hairline edge —
152
- * pressed sits darker with a stronger edge, raised sits lighter with a quieter
153
- * one. It used to come from a two-layer neumorphic shadow, which the surface
154
- * exploration ruled out for inline hierarchy: it needs a mid-tone background
155
- * for the dark half to have somewhere to fall, and these buttons sit at ~10%
156
- * lightness.
144
+ * Raised/pressed treatment on the depth model: the raised face is a plane one
145
+ * step above the toolbar and wears the lift, the active face is pressed into
146
+ * it and wears the recess. The hairline ring both used to carry is an edge,
147
+ * not depth, and is gone with the rest of the rings on this pass.
157
148
  */
158
149
  const getRaisedStyle = () => {
159
- if (isDisabled) {
160
- return styles.disabledBg
161
- }
150
+ if (isDisabled) return styles.disabledBg
162
151
  if (showActive) {
163
- return {
164
- backgroundColor: isHovered ? hoverColors.pressed : greyRamp[900],
165
- ...EDGE_PRESSED,
166
- }
152
+ const fill = isHovered ? hoverColors.pressed : greyRamp[900]
153
+ return { backgroundColor: fill, ...getPressedRecessShadow(fill, mode) }
167
154
  }
168
155
  return {
169
156
  backgroundColor: isHovered ? hoverColors.raised : BUTTON_BG,
170
- ...EDGE,
157
+ ...liftStyle(1, mode),
171
158
  }
172
159
  }
173
160
 
@@ -202,20 +189,19 @@ export function ToolbarButton({
202
189
  >
203
190
  {icon && (
204
191
  <View className="w-5 h-5 items-center justify-center">
205
- {React.isValidElement(icon)
192
+ {React.isValidElement(icon)
206
193
  ? React.cloneElement(icon as React.ReactElement<any>, {
207
194
  size: 20,
208
195
  width: 20,
209
196
  height: 20,
210
197
  color: getIconColor(),
211
- style: {
198
+ style: {
212
199
  color: getIconColor(),
213
200
  width: 20,
214
201
  height: 20,
215
202
  },
216
203
  })
217
- : icon
218
- }
204
+ : icon}
219
205
  </View>
220
206
  )}
221
207
  {showLabel && (
@@ -237,22 +223,18 @@ export function ToolbarButton({
237
223
  {menuContent && isOpen && (
238
224
  <>
239
225
  {/* Backdrop */}
240
- <Pressable
241
- onPress={handleClose}
242
- style={StyleSheet.absoluteFill}
243
- className="z-40"
244
- />
245
- {/* Menu Content */}
246
- <View
226
+ <Pressable onPress={handleClose} style={StyleSheet.absoluteFill} className="z-40" />
227
+ {/* Menu Content — floating: overlay plane + lift, no ring. */}
228
+ <Surface
229
+ elevation={4}
230
+ rounded={false}
247
231
  className={cn(
248
232
  'absolute z-50 top-full left-0 mt-1',
249
- 'rounded-lg shadow-lg border border-hairline',
250
- 'min-w-[150px] overflow-hidden',
251
- `bg-[${MENU_BG}]`
233
+ 'rounded-lg min-w-[150px] overflow-hidden'
252
234
  )}
253
235
  >
254
236
  {menuContent}
255
- </View>
237
+ </Surface>
256
238
  </>
257
239
  )}
258
240
  </View>
@@ -10,7 +10,16 @@ const meta: Meta<typeof Tooltip> = {
10
10
  argTypes: {
11
11
  placement: {
12
12
  control: 'select',
13
- options: ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'right'],
13
+ options: [
14
+ 'top',
15
+ 'top-start',
16
+ 'top-end',
17
+ 'bottom',
18
+ 'bottom-start',
19
+ 'bottom-end',
20
+ 'left',
21
+ 'right',
22
+ ],
14
23
  },
15
24
  hasArrow: { control: 'boolean' },
16
25
  isDisabled: { control: 'boolean' },
@@ -118,8 +127,8 @@ export const OnText: Story = {
118
127
  Hover over the{' '}
119
128
  <Tooltip label="This is helpful information">
120
129
  <Text className="text-brand-primary underline">underlined text</Text>
121
- </Tooltip>
122
- {' '}to see a tooltip.
130
+ </Tooltip>{' '}
131
+ to see a tooltip.
123
132
  </Text>
124
133
  </View>
125
134
  ),
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest'
2
2
  import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { Text } from 'react-native'
3
4
  import { axe } from 'jest-axe'
4
5
  import { Tooltip } from './Tooltip'
5
6
 
@@ -202,9 +203,7 @@ describe('Tooltip', () => {
202
203
  hoverTrigger('Hover me')
203
204
  expect(screen.getByText('Portal tooltip')).toBeInTheDocument()
204
205
  expect(screen.getByTestId('tooltip-portal')).toBeInTheDocument()
205
- expect(screen.getByTestId('tooltip-portal').parentElement).toBe(
206
- document.body
207
- )
206
+ expect(screen.getByTestId('tooltip-portal').parentElement).toBe(document.body)
208
207
  })
209
208
 
210
209
  it('applies fixed positioning and z-index to portal tooltip', () => {
@@ -260,3 +259,22 @@ describe('Tooltip', () => {
260
259
  })
261
260
  })
262
261
  })
262
+
263
+ describe('controlled visibility', () => {
264
+ it('follows isOpen instead of its own hover, and renders no pressable wrapper', () => {
265
+ const { rerender } = render(
266
+ <Tooltip label="Controlled" isOpen={false}>
267
+ <Text>Trigger</Text>
268
+ </Tooltip>
269
+ )
270
+ hoverTrigger('Trigger')
271
+ expect(screen.queryByText('Controlled')).not.toBeInTheDocument()
272
+ rerender(
273
+ <Tooltip label="Controlled" isOpen>
274
+ <Text>Trigger</Text>
275
+ </Tooltip>
276
+ )
277
+ expect(screen.getByText('Controlled')).toBeInTheDocument()
278
+ expect(screen.getByText('Trigger').parentElement).not.toHaveAttribute('tabindex')
279
+ })
280
+ })
@@ -1,6 +1,8 @@
1
- import React, { useState, useRef, useEffect } from 'react'
2
- import { View, Text, Pressable, Platform, type ViewProps } from 'react-native'
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react'
2
+ import { View, Text, Platform, type ViewProps } from 'react-native'
3
3
  import { cn } from '../../../utils/cn'
4
+ import { Surface } from '../surface'
5
+ import { TriggerSurface } from '../trigger'
4
6
 
5
7
  let createPortal: typeof import('react-dom').createPortal | undefined
6
8
  if (Platform.OS === 'web') {
@@ -45,6 +47,21 @@ export interface TooltipProps extends ViewProps {
45
47
  className?: string
46
48
  /** Render tooltip via portal to escape overflow:hidden ancestors (web only) */
47
49
  usePortal?: boolean
50
+ /**
51
+ * Controlled visibility. In this mode the tooltip renders no Pressable of its
52
+ * own, so it can sit inside a pressable row without taking its press; the
53
+ * caller owns hover (see {@link useHoverState}). Needed because RNW ends a
54
+ * wrapper's hover the moment a nested Pressable claims the pointer.
55
+ */
56
+ isOpen?: boolean
57
+ }
58
+
59
+ /** Hover state from pointer enter/leave on any View, for driving a controlled Tooltip. */
60
+ export function useHoverState() {
61
+ const [hovered, setHovered] = useState(false)
62
+ const onPointerEnter = useCallback(() => setHovered(true), [])
63
+ const onPointerLeave = useCallback(() => setHovered(false), [])
64
+ return { hovered, hoverProps: { onPointerEnter, onPointerLeave } }
48
65
  }
49
66
 
50
67
  /**
@@ -74,9 +91,11 @@ export function Tooltip({
74
91
  isDisabled = false,
75
92
  className,
76
93
  usePortal: usePortalProp = false,
94
+ isOpen,
77
95
  ...props
78
96
  }: TooltipProps) {
79
- const [isVisible, setIsVisible] = useState(false)
97
+ const [hovered, setHovered] = useState(false)
98
+ const isVisible = isOpen ?? hovered
80
99
  const [portalPos, setPortalPos] = useState<PortalPosition | null>(null)
81
100
  const openTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
82
101
  const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
@@ -129,31 +148,37 @@ export function Tooltip({
129
148
  if (isDisabled) return
130
149
  clearTimeouts()
131
150
  if (openDelay > 0) {
132
- openTimeoutRef.current = setTimeout(() => setIsVisible(true), openDelay)
151
+ openTimeoutRef.current = setTimeout(() => setHovered(true), openDelay)
133
152
  } else {
134
- setIsVisible(true)
153
+ setHovered(true)
135
154
  }
136
155
  }
137
156
 
138
157
  const hide = () => {
139
158
  clearTimeouts()
140
159
  if (closeDelay > 0) {
141
- closeTimeoutRef.current = setTimeout(() => setIsVisible(false), closeDelay)
160
+ closeTimeoutRef.current = setTimeout(() => setHovered(false), closeDelay)
142
161
  } else {
143
- setIsVisible(false)
162
+ setHovered(false)
144
163
  }
145
164
  }
146
165
 
147
- // Arrow positioning
166
+ // Arrow positioning. The arrow is a border triangle, so it takes the same surface token as the box.
148
167
  const arrowStyles = {
149
- top: 'bottom-0 left-1/2 -translate-x-1/2 translate-y-full border-l-transparent border-r-transparent border-b-transparent border-t-neutral-800',
150
- 'top-start': 'bottom-0 left-4 translate-y-full border-l-transparent border-r-transparent border-b-transparent border-t-neutral-800',
151
- 'top-end': 'bottom-0 right-4 translate-y-full border-l-transparent border-r-transparent border-b-transparent border-t-neutral-800',
152
- bottom: 'top-0 left-1/2 -translate-x-1/2 -translate-y-full border-l-transparent border-r-transparent border-t-transparent border-b-neutral-800',
153
- 'bottom-start': 'top-0 left-4 -translate-y-full border-l-transparent border-r-transparent border-t-transparent border-b-neutral-800',
154
- 'bottom-end': 'top-0 right-4 -translate-y-full border-l-transparent border-r-transparent border-t-transparent border-b-neutral-800',
155
- left: 'right-0 top-1/2 translate-x-full -translate-y-1/2 border-t-transparent border-b-transparent border-r-transparent border-l-neutral-800',
156
- right: 'left-0 top-1/2 -translate-x-full -translate-y-1/2 border-t-transparent border-b-transparent border-l-transparent border-r-neutral-800',
168
+ top: 'bottom-0 left-1/2 -translate-x-1/2 translate-y-full border-l-transparent border-r-transparent border-b-transparent border-t-surface-overlay',
169
+ 'top-start':
170
+ 'bottom-0 left-4 translate-y-full border-l-transparent border-r-transparent border-b-transparent border-t-surface-overlay',
171
+ 'top-end':
172
+ 'bottom-0 right-4 translate-y-full border-l-transparent border-r-transparent border-b-transparent border-t-surface-overlay',
173
+ bottom:
174
+ 'top-0 left-1/2 -translate-x-1/2 -translate-y-full border-l-transparent border-r-transparent border-t-transparent border-b-surface-overlay',
175
+ 'bottom-start':
176
+ 'top-0 left-4 -translate-y-full border-l-transparent border-r-transparent border-t-transparent border-b-surface-overlay',
177
+ 'bottom-end':
178
+ 'top-0 right-4 -translate-y-full border-l-transparent border-r-transparent border-t-transparent border-b-surface-overlay',
179
+ left: 'right-0 top-1/2 translate-x-full -translate-y-1/2 border-t-transparent border-b-transparent border-r-transparent border-l-surface-overlay',
180
+ right:
181
+ 'left-0 top-1/2 -translate-x-full -translate-y-1/2 border-t-transparent border-b-transparent border-l-transparent border-r-surface-overlay',
157
182
  }
158
183
 
159
184
  // Tooltip positioning
@@ -168,18 +193,12 @@ export function Tooltip({
168
193
  right: 'left-full top-1/2 -translate-y-1/2 ml-2',
169
194
  }
170
195
 
196
+ // Floating: overlay plane + lift, no ring.
171
197
  const tooltipContent = (
172
- <View className="bg-neutral-800 px-3 py-2 rounded-md shadow-lg max-w-xs">
173
- {content ?? <Text className="text-white text-sm">{label}</Text>}
174
- {hasArrow && (
175
- <View
176
- className={cn(
177
- 'absolute w-0 h-0 border-4',
178
- arrowStyles[placement]
179
- )}
180
- />
181
- )}
182
- </View>
198
+ <Surface elevation={4} rounded={false} className="px-3 py-2 rounded-md max-w-xs">
199
+ {content ?? <Text className="text-text-primary text-sm">{label}</Text>}
200
+ {hasArrow && <View className={cn('absolute w-0 h-0 border-4', arrowStyles[placement])} />}
201
+ </Surface>
183
202
  )
184
203
 
185
204
  const renderPortalTooltip = () => {
@@ -205,15 +224,15 @@ export function Tooltip({
205
224
 
206
225
  return (
207
226
  <View className="relative" ref={triggerRef} {...props}>
208
- <Pressable
209
- onHoverIn={show}
210
- onHoverOut={hide}
211
- onLongPress={show}
212
- onPressOut={hide}
213
- delayLongPress={500}
214
- >
215
- {children}
216
- </Pressable>
227
+ {isOpen === undefined ? (
228
+ <TriggerSurface
229
+ handlers={{ onHoverIn: show, onHoverOut: hide, onLongPress: show, onPressOut: hide }}
230
+ >
231
+ {children}
232
+ </TriggerSurface>
233
+ ) : (
234
+ children
235
+ )}
217
236
 
218
237
  {isPortalMode
219
238
  ? renderPortalTooltip()
@@ -1,2 +1,2 @@
1
- export { Tooltip } from './Tooltip'
1
+ export { Tooltip, useHoverState } from './Tooltip'
2
2
  export type { TooltipProps, TooltipPlacement } from './Tooltip'
@@ -0,0 +1,215 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { View, Text } from 'react-native'
5
+ import { TriggerSurface } from './TriggerSurface'
6
+ import { Button, ButtonText } from '../button'
7
+ import { Menu, MenuTrigger, MenuList, MenuItem } from '../menu'
8
+ import { Popover, PopoverTrigger, PopoverContent } from '../popover'
9
+ import { Tooltip } from '../tooltip'
10
+
11
+ /**
12
+ * A real pointer press, not a bare synthetic click.
13
+ *
14
+ * The bug (AW-130) only shows when the whole gesture reaches the innermost
15
+ * element: React Native Web's PressResponder calls `stopPropagation()` on the
16
+ * click it handles, so an outer Pressable wrapping a Pressable child never runs
17
+ * its handler. `fireEvent.click` on the outer element sidesteps that entirely.
18
+ */
19
+ function pressWithPointer(element: Element) {
20
+ fireEvent.mouseDown(element, { button: 0, detail: 1 })
21
+ fireEvent.mouseUp(element, { button: 0, detail: 1 })
22
+ fireEvent.click(element, { button: 0, detail: 1 })
23
+ }
24
+
25
+ /**
26
+ * Hover the element itself. Enter events do not bubble, and RNW's `useHover`
27
+ * contains hover to the innermost node, so a wrapper never learns about a hover
28
+ * that lands on a Pressable child.
29
+ */
30
+ function hoverWithPointer(element: Element) {
31
+ fireEvent.pointerEnter(element)
32
+ fireEvent.mouseEnter(element)
33
+ }
34
+
35
+ describe('TriggerSurface', () => {
36
+ it('fires when the child is a Pressable component that claims the gesture', () => {
37
+ const onPress = vi.fn()
38
+ render(
39
+ <TriggerSurface handlers={{ onPress }} accessibilityRole="button">
40
+ <Button>
41
+ <ButtonText>Options</ButtonText>
42
+ </Button>
43
+ </TriggerSurface>
44
+ )
45
+
46
+ pressWithPointer(screen.getByText('Options'))
47
+ expect(onPress).toHaveBeenCalledTimes(1)
48
+ })
49
+
50
+ it('fires exactly once, not once per nested pressable', () => {
51
+ const onPress = vi.fn()
52
+ render(
53
+ <TriggerSurface handlers={{ onPress }} accessibilityRole="button">
54
+ <Button>
55
+ <ButtonText>Options</ButtonText>
56
+ </Button>
57
+ </TriggerSurface>
58
+ )
59
+
60
+ pressWithPointer(screen.getByText('Options'))
61
+ pressWithPointer(screen.getByText('Options'))
62
+ expect(onPress).toHaveBeenCalledTimes(2)
63
+ })
64
+
65
+ it('keeps the child own onPress alongside the trigger handler', () => {
66
+ const onPress = vi.fn()
67
+ const childPress = vi.fn()
68
+ render(
69
+ <TriggerSurface handlers={{ onPress }} accessibilityRole="button">
70
+ <Button onPress={childPress}>
71
+ <ButtonText>Options</ButtonText>
72
+ </Button>
73
+ </TriggerSurface>
74
+ )
75
+
76
+ pressWithPointer(screen.getByText('Options'))
77
+ expect(childPress).toHaveBeenCalledTimes(1)
78
+ expect(onPress).toHaveBeenCalledTimes(1)
79
+ })
80
+
81
+ it('fires when the child is a host element that cannot take onPress', () => {
82
+ const onPress = vi.fn()
83
+ render(
84
+ <TriggerSurface handlers={{ onPress }} accessibilityRole="button">
85
+ <span>Options</span>
86
+ </TriggerSurface>
87
+ )
88
+
89
+ pressWithPointer(screen.getByText('Options'))
90
+ expect(onPress).toHaveBeenCalledTimes(1)
91
+ })
92
+
93
+ it('fires when the child is a component with no press handling of its own', () => {
94
+ const onPress = vi.fn()
95
+ render(
96
+ <TriggerSurface handlers={{ onPress }} accessibilityRole="button">
97
+ <View>
98
+ <Text>Options</Text>
99
+ </View>
100
+ </TriggerSurface>
101
+ )
102
+
103
+ pressWithPointer(screen.getByText('Options'))
104
+ expect(onPress).toHaveBeenCalledTimes(1)
105
+ })
106
+
107
+ it('leaves a single button role rather than nesting two', () => {
108
+ const { container } = render(
109
+ <TriggerSurface
110
+ handlers={{ onPress: () => {} }}
111
+ accessibilityRole="button"
112
+ accessibilityState={{ expanded: false }}
113
+ >
114
+ <Button>
115
+ <ButtonText>Options</ButtonText>
116
+ </Button>
117
+ </TriggerSurface>
118
+ )
119
+
120
+ expect(container.querySelectorAll('[role="button"]')).toHaveLength(1)
121
+ expect(container.querySelector('button button')).toBeNull()
122
+ })
123
+
124
+ it('keeps the wrapper out of the tab order when the child owns the press', () => {
125
+ const { container } = render(
126
+ <TriggerSurface handlers={{ onPress: () => {} }} accessibilityRole="button">
127
+ <Button>
128
+ <ButtonText>Options</ButtonText>
129
+ </Button>
130
+ </TriggerSurface>
131
+ )
132
+
133
+ expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(1)
134
+ })
135
+
136
+ it('has no accessibility violations', async () => {
137
+ const { container } = render(
138
+ <TriggerSurface
139
+ handlers={{ onPress: () => {} }}
140
+ accessibilityRole="button"
141
+ accessibilityState={{ expanded: false }}
142
+ >
143
+ <Button>
144
+ <ButtonText>Options</ButtonText>
145
+ </Button>
146
+ </TriggerSurface>
147
+ )
148
+ expect(await axe(container)).toHaveNoViolations()
149
+ })
150
+ })
151
+
152
+ describe('trigger families under a real pointer press', () => {
153
+ it('Menu opens when its Button trigger is pressed', () => {
154
+ render(
155
+ <Menu>
156
+ <MenuTrigger>
157
+ <Button>
158
+ <ButtonText>Options</ButtonText>
159
+ </Button>
160
+ </MenuTrigger>
161
+ <MenuList>
162
+ <MenuItem>Edit</MenuItem>
163
+ </MenuList>
164
+ </Menu>
165
+ )
166
+
167
+ pressWithPointer(screen.getByText('Options'))
168
+ expect(screen.getByText('Edit')).toBeInTheDocument()
169
+ })
170
+
171
+ it('Popover opens when its Button trigger is pressed', () => {
172
+ render(
173
+ <Popover>
174
+ <PopoverTrigger>
175
+ <Button>
176
+ <ButtonText>Open</ButtonText>
177
+ </Button>
178
+ </PopoverTrigger>
179
+ <PopoverContent>
180
+ <Text>Details</Text>
181
+ </PopoverContent>
182
+ </Popover>
183
+ )
184
+
185
+ pressWithPointer(screen.getByText('Open'))
186
+ expect(screen.getByText('Details')).toBeInTheDocument()
187
+ })
188
+
189
+ it('Tooltip shows when its Button child is hovered', () => {
190
+ render(
191
+ <Tooltip label="Helpful hint">
192
+ <Button>
193
+ <ButtonText>Hover me</ButtonText>
194
+ </Button>
195
+ </Tooltip>
196
+ )
197
+
198
+ hoverWithPointer(screen.getByRole('button'))
199
+ expect(screen.getByText('Helpful hint')).toBeInTheDocument()
200
+ })
201
+
202
+ it('Tooltip leaves its Button child clickable', () => {
203
+ const onPress = vi.fn()
204
+ render(
205
+ <Tooltip label="Helpful hint">
206
+ <Button onPress={onPress}>
207
+ <ButtonText>Hover me</ButtonText>
208
+ </Button>
209
+ </Tooltip>
210
+ )
211
+
212
+ pressWithPointer(screen.getByText('Hover me'))
213
+ expect(onPress).toHaveBeenCalledTimes(1)
214
+ })
215
+ })