@titan-design/react-ui 0.12.1 → 0.14.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 (531) hide show
  1. package/README.md +60 -47
  2. package/dist/bodymap.js +856 -736
  3. package/dist/bodymap.js.map +1 -1
  4. package/dist/bodymap.mjs +858 -738
  5. package/dist/bodymap.mjs.map +1 -1
  6. package/dist/index.d.mts +1431 -237
  7. package/dist/index.d.ts +1431 -237
  8. package/dist/index.js +6506 -4781
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +6280 -4642
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/{pages-B1hqnGIC.d.ts → pages-CsjsUn0G.d.ts} +51 -16
  13. package/dist/{pages-CvsFqNtx.d.mts → pages-ZbT_pD_U.d.mts} +51 -16
  14. package/dist/pages.d.mts +1 -1
  15. package/dist/pages.d.ts +1 -1
  16. package/dist/pages.js +1919 -1937
  17. package/dist/pages.js.map +1 -1
  18. package/dist/pages.mjs +1927 -1945
  19. package/dist/pages.mjs.map +1 -1
  20. package/dist/theme/index.d.mts +225 -140
  21. package/dist/theme/index.d.ts +225 -140
  22. package/dist/theme/index.js +298 -319
  23. package/dist/theme/index.js.map +1 -1
  24. package/dist/theme/index.mjs +283 -315
  25. package/dist/theme/index.mjs.map +1 -1
  26. package/dist/theme/tokens-css.js +145 -24
  27. package/dist/theme/tokens-css.js.map +1 -1
  28. package/dist/theme/tokens-css.mjs +145 -24
  29. package/dist/theme/tokens-css.mjs.map +1 -1
  30. package/dist/theme/tokens.d.mts +128 -11
  31. package/dist/theme/tokens.d.ts +128 -11
  32. package/dist/theme/tokens.js +126 -26
  33. package/dist/theme/tokens.js.map +1 -1
  34. package/dist/theme/tokens.mjs +124 -27
  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/docs/render-testing-pattern.md +130 -0
  40. package/package.json +1 -1
  41. package/src/arch/Architecture.stories.tsx +3 -1
  42. package/src/arch/arch-graph.freshness.test.ts +52 -0
  43. package/src/arch/arch-graph.json +4989 -1583
  44. package/src/components/custom/ActiveWork/CoChangeChip.stories.tsx +44 -0
  45. package/src/components/custom/ActiveWork/CoChangeChip.test.tsx +29 -0
  46. package/src/components/custom/ActiveWork/CoChangeChip.tsx +48 -0
  47. package/src/components/custom/ActiveWork/Eyebrow.stories.tsx +25 -0
  48. package/src/components/custom/ActiveWork/Eyebrow.test.tsx +16 -0
  49. package/src/components/custom/ActiveWork/Eyebrow.tsx +26 -0
  50. package/src/components/custom/ActiveWork/FileActivityDetail.stories.tsx +50 -0
  51. package/src/components/custom/ActiveWork/FileActivityDetail.test.tsx +66 -0
  52. package/src/components/custom/ActiveWork/FileActivityDetail.tsx +202 -0
  53. package/src/components/custom/ActiveWork/FileActivityRow.stories.tsx +70 -0
  54. package/src/components/custom/ActiveWork/FileActivityRow.test.tsx +67 -0
  55. package/src/components/custom/ActiveWork/FileActivityRow.tsx +130 -0
  56. package/src/components/custom/ActiveWork/FileHistoryExplorer.stories.tsx +84 -0
  57. package/src/components/custom/ActiveWork/FileHistoryExplorer.test.tsx +99 -0
  58. package/src/components/custom/ActiveWork/FileHistoryExplorer.tsx +138 -0
  59. package/src/components/custom/ActiveWork/FilePathLabel.stories.tsx +39 -0
  60. package/src/components/custom/ActiveWork/FilePathLabel.test.tsx +53 -0
  61. package/src/components/custom/ActiveWork/FilePathLabel.tsx +63 -0
  62. package/src/components/custom/ActiveWork/InitiativeBrief.stories.tsx +50 -0
  63. package/src/components/custom/ActiveWork/InitiativeBrief.test.tsx +62 -0
  64. package/src/components/custom/ActiveWork/InitiativeBrief.tsx +182 -0
  65. package/src/components/custom/ActiveWork/InitiativeCard.stories.tsx +73 -0
  66. package/src/components/custom/ActiveWork/InitiativeCard.test.tsx +68 -0
  67. package/src/components/custom/ActiveWork/InitiativeCard.tsx +136 -0
  68. package/src/components/custom/ActiveWork/InitiativeHeader.stories.tsx +59 -0
  69. package/src/components/custom/ActiveWork/InitiativeHeader.test.tsx +40 -0
  70. package/src/components/custom/ActiveWork/InitiativeHeader.tsx +73 -0
  71. package/src/components/custom/ActiveWork/InitiativeReader.stories.tsx +145 -0
  72. package/src/components/custom/ActiveWork/OpenLoops.stories.tsx +58 -0
  73. package/src/components/custom/ActiveWork/OpenLoops.test.tsx +61 -0
  74. package/src/components/custom/ActiveWork/OpenLoops.tsx +107 -0
  75. package/src/components/custom/ActiveWork/PortfolioOverview.stories.tsx +101 -0
  76. package/src/components/custom/ActiveWork/PortfolioOverview.test.tsx +81 -0
  77. package/src/components/custom/ActiveWork/PortfolioOverview.tsx +83 -0
  78. package/src/components/custom/ActiveWork/README.md +305 -0
  79. package/src/components/custom/ActiveWork/SessionDetail.stories.tsx +71 -0
  80. package/src/components/custom/ActiveWork/SessionDetail.test.tsx +84 -0
  81. package/src/components/custom/ActiveWork/SessionDetail.tsx +180 -0
  82. package/src/components/custom/ActiveWork/SessionList.stories.tsx +69 -0
  83. package/src/components/custom/ActiveWork/SessionList.test.tsx +78 -0
  84. package/src/components/custom/ActiveWork/SessionList.tsx +103 -0
  85. package/src/components/custom/ActiveWork/SessionListItem.stories.tsx +64 -0
  86. package/src/components/custom/ActiveWork/SessionListItem.test.tsx +62 -0
  87. package/src/components/custom/ActiveWork/SessionListItem.tsx +152 -0
  88. package/src/components/custom/ActiveWork/SessionReader.stories.tsx +113 -0
  89. package/src/components/custom/ActiveWork/SeverityLabel.stories.tsx +51 -0
  90. package/src/components/custom/ActiveWork/SeverityLabel.test.tsx +68 -0
  91. package/src/components/custom/ActiveWork/SeverityLabel.tsx +97 -0
  92. package/src/components/custom/ActiveWork/TaskRow.stories.tsx +63 -0
  93. package/src/components/custom/ActiveWork/TaskRow.test.tsx +112 -0
  94. package/src/components/custom/ActiveWork/TaskRow.tsx +176 -0
  95. package/src/components/custom/ActiveWork/TaskTable.stories.tsx +123 -0
  96. package/src/components/custom/ActiveWork/TaskTable.test.tsx +233 -0
  97. package/src/components/custom/ActiveWork/TaskTable.tsx +321 -0
  98. package/src/components/custom/ActiveWork/active-work-depth.test.tsx +102 -0
  99. package/src/components/custom/ActiveWork/file-history-fixture.ts +91 -0
  100. package/src/components/custom/ActiveWork/format-time.test.ts +23 -0
  101. package/src/components/custom/ActiveWork/format-time.ts +32 -0
  102. package/src/components/custom/ActiveWork/index.ts +19 -0
  103. package/src/components/custom/ActiveWork/initiative-fixture.ts +104 -0
  104. package/src/components/custom/ActiveWork/session-fixture.ts +267 -0
  105. package/src/components/custom/ActiveWork/session-linkers.ts +56 -0
  106. package/src/components/custom/ActiveWork/task-list-fixture.ts +97 -0
  107. package/src/components/custom/CircularTimer/CircularTimer.stories.tsx +12 -6
  108. package/src/components/custom/DateTime/DateTime.stories.tsx +2 -1
  109. package/src/components/custom/DateTime/DateTime.test.tsx +3 -1
  110. package/src/components/custom/EmptyState/EmptyState.stories.tsx +22 -9
  111. package/src/components/custom/EmptyState/EmptyState.test.tsx +7 -13
  112. package/src/components/custom/EmptyState/EmptyState.tsx +4 -18
  113. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +4 -3
  114. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +2 -2
  115. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +4 -3
  116. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +2 -2
  117. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +8 -4
  118. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +116 -6
  119. package/src/components/custom/Fatigue/LiveFatiguePanel.test.tsx +134 -0
  120. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +54 -83
  121. package/src/components/custom/Fatigue/README.md +40 -3
  122. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +2 -2
  123. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +2 -1
  124. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +4 -3
  125. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +2 -2
  126. package/src/components/custom/Fatigue/fatigue-mock.test.ts +0 -5
  127. package/src/components/custom/Fatigue/fatigue-mock.ts +2 -10
  128. package/src/components/custom/Fatigue/fatigue-tokens.ts +12 -0
  129. package/src/components/custom/Fatigue/index.ts +17 -5
  130. package/src/components/custom/Fatigue/panel-layout.test.ts +149 -0
  131. package/src/components/custom/Fatigue/panel-layout.ts +185 -0
  132. package/src/components/custom/Gauge/Gauge.stories.tsx +6 -3
  133. package/src/components/custom/Gauge/Gauge.test.tsx +1 -1
  134. package/src/components/custom/Gauge/Gauge.tsx +7 -2
  135. package/src/components/custom/Metric/Metric.stories.tsx +10 -1
  136. package/src/components/custom/Metric/Metric.test.tsx +1 -3
  137. package/src/components/custom/Metric/Metric.tsx +5 -20
  138. package/src/components/custom/Prose/MarkdownProse.stories.tsx +88 -0
  139. package/src/components/custom/Prose/MarkdownProse.test.tsx +87 -0
  140. package/src/components/custom/Prose/MarkdownProse.tsx +201 -0
  141. package/src/components/custom/Prose/README.md +58 -0
  142. package/src/components/custom/Prose/index.ts +1 -0
  143. package/src/components/custom/Scatter/Scatter.stories.tsx +55 -14
  144. package/src/components/custom/Scatter/Scatter.test.tsx +2 -2
  145. package/src/components/custom/Scatter/Scatter.tsx +122 -18
  146. package/src/components/custom/Sidebar/Sidebar.stories.tsx +41 -8
  147. package/src/components/custom/Sidebar/Sidebar.test.tsx +5 -1
  148. package/src/components/custom/Sidebar/Sidebar.tsx +16 -33
  149. package/src/components/custom/Table/Table.stories.tsx +99 -16
  150. package/src/components/custom/Table/Table.test.tsx +273 -86
  151. package/src/components/custom/Table/Table.tsx +159 -67
  152. package/src/components/custom/Table/column-fit.test.ts +75 -0
  153. package/src/components/custom/Table/column-fit.ts +116 -0
  154. package/src/components/custom/Table/index.ts +9 -0
  155. package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +8 -6
  156. package/src/components/custom/Treemap/Treemap.stories.tsx +15 -12
  157. package/src/components/custom/Treemap/Treemap.test.tsx +1 -1
  158. package/src/components/custom/Treemap/Treemap.tsx +11 -3
  159. package/src/components/custom/Typography/Typography.stories.tsx +28 -1
  160. package/src/components/custom/Typography/Typography.test.tsx +34 -0
  161. package/src/components/custom/Typography/Typography.tsx +6 -0
  162. package/src/components/custom/Typography/index.ts +1 -8
  163. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +11 -2
  164. package/src/components/custom/Workout/BaseBadge.stories.tsx +27 -11
  165. package/src/components/custom/Workout/BaseBadge.test.tsx +31 -11
  166. package/src/components/custom/Workout/BaseBadge.tsx +6 -2
  167. package/src/components/custom/Workout/BodyMap.stories.tsx +4 -1
  168. package/src/components/custom/Workout/BodyMap.tsx +8 -4
  169. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +28 -6
  170. package/src/components/custom/Workout/BodyMapDetailPanel.test.tsx +11 -15
  171. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +50 -12
  172. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +12 -9
  173. package/src/components/custom/Workout/CapacityBandChart.test.tsx +24 -51
  174. package/src/components/custom/Workout/CapacityBandChart.tsx +144 -111
  175. package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
  176. package/src/components/custom/Workout/DeviationBar.test.tsx +10 -4
  177. package/src/components/custom/Workout/DeviationBar.tsx +24 -16
  178. package/src/components/custom/Workout/DualVelocityStrip.stories.tsx +183 -0
  179. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +32 -18
  180. package/src/components/custom/Workout/ExerciseCard.stories.tsx +12 -11
  181. package/src/components/custom/Workout/ExerciseCard.test.tsx +52 -22
  182. package/src/components/custom/Workout/ExerciseCard.tsx +99 -183
  183. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +95 -12
  184. package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +189 -1
  185. package/src/components/custom/Workout/ExerciseCardHeading.tsx +167 -18
  186. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +9 -1
  187. package/src/components/custom/Workout/ExerciseDetailPage.tsx +3 -2
  188. package/src/components/custom/Workout/ExerciseHeading.stories.tsx +8 -8
  189. package/src/components/custom/Workout/ExerciseHeading.tsx +194 -46
  190. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +12 -9
  191. package/src/components/custom/Workout/ExerciseIndicator.test.tsx +27 -11
  192. package/src/components/custom/Workout/ExerciseIndicator.tsx +14 -15
  193. package/src/components/custom/Workout/FatigueMeter.stories.tsx +5 -5
  194. package/src/components/custom/Workout/InputBar.stories.tsx +4 -5
  195. package/src/components/custom/Workout/InputBar.tsx +4 -2
  196. package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
  197. package/src/components/custom/Workout/IntensityBar.test.tsx +5 -1
  198. package/src/components/custom/Workout/IntensityBar.tsx +8 -3
  199. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +5 -4
  200. package/src/components/custom/Workout/MesoCard.stories.tsx +4 -1
  201. package/src/components/custom/Workout/MesoCard.test.tsx +7 -18
  202. package/src/components/custom/Workout/MesoCard.tsx +14 -10
  203. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
  204. package/src/components/custom/Workout/MesoProgressBar.test.tsx +18 -11
  205. package/src/components/custom/Workout/MesoProgressBar.tsx +15 -13
  206. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +2 -1
  207. package/src/components/custom/Workout/MesoStatusCard.test.tsx +14 -44
  208. package/src/components/custom/Workout/MesoStatusCard.tsx +20 -33
  209. package/src/components/custom/Workout/MetricTiles.stories.tsx +3 -2
  210. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +2 -8
  211. package/src/components/custom/Workout/MuscleGroupChip.test.tsx +23 -31
  212. package/src/components/custom/Workout/MuscleGroupChip.tsx +28 -67
  213. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +9 -4
  214. package/src/components/custom/Workout/PlaceholderStrip.test.tsx +17 -10
  215. package/src/components/custom/Workout/PlaceholderStrip.tsx +9 -8
  216. package/src/components/custom/Workout/PrBadge.stories.tsx +2 -1
  217. package/src/components/custom/Workout/PrBadge.test.tsx +3 -9
  218. package/src/components/custom/Workout/PrBadge.tsx +13 -17
  219. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +16 -3
  220. package/src/components/custom/Workout/PrHistoryModal.test.tsx +13 -36
  221. package/src/components/custom/Workout/PrHistoryModal.tsx +8 -4
  222. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +6 -1
  223. package/src/components/custom/Workout/README.md +68 -17
  224. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +3 -2
  225. package/src/components/custom/Workout/ReadinessCheck.test.tsx +12 -18
  226. package/src/components/custom/Workout/ReadinessCheck.tsx +22 -4
  227. package/src/components/custom/Workout/RestTimer.stories.tsx +16 -4
  228. package/src/components/custom/Workout/RestTimer.tsx +4 -2
  229. package/src/components/custom/Workout/ScheduleTiles.stories.tsx +3 -2
  230. package/src/components/custom/Workout/ScheduleTiles.test.tsx +3 -4
  231. package/src/components/custom/Workout/ScheduleTiles.tsx +4 -7
  232. package/src/components/custom/Workout/SegmentedBar.stories.tsx +21 -17
  233. package/src/components/custom/Workout/SegmentedBar.tsx +10 -5
  234. package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +5 -5
  235. package/src/components/custom/Workout/SessionHeader.stories.tsx +6 -6
  236. package/src/components/custom/Workout/SessionHeader.tsx +12 -12
  237. package/src/components/custom/Workout/SessionRail.stories.tsx +12 -11
  238. package/src/components/custom/Workout/SessionRail.tsx +7 -9
  239. package/src/components/custom/Workout/SetBar.stories.tsx +10 -6
  240. package/src/components/custom/Workout/SetBar.test.tsx +49 -4
  241. package/src/components/custom/Workout/SetBar.tsx +38 -12
  242. package/src/components/custom/Workout/SetRow.stories.tsx +3 -2
  243. package/src/components/custom/Workout/SetRow.test.tsx +16 -2
  244. package/src/components/custom/Workout/SetRow.tsx +61 -23
  245. package/src/components/custom/Workout/SetStrip.stories.tsx +63 -7
  246. package/src/components/custom/Workout/SetStrip.test.tsx +25 -0
  247. package/src/components/custom/Workout/SetStrip.tsx +24 -4
  248. package/src/components/custom/Workout/SetTableHeader.stories.tsx +6 -6
  249. package/src/components/custom/Workout/SetTableHeader.tsx +9 -12
  250. package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +7 -7
  251. package/src/components/custom/Workout/SetsRepsLoad.tsx +24 -12
  252. package/src/components/custom/Workout/Sparkline.stories.tsx +14 -11
  253. package/src/components/custom/Workout/Sparkline.test.tsx +37 -19
  254. package/src/components/custom/Workout/Sparkline.tsx +14 -18
  255. package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
  256. package/src/components/custom/Workout/StatusDot.test.tsx +44 -5
  257. package/src/components/custom/Workout/StatusDot.tsx +93 -63
  258. package/src/components/custom/Workout/StatusPill.stories.tsx +1 -1
  259. package/src/components/custom/Workout/StatusPill.test.tsx +6 -15
  260. package/src/components/custom/Workout/StatusPill.tsx +27 -38
  261. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +11 -4
  262. package/src/components/custom/Workout/StrengthTrendChart.test.tsx +15 -37
  263. package/src/components/custom/Workout/StrengthTrendChart.tsx +39 -36
  264. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +27 -64
  265. package/src/components/custom/Workout/SupersetWrapper.test.tsx +16 -14
  266. package/src/components/custom/Workout/SupersetWrapper.tsx +17 -21
  267. package/src/components/custom/Workout/TempoDisplay.stories.tsx +26 -10
  268. package/src/components/custom/Workout/TempoDisplay.test.tsx +18 -4
  269. package/src/components/custom/Workout/TempoDisplay.tsx +2 -0
  270. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +9 -1
  271. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +4 -1
  272. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +10 -6
  273. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +5 -2
  274. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +44 -1
  275. package/src/components/custom/Workout/VelocityStrip.stories.tsx +12 -6
  276. package/src/components/custom/Workout/VelocityStrip.test.tsx +48 -6
  277. package/src/components/custom/Workout/VelocityStrip.tsx +26 -14
  278. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +6 -7
  279. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +15 -6
  280. package/src/components/custom/Workout/WeekRow.stories.tsx +4 -1
  281. package/src/components/custom/Workout/WeekRow.test.tsx +18 -15
  282. package/src/components/custom/Workout/WeekRow.tsx +23 -16
  283. package/src/components/custom/Workout/WeightBadge.stories.tsx +2 -1
  284. package/src/components/custom/Workout/WeightBadge.test.tsx +50 -19
  285. package/src/components/custom/Workout/WeightBadge.tsx +28 -27
  286. package/src/components/custom/Workout/WorkoutCard.stories.tsx +6 -1
  287. package/src/components/custom/Workout/WorkoutCard.test.tsx +18 -0
  288. package/src/components/custom/Workout/WorkoutCard.tsx +35 -57
  289. package/src/components/custom/Workout/WorkoutPill.stories.tsx +5 -2
  290. package/src/components/custom/Workout/WorkoutPill.test.tsx +39 -7
  291. package/src/components/custom/Workout/WorkoutPill.tsx +88 -57
  292. package/src/components/custom/Workout/ZoneTrack.stories.tsx +10 -7
  293. package/src/components/custom/Workout/ZoneTrack.tsx +10 -9
  294. package/src/components/custom/Workout/exerciseRowState.ts +29 -0
  295. package/src/components/custom/Workout/index.ts +17 -2
  296. package/src/components/custom/Workout/metricText.stories.tsx +15 -13
  297. package/src/components/custom/Workout/metricText.tsx +8 -2
  298. package/src/components/custom/Workout/setHeadingKit.tsx +219 -36
  299. package/src/components/custom/Workout/velocity-story-kit.tsx +2 -2
  300. package/src/components/custom/charts/README.md +66 -0
  301. package/src/components/custom/charts/SetBarChart.test.tsx +46 -1
  302. package/src/components/custom/charts/SetBarChart.tsx +56 -9
  303. package/src/components/custom/charts/SparkBars.stories.tsx +55 -0
  304. package/src/components/custom/charts/SparkBars.test.tsx +69 -0
  305. package/src/components/custom/charts/SparkBars.tsx +85 -0
  306. package/src/components/custom/charts/flatBarGeometry.test.ts +41 -0
  307. package/src/components/custom/charts/flatBarGeometry.ts +32 -0
  308. package/src/components/custom/charts/index.ts +3 -0
  309. package/src/components/custom/index.ts +3 -0
  310. package/src/components/custom/stepper/Stepper.stories.tsx +13 -10
  311. package/src/components/custom/stepper/Stepper.test.tsx +1 -7
  312. package/src/components/custom/stepper/Stepper.tsx +21 -34
  313. package/src/components/icons/icons.stories.tsx +1 -1
  314. package/src/components/icons/icons.tsx +51 -0
  315. package/src/components/shell/AppShell.stories.tsx +150 -0
  316. package/src/components/shell/AppShell.test.tsx +70 -0
  317. package/src/components/shell/AppShell.tsx +90 -0
  318. package/src/components/shell/BrandLockup.stories.tsx +66 -5
  319. package/src/components/shell/BrandLockup.test.tsx +34 -1
  320. package/src/components/shell/BrandLockup.tsx +27 -11
  321. package/src/components/shell/NavItem.stories.tsx +4 -4
  322. package/src/components/shell/NavItem.test.tsx +17 -0
  323. package/src/components/shell/NavItem.tsx +17 -5
  324. package/src/components/shell/README.md +89 -26
  325. package/src/components/shell/SideNav.stories.tsx +38 -7
  326. package/src/components/shell/SideNav.test.tsx +16 -8
  327. package/src/components/shell/SideNav.tsx +17 -17
  328. package/src/components/shell/TopBar.stories.tsx +44 -19
  329. package/src/components/shell/TopBar.test.tsx +34 -12
  330. package/src/components/shell/TopBar.tsx +61 -40
  331. package/src/components/shell/brands.tsx +66 -0
  332. package/src/components/shell/index.ts +9 -8
  333. package/src/components/shell/{DeviceIndicator.stories.tsx → workout/DeviceIndicator.stories.tsx} +2 -2
  334. package/src/components/shell/{DeviceIndicator.tsx → workout/DeviceIndicator.tsx} +2 -2
  335. package/src/components/shell/{DeviceMenu.stories.tsx → workout/DeviceMenu.stories.tsx} +2 -2
  336. package/src/components/shell/{DeviceMenu.tsx → workout/DeviceMenu.tsx} +3 -3
  337. package/src/components/shell/{DeviceRow.stories.tsx → workout/DeviceRow.stories.tsx} +2 -2
  338. package/src/components/shell/{DeviceRow.tsx → workout/DeviceRow.tsx} +3 -3
  339. package/src/components/shell/{SessionStatePill.stories.tsx → workout/SessionStatePill.stories.tsx} +2 -2
  340. package/src/components/shell/{SessionStatePill.tsx → workout/SessionStatePill.tsx} +5 -3
  341. package/src/components/shell/workout/WorkoutShell.stories.tsx +71 -0
  342. package/src/components/shell/{DashboardShell.test.tsx → workout/WorkoutShell.test.tsx} +19 -8
  343. package/src/components/shell/workout/WorkoutShell.tsx +77 -0
  344. package/src/components/shell/workout/WorkoutTopBar.stories.tsx +58 -0
  345. package/src/components/shell/workout/WorkoutTopBar.test.tsx +39 -0
  346. package/src/components/shell/workout/WorkoutTopBar.tsx +52 -0
  347. package/src/components/shell/workout/index.ts +9 -0
  348. package/src/components/shell/workout/workoutNavItems.tsx +16 -0
  349. package/src/components/ui/README.md +110 -0
  350. package/src/components/ui/alert/Alert.stories.tsx +15 -5
  351. package/src/components/ui/alert/Alert.tsx +24 -3
  352. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +11 -13
  353. package/src/components/ui/autocomplete/Autocomplete.test.tsx +7 -25
  354. package/src/components/ui/autocomplete/Autocomplete.tsx +21 -28
  355. package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
  356. package/src/components/ui/avatar/Avatar.tsx +2 -10
  357. package/src/components/ui/badge/Badge.stories.tsx +83 -28
  358. package/src/components/ui/badge/Badge.test.tsx +19 -3
  359. package/src/components/ui/badge/Badge.tsx +48 -55
  360. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  361. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +4 -16
  362. package/src/components/ui/button/Button.stories.tsx +46 -12
  363. package/src/components/ui/button/Button.tsx +44 -37
  364. package/src/components/ui/card/Card.stories.tsx +90 -94
  365. package/src/components/ui/card/Card.test.tsx +80 -6
  366. package/src/components/ui/card/Card.tsx +150 -203
  367. package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
  368. package/src/components/ui/checkbox/Checkbox.tsx +7 -27
  369. package/src/components/ui/chip/Chip.stories.tsx +37 -13
  370. package/src/components/ui/chip/Chip.test.tsx +10 -6
  371. package/src/components/ui/chip/Chip.tsx +76 -91
  372. package/src/components/ui/collapse/Collapse.stories.tsx +23 -23
  373. package/src/components/ui/collapse/Collapse.tsx +6 -11
  374. package/src/components/ui/data-row/DataRow.stories.tsx +14 -3
  375. package/src/components/ui/data-row/DataRow.test.tsx +5 -15
  376. package/src/components/ui/data-row/DataRow.tsx +18 -7
  377. package/src/components/ui/divider/Divider.stories.tsx +1 -1
  378. package/src/components/ui/divider/Divider.tsx +1 -5
  379. package/src/components/ui/drawer/Drawer.stories.tsx +5 -16
  380. package/src/components/ui/drawer/Drawer.tsx +13 -27
  381. package/src/components/ui/floating-overlay-depth.test.tsx +143 -0
  382. package/src/components/ui/form-field/FormField.stories.tsx +6 -20
  383. package/src/components/ui/form-field/FormField.test.tsx +1 -5
  384. package/src/components/ui/form-field/FormField.tsx +10 -46
  385. package/src/components/ui/help-tip/HelpTip.stories.tsx +8 -19
  386. package/src/components/ui/help-tip/HelpTip.test.tsx +1 -3
  387. package/src/components/ui/help-tip/HelpTip.tsx +21 -22
  388. package/src/components/ui/help-tip/index.ts +7 -1
  389. package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
  390. package/src/components/ui/icon-box/IconBox.test.tsx +1 -7
  391. package/src/components/ui/icon-box/IconBox.tsx +10 -18
  392. package/src/components/ui/indicator/Indicator.stories.tsx +5 -4
  393. package/src/components/ui/indicator/Indicator.tsx +20 -14
  394. package/src/components/ui/input/Input.stories.tsx +3 -11
  395. package/src/components/ui/input/Input.test.tsx +2 -10
  396. package/src/components/ui/input/Input.tsx +10 -25
  397. package/src/components/ui/input/index.ts +7 -1
  398. package/src/components/ui/link/Link.stories.tsx +19 -7
  399. package/src/components/ui/list-item/ListItem.stories.tsx +17 -5
  400. package/src/components/ui/list-item/ListItem.tsx +2 -9
  401. package/src/components/ui/menu/Menu.stories.tsx +21 -15
  402. package/src/components/ui/menu/Menu.test.tsx +2 -11
  403. package/src/components/ui/menu/Menu.tsx +12 -14
  404. package/src/components/ui/menu/index.ts +8 -1
  405. package/src/components/ui/modal/Modal.stories.tsx +2 -2
  406. package/src/components/ui/modal/Modal.tsx +31 -19
  407. package/src/components/ui/pill/Pill.stories.tsx +111 -23
  408. package/src/components/ui/pill/Pill.test.tsx +101 -3
  409. package/src/components/ui/pill/Pill.tsx +168 -49
  410. package/src/components/ui/pill/index.ts +1 -1
  411. package/src/components/ui/popover/Popover.stories.tsx +6 -8
  412. package/src/components/ui/popover/Popover.test.tsx +1 -6
  413. package/src/components/ui/popover/Popover.tsx +20 -15
  414. package/src/components/ui/popover/index.ts +7 -1
  415. package/src/components/ui/progress/Progress.stories.tsx +3 -12
  416. package/src/components/ui/progress/Progress.test.tsx +1 -5
  417. package/src/components/ui/progress/Progress.tsx +9 -6
  418. package/src/components/ui/radio/Radio.stories.tsx +42 -21
  419. package/src/components/ui/radio/Radio.test.tsx +6 -2
  420. package/src/components/ui/radio/Radio.tsx +23 -22
  421. package/src/components/ui/section/Section.stories.tsx +12 -21
  422. package/src/components/ui/section/Section.test.tsx +1 -6
  423. package/src/components/ui/section/Section.tsx +1 -3
  424. package/src/components/ui/section/index.ts +1 -5
  425. package/src/components/ui/select/Select.stories.tsx +3 -13
  426. package/src/components/ui/select/Select.test.tsx +2 -11
  427. package/src/components/ui/select/Select.tsx +16 -15
  428. package/src/components/ui/skeleton/Skeleton.stories.tsx +3 -11
  429. package/src/components/ui/skeleton/Skeleton.tsx +3 -14
  430. package/src/components/ui/skeleton/index.ts +1 -7
  431. package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
  432. package/src/components/ui/spinner/Spinner.tsx +4 -5
  433. package/src/components/ui/stack/Stack.stories.tsx +8 -8
  434. package/src/components/ui/stack/Stack.tsx +1 -1
  435. package/src/components/ui/surface/Surface.stories.tsx +92 -33
  436. package/src/components/ui/surface/Surface.test.tsx +88 -28
  437. package/src/components/ui/surface/Surface.tsx +37 -87
  438. package/src/components/ui/surface/SurfaceContext.ts +22 -62
  439. package/src/components/ui/surface/index.ts +8 -0
  440. package/src/components/ui/surface/resolveSurface.ts +98 -0
  441. package/src/components/ui/surface/surface.contract.test.ts +6 -1
  442. package/src/components/ui/switch/Switch.stories.tsx +1 -1
  443. package/src/components/ui/switch/Switch.tsx +7 -6
  444. package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
  445. package/src/components/ui/tabs/Tabs.tsx +8 -21
  446. package/src/components/ui/tabs/index.ts +1 -7
  447. package/src/components/ui/tile/Tile.stories.tsx +1 -1
  448. package/src/components/ui/tile/Tile.tsx +3 -0
  449. package/src/components/ui/toast/Toast.stories.tsx +1 -1
  450. package/src/components/ui/toast/Toast.test.tsx +2 -8
  451. package/src/components/ui/toast/Toast.tsx +49 -57
  452. package/src/components/ui/toast/index.ts +0 -1
  453. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +92 -71
  454. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +3 -18
  455. package/src/components/ui/toolbar-button/ToolbarButton.tsx +38 -49
  456. package/src/components/ui/tooltip/Tooltip.stories.tsx +12 -3
  457. package/src/components/ui/tooltip/Tooltip.test.tsx +21 -3
  458. package/src/components/ui/tooltip/Tooltip.tsx +55 -36
  459. package/src/components/ui/tooltip/index.ts +1 -1
  460. package/src/components/ui/trigger/TriggerSurface.test.tsx +215 -0
  461. package/src/components/ui/trigger/TriggerSurface.tsx +81 -0
  462. package/src/components/ui/trigger/index.ts +2 -0
  463. package/src/stories/PresetShowcase.stories.tsx +101 -48
  464. package/src/test/no-device-internals.test.ts +3 -3
  465. package/src/test/no-raw-color.test.ts +39 -0
  466. package/src/theme/Alpha.stories.tsx +103 -0
  467. package/src/theme/ChoosingTokens.stories.tsx +48 -0
  468. package/src/theme/ColorPalettes.stories.tsx +62 -45
  469. package/src/theme/ColorPrimitives.stories.tsx +35 -35
  470. package/src/theme/Depth.stories.tsx +132 -65
  471. package/src/theme/DepthCalibration.stories.tsx +23 -21
  472. package/src/theme/ProposedTokensVW82.stories.tsx +414 -0
  473. package/src/theme/ThemeProvider.test.tsx +4 -4
  474. package/src/theme/barrel.ts +7 -6
  475. package/src/theme/color-stories.coverage.test.ts +1 -1
  476. package/src/theme/color-utils.ts +41 -29
  477. package/src/theme/config.completeness.test.ts +1 -1
  478. package/src/theme/config.ts +40 -0
  479. package/src/theme/elevation.test.ts +82 -56
  480. package/src/theme/elevation.ts +77 -561
  481. package/src/theme/global.css +45 -5
  482. package/src/theme/grey-ramp.test.ts +24 -8
  483. package/src/theme/lift.test.ts +39 -0
  484. package/src/theme/lift.ts +102 -0
  485. package/src/theme/materials.test.ts +5 -2
  486. package/src/theme/materials.ts +38 -4
  487. package/src/theme/native-theming-guard.test.ts +5 -5
  488. package/src/theme/presets/apply.ts +4 -0
  489. package/src/theme/presets/audiobook.ts +5 -2
  490. package/src/theme/presets/default.ts +4 -0
  491. package/src/theme/presets/index.ts +14 -1
  492. package/src/theme/presets/types.ts +1 -1
  493. package/src/theme/semantic-contrast.test.ts +13 -4
  494. package/src/theme/status-distinctness.test.ts +66 -0
  495. package/src/theme/subtle-tone-weight.test.ts +163 -0
  496. package/src/theme/surface-planes.test.ts +37 -0
  497. package/src/theme/surface-planes.ts +68 -0
  498. package/src/theme/tailwind-var-opacity.test.ts +163 -0
  499. package/src/theme/token-export-guard.test.ts +1 -3
  500. package/src/theme/tokens/primitives.test.ts +15 -7
  501. package/src/theme/tokens/primitives.ts +98 -33
  502. package/src/theme/tokens/semantic-hex-guard.test.ts +322 -0
  503. package/src/theme/tokens/semantic.ts +124 -54
  504. package/src/theme/wash-ladder.test.ts +58 -0
  505. package/src/theme/workout-tokens.ts +1 -7
  506. package/src/types/jest-axe.d.ts +2 -6
  507. package/src/types/raw-md.d.ts +5 -0
  508. package/src/utils/colors.alpha-adoption.test.ts +408 -0
  509. package/src/utils/colors.ts +4 -4
  510. package/src/utils/form.ts +1 -3
  511. package/src/utils/index.ts +2 -7
  512. package/src/utils/number-format.ts +17 -0
  513. package/src/utils/workout-format.ts +15 -6
  514. package/src/web-jsx/jsx-runtime.ts +1 -5
  515. package/tailwind.config.js +55 -9
  516. package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +0 -151
  517. package/src/components/custom/Workout/RowInventory.stories.tsx +0 -408
  518. package/src/components/custom/Workout/S3FullRail.stories.tsx +0 -46
  519. package/src/components/custom/Workout/S3SessionPace.stories.tsx +0 -95
  520. package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +0 -61
  521. package/src/components/custom/Workout/S3SetTypes.stories.tsx +0 -176
  522. package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +0 -240
  523. package/src/components/shell/DashboardShell.stories.tsx +0 -65
  524. package/src/components/shell/DashboardShell.tsx +0 -75
  525. package/src/components/shell/SessionRailSpecimen.stories.tsx +0 -753
  526. package/src/theme/elevation.stories.tsx +0 -346
  527. package/src/utils/useTheme.ts +0 -72
  528. /package/src/components/shell/{DeviceIndicator.test.tsx → workout/DeviceIndicator.test.tsx} +0 -0
  529. /package/src/components/shell/{DeviceMenu.test.tsx → workout/DeviceMenu.test.tsx} +0 -0
  530. /package/src/components/shell/{DeviceRow.test.tsx → workout/DeviceRow.test.tsx} +0 -0
  531. /package/src/components/shell/{SessionStatePill.test.tsx → workout/SessionStatePill.test.tsx} +0 -0
