@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
@@ -0,0 +1,150 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import { Surface } from '../ui/surface'
4
+ import { Pill } from '../ui/pill'
5
+ import { Typography } from '../custom/Typography'
6
+ import { BotIcon, BrainIcon, HeadphonesIcon, LayersIcon, StarIcon } from '../icons'
7
+ import { AppShell } from './AppShell'
8
+ import { brandKeys } from './brands'
9
+ import { type SideNavItem } from './SideNav'
10
+
11
+ const brainNav: SideNavItem[] = [
12
+ { key: 'notes', label: 'Notes', icon: <LayersIcon size={20} color="currentColor" /> },
13
+ { key: 'graph', label: 'Graph', icon: <BrainIcon size={20} color="currentColor" /> },
14
+ { key: 'agents', label: 'Agents', icon: <BotIcon size={20} color="currentColor" /> },
15
+ ]
16
+
17
+ const audiobookNav: SideNavItem[] = [
18
+ { key: 'library', label: 'Library', icon: <HeadphonesIcon size={20} color="currentColor" /> },
19
+ { key: 'saved', label: 'Saved', icon: <StarIcon size={20} color="currentColor" /> },
20
+ ]
21
+
22
+ function Page({ title }: { title: string }) {
23
+ return (
24
+ <View className="flex-1 gap-1.5 p-7">
25
+ <Typography variant="h4" color="primary">
26
+ {title}
27
+ </Typography>
28
+ <Typography variant="body2" color="tertiary">
29
+ A page mounts its content here; the shell keeps the nav + top-bar chrome persistent.
30
+ </Typography>
31
+ </View>
32
+ )
33
+ }
34
+
35
+ /**
36
+ * `Pages/AppShell` — the generic dashboard chrome as one registered surface.
37
+ * The app supplies its brand, its nav categories and its own top-bar chrome.
38
+ */
39
+ const meta: Meta<typeof AppShell> = {
40
+ title: 'Pages/AppShell',
41
+ component: AppShell,
42
+ tags: ['autodocs', 'status:candidate', '!status:review'],
43
+ parameters: {
44
+ layout: 'fullscreen',
45
+ docs: {
46
+ description: {
47
+ component:
48
+ '**Page shell (generic).** Composes ' +
49
+ '[SideNav](?path=/docs/shell-sidenav--docs) + ' +
50
+ '[TopBar](?path=/docs/shell-topbar--docs) over a `children` content slot.\n\n' +
51
+ '**Composition (AW-132).** It knows nothing about any one app. Chrome arrives ' +
52
+ 'through `topBarTrailing`; categories through `navItems`; `topBar` and `nav` ' +
53
+ 'replace those regions outright. An app builds its own shell by composing this one ' +
54
+ '— see [WorkoutShell](?path=/docs/pages-workoutshell--docs).',
55
+ },
56
+ },
57
+ },
58
+ argTypes: {
59
+ brand: { control: 'select', options: brandKeys },
60
+ onNavigate: { control: false },
61
+ topBar: { control: false },
62
+ nav: { control: false },
63
+ topBarTrailing: { control: false },
64
+ navItems: { control: false },
65
+ },
66
+ decorators: [
67
+ (Story) => (
68
+ <Surface level="base" className="min-h-screen">
69
+ <Story />
70
+ </Surface>
71
+ ),
72
+ ],
73
+ }
74
+
75
+ export default meta
76
+ type Story = StoryObj<typeof AppShell>
77
+
78
+ /** No app chrome at all — the frame on its own. */
79
+ export const Default: Story = {
80
+ args: { brand: 'voltras', navItems: brainNav, activeKey: 'notes' },
81
+ }
82
+
83
+ /** The brain app: its own brand, categories, top-bar chrome and page. */
84
+ export const BrainApp: Story = {
85
+ args: {
86
+ brand: 'brain',
87
+ navItems: brainNav,
88
+ activeKey: 'graph',
89
+ topBarTrailing: [
90
+ <Pill key="index" tone="success" leading="dot" size="xs">
91
+ indexed
92
+ </Pill>,
93
+ <Pill key="vaults" tone="neutral" size="xs">
94
+ 4 vaults
95
+ </Pill>,
96
+ ],
97
+ },
98
+ render: (args) => (
99
+ <AppShell {...args}>
100
+ <Page title="Graph" />
101
+ </AppShell>
102
+ ),
103
+ }
104
+
105
+ /** The audiobook app in the same frame — different brand, categories and chrome. */
106
+ export const AudiobookApp: Story = {
107
+ args: {
108
+ brand: 'audiobook',
109
+ navItems: audiobookNav,
110
+ activeKey: 'library',
111
+ liveKey: 'saved',
112
+ topBarTrailing: (
113
+ <Pill tone="brand-secondary" leading="dot" size="xs">
114
+ 3 downloading
115
+ </Pill>
116
+ ),
117
+ },
118
+ render: (args) => (
119
+ <AppShell {...args}>
120
+ <Page title="Library" />
121
+ </AppShell>
122
+ ),
123
+ }
124
+
125
+ /** Both slots replaced: the app brings its own bar and its own rail. */
126
+ export const ReplacedRegions: Story = {
127
+ args: { navItems: brainNav, activeKey: 'notes' },
128
+ render: (args) => (
129
+ <AppShell
130
+ {...args}
131
+ topBar={
132
+ <Surface level="elevated" className="h-[46px] justify-center border-b border-hairline px-4">
133
+ <Text className="font-heading text-sm text-text-secondary">an app-owned bar</Text>
134
+ </Surface>
135
+ }
136
+ nav={
137
+ <Surface
138
+ level="base"
139
+ className="w-[60px] items-center justify-center border-r border-hairline"
140
+ >
141
+ <Typography variant="microLabel" color="tertiary">
142
+ rail
143
+ </Typography>
144
+ </Surface>
145
+ }
146
+ >
147
+ <Page title="Anything" />
148
+ </AppShell>
149
+ ),
150
+ }
@@ -0,0 +1,70 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { Text } from 'react-native'
4
+ import { axe } from 'jest-axe'
5
+ import { AppShell } from './AppShell'
6
+ import { type SideNavItem } from './SideNav'
7
+
8
+ const navItems: SideNavItem[] = [
9
+ { key: 'notes', label: 'Notes', icon: null },
10
+ { key: 'graph', label: 'Graph', icon: null },
11
+ ]
12
+
13
+ describe('AppShell', () => {
14
+ it('composes the SideNav rail from the supplied categories', () => {
15
+ render(<AppShell navItems={navItems} activeKey="notes" />)
16
+ expect(screen.getByRole('tablist')).toBeInTheDocument()
17
+ ;['Notes', 'Graph'].forEach((name) => {
18
+ expect(screen.getByRole('tab', { name })).toBeInTheDocument()
19
+ })
20
+ })
21
+
22
+ it('renders the content-slot placeholder when no children are given', () => {
23
+ render(<AppShell navItems={navItems} activeKey="notes" />)
24
+ expect(screen.getByText('main content region')).toBeInTheDocument()
25
+ })
26
+
27
+ it('mounts children in the content slot and drops the placeholder', () => {
28
+ render(
29
+ <AppShell navItems={navItems} activeKey="graph">
30
+ <Text>my page body</Text>
31
+ </AppShell>
32
+ )
33
+ expect(screen.getByText('my page body')).toBeInTheDocument()
34
+ expect(screen.queryByText('main content region')).not.toBeInTheDocument()
35
+ })
36
+
37
+ it('forwards nav taps through onNavigate', () => {
38
+ const onNavigate = vi.fn()
39
+ render(<AppShell navItems={navItems} activeKey="notes" onNavigate={onNavigate} />)
40
+ fireEvent.click(screen.getByRole('tab', { name: 'Graph' }))
41
+ expect(onNavigate).toHaveBeenCalledWith('graph')
42
+ })
43
+
44
+ it('renders app chrome in the top bar without knowing what it is', () => {
45
+ render(<AppShell navItems={navItems} activeKey="notes" topBarTrailing={<Text>3 jobs</Text>} />)
46
+ expect(screen.getByText('3 jobs')).toBeInTheDocument()
47
+ })
48
+
49
+ it('takes a whole replacement top bar and nav', () => {
50
+ render(
51
+ <AppShell topBar={<Text>my own bar</Text>} nav={<Text>my own rail</Text>}>
52
+ <Text>body</Text>
53
+ </AppShell>
54
+ )
55
+ expect(screen.getByText('my own bar')).toBeInTheDocument()
56
+ expect(screen.getByText('my own rail')).toBeInTheDocument()
57
+ expect(screen.queryByText('VOLTRAS')).not.toBeInTheDocument()
58
+ expect(screen.queryByRole('tablist')).not.toBeInTheDocument()
59
+ })
60
+
61
+ it('carries the brand through to the top bar', () => {
62
+ render(<AppShell brand="agents" navItems={navItems} activeKey="notes" />)
63
+ expect(screen.getByText('AGENTS')).toBeInTheDocument()
64
+ })
65
+
66
+ it('has no a11y violations', async () => {
67
+ const { container } = render(<AppShell navItems={navItems} activeKey="notes" />)
68
+ expect(await axe(container)).toHaveNoViolations()
69
+ })
70
+ })
@@ -0,0 +1,90 @@
1
+ import type { ReactNode } from 'react'
2
+ import { View, Text } from 'react-native'
3
+ import { cn } from '../../utils/cn'
4
+ import { Surface } from '../ui/surface'
5
+ import { SideNav, type SideNavItem } from './SideNav'
6
+ import { TopBar } from './TopBar'
7
+ import { brandPresets, type BrandKey } from './brands'
8
+
9
+ export interface AppShellProps {
10
+ /** Which app identity the default {@link TopBar} and nav accent render. */
11
+ brand?: BrandKey
12
+ /** Brand subtitle on the default top bar. */
13
+ subtitle?: string
14
+ /** App chrome for the default top bar's right cluster (divider-separated). */
15
+ topBarTrailing?: ReactNode | ReactNode[]
16
+ /** Replace the whole top bar. Wins over `brand` / `subtitle` / `topBarTrailing`. */
17
+ topBar?: ReactNode
18
+ /** Nav categories, top → bottom. */
19
+ navItems?: SideNavItem[]
20
+ /** Active nav category key. */
21
+ activeKey?: string
22
+ /** A category with off-view live activity → a quiet cue on that item. */
23
+ liveKey?: string | null
24
+ onNavigate?: (key: string) => void
25
+ /** Replace the whole left rail. Wins over `navItems` / `activeKey` / `liveKey`. */
26
+ nav?: ReactNode
27
+ /** Main content region. A placeholder renders when omitted. */
28
+ children?: ReactNode
29
+ className?: string
30
+ }
31
+
32
+ function ContentPlaceholder() {
33
+ return (
34
+ <View className="flex-1 items-center justify-center">
35
+ <Text className="font-heading text-sm text-text-tertiary">main content region</Text>
36
+ </View>
37
+ )
38
+ }
39
+
40
+ /**
41
+ * `AppShell` — the generic dashboard chrome: a {@link TopBar} band over a
42
+ * {@link SideNav} rail and a `children` content region. It knows nothing about
43
+ * any one app: chrome arrives through the `topBarTrailing` slot (or the whole
44
+ * `topBar` / `nav` slots), and categories through `navItems`. An app builds its
45
+ * own shell by composing this one — see `shell/workout/WorkoutShell`.
46
+ *
47
+ * @example
48
+ * <AppShell brand="brain" navItems={brainNavItems} activeKey={key} onNavigate={go}>
49
+ * <BrainPage />
50
+ * </AppShell>
51
+ */
52
+ export function AppShell({
53
+ brand = 'voltras',
54
+ subtitle,
55
+ topBarTrailing,
56
+ topBar,
57
+ navItems = [],
58
+ activeKey = '',
59
+ liveKey = null,
60
+ onNavigate,
61
+ nav,
62
+ children,
63
+ className,
64
+ }: AppShellProps) {
65
+ const { accentClassName, accentBarClassName } = brandPresets[brand]
66
+
67
+ return (
68
+ // Column: the TopBar spans the FULL width across the top, and the SideNav sits BELOW it
69
+ // in the content row (not a full-height left rail). This keeps the brand/status band
70
+ // unbroken edge-to-edge and lets the nav align under it. The shell is the outermost
71
+ // Surface — it owns the base plane and seeds the on-surface colour context (mode) for
72
+ // the whole dashboard tree.
73
+ <Surface level="base" className={cn('flex-1', className)}>
74
+ {topBar ?? <TopBar brand={brand} subtitle={subtitle} trailing={topBarTrailing} />}
75
+ <View className="flex-1 flex-row">
76
+ {nav ?? (
77
+ <SideNav
78
+ items={navItems}
79
+ activeKey={activeKey}
80
+ liveKey={liveKey}
81
+ accentClassName={accentClassName}
82
+ accentBarClassName={accentBarClassName}
83
+ onNavigate={onNavigate}
84
+ />
85
+ )}
86
+ <View className="flex-1">{children ?? <ContentPlaceholder />}</View>
87
+ </View>
88
+ </Surface>
89
+ )
90
+ }
@@ -1,22 +1,37 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { Surface } from '../ui/surface'
4
+ import { Typography } from '../custom/Typography'
5
+ import { StarIcon } from '../icons'
2
6
  import { BrandLockup } from './BrandLockup'
7
+ import { brandKeys } from './brands'
3
8
 
4
9
  const meta: Meta<typeof BrandLockup> = {
5
10
  title: 'Shell/BrandLockup',
6
11
  component: BrandLockup,
7
- tags: ['autodocs'],
8
- args: { subtitle: 'wall dashboard', showSubtitle: true },
12
+ tags: ['autodocs', 'status:candidate', '!status:review'],
13
+ args: { brand: 'voltras', showSubtitle: true },
9
14
  argTypes: {
15
+ brand: { control: 'select', options: brandKeys },
10
16
  subtitle: { control: 'text' },
17
+ wordmark: { control: 'text' },
18
+ accentClassName: { control: 'text' },
11
19
  showSubtitle: { control: 'boolean' },
20
+ mark: { control: false },
12
21
  },
13
22
  parameters: {
14
23
  docs: {
15
24
  description: {
16
25
  component:
17
- '**Molecule.** Composes [VoltrasMark](?path=/docs/foundations-icons--docs) (icon) + ' +
18
- '[Typography](?path=/docs/foundations-typography--docs) (wordmark + subtitle). ' +
19
- 'Toggle `showSubtitle` to see the responsive collapse.',
26
+ '**Molecule.** Composes an [icon](?path=/docs/foundations-icons--docs) mark + ' +
27
+ '[Typography](?path=/docs/foundations-typography--docs) (wordmark + subtitle).\n\n' +
28
+ '**Generic over the app (AW-132).** Pick a `brand` preset, or override `mark` / ' +
29
+ '`wordmark` / `accentClassName` / `subtitle` for an app that has no preset yet. ' +
30
+ "Accents come from `data-*` rather than `status-*`: `data-*` is the library's set of " +
31
+ 'distinct, CVD-checked hues with no semantic load, which is what a per-app identity ' +
32
+ 'accent needs. Voltras keeps the real `brand-primary` token.\n\n' +
33
+ '**Try it:** switch `brand` in the **Controls**, or toggle `showSubtitle` to see the ' +
34
+ 'responsive collapse.',
20
35
  },
21
36
  },
22
37
  },
@@ -25,3 +40,49 @@ export default meta
25
40
  type Story = StoryObj<typeof BrandLockup>
26
41
 
27
42
  export const Default: Story = {}
43
+
44
+ /** Every app likely to mount this shell, in the chrome band's own surface. */
45
+ export const BrandVariants: Story = {
46
+ parameters: {
47
+ docs: {
48
+ description: {
49
+ story:
50
+ 'The five branding presets. Each is a mark, a wordmark, a semantic accent token and a ' +
51
+ 'default subtitle — nothing else in the shell changes between them.',
52
+ },
53
+ },
54
+ },
55
+ render: () => (
56
+ <Surface level="elevated" className="gap-4 p-5">
57
+ {brandKeys.map((brand) => (
58
+ <View key={brand} className="gap-1">
59
+ <Typography variant="microLabel" color="tertiary">
60
+ {brand}
61
+ </Typography>
62
+ <BrandLockup brand={brand} />
63
+ </View>
64
+ ))}
65
+ </Surface>
66
+ ),
67
+ }
68
+
69
+ /** Collapsed subtitle — what every brand looks like below ~1024px. */
70
+ export const BrandVariantsCollapsed: Story = {
71
+ render: () => (
72
+ <Surface level="elevated" className="gap-4 p-5">
73
+ {brandKeys.map((brand) => (
74
+ <BrandLockup key={brand} brand={brand} showSubtitle={false} />
75
+ ))}
76
+ </Surface>
77
+ ),
78
+ }
79
+
80
+ /** An app with no preset: supply mark, wordmark, accent and subtitle directly. */
81
+ export const CustomBrand: Story = {
82
+ args: {
83
+ mark: <StarIcon size={14} color="currentColor" />,
84
+ wordmark: 'HYPERFRAMES',
85
+ subtitle: 'renders',
86
+ accentClassName: 'text-data-6',
87
+ },
88
+ }
@@ -2,9 +2,10 @@ import { describe, it, expect } from 'vitest'
2
2
  import { render, screen } from '@testing-library/react'
3
3
  import { axe } from 'jest-axe'
4
4
  import { BrandLockup } from './BrandLockup'
5
+ import { brandKeys, brandPresets } from './brands'
5
6
 
6
7
  describe('BrandLockup', () => {
7
- it('renders the wordmark', () => {
8
+ it('renders the voltras wordmark by default', () => {
8
9
  render(<BrandLockup />)
9
10
  expect(screen.getByText('VOLTRAS')).toBeInTheDocument()
10
11
  })
@@ -16,6 +17,38 @@ describe('BrandLockup', () => {
16
17
  expect(screen.queryByText('/ wall dashboard')).not.toBeInTheDocument()
17
18
  })
18
19
 
20
+ it.each(brandKeys)('renders the %s preset wordmark and subtitle', (brand) => {
21
+ render(<BrandLockup brand={brand} />)
22
+ expect(screen.getByText(brandPresets[brand].wordmark)).toBeInTheDocument()
23
+ expect(screen.getByText(`/ ${brandPresets[brand].subtitle}`)).toBeInTheDocument()
24
+ })
25
+
26
+ it('lets an app override the preset parts', () => {
27
+ render(<BrandLockup brand="brain" wordmark="HYPERFRAMES" subtitle="renders" />)
28
+ expect(screen.getByText('HYPERFRAMES')).toBeInTheDocument()
29
+ expect(screen.getByText('/ renders')).toBeInTheDocument()
30
+ expect(screen.queryByText('BRAIN')).not.toBeInTheDocument()
31
+ })
32
+
33
+ // nativewind compiles className to style, so the rendered accent colour is not
34
+ // assertable in jsdom. Assert the token CHOICE instead: every preset picks a
35
+ // semantic `text-*` token, and no two apps share one.
36
+ it('gives every brand a distinct semantic accent token', () => {
37
+ const accents = brandKeys.map((brand) => brandPresets[brand].accentClassName)
38
+ accents.forEach((accent) => expect(accent).toMatch(/^text-(brand|data)-/))
39
+ expect(new Set(accents).size).toBe(accents.length)
40
+ })
41
+
42
+ // The nav's active bar needs the accent as a background, and a mismatched pair
43
+ // would render a lockup and an active nav item in two different hues.
44
+ it('pairs every accent with the same token as a background', () => {
45
+ brandKeys.forEach((brand) => {
46
+ const { accentClassName, accentBarClassName } = brandPresets[brand]
47
+ expect(accentBarClassName).toMatch(/^bg-(brand|data)-/)
48
+ expect(accentBarClassName).toBe(accentClassName.replace(/^text-/, 'bg-'))
49
+ })
50
+ })
51
+
19
52
  it('has no accessibility violations', async () => {
20
53
  const { container } = render(<BrandLockup />)
21
54
  expect(await axe(container)).toHaveNoViolations()
@@ -1,10 +1,19 @@
1
+ import type { ReactNode } from 'react'
1
2
  import { View, type ViewProps } from 'react-native'
2
3
  import { cn } from '../../utils/cn'
3
4
  import { Typography } from '../custom/Typography'
4
- import { VoltrasMark } from '../icons'
5
+ import { brandPresets, type BrandKey } from './brands'
5
6
 
6
7
  export interface BrandLockupProps extends ViewProps {
7
- /** The "/ subtitle" that follows the wordmark. Default "wall dashboard". */
8
+ /** Which app's identity to render. Defaults to `voltras`. */
9
+ brand?: BrandKey
10
+ /** Replace the preset's mark glyph. */
11
+ mark?: ReactNode
12
+ /** Replace the preset's wordmark. */
13
+ wordmark?: string
14
+ /** Replace the preset's accent — a semantic `text-*` token class for the mark. */
15
+ accentClassName?: string
16
+ /** The "/ subtitle" that follows the wordmark. Defaults to the brand's own. */
8
17
  subtitle?: string
9
18
  /** Show the subtitle. Hidden at tablet width and below (S1-Q4 responsive). */
10
19
  showSubtitle?: boolean
@@ -12,34 +21,41 @@ export interface BrandLockupProps extends ViewProps {
12
21
  }
13
22
 
14
23
  /**
15
- * Product identity lockup for the top bar: mark + VOLTRAS wordmark + optional
16
- * subtitle. Composes the VoltrasMark icon + Typography. S1 · BrandLockup.
24
+ * Product identity lockup for the top bar: mark + wordmark + optional subtitle.
25
+ * Generic over the app: pick a `brand` preset, or override `mark` / `wordmark` /
26
+ * `accentClassName` / `subtitle` piecemeal for an app that has no preset yet.
27
+ * Composes an icon primitive + Typography. S1 · BrandLockup.
17
28
  */
18
29
  export function BrandLockup({
19
- subtitle = 'wall dashboard',
30
+ brand = 'voltras',
31
+ mark,
32
+ wordmark,
33
+ accentClassName,
34
+ subtitle,
20
35
  showSubtitle = true,
21
36
  className,
22
37
  ...props
23
38
  }: BrandLockupProps) {
39
+ const preset = brandPresets[brand]
40
+ const resolvedSubtitle = subtitle ?? preset.subtitle
41
+
24
42
  return (
25
43
  <View className={cn('flex-row items-center gap-[7px]', className)} {...props}>
26
- <View className="text-brand-primary">
27
- <VoltrasMark size={14} color="currentColor" />
28
- </View>
44
+ <View className={accentClassName ?? preset.accentClassName}>{mark ?? preset.mark}</View>
29
45
  <Typography
30
46
  variant="button"
31
47
  color="primary"
32
48
  className="text-[12px] font-extrabold tracking-[1.5px]"
33
49
  >
34
- VOLTRAS
50
+ {wordmark ?? preset.wordmark}
35
51
  </Typography>
36
- {showSubtitle && subtitle ? (
52
+ {showSubtitle && resolvedSubtitle ? (
37
53
  <Typography
38
54
  variant="button"
39
55
  color="tertiary"
40
56
  className="text-[10px] font-semibold tracking-[0.4px]"
41
57
  >
42
- / {subtitle}
58
+ / {resolvedSubtitle}
43
59
  </Typography>
44
60
  ) : null}
45
61
  </View>
@@ -1,12 +1,12 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
- import { View } from 'react-native'
3
2
  import { NavItem } from './NavItem'
4
3
  import { ActivityIcon } from '../icons'
4
+ import { Surface } from '../ui/surface'
5
5
 
6
6
  const meta: Meta<typeof NavItem> = {
7
7
  title: 'Shell/NavItem',
8
8
  component: NavItem,
9
- tags: ['autodocs'],
9
+ tags: ['autodocs', 'status:candidate', '!status:review'],
10
10
  args: { label: 'Live', active: false, live: false },
11
11
  argTypes: {
12
12
  label: { control: 'text' },
@@ -17,9 +17,9 @@ const meta: Meta<typeof NavItem> = {
17
17
  },
18
18
  decorators: [
19
19
  (Story) => (
20
- <View className="w-[60px] items-center bg-background-base py-2">
20
+ <Surface level="background" className="w-[60px] items-center py-2">
21
21
  <Story />
22
- </View>
22
+ </Surface>
23
23
  ),
24
24
  ],
25
25
  parameters: {
@@ -32,4 +32,21 @@ describe('NavItem', () => {
32
32
  const { container } = render(<NavItem icon={icon} label="Live" live />)
33
33
  expect(container.firstChild).toBeInTheDocument()
34
34
  })
35
+
36
+ // nativewind compiles className to style, so jsdom cannot see the accent colour.
37
+ // Assert an app-supplied accent leaves the rest of the active state intact; the
38
+ // colour itself is guarded by the paired-token test in BrandLockup.test.
39
+ it('keeps the active state when an app supplies its own accent', () => {
40
+ render(
41
+ <NavItem
42
+ icon={icon}
43
+ label="Graph"
44
+ active
45
+ accentClassName="text-data-3"
46
+ accentBarClassName="bg-data-3"
47
+ />
48
+ )
49
+ expect(screen.getByTestId('nav-item-accent')).toBeInTheDocument()
50
+ expect(screen.getByRole('tab', { name: 'Graph' })).toBeInTheDocument()
51
+ })
35
52
  })
@@ -8,13 +8,17 @@ export interface NavItemProps {
8
8
  icon: ReactNode
9
9
  /** Short label shown under the glyph (uppercased for display; used as the accessible name). */
10
10
  label: string
11
- /** Active category → left accent bar + brand-colored glyph & label. */
11
+ /** Active category → left accent bar + accent-colored glyph & label. */
12
12
  active?: boolean
13
13
  /**
14
14
  * A set is running for this category while it is NOT the active view
15
15
  * (live-elsewhere) → a quiet muted-green label tint. Ignored when `active`.
16
16
  */
17
17
  live?: boolean
18
+ /** Semantic `text-*` token for the active glyph and label. Defaults to the Voltras brand. */
19
+ accentClassName?: string
20
+ /** Semantic `bg-*` token for the active bar. Pair it with `accentClassName`. */
21
+ accentBarClassName?: string
18
22
  onPress?: () => void
19
23
  className?: string
20
24
  }
@@ -23,20 +27,25 @@ export interface NavItemProps {
23
27
  * Shell S2 · NavItem — one category button in the {@link SideNav}: a 20px glyph
24
28
  * over an uppercase micro-label in a 46×46 target. The button spans the full 60px
25
29
  * rail so the active **left accent bar** sits flush to the rail's edge. States:
26
- * active = accent bar + `brand-primary`; `live` (while not active) tints only the
30
+ * active = accent bar + the accent token; `live` (while not active) tints only the
27
31
  * label `status-success-dark` (the glyph stays dim); otherwise dim `text-tertiary`.
32
+ *
33
+ * The accent follows the mounting app's brand, so a Brain shell reads yellow
34
+ * throughout rather than showing a Voltras-orange active item under its own lockup.
28
35
  */
29
36
  export function NavItem({
30
37
  icon,
31
38
  label,
32
39
  active = false,
33
40
  live = false,
41
+ accentClassName = 'text-brand-primary',
42
+ accentBarClassName = 'bg-brand-primary',
34
43
  onPress,
35
44
  className,
36
45
  }: NavItemProps) {
37
- const glyphColor = active ? 'text-brand-primary' : 'text-text-tertiary'
46
+ const glyphColor = active ? accentClassName : 'text-text-tertiary'
38
47
  const labelColor = active
39
- ? 'text-brand-primary'
48
+ ? accentClassName
40
49
  : live
41
50
  ? 'text-status-live-muted'
42
51
  : 'text-text-tertiary'
@@ -52,7 +61,10 @@ export function NavItem({
52
61
  {active ? (
53
62
  <View
54
63
  testID="nav-item-accent"
55
- className="absolute left-0 top-[13px] bottom-[13px] w-[3px] rounded-r-[3px] bg-brand-primary"
64
+ className={cn(
65
+ 'absolute left-0 top-[13px] bottom-[13px] w-[3px] rounded-r-[3px]',
66
+ accentBarClassName
67
+ )}
56
68
  />
57
69
  ) : null}
58
70
  <View