@@ -2,9 +2,12 @@ import React, { useState, useCallback, createContext, useContext } from 'react'
2
2
  import { View, Text, Pressable, type ViewProps, StyleSheet, Platform } from 'react-native'
3
3
  import { cn } from '../../../utils/cn'
4
4
  import { getHoverColors } from '../../../theme'
5
- import { greyRamp } from '../../../theme/tokens/primitives'
6
- import { getSemanticColors } from '../../../theme/tokens/semantic'
5
+ import { greyRamp, primitiveColors } from '../../../theme/tokens/primitives'
7
6
  import { resolveColor } from '../../../theme/resolve-color'
7
+ import { getPressedRecessShadow } from '../../../theme/elevation'
8
+ import { liftStyle } from '../../../theme/lift'
9
+ import { alpha } from '../../../utils/colors'
10
+ import { Surface, useSurfaceMode } from '../surface'
8
11
 
9
12
  export type ToolbarButtonVariant = 'default' | 'raised'
10
13
  export type ToolbarButtonSize = 'sm' | 'md' | 'lg'
@@ -14,6 +17,14 @@ interface ToolbarButtonContextType {
14
17
  setIsOpen: (open: boolean) => void
15
18
  }
16
19
 
20
+ /**
21
+ * Idle icon/label tone. VW-82: no token resolves to this — it sits between
22
+ * `text-secondary` (#A29F9D) and `text-primary` (#F9F6F3) and is cooler than
23
+ * either, so snapping it to the warm grey ramp is a visual change awaiting the
24
+ * proposed `on-control-idle` role rather than a silent swap.
25
+ */
26
+ const ICON_IDLE = '#D1D1D1'
27
+
17
28
  const ToolbarButtonContext = createContext<ToolbarButtonContextType>({
18
29
  isOpen: false,
19
30
  setIsOpen: () => {},
@@ -24,7 +35,7 @@ export interface ToolbarButtonProps extends ViewProps {
24
35
  label: string
25
36
  /** Icon component */
26
37
  icon?: React.ReactNode
27
- /**
38
+ /**
28
39
  * Whether the button is in active/selected state.
29
40
  * - `true`: pressed/active appearance (darker, white text, orange icon)
30
41
  * - `undefined`: pressed appearance (darker, white text/icon)
@@ -49,19 +60,8 @@ export interface ToolbarButtonProps extends ViewProps {
49
60
  className?: string
50
61
  }
51
62
 
52
- const SEMANTIC = getSemanticColors('dark')
53
-
54
- // Base button colours. These were raw `#3C3C3C`/`#2C2C2C` literals — old cold
55
- // charcoal steps that survived the grey migration only because they were plain
56
- // strings rather than scale references.
63
+ // Base button colours.
57
64
  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
65
 
66
66
  // Calculate hover colors using color math
67
67
  const hoverColors = getHoverColors(BUTTON_BG, 'medium')
@@ -83,11 +83,11 @@ const textSizeStyles: Record<ToolbarButtonSize, string> = {
83
83
  * ToolbarButton component for toolbar actions with toggle state support.
84
84
  *
85
85
  * Features:
86
- * - Neumorphic styling with inset shadows for pressed/raised 3D effect
86
+ * - Depth from the fill plus the lift (raised) or the inset recess (active)
87
87
  * - Active (pressed) state is the default visual appearance
88
88
  * - Set isActive={false} explicitly for the raised/inactive appearance
89
89
  * - Orange accent color on icon when isActive={true}
90
- * - Hover states that lighten background and remove shadows
90
+ * - Hover states that lighten the background
91
91
  *
92
92
  * @example
93
93
  * // Toggle button - orange icon when active
@@ -123,6 +123,7 @@ export function ToolbarButton({
123
123
  }: ToolbarButtonProps) {
124
124
  const [isOpen, setIsOpen] = useState(false)
125
125
  const [isHovered, setIsHovered] = useState(false)
126
+ const mode = useSurfaceMode()
126
127
 
127
128
  const handlePress = useCallback(() => {
128
129
  if (isDisabled) return
@@ -143,31 +144,24 @@ export function ToolbarButton({
143
144
  // Determine icon color based on state
144
145
  const getIconColor = () => {
145
146
  if (isActive === true) return resolveColor('brand-primary')
146
- return showActive ? '#FFFFFF' : '#D1D1D1'
147
+ return showActive ? primitiveColors.white : ICON_IDLE
147
148
  }
148
149
 
149
- // Get shadow and background styles for neumorphic variant
150
150
  /**
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.
151
+ * Raised/pressed treatment on the depth model: the raised face is a plane one
152
+ * step above the toolbar and wears the lift, the active face is pressed into
153
+ * it and wears the recess. The hairline ring both used to carry is an edge,
154
+ * not depth, and is gone with the rest of the rings on this pass.
157
155
  */
158
156
  const getRaisedStyle = () => {
159
- if (isDisabled) {
160
- return styles.disabledBg
161
- }
157
+ if (isDisabled) return styles.disabledBg
162
158
  if (showActive) {
163
- return {
164
- backgroundColor: isHovered ? hoverColors.pressed : greyRamp[900],
165
- ...EDGE_PRESSED,
166
- }
159
+ const fill = isHovered ? hoverColors.pressed : greyRamp[900]
160
+ return { backgroundColor: fill, ...getPressedRecessShadow(fill, mode) }
167
161
  }
168
162
  return {
169
163
  backgroundColor: isHovered ? hoverColors.raised : BUTTON_BG,
170
- ...EDGE,
164
+ ...liftStyle(1, mode),
171
165
  }
172
166
  }
173
167
 
@@ -202,20 +196,19 @@ export function ToolbarButton({
202
196
  >
203
197
  {icon && (
204
198
  <View className="w-5 h-5 items-center justify-center">
205
- {React.isValidElement(icon)
199
+ {React.isValidElement(icon)
206
200
  ? React.cloneElement(icon as React.ReactElement<any>, {
207
201
  size: 20,
208
202
  width: 20,
209
203
  height: 20,
210
204
  color: getIconColor(),
211
- style: {
205
+ style: {
212
206
  color: getIconColor(),
213
207
  width: 20,
214
208
  height: 20,
215
209
  },
216
210
  })
217
- : icon
218
- }
211
+ : icon}
219
212
  </View>
220
213
  )}
221
214
  {showLabel && (
@@ -237,22 +230,18 @@ export function ToolbarButton({
237
230
  {menuContent && isOpen && (
238
231
  <>
239
232
  {/* Backdrop */}
240
- <Pressable
241
- onPress={handleClose}
242
- style={StyleSheet.absoluteFill}
243
- className="z-40"
244
- />
245
- {/* Menu Content */}
246
- <View
233
+ <Pressable onPress={handleClose} style={StyleSheet.absoluteFill} className="z-40" />
234
+ {/* Menu Content — floating: overlay plane + lift, no ring. */}
235
+ <Surface
236
+ elevation={4}
237
+ rounded={false}
247
238
  className={cn(
248
239
  '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}]`
240
+ 'rounded-lg min-w-[150px] overflow-hidden'
252
241
  )}
253
242
  >
254
243
  {menuContent}
255
- </View>
244
+ </Surface>
256
245
  </>
257
246
  )}
258
247
  </View>
@@ -264,7 +253,7 @@ export function ToolbarButton({
264
253
  const styles = StyleSheet.create({
265
254
  // Disabled - flat gray background, no shadows
266
255
  disabledBg: {
267
- backgroundColor: 'rgba(255, 255, 255, 0.12)',
256
+ backgroundColor: alpha(primitiveColors.white, 0.12),
268
257
  ...Platform.select({
269
258
  web: { boxShadow: 'none' } as any,
270
259
  default: { shadowOpacity: 0, elevation: 0 },
@@ -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
+ })