@titan-design/react-ui 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (509) hide show
  1. package/README.md +46 -47
  2. package/dist/bodymap.js +768 -701
  3. package/dist/bodymap.js.map +1 -1
  4. package/dist/bodymap.mjs +770 -703
  5. package/dist/bodymap.mjs.map +1 -1
  6. package/dist/index.d.mts +1320 -215
  7. package/dist/index.d.ts +1320 -215
  8. package/dist/index.js +5996 -4498
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +5773 -4357
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/{pages-CvsFqNtx.d.mts → pages-CpW3OB3g.d.mts} +21 -3
  13. package/dist/{pages-B1hqnGIC.d.ts → pages-_3Ajpiqu.d.ts} +21 -3
  14. package/dist/pages.d.mts +1 -1
  15. package/dist/pages.d.ts +1 -1
  16. package/dist/pages.js +1435 -1428
  17. package/dist/pages.js.map +1 -1
  18. package/dist/pages.mjs +1443 -1436
  19. package/dist/pages.mjs.map +1 -1
  20. package/dist/theme/index.d.mts +202 -1042
  21. package/dist/theme/index.d.ts +202 -1042
  22. package/dist/theme/index.js +215 -300
  23. package/dist/theme/index.js.map +1 -1
  24. package/dist/theme/index.mjs +202 -296
  25. package/dist/theme/index.mjs.map +1 -1
  26. package/dist/theme/tokens-css.js +62 -5
  27. package/dist/theme/tokens-css.js.map +1 -1
  28. package/dist/theme/tokens-css.mjs +62 -5
  29. package/dist/theme/tokens-css.mjs.map +1 -1
  30. package/dist/theme/tokens.d.mts +972 -0
  31. package/dist/theme/tokens.d.ts +972 -0
  32. package/dist/theme/tokens.js +904 -0
  33. package/dist/theme/tokens.js.map +1 -0
  34. package/dist/theme/tokens.mjs +880 -0
  35. package/dist/theme/tokens.mjs.map +1 -0
  36. package/docs/audits/2026-09-08-critique-addenda.md +51 -0
  37. package/docs/audits/2026-09-08-library-critique.md +212 -0
  38. package/docs/library-roadmap.md +99 -0
  39. package/package.json +7 -1
  40. package/src/arch/arch-graph.freshness.test.ts +52 -0
  41. package/src/arch/arch-graph.json +5021 -1617
  42. package/src/components/custom/ActiveWork/CoChangeChip.stories.tsx +44 -0
  43. package/src/components/custom/ActiveWork/CoChangeChip.test.tsx +29 -0
  44. package/src/components/custom/ActiveWork/CoChangeChip.tsx +48 -0
  45. package/src/components/custom/ActiveWork/Eyebrow.stories.tsx +25 -0
  46. package/src/components/custom/ActiveWork/Eyebrow.test.tsx +16 -0
  47. package/src/components/custom/ActiveWork/Eyebrow.tsx +26 -0
  48. package/src/components/custom/ActiveWork/FileActivityDetail.stories.tsx +50 -0
  49. package/src/components/custom/ActiveWork/FileActivityDetail.test.tsx +66 -0
  50. package/src/components/custom/ActiveWork/FileActivityDetail.tsx +202 -0
  51. package/src/components/custom/ActiveWork/FileActivityRow.stories.tsx +70 -0
  52. package/src/components/custom/ActiveWork/FileActivityRow.test.tsx +67 -0
  53. package/src/components/custom/ActiveWork/FileActivityRow.tsx +130 -0
  54. package/src/components/custom/ActiveWork/FileHistoryExplorer.stories.tsx +84 -0
  55. package/src/components/custom/ActiveWork/FileHistoryExplorer.test.tsx +99 -0
  56. package/src/components/custom/ActiveWork/FileHistoryExplorer.tsx +138 -0
  57. package/src/components/custom/ActiveWork/FilePathLabel.stories.tsx +39 -0
  58. package/src/components/custom/ActiveWork/FilePathLabel.test.tsx +53 -0
  59. package/src/components/custom/ActiveWork/FilePathLabel.tsx +63 -0
  60. package/src/components/custom/ActiveWork/InitiativeBrief.stories.tsx +50 -0
  61. package/src/components/custom/ActiveWork/InitiativeBrief.test.tsx +62 -0
  62. package/src/components/custom/ActiveWork/InitiativeBrief.tsx +181 -0
  63. package/src/components/custom/ActiveWork/InitiativeCard.stories.tsx +73 -0
  64. package/src/components/custom/ActiveWork/InitiativeCard.test.tsx +68 -0
  65. package/src/components/custom/ActiveWork/InitiativeCard.tsx +136 -0
  66. package/src/components/custom/ActiveWork/InitiativeHeader.stories.tsx +59 -0
  67. package/src/components/custom/ActiveWork/InitiativeHeader.test.tsx +40 -0
  68. package/src/components/custom/ActiveWork/InitiativeHeader.tsx +73 -0
  69. package/src/components/custom/ActiveWork/InitiativeReader.stories.tsx +145 -0
  70. package/src/components/custom/ActiveWork/OpenLoops.stories.tsx +58 -0
  71. package/src/components/custom/ActiveWork/OpenLoops.test.tsx +61 -0
  72. package/src/components/custom/ActiveWork/OpenLoops.tsx +107 -0
  73. package/src/components/custom/ActiveWork/PortfolioOverview.stories.tsx +101 -0
  74. package/src/components/custom/ActiveWork/PortfolioOverview.test.tsx +81 -0
  75. package/src/components/custom/ActiveWork/PortfolioOverview.tsx +83 -0
  76. package/src/components/custom/ActiveWork/README.md +305 -0
  77. package/src/components/custom/ActiveWork/SessionDetail.stories.tsx +71 -0
  78. package/src/components/custom/ActiveWork/SessionDetail.test.tsx +84 -0
  79. package/src/components/custom/ActiveWork/SessionDetail.tsx +180 -0
  80. package/src/components/custom/ActiveWork/SessionList.stories.tsx +69 -0
  81. package/src/components/custom/ActiveWork/SessionList.test.tsx +78 -0
  82. package/src/components/custom/ActiveWork/SessionList.tsx +103 -0
  83. package/src/components/custom/ActiveWork/SessionListItem.stories.tsx +64 -0
  84. package/src/components/custom/ActiveWork/SessionListItem.test.tsx +62 -0
  85. package/src/components/custom/ActiveWork/SessionListItem.tsx +152 -0
  86. package/src/components/custom/ActiveWork/SessionReader.stories.tsx +113 -0
  87. package/src/components/custom/ActiveWork/SeverityLabel.stories.tsx +51 -0
  88. package/src/components/custom/ActiveWork/SeverityLabel.test.tsx +68 -0
  89. package/src/components/custom/ActiveWork/SeverityLabel.tsx +97 -0
  90. package/src/components/custom/ActiveWork/TaskRow.stories.tsx +63 -0
  91. package/src/components/custom/ActiveWork/TaskRow.test.tsx +112 -0
  92. package/src/components/custom/ActiveWork/TaskRow.tsx +176 -0
  93. package/src/components/custom/ActiveWork/TaskTable.stories.tsx +123 -0
  94. package/src/components/custom/ActiveWork/TaskTable.test.tsx +233 -0
  95. package/src/components/custom/ActiveWork/TaskTable.tsx +321 -0
  96. package/src/components/custom/ActiveWork/active-work-depth.test.tsx +102 -0
  97. package/src/components/custom/ActiveWork/file-history-fixture.ts +91 -0
  98. package/src/components/custom/ActiveWork/format-time.test.ts +23 -0
  99. package/src/components/custom/ActiveWork/format-time.ts +32 -0
  100. package/src/components/custom/ActiveWork/index.ts +19 -0
  101. package/src/components/custom/ActiveWork/initiative-fixture.ts +104 -0
  102. package/src/components/custom/ActiveWork/session-fixture.ts +267 -0
  103. package/src/components/custom/ActiveWork/session-linkers.ts +56 -0
  104. package/src/components/custom/ActiveWork/task-list-fixture.ts +97 -0
  105. package/src/components/custom/CircularTimer/CircularTimer.stories.tsx +4 -3
  106. package/src/components/custom/DateTime/DateTime.stories.tsx +2 -1
  107. package/src/components/custom/DateTime/DateTime.test.tsx +3 -1
  108. package/src/components/custom/EmptyState/EmptyState.stories.tsx +22 -9
  109. package/src/components/custom/EmptyState/EmptyState.test.tsx +7 -13
  110. package/src/components/custom/EmptyState/EmptyState.tsx +4 -18
  111. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +4 -3
  112. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +2 -2
  113. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +4 -3
  114. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +2 -2
  115. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +8 -4
  116. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +116 -6
  117. package/src/components/custom/Fatigue/LiveFatiguePanel.test.tsx +134 -0
  118. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +54 -83
  119. package/src/components/custom/Fatigue/README.md +40 -3
  120. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +2 -2
  121. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +2 -1
  122. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +4 -3
  123. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +2 -2
  124. package/src/components/custom/Fatigue/fatigue-mock.test.ts +0 -5
  125. package/src/components/custom/Fatigue/fatigue-mock.ts +2 -10
  126. package/src/components/custom/Fatigue/fatigue-tokens.ts +12 -0
  127. package/src/components/custom/Fatigue/index.ts +17 -5
  128. package/src/components/custom/Fatigue/panel-layout.test.ts +149 -0
  129. package/src/components/custom/Fatigue/panel-layout.ts +185 -0
  130. package/src/components/custom/Gauge/Gauge.stories.tsx +2 -2
  131. package/src/components/custom/Gauge/Gauge.test.tsx +1 -1
  132. package/src/components/custom/Gauge/Gauge.tsx +4 -1
  133. package/src/components/custom/Metric/Metric.stories.tsx +10 -1
  134. package/src/components/custom/Metric/Metric.test.tsx +1 -3
  135. package/src/components/custom/Metric/Metric.tsx +5 -20
  136. package/src/components/custom/Prose/MarkdownProse.stories.tsx +88 -0
  137. package/src/components/custom/Prose/MarkdownProse.test.tsx +87 -0
  138. package/src/components/custom/Prose/MarkdownProse.tsx +201 -0
  139. package/src/components/custom/Prose/README.md +58 -0
  140. package/src/components/custom/Prose/index.ts +1 -0
  141. package/src/components/custom/Scatter/Scatter.stories.tsx +46 -8
  142. package/src/components/custom/Scatter/Scatter.test.tsx +2 -2
  143. package/src/components/custom/Scatter/Scatter.tsx +116 -14
  144. package/src/components/custom/Sidebar/Sidebar.stories.tsx +40 -7
  145. package/src/components/custom/Sidebar/Sidebar.test.tsx +5 -1
  146. package/src/components/custom/Sidebar/Sidebar.tsx +16 -33
  147. package/src/components/custom/Table/Table.stories.tsx +98 -15
  148. package/src/components/custom/Table/Table.test.tsx +273 -86
  149. package/src/components/custom/Table/Table.tsx +159 -67
  150. package/src/components/custom/Table/column-fit.test.ts +75 -0
  151. package/src/components/custom/Table/column-fit.ts +116 -0
  152. package/src/components/custom/Table/index.ts +9 -0
  153. package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +8 -6
  154. package/src/components/custom/Treemap/Treemap.stories.tsx +2 -2
  155. package/src/components/custom/Treemap/Treemap.test.tsx +1 -1
  156. package/src/components/custom/Treemap/Treemap.tsx +7 -2
  157. package/src/components/custom/Typography/Typography.stories.tsx +28 -1
  158. package/src/components/custom/Typography/Typography.test.tsx +34 -0
  159. package/src/components/custom/Typography/Typography.tsx +6 -0
  160. package/src/components/custom/Typography/index.ts +1 -8
  161. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +9 -1
  162. package/src/components/custom/Workout/BaseBadge.stories.tsx +19 -7
  163. package/src/components/custom/Workout/BaseBadge.test.tsx +31 -11
  164. package/src/components/custom/Workout/BaseBadge.tsx +3 -0
  165. package/src/components/custom/Workout/BodyMap.stories.tsx +4 -1
  166. package/src/components/custom/Workout/BodyMap.tsx +3 -1
  167. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +16 -4
  168. package/src/components/custom/Workout/BodyMapDetailPanel.test.tsx +11 -15
  169. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +45 -9
  170. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +10 -8
  171. package/src/components/custom/Workout/CapacityBandChart.test.tsx +24 -51
  172. package/src/components/custom/Workout/CapacityBandChart.tsx +139 -108
  173. package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
  174. package/src/components/custom/Workout/DeviationBar.test.tsx +5 -2
  175. package/src/components/custom/Workout/DeviationBar.tsx +21 -16
  176. package/src/components/custom/Workout/DualVelocityStrip.stories.tsx +182 -0
  177. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +3 -2
  178. package/src/components/custom/Workout/ExerciseCard.stories.tsx +8 -7
  179. package/src/components/custom/Workout/ExerciseCard.test.tsx +13 -3
  180. package/src/components/custom/Workout/ExerciseCard.tsx +42 -51
  181. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +7 -7
  182. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +9 -1
  183. package/src/components/custom/Workout/ExerciseHeading.stories.tsx +8 -8
  184. package/src/components/custom/Workout/ExerciseHeading.tsx +8 -5
  185. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +6 -4
  186. package/src/components/custom/Workout/ExerciseIndicator.test.tsx +27 -11
  187. package/src/components/custom/Workout/ExerciseIndicator.tsx +14 -15
  188. package/src/components/custom/Workout/FatigueMeter.stories.tsx +5 -5
  189. package/src/components/custom/Workout/InputBar.stories.tsx +4 -5
  190. package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
  191. package/src/components/custom/Workout/IntensityBar.test.tsx +5 -1
  192. package/src/components/custom/Workout/IntensityBar.tsx +4 -2
  193. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +5 -4
  194. package/src/components/custom/Workout/MesoCard.stories.tsx +4 -1
  195. package/src/components/custom/Workout/MesoCard.test.tsx +7 -18
  196. package/src/components/custom/Workout/MesoCard.tsx +12 -9
  197. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
  198. package/src/components/custom/Workout/MesoProgressBar.test.tsx +18 -11
  199. package/src/components/custom/Workout/MesoProgressBar.tsx +15 -13
  200. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +2 -1
  201. package/src/components/custom/Workout/MesoStatusCard.test.tsx +14 -44
  202. package/src/components/custom/Workout/MesoStatusCard.tsx +9 -23
  203. package/src/components/custom/Workout/MetricTiles.stories.tsx +3 -2
  204. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +2 -8
  205. package/src/components/custom/Workout/MuscleGroupChip.test.tsx +23 -31
  206. package/src/components/custom/Workout/MuscleGroupChip.tsx +28 -67
  207. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
  208. package/src/components/custom/Workout/PlaceholderStrip.test.tsx +17 -10
  209. package/src/components/custom/Workout/PlaceholderStrip.tsx +9 -8
  210. package/src/components/custom/Workout/PrBadge.stories.tsx +2 -1
  211. package/src/components/custom/Workout/PrBadge.test.tsx +3 -9
  212. package/src/components/custom/Workout/PrBadge.tsx +13 -17
  213. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +4 -1
  214. package/src/components/custom/Workout/PrHistoryModal.test.tsx +13 -36
  215. package/src/components/custom/Workout/PrHistoryModal.tsx +8 -4
  216. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +6 -1
  217. package/src/components/custom/Workout/README.md +12 -7
  218. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +3 -2
  219. package/src/components/custom/Workout/ReadinessCheck.test.tsx +12 -18
  220. package/src/components/custom/Workout/ReadinessCheck.tsx +19 -3
  221. package/src/components/custom/Workout/RestTimer.stories.tsx +3 -2
  222. package/src/components/custom/Workout/ScheduleTiles.stories.tsx +3 -2
  223. package/src/components/custom/Workout/ScheduleTiles.test.tsx +3 -4
  224. package/src/components/custom/Workout/ScheduleTiles.tsx +4 -7
  225. package/src/components/custom/Workout/SegmentedBar.stories.tsx +5 -5
  226. package/src/components/custom/Workout/SegmentedBar.tsx +6 -2
  227. package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +5 -5
  228. package/src/components/custom/Workout/SessionHeader.stories.tsx +6 -6
  229. package/src/components/custom/Workout/SessionHeader.tsx +12 -12
  230. package/src/components/custom/Workout/SessionRail.stories.tsx +12 -11
  231. package/src/components/custom/Workout/SessionRail.tsx +7 -9
  232. package/src/components/custom/Workout/SetBar.stories.tsx +10 -6
  233. package/src/components/custom/Workout/SetBar.test.tsx +49 -4
  234. package/src/components/custom/Workout/SetBar.tsx +38 -12
  235. package/src/components/custom/Workout/SetRow.stories.tsx +3 -2
  236. package/src/components/custom/Workout/SetRow.test.tsx +16 -2
  237. package/src/components/custom/Workout/SetRow.tsx +61 -23
  238. package/src/components/custom/Workout/SetStrip.stories.tsx +60 -5
  239. package/src/components/custom/Workout/SetStrip.test.tsx +25 -0
  240. package/src/components/custom/Workout/SetStrip.tsx +24 -4
  241. package/src/components/custom/Workout/SetTableHeader.stories.tsx +6 -6
  242. package/src/components/custom/Workout/SetTableHeader.tsx +9 -12
  243. package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +7 -7
  244. package/src/components/custom/Workout/SetsRepsLoad.tsx +3 -5
  245. package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
  246. package/src/components/custom/Workout/Sparkline.test.tsx +37 -19
  247. package/src/components/custom/Workout/Sparkline.tsx +14 -18
  248. package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
  249. package/src/components/custom/Workout/StatusDot.test.tsx +44 -5
  250. package/src/components/custom/Workout/StatusDot.tsx +93 -63
  251. package/src/components/custom/Workout/StatusPill.stories.tsx +1 -1
  252. package/src/components/custom/Workout/StatusPill.test.tsx +6 -15
  253. package/src/components/custom/Workout/StatusPill.tsx +27 -38
  254. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +9 -3
  255. package/src/components/custom/Workout/StrengthTrendChart.test.tsx +15 -37
  256. package/src/components/custom/Workout/StrengthTrendChart.tsx +31 -30
  257. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +27 -64
  258. package/src/components/custom/Workout/SupersetWrapper.test.tsx +16 -14
  259. package/src/components/custom/Workout/SupersetWrapper.tsx +17 -21
  260. package/src/components/custom/Workout/TempoDisplay.stories.tsx +22 -7
  261. package/src/components/custom/Workout/TempoDisplay.test.tsx +18 -4
  262. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +9 -1
  263. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +4 -1
  264. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +7 -4
  265. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +4 -1
  266. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +4 -1
  267. package/src/components/custom/Workout/VelocityStrip.stories.tsx +12 -6
  268. package/src/components/custom/Workout/VelocityStrip.tsx +3 -0
  269. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +6 -7
  270. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +15 -6
  271. package/src/components/custom/Workout/WeekRow.stories.tsx +4 -1
  272. package/src/components/custom/Workout/WeekRow.test.tsx +18 -15
  273. package/src/components/custom/Workout/WeekRow.tsx +23 -16
  274. package/src/components/custom/Workout/WeightBadge.stories.tsx +2 -1
  275. package/src/components/custom/Workout/WeightBadge.test.tsx +50 -19
  276. package/src/components/custom/Workout/WeightBadge.tsx +28 -27
  277. package/src/components/custom/Workout/WorkoutCard.stories.tsx +6 -1
  278. package/src/components/custom/Workout/WorkoutCard.test.tsx +18 -0
  279. package/src/components/custom/Workout/WorkoutCard.tsx +35 -57
  280. package/src/components/custom/Workout/WorkoutPill.stories.tsx +5 -2
  281. package/src/components/custom/Workout/WorkoutPill.test.tsx +39 -7
  282. package/src/components/custom/Workout/WorkoutPill.tsx +88 -57
  283. package/src/components/custom/Workout/ZoneTrack.stories.tsx +7 -6
  284. package/src/components/custom/Workout/ZoneTrack.tsx +10 -9
  285. package/src/components/custom/Workout/index.ts +5 -0
  286. package/src/components/custom/Workout/metricText.stories.tsx +6 -5
  287. package/src/components/custom/Workout/metricText.tsx +4 -0
  288. package/src/components/custom/Workout/setHeadingKit.tsx +207 -26
  289. package/src/components/custom/charts/README.md +61 -0
  290. package/src/components/custom/charts/SetBarChart.test.tsx +30 -1
  291. package/src/components/custom/charts/SetBarChart.tsx +42 -4
  292. package/src/components/custom/charts/SparkBars.stories.tsx +55 -0
  293. package/src/components/custom/charts/SparkBars.test.tsx +69 -0
  294. package/src/components/custom/charts/SparkBars.tsx +85 -0
  295. package/src/components/custom/charts/index.ts +3 -0
  296. package/src/components/custom/index.ts +3 -0
  297. package/src/components/custom/stepper/Stepper.stories.tsx +13 -10
  298. package/src/components/custom/stepper/Stepper.test.tsx +1 -7
  299. package/src/components/custom/stepper/Stepper.tsx +13 -28
  300. package/src/components/icons/icons.stories.tsx +1 -1
  301. package/src/components/icons/icons.tsx +51 -0
  302. package/src/components/shell/AppShell.stories.tsx +150 -0
  303. package/src/components/shell/AppShell.test.tsx +70 -0
  304. package/src/components/shell/AppShell.tsx +90 -0
  305. package/src/components/shell/BrandLockup.stories.tsx +66 -5
  306. package/src/components/shell/BrandLockup.test.tsx +34 -1
  307. package/src/components/shell/BrandLockup.tsx +27 -11
  308. package/src/components/shell/NavItem.stories.tsx +4 -4
  309. package/src/components/shell/NavItem.test.tsx +17 -0
  310. package/src/components/shell/NavItem.tsx +17 -5
  311. package/src/components/shell/README.md +90 -27
  312. package/src/components/shell/SideNav.stories.tsx +38 -7
  313. package/src/components/shell/SideNav.test.tsx +16 -8
  314. package/src/components/shell/SideNav.tsx +17 -17
  315. package/src/components/shell/TopBar.stories.tsx +44 -19
  316. package/src/components/shell/TopBar.test.tsx +34 -12
  317. package/src/components/shell/TopBar.tsx +61 -40
  318. package/src/components/shell/brands.tsx +66 -0
  319. package/src/components/shell/index.ts +9 -8
  320. package/src/components/shell/{DeviceIndicator.stories.tsx → workout/DeviceIndicator.stories.tsx} +2 -2
  321. package/src/components/shell/{DeviceIndicator.tsx → workout/DeviceIndicator.tsx} +2 -2
  322. package/src/components/shell/{DeviceMenu.stories.tsx → workout/DeviceMenu.stories.tsx} +2 -2
  323. package/src/components/shell/{DeviceMenu.tsx → workout/DeviceMenu.tsx} +3 -3
  324. package/src/components/shell/{DeviceRow.stories.tsx → workout/DeviceRow.stories.tsx} +2 -2
  325. package/src/components/shell/{DeviceRow.tsx → workout/DeviceRow.tsx} +3 -3
  326. package/src/components/shell/{SessionStatePill.stories.tsx → workout/SessionStatePill.stories.tsx} +2 -2
  327. package/src/components/shell/{SessionStatePill.tsx → workout/SessionStatePill.tsx} +5 -3
  328. package/src/components/shell/workout/WorkoutShell.stories.tsx +71 -0
  329. package/src/components/shell/{DashboardShell.test.tsx → workout/WorkoutShell.test.tsx} +19 -8
  330. package/src/components/shell/workout/WorkoutShell.tsx +77 -0
  331. package/src/components/shell/workout/WorkoutTopBar.stories.tsx +58 -0
  332. package/src/components/shell/workout/WorkoutTopBar.test.tsx +39 -0
  333. package/src/components/shell/workout/WorkoutTopBar.tsx +52 -0
  334. package/src/components/shell/workout/index.ts +9 -0
  335. package/src/components/shell/workout/workoutNavItems.tsx +16 -0
  336. package/src/components/ui/README.md +110 -0
  337. package/src/components/ui/alert/Alert.stories.tsx +1 -1
  338. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +11 -13
  339. package/src/components/ui/autocomplete/Autocomplete.test.tsx +7 -25
  340. package/src/components/ui/autocomplete/Autocomplete.tsx +21 -28
  341. package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
  342. package/src/components/ui/avatar/Avatar.tsx +2 -10
  343. package/src/components/ui/badge/Badge.stories.tsx +83 -28
  344. package/src/components/ui/badge/Badge.test.tsx +19 -3
  345. package/src/components/ui/badge/Badge.tsx +48 -55
  346. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  347. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +4 -16
  348. package/src/components/ui/button/Button.stories.tsx +46 -12
  349. package/src/components/ui/button/Button.tsx +32 -25
  350. package/src/components/ui/card/Card.stories.tsx +90 -94
  351. package/src/components/ui/card/Card.test.tsx +80 -6
  352. package/src/components/ui/card/Card.tsx +150 -203
  353. package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
  354. package/src/components/ui/checkbox/Checkbox.tsx +6 -26
  355. package/src/components/ui/chip/Chip.stories.tsx +37 -13
  356. package/src/components/ui/chip/Chip.test.tsx +10 -6
  357. package/src/components/ui/chip/Chip.tsx +76 -91
  358. package/src/components/ui/collapse/Collapse.stories.tsx +23 -23
  359. package/src/components/ui/collapse/Collapse.tsx +6 -11
  360. package/src/components/ui/data-row/DataRow.stories.tsx +9 -2
  361. package/src/components/ui/data-row/DataRow.test.tsx +5 -15
  362. package/src/components/ui/data-row/DataRow.tsx +18 -7
  363. package/src/components/ui/divider/Divider.stories.tsx +1 -1
  364. package/src/components/ui/divider/Divider.tsx +1 -5
  365. package/src/components/ui/drawer/Drawer.stories.tsx +5 -16
  366. package/src/components/ui/drawer/Drawer.tsx +13 -30
  367. package/src/components/ui/floating-overlay-depth.test.tsx +143 -0
  368. package/src/components/ui/form-field/FormField.stories.tsx +6 -20
  369. package/src/components/ui/form-field/FormField.test.tsx +1 -5
  370. package/src/components/ui/form-field/FormField.tsx +10 -46
  371. package/src/components/ui/help-tip/HelpTip.stories.tsx +8 -19
  372. package/src/components/ui/help-tip/HelpTip.test.tsx +1 -3
  373. package/src/components/ui/help-tip/HelpTip.tsx +21 -22
  374. package/src/components/ui/help-tip/index.ts +7 -1
  375. package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
  376. package/src/components/ui/icon-box/IconBox.test.tsx +1 -7
  377. package/src/components/ui/icon-box/IconBox.tsx +2 -12
  378. package/src/components/ui/indicator/Indicator.stories.tsx +1 -1
  379. package/src/components/ui/indicator/Indicator.tsx +20 -14
  380. package/src/components/ui/input/Input.stories.tsx +3 -11
  381. package/src/components/ui/input/Input.test.tsx +2 -10
  382. package/src/components/ui/input/Input.tsx +10 -25
  383. package/src/components/ui/input/index.ts +7 -1
  384. package/src/components/ui/link/Link.stories.tsx +19 -7
  385. package/src/components/ui/list-item/ListItem.stories.tsx +11 -2
  386. package/src/components/ui/list-item/ListItem.tsx +2 -9
  387. package/src/components/ui/menu/Menu.stories.tsx +21 -15
  388. package/src/components/ui/menu/Menu.test.tsx +2 -11
  389. package/src/components/ui/menu/Menu.tsx +12 -14
  390. package/src/components/ui/menu/index.ts +8 -1
  391. package/src/components/ui/modal/Modal.stories.tsx +2 -2
  392. package/src/components/ui/modal/Modal.tsx +29 -19
  393. package/src/components/ui/pill/Pill.stories.tsx +58 -23
  394. package/src/components/ui/pill/Pill.test.tsx +101 -3
  395. package/src/components/ui/pill/Pill.tsx +164 -49
  396. package/src/components/ui/pill/index.ts +1 -1
  397. package/src/components/ui/popover/Popover.stories.tsx +5 -7
  398. package/src/components/ui/popover/Popover.test.tsx +1 -6
  399. package/src/components/ui/popover/Popover.tsx +20 -15
  400. package/src/components/ui/popover/index.ts +7 -1
  401. package/src/components/ui/progress/Progress.stories.tsx +3 -12
  402. package/src/components/ui/progress/Progress.test.tsx +1 -5
  403. package/src/components/ui/radio/Radio.stories.tsx +42 -21
  404. package/src/components/ui/radio/Radio.test.tsx +6 -2
  405. package/src/components/ui/radio/Radio.tsx +23 -22
  406. package/src/components/ui/section/Section.stories.tsx +5 -14
  407. package/src/components/ui/section/Section.test.tsx +1 -6
  408. package/src/components/ui/section/Section.tsx +1 -3
  409. package/src/components/ui/section/index.ts +1 -5
  410. package/src/components/ui/select/Select.stories.tsx +3 -13
  411. package/src/components/ui/select/Select.test.tsx +2 -11
  412. package/src/components/ui/select/Select.tsx +14 -14
  413. package/src/components/ui/skeleton/Skeleton.stories.tsx +3 -11
  414. package/src/components/ui/skeleton/Skeleton.tsx +3 -14
  415. package/src/components/ui/skeleton/index.ts +1 -7
  416. package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
  417. package/src/components/ui/spinner/Spinner.tsx +1 -4
  418. package/src/components/ui/stack/Stack.stories.tsx +1 -1
  419. package/src/components/ui/stack/Stack.tsx +1 -1
  420. package/src/components/ui/surface/Surface.stories.tsx +17 -5
  421. package/src/components/ui/surface/Surface.test.tsx +88 -28
  422. package/src/components/ui/surface/Surface.tsx +37 -87
  423. package/src/components/ui/surface/SurfaceContext.ts +22 -62
  424. package/src/components/ui/surface/index.ts +8 -0
  425. package/src/components/ui/surface/resolveSurface.ts +98 -0
  426. package/src/components/ui/surface/surface.contract.test.ts +6 -1
  427. package/src/components/ui/switch/Switch.stories.tsx +1 -1
  428. package/src/components/ui/switch/Switch.tsx +7 -6
  429. package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
  430. package/src/components/ui/tabs/Tabs.tsx +7 -22
  431. package/src/components/ui/tabs/index.ts +1 -7
  432. package/src/components/ui/tile/Tile.stories.tsx +1 -1
  433. package/src/components/ui/tile/Tile.tsx +3 -0
  434. package/src/components/ui/toast/Toast.stories.tsx +1 -1
  435. package/src/components/ui/toast/Toast.test.tsx +2 -8
  436. package/src/components/ui/toast/Toast.tsx +43 -53
  437. package/src/components/ui/toast/index.ts +0 -1
  438. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +92 -71
  439. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +3 -18
  440. package/src/components/ui/toolbar-button/ToolbarButton.tsx +25 -43
  441. package/src/components/ui/tooltip/Tooltip.stories.tsx +12 -3
  442. package/src/components/ui/tooltip/Tooltip.test.tsx +21 -3
  443. package/src/components/ui/tooltip/Tooltip.tsx +55 -36
  444. package/src/components/ui/tooltip/index.ts +1 -1
  445. package/src/components/ui/trigger/TriggerSurface.test.tsx +215 -0
  446. package/src/components/ui/trigger/TriggerSurface.tsx +81 -0
  447. package/src/components/ui/trigger/index.ts +2 -0
  448. package/src/stories/PresetShowcase.stories.tsx +100 -47
  449. package/src/test/no-device-internals.test.ts +3 -3
  450. package/src/theme/ChoosingTokens.stories.tsx +48 -0
  451. package/src/theme/ColorPalettes.stories.tsx +62 -45
  452. package/src/theme/ColorPrimitives.stories.tsx +35 -35
  453. package/src/theme/Depth.stories.tsx +132 -65
  454. package/src/theme/DepthCalibration.stories.tsx +24 -22
  455. package/src/theme/ThemeProvider.test.tsx +4 -4
  456. package/src/theme/barrel.ts +7 -6
  457. package/src/theme/color-stories.coverage.test.ts +1 -1
  458. package/src/theme/color-utils.ts +41 -29
  459. package/src/theme/config.completeness.test.ts +1 -1
  460. package/src/theme/config.ts +28 -0
  461. package/src/theme/elevation.test.ts +82 -56
  462. package/src/theme/elevation.ts +77 -561
  463. package/src/theme/global.css +30 -2
  464. package/src/theme/grey-ramp.test.ts +24 -8
  465. package/src/theme/lift.test.ts +39 -0
  466. package/src/theme/lift.ts +102 -0
  467. package/src/theme/materials.test.ts +5 -2
  468. package/src/theme/materials.ts +38 -4
  469. package/src/theme/native-theming-guard.test.ts +5 -5
  470. package/src/theme/presets/apply.ts +4 -0
  471. package/src/theme/presets/audiobook.ts +5 -2
  472. package/src/theme/presets/default.ts +4 -0
  473. package/src/theme/presets/index.ts +14 -1
  474. package/src/theme/presets/types.ts +1 -1
  475. package/src/theme/semantic-contrast.test.ts +13 -4
  476. package/src/theme/status-distinctness.test.ts +66 -0
  477. package/src/theme/surface-planes.test.ts +37 -0
  478. package/src/theme/surface-planes.ts +68 -0
  479. package/src/theme/token-export-guard.test.ts +102 -0
  480. package/src/theme/tokens/primitives.test.ts +15 -7
  481. package/src/theme/tokens/primitives.ts +67 -35
  482. package/src/theme/tokens/semantic.ts +71 -37
  483. package/src/theme/wash-ladder.test.ts +58 -0
  484. package/src/theme/workout-tokens.ts +1 -7
  485. package/src/types/jest-axe.d.ts +2 -6
  486. package/src/types/raw-md.d.ts +5 -0
  487. package/src/utils/colors.ts +4 -4
  488. package/src/utils/form.ts +1 -3
  489. package/src/utils/index.ts +2 -7
  490. package/src/utils/number-format.ts +17 -0
  491. package/src/utils/workout-format.ts +15 -6
  492. package/src/web-jsx/jsx-runtime.ts +1 -5
  493. package/tailwind.config.js +5 -0
  494. package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +0 -151
  495. package/src/components/custom/Workout/RowInventory.stories.tsx +0 -408
  496. package/src/components/custom/Workout/S3FullRail.stories.tsx +0 -46
  497. package/src/components/custom/Workout/S3SessionPace.stories.tsx +0 -95
  498. package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +0 -61
  499. package/src/components/custom/Workout/S3SetTypes.stories.tsx +0 -176
  500. package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +0 -240
  501. package/src/components/shell/DashboardShell.stories.tsx +0 -65
  502. package/src/components/shell/DashboardShell.tsx +0 -75
  503. package/src/components/shell/SessionRailSpecimen.stories.tsx +0 -753
  504. package/src/theme/elevation.stories.tsx +0 -346
  505. package/src/utils/useTheme.ts +0 -72
  506. /package/src/components/shell/{DeviceIndicator.test.tsx → workout/DeviceIndicator.test.tsx} +0 -0
  507. /package/src/components/shell/{DeviceMenu.test.tsx → workout/DeviceMenu.test.tsx} +0 -0
  508. /package/src/components/shell/{DeviceRow.test.tsx → workout/DeviceRow.test.tsx} +0 -0
  509. /package/src/components/shell/{SessionStatePill.test.tsx → workout/SessionStatePill.test.tsx} +0 -0
@@ -19,10 +19,14 @@
19
19
  --color-brand-primary-light: #FFA063;
20
20
  --color-brand-primary-dark: #DA5F00;
21
21
  --color-brand-primary-subtle: rgba(255, 121, 0, 0.12);
22
+ --color-brand-primary-muted: rgba(255, 121, 0, 0.30);
23
+ --color-brand-primary-strong: rgba(255, 121, 0, 0.50);
22
24
  --color-brand-secondary: #307B9B;
23
25
  --color-brand-secondary-light: #2697B7;
24
26
  --color-brand-secondary-dark: #2A617F;
25
27
  --color-brand-secondary-subtle: rgba(48, 123, 155, 0.12);
28
+ --color-brand-secondary-muted: rgba(48, 123, 155, 0.30);
29
+ --color-brand-secondary-strong: rgba(48, 123, 155, 0.50);
26
30
  --color-brand-primary-hover: #FFA063;
27
31
  --color-brand-primary-active: #FFC7A2;
28
32
  --color-brand-secondary-hover: #2697B7;
@@ -35,26 +39,36 @@
35
39
  --color-status-success-light: #58F69E;
36
40
  --color-status-success-dark: #298732;
37
41
  --color-status-success-subtle: rgba(46, 213, 115, 0.12);
42
+ --color-status-success-muted: rgba(46, 213, 115, 0.30);
43
+ --color-status-success-strong: rgba(46, 213, 115, 0.50);
38
44
  --color-status-live: #2ED573;
39
45
  --color-status-live-muted: #22A444;
40
46
  --color-status-error: #D14343;
41
47
  --color-status-error-light: #E05254;
42
48
  --color-status-error-dark: #A4221C;
43
49
  --color-status-error-subtle: rgba(209, 67, 67, 0.12);
44
- --color-status-error-vivid: #D14343;
50
+ --color-status-error-muted: rgba(209, 67, 67, 0.30);
51
+ --color-status-error-strong: rgba(209, 67, 67, 0.50);
52
+ --color-status-error-vivid: #FF4757;
45
53
  --color-status-error-vivid-light: #E05254;
46
54
  --color-status-error-vivid-dark: #A4221C;
47
55
  --color-status-error-vivid-subtle: rgba(255, 71, 87, 0.12);
56
+ --color-status-error-vivid-muted: rgba(255, 71, 87, 0.30);
57
+ --color-status-error-vivid-strong: rgba(255, 71, 87, 0.50);
48
58
  --color-status-error-vivid-rgb: 255, 71, 87;
49
59
  --color-border-prominent: #424140;
50
60
  --color-status-warning: #F9B415;
51
61
  --color-status-warning-light: #FFD352;
52
62
  --color-status-warning-dark: #C27400;
53
63
  --color-status-warning-subtle: rgba(249, 180, 21, 0.12);
64
+ --color-status-warning-muted: rgba(249, 180, 21, 0.30);
65
+ --color-status-warning-strong: rgba(249, 180, 21, 0.50);
54
66
  --color-status-info: #2196F3;
55
67
  --color-status-info-light: #78C2FF;
56
68
  --color-status-info-dark: #1072CB;
57
69
  --color-status-info-subtle: rgba(33, 150, 243, 0.12);
70
+ --color-status-info-muted: rgba(33, 150, 243, 0.30);
71
+ --color-status-info-strong: rgba(33, 150, 243, 0.50);
58
72
 
59
73
  --color-on-status-success: #FFFFFF;
60
74
  --color-on-status-error: #FFFFFF;
@@ -171,10 +185,14 @@
171
185
  --color-brand-primary-light: #FFA063;
172
186
  --color-brand-primary-dark: #DA5F00;
173
187
  --color-brand-primary-subtle: rgba(255, 121, 0, 0.08);
188
+ --color-brand-primary-muted: rgba(255, 121, 0, 0.30);
189
+ --color-brand-primary-strong: rgba(255, 121, 0, 0.50);
174
190
  --color-brand-secondary: #307B9B;
175
191
  --color-brand-secondary-light: #2697B7;
176
192
  --color-brand-secondary-dark: #2A617F;
177
193
  --color-brand-secondary-subtle: rgba(48, 123, 155, 0.08);
194
+ --color-brand-secondary-muted: rgba(48, 123, 155, 0.30);
195
+ --color-brand-secondary-strong: rgba(48, 123, 155, 0.50);
178
196
  --color-brand-primary-hover: #DA5F00;
179
197
  --color-brand-primary-active: #B94A00;
180
198
  --color-brand-secondary-hover: #2A617F;
@@ -187,26 +205,36 @@
187
205
  --color-status-success-light: #58F69E;
188
206
  --color-status-success-dark: #298732;
189
207
  --color-status-success-subtle: #E3FFEE;
208
+ --color-status-success-muted: rgba(46, 213, 115, 0.30);
209
+ --color-status-success-strong: rgba(46, 213, 115, 0.50);
190
210
  --color-status-live: #2ED573;
191
211
  --color-status-live-muted: #22A444;
192
212
  --color-status-error: #D14343;
193
213
  --color-status-error-light: #E05254;
194
214
  --color-status-error-dark: #A4221C;
195
215
  --color-status-error-subtle: #FFF4F4;
196
- --color-status-error-vivid: #D14343;
216
+ --color-status-error-muted: rgba(209, 67, 67, 0.30);
217
+ --color-status-error-strong: rgba(209, 67, 67, 0.50);
218
+ --color-status-error-vivid: #FF4757;
197
219
  --color-status-error-vivid-light: #E05254;
198
220
  --color-status-error-vivid-dark: #A4221C;
199
221
  --color-status-error-vivid-subtle: rgba(255, 71, 87, 0.12);
222
+ --color-status-error-vivid-muted: rgba(255, 71, 87, 0.30);
223
+ --color-status-error-vivid-strong: rgba(255, 71, 87, 0.50);
200
224
  --color-status-error-vivid-rgb: 255, 71, 87;
201
225
  --color-border-prominent: #A29F9D;
202
226
  --color-status-warning: #F9B415;
203
227
  --color-status-warning-light: #FFD352;
204
228
  --color-status-warning-dark: #C27400;
205
229
  --color-status-warning-subtle: #FFF7DD;
230
+ --color-status-warning-muted: rgba(249, 180, 21, 0.30);
231
+ --color-status-warning-strong: rgba(249, 180, 21, 0.50);
206
232
  --color-status-info: #2196F3;
207
233
  --color-status-info-light: #78C2FF;
208
234
  --color-status-info-dark: #1072CB;
209
235
  --color-status-info-subtle: #EFF8FF;
236
+ --color-status-info-muted: rgba(33, 150, 243, 0.30);
237
+ --color-status-info-strong: rgba(33, 150, 243, 0.50);
210
238
 
211
239
  --color-on-status-success: #FFFFFF;
212
240
  --color-on-status-error: #FFFFFF;
@@ -11,7 +11,6 @@ import { execFileSync } from 'node:child_process'
11
11
  import { join } from 'node:path'
12
12
  import { greyRamp, SURFACE_PLANE_STEPS, primitiveRamps } from './tokens/primitives'
13
13
 
14
-
15
14
  // ── colour math (CIELAB D65 + WCAG relative luminance) ──────────────────────
16
15
  const hex2rgb = (h: string): [number, number, number] => {
17
16
  const n = parseInt(h.slice(1), 16)
@@ -42,9 +41,15 @@ const warmth = (hex: string) => {
42
41
 
43
42
  /** Machado-2009 dichromacy, severity 1.0. */
44
43
  const CVD = {
45
- deuteranopia: [0.367322, 0.860646, -0.227968, 0.280085, 0.672501, 0.047413, -0.01182, 0.04294, 0.968881],
46
- protanopia: [0.152286, 1.052583, -0.204868, 0.114503, 0.786281, 0.099216, -0.003882, -0.048116, 1.051998],
47
- tritanopia: [1.255528, -0.076749, -0.178779, -0.078411, 0.930809, 0.147602, 0.004733, 0.691367, 0.3039],
44
+ deuteranopia: [
45
+ 0.367322, 0.860646, -0.227968, 0.280085, 0.672501, 0.047413, -0.01182, 0.04294, 0.968881,
46
+ ],
47
+ protanopia: [
48
+ 0.152286, 1.052583, -0.204868, 0.114503, 0.786281, 0.099216, -0.003882, -0.048116, 1.051998,
49
+ ],
50
+ tritanopia: [
51
+ 1.255528, -0.076749, -0.178779, -0.078411, 0.930809, 0.147602, 0.004733, 0.691367, 0.3039,
52
+ ],
48
53
  } as const
49
54
  function simulateCvd(m: readonly number[], hex: string): string {
50
55
  const [r, g, b] = hex2rgb(hex)
@@ -56,13 +61,19 @@ function simulateCvd(m: readonly number[], hex: string): string {
56
61
  return (
57
62
  '#' +
58
63
  out
59
- .map((v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, '0'))
64
+ .map((v) =>
65
+ Math.round(Math.max(0, Math.min(255, v)))
66
+ .toString(16)
67
+ .padStart(2, '0')
68
+ )
60
69
  .join('')
61
70
  .toUpperCase()
62
71
  )
63
72
  }
64
73
 
65
- const STEPS = Object.keys(greyRamp).map(Number).sort((a, b) => a - b)
74
+ const STEPS = Object.keys(greyRamp)
75
+ .map(Number)
76
+ .sort((a, b) => a - b)
66
77
  const hexAt = (step: number) => greyRamp[step as keyof typeof greyRamp]
67
78
 
68
79
  describe('greyRamp — shape', () => {
@@ -82,7 +93,9 @@ describe('greyRamp — shape', () => {
82
93
  it('has no two steps a reader could not tell apart', () => {
83
94
  for (let i = 1; i < STEPS.length; i++) {
84
95
  const d = dE76(hexAt(STEPS[i - 1]), hexAt(STEPS[i]))
85
- expect(d, `steps ${STEPS[i - 1]}/${STEPS[i]} collapse at ΔE ${d.toFixed(2)}`).toBeGreaterThan(1.5)
96
+ expect(d, `steps ${STEPS[i - 1]}/${STEPS[i]} collapse at ΔE ${d.toFixed(2)}`).toBeGreaterThan(
97
+ 1.5
98
+ )
86
99
  }
87
100
  })
88
101
 
@@ -150,7 +163,10 @@ describe('greyRamp — accessibility gate (TD-07.15)', () => {
150
163
  // Adjacent steps must stay separable AFTER simulation, or the depth
151
164
  // ordering of the surface stack stops reading for those viewers.
152
165
  for (let i = 1; i < STEPS.length; i++) {
153
- const d = dE76(simulateCvd(matrix, hexAt(STEPS[i - 1])), simulateCvd(matrix, hexAt(STEPS[i])))
166
+ const d = dE76(
167
+ simulateCvd(matrix, hexAt(STEPS[i - 1])),
168
+ simulateCvd(matrix, hexAt(STEPS[i]))
169
+ )
154
170
  expect(d, `${mode}: grey-${STEPS[i - 1]}/${STEPS[i]} merge`).toBeGreaterThan(1.5)
155
171
  }
156
172
  }
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { liftShadow, liftStyle, LIFT_RIM_ALPHA, FLOATING_LIFT_MIN } from './lift'
3
+
4
+ describe('liftShadow', () => {
5
+ it('leads with the card rim on dark: one grade under the paperSheet hero rim', () => {
6
+ expect(liftShadow(1, 'dark')).toMatch(/^inset 0 1px 0 rgba\(255,255,255,0\.12\)/)
7
+ expect(LIFT_RIM_ALPHA.dark).toBe(0.12)
8
+ })
9
+
10
+ it('omits the rim when asked, keeping the ambient layers', () => {
11
+ const shadow = liftShadow(2, 'dark', { rim: 0 })
12
+ expect(shadow).not.toContain('inset')
13
+ expect(shadow.split(', ').length).toBe(2)
14
+ })
15
+
16
+ it('honours a rim override (a hero surface can ask for the paperSheet rim)', () => {
17
+ expect(liftShadow(2, 'dark', { rim: 0.2 })).toContain('rgba(255,255,255,0.20)')
18
+ })
19
+
20
+ it('casts a lighter shadow on a light surface, same geometry', () => {
21
+ const dark = liftShadow(3, 'dark', { rim: 0 }).split(', ')
22
+ const light = liftShadow(3, 'light', { rim: 0 }).split(', ')
23
+ expect(light.length).toBe(dark.length)
24
+ const alpha = (layer: string) => Number(/rgba\(0,0,0,([\d.]+)\)/.exec(layer)![1])
25
+ for (let i = 0; i < dark.length; i++) expect(alpha(light[i])).toBeLessThan(alpha(dark[i]))
26
+ })
27
+
28
+ it('uses three ambient layers from the floating threshold up', () => {
29
+ expect(liftShadow(FLOATING_LIFT_MIN, 'dark', { rim: 0 }).split(', ').length).toBe(3)
30
+ expect(liftShadow(3, 'dark', { rim: 0 }).split(', ').length).toBe(2)
31
+ })
32
+ })
33
+
34
+ describe('liftStyle', () => {
35
+ it('returns a web boxShadow under the RNW alias', () => {
36
+ const style = liftStyle(2) as { boxShadow?: string }
37
+ expect(style.boxShadow).toContain('inset 0 1px 0')
38
+ })
39
+ })
@@ -0,0 +1,102 @@
1
+ /**
2
+ * LIFT — the treatment a plane wears when it sits above its neighbour.
3
+ *
4
+ * Tone says which plane a container sits on; lift says it is resting ON the one
5
+ * below. Two cues, both lit from above:
6
+ *
7
+ * RIM a crisp 1px top rim-light. The one neumorphism half that survives
8
+ * near black. paperSheet's hero rim is 0.20 (VW-99 run 2); a card is
9
+ * not a hero, so the default lift sits one grade quieter at 0.12,
10
+ * chosen against 0.20 side by side in Lab/Depth (2026-09-08).
11
+ * AMBIENT a soft shadow cast onto the plane below, growing with the number of
12
+ * planes crossed. Content lifts (1–3) are tight; floating lifts (4–5)
13
+ * are large and soft, describing separation from the page rather than
14
+ * rank in a stack.
15
+ *
16
+ * Neither cue is a hairline ring: a ring is an edge, not a lift, and stays the
17
+ * divider's job. Web carries the multi-layer boxShadow; native gets the single
18
+ * shadow* set. The tone underneath is load-bearing on its own either way.
19
+ */
20
+ import { Platform, type ViewStyle } from 'react-native'
21
+ import type { ThemeMode } from './tokens/semantic'
22
+
23
+ export type LiftStep = 1 | 2 | 3 | 4 | 5
24
+
25
+ /** The lowest lift that is FLOATING (menu, popover, toast) rather than content. */
26
+ export const FLOATING_LIFT_MIN: LiftStep = 4
27
+
28
+ /** Rim-light alpha per mode. Light mode is a placeholder pending its own pass. */
29
+ export const LIFT_RIM_ALPHA: Record<ThemeMode, number> = { dark: 0.12, light: 0.9 }
30
+
31
+ interface ShadowLayer {
32
+ y: number
33
+ blur: number
34
+ alpha: number
35
+ }
36
+
37
+ const AMBIENT: Record<LiftStep, ShadowLayer[]> = {
38
+ 1: [
39
+ { y: 1, blur: 2, alpha: 0.35 },
40
+ { y: 2, blur: 6, alpha: 0.3 },
41
+ ],
42
+ 2: [
43
+ { y: 1, blur: 2, alpha: 0.35 },
44
+ { y: 4, blur: 12, alpha: 0.32 },
45
+ ],
46
+ 3: [
47
+ { y: 2, blur: 4, alpha: 0.35 },
48
+ { y: 8, blur: 22, alpha: 0.35 },
49
+ ],
50
+ 4: [
51
+ { y: 1, blur: 2, alpha: 0.35 },
52
+ { y: 10, blur: 24, alpha: 0.3 },
53
+ { y: 24, blur: 48, alpha: 0.2 },
54
+ ],
55
+ 5: [
56
+ { y: 2, blur: 4, alpha: 0.4 },
57
+ { y: 16, blur: 32, alpha: 0.35 },
58
+ { y: 32, blur: 64, alpha: 0.25 },
59
+ ],
60
+ }
61
+
62
+ /** A light surface casts a lighter shadow: same geometry, scaled alpha. */
63
+ const AMBIENT_ALPHA_SCALE: Record<ThemeMode, number> = { dark: 1, light: 0.4 }
64
+
65
+ export interface LiftOptions {
66
+ /** Override the rim alpha. `0` drops the rim and leaves the ambient shadow alone. */
67
+ rim?: number
68
+ }
69
+
70
+ function ambientLayers(step: LiftStep, mode: ThemeMode): string[] {
71
+ const scale = AMBIENT_ALPHA_SCALE[mode]
72
+ return AMBIENT[step].map(
73
+ ({ y, blur, alpha }) => `0 ${y}px ${blur}px rgba(0,0,0,${(alpha * scale).toFixed(2)})`
74
+ )
75
+ }
76
+
77
+ /** The web boxShadow string for a lift: rim first, then the ambient layers. */
78
+ export function liftShadow(step: LiftStep, mode: ThemeMode = 'dark', opts: LiftOptions = {}) {
79
+ const rim = opts.rim ?? LIFT_RIM_ALPHA[mode]
80
+ const layers = ambientLayers(step, mode)
81
+ if (rim > 0) layers.unshift(`inset 0 1px 0 rgba(255,255,255,${rim.toFixed(2)})`)
82
+ return layers.join(', ')
83
+ }
84
+
85
+ /** Platform style for a lift. Compose after `backgroundColor`; never instead of it. */
86
+ export function liftStyle(
87
+ step: LiftStep,
88
+ mode: ThemeMode = 'dark',
89
+ opts: LiftOptions = {}
90
+ ): ViewStyle {
91
+ const deepest = AMBIENT[step][AMBIENT[step].length - 1]
92
+ return Platform.select({
93
+ web: { boxShadow: liftShadow(step, mode, opts) } as unknown as ViewStyle,
94
+ default: {
95
+ shadowColor: '#000',
96
+ shadowOffset: { width: 0, height: Math.round(deepest.y / 2) },
97
+ shadowOpacity: deepest.alpha * AMBIENT_ALPHA_SCALE[mode],
98
+ shadowRadius: deepest.blur / 2,
99
+ elevation: step,
100
+ },
101
+ }) as ViewStyle
102
+ }
@@ -15,7 +15,8 @@ import { describe, it, expect } from 'vitest'
15
15
  import { grainOpacityForTone, grainForTone, paperSheet, insetWell, barPaper } from './materials'
16
16
  import { greyRamp } from './tokens/primitives'
17
17
 
18
- const styleOf = (s: Record<string, unknown>) => s as { backgroundImage?: string; boxShadow?: string; backgroundColor?: string }
18
+ const styleOf = (s: Record<string, unknown>) =>
19
+ s as { backgroundImage?: string; boxShadow?: string; backgroundColor?: string }
19
20
 
20
21
  describe('grain', () => {
21
22
  it('scales opacity with the tone it sits on', () => {
@@ -75,7 +76,9 @@ describe('insetWell', () => {
75
76
  // a darker rectangle. It is the light on the floor that says "below".
76
77
  const shadow = styleOf(insetWell(greyRamp[950])).boxShadow!
77
78
  expect(shadow).toContain('inset 0 2px')
78
- expect(shadow, 'missing the bottom floor rim').toContain('inset 0 -1px 0 rgba(255,255,255,0.12)')
79
+ expect(shadow, 'missing the bottom floor rim').toContain(
80
+ 'inset 0 -1px 0 rgba(255,255,255,0.12)'
81
+ )
79
82
  })
80
83
 
81
84
  it('is entirely inset — a well must not cast outward', () => {
@@ -54,10 +54,12 @@
54
54
  *
55
55
  * Kept: grain, rim-light, contact shadow.
56
56
  *
57
- * See coordination/design-explorations/surface-system-north-star.md §4 and
58
- * coordination/validation-runbooks/VW-99-depth-wall-calibration.md.
57
+ * See `sources/design/surface-system-north-star.md` §4 and
58
+ * `sources/runbooks/VW-99-depth-wall-calibration.md`. Those paths are relative to
59
+ * the `voltras-workspace` active-work initiative (`active-work open voltras-workspace`), not this repo —
60
+ * titan-design has no `sources/` directory, so they do not resolve from here.
59
61
  */
60
- import type { ViewStyle } from 'react-native'
62
+ import type { TextStyle, ViewStyle } from 'react-native'
61
63
  import { getSemanticColors } from './tokens/semantic'
62
64
 
63
65
  const c = getSemanticColors('dark')
@@ -66,7 +68,13 @@ const c = getSemanticColors('dark')
66
68
 
67
69
  function parseHex(hex: string): [number, number, number] | null {
68
70
  const h = hex.replace('#', '')
69
- const full = h.length === 3 ? h.split('').map((ch) => ch + ch).join('') : h
71
+ const full =
72
+ h.length === 3
73
+ ? h
74
+ .split('')
75
+ .map((ch) => ch + ch)
76
+ .join('')
77
+ : h
70
78
  const rgb: [number, number, number] = [
71
79
  parseInt(full.slice(0, 2), 16),
72
80
  parseInt(full.slice(2, 4), 16),
@@ -209,3 +217,29 @@ export function barPaper(color: string, flip = false): ViewStyle {
209
217
  : 'inset 0 1.5px 0 rgba(255,255,255,0.22), 0 6px 16px rgba(0,0,0,0.45)',
210
218
  } as unknown as ViewStyle
211
219
  }
220
+
221
+ /**
222
+ * A POST-IT card: a matte tinted sheet with a crisp rim, a defined contact
223
+ * shadow, and a deliberate CANT (`deg`) so it reads as a stuck note — where a
224
+ * small unit of data wants to feel pinned to the surface (alerts, next-set). A
225
+ * degree or two, not a fraction, so the tilt is intentional rather than a bug.
226
+ */
227
+ export function postIt(tone: string = c['surface-raised'], deg = -1.5): ViewStyle {
228
+ return {
229
+ backgroundColor: tone,
230
+ backgroundImage: grainForTone(tone),
231
+ boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.12), 0 6px 14px rgba(0,0,0,0.45)',
232
+ transform: [{ rotate: `${deg}deg` }],
233
+ } as unknown as ViewStyle
234
+ }
235
+
236
+ /**
237
+ * DEBOSSED (engraved) text: a 1px light highlight dropped just below the glyphs
238
+ * so a label reads as pressed INTO the surface. Deliberate, for section eyebrows
239
+ * on a matte plane — not body copy. Pair with a tertiary/dim text colour.
240
+ */
241
+ export const debossLabel: TextStyle = {
242
+ textShadowColor: 'rgba(255,255,255,0.11)',
243
+ textShadowOffset: { width: 0, height: 1 },
244
+ textShadowRadius: 0,
245
+ }
@@ -37,7 +37,7 @@ const colorTree = tailwindConfig.theme?.extend?.colors ?? {}
37
37
  // `brand-primary` class).
38
38
  function flattenColors(
39
39
  node: Record<string, unknown>,
40
- prefix: string[] = [],
40
+ prefix: string[] = []
41
41
  ): Record<string, string> {
42
42
  const out: Record<string, string> = {}
43
43
  for (const [key, value] of Object.entries(node)) {
@@ -83,11 +83,11 @@ describe('native theming guard (no color token renders black on native)', () =>
83
83
  }
84
84
  expect(
85
85
  undefinedDark,
86
- 'tailwind colors point at vars missing from darkThemeCSSVars (className renders black on native dark)',
86
+ 'tailwind colors point at vars missing from darkThemeCSSVars (className renders black on native dark)'
87
87
  ).toEqual([])
88
88
  expect(
89
89
  undefinedLight,
90
- 'tailwind colors point at vars missing from lightThemeCSSVars (className renders black on native light)',
90
+ 'tailwind colors point at vars missing from lightThemeCSSVars (className renders black on native light)'
91
91
  ).toEqual([])
92
92
  })
93
93
 
@@ -106,11 +106,11 @@ describe('native theming guard (no color token renders black on native)', () =>
106
106
  }
107
107
  expect(
108
108
  invalidValues,
109
- `getSemanticColors('${mode}') has tokens with no valid color (renders black/unset on native)`,
109
+ `getSemanticColors('${mode}') has tokens with no valid color (renders black/unset on native)`
110
110
  ).toEqual([])
111
111
  expect(
112
112
  missingVars,
113
- `resolveColor tokens missing a --color-<token> in ${mode} vars (renders unset on web)`,
113
+ `resolveColor tokens missing a --color-<token> in ${mode} vars (renders unset on web)`
114
114
  ).toEqual([])
115
115
  }
116
116
  })
@@ -36,6 +36,10 @@ function injectFontImport(url: string) {
36
36
  document.head.appendChild(link)
37
37
  }
38
38
 
39
+ /**
40
+ * @deprecated Use `ThemeProvider` with semantic tokens — removed once audiobook
41
+ * migrates off presets (AW-129).
42
+ */
39
43
  export function applyThemePreset(preset: ThemePreset): () => void {
40
44
  if (typeof document === 'undefined') {
41
45
  return () => {}
@@ -5,12 +5,15 @@ import type { ThemePreset } from './types'
5
5
  *
6
6
  * Warm copper/patina aesthetic inspired by vintage audio equipment
7
7
  * and reading lounges. Dark-mode only design with editorial typography.
8
+ *
9
+ * @deprecated Use `ThemeProvider` with semantic tokens — removed once audiobook
10
+ * migrates off presets (AW-129).
8
11
  */
9
12
  export const audiobookPreset: ThemePreset = {
10
13
  name: 'audiobook',
11
14
  description: 'Warm copper/patina theme with editorial typography',
12
15
  fontImport:
13
- "https://fonts.googleapis.com/css2?family=Newsreader:ital,wght@0,400;0,600;0,700;1,400&family=Outfit:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap",
16
+ 'https://fonts.googleapis.com/css2?family=Newsreader:ital,wght@0,400;0,600;0,700;1,400&family=Outfit:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap',
14
17
  fonts: {
15
18
  sans: "'Outfit', 'Avenir', 'Avenir Next', system-ui, sans-serif",
16
19
  body: "'Outfit', 'Avenir', 'Avenir Next', system-ui, sans-serif",
@@ -77,7 +80,7 @@ export const audiobookPreset: ThemePreset = {
77
80
  'interactive-active': 'rgba(255, 255, 255, 0.16)',
78
81
  'interactive-selected': 'rgba(255, 255, 255, 0.08)',
79
82
 
80
- 'divider': 'rgba(255, 255, 255, 0.06)',
83
+ divider: 'rgba(255, 255, 255, 0.06)',
81
84
 
82
85
  // Glow RGB values for shadow utilities
83
86
  'brand-primary-rgb': '212, 120, 42',
@@ -1,5 +1,9 @@
1
1
  import type { ThemePreset } from './types'
2
2
 
3
+ /**
4
+ * @deprecated Use `ThemeProvider` with semantic tokens — removed once audiobook
5
+ * migrates off presets (AW-129).
6
+ */
3
7
  export const defaultPreset: ThemePreset = {
4
8
  name: 'default',
5
9
  description: 'Titan Design System default theme — orange accent, steel secondary',
@@ -1,4 +1,17 @@
1
- export type { ThemePreset, ThemePresetColors, ThemePresetFonts, ThemePresetRadii, ThemePresetShadows } from './types'
1
+ /**
2
+ * @deprecated The whole `theme/presets` barrel. Use `ThemeProvider` with
3
+ * semantic tokens — removed once audiobook migrates off presets (AW-129).
4
+ */
5
+ export type {
6
+ ThemePreset,
7
+ ThemePresetColors,
8
+ ThemePresetFonts,
9
+ ThemePresetRadii,
10
+ ThemePresetShadows,
11
+ } from './types'
12
+ /** @deprecated Use `ThemeProvider` with semantic tokens (AW-129). */
2
13
  export { applyThemePreset } from './apply'
14
+ /** @deprecated Use `ThemeProvider` with semantic tokens (AW-129). */
3
15
  export { defaultPreset } from './default'
16
+ /** @deprecated Use `ThemeProvider` with semantic tokens (AW-129). */
4
17
  export { audiobookPreset } from './audiobook'
@@ -32,7 +32,7 @@ export interface ThemePresetColors {
32
32
  'hairline-subtle'?: string
33
33
  'hairline-strong'?: string
34
34
  'border-focus'?: string
35
- 'divider'?: string
35
+ divider?: string
36
36
  [key: `${string}`]: string | undefined
37
37
  }
38
38
 
@@ -54,7 +54,7 @@ describe('semantic text on the surface planes (dark)', () => {
54
54
  const ratio = contrast(fg, greyRamp[plane])
55
55
  expect(
56
56
  ratio,
57
- `${role} ${fg} on grey-${plane} ${greyRamp[plane]} is ${ratio.toFixed(2)}:1, needs ${min}`,
57
+ `${role} ${fg} on grey-${plane} ${greyRamp[plane]} is ${ratio.toFixed(2)}:1, needs ${min}`
58
58
  ).toBeGreaterThanOrEqual(min)
59
59
  })
60
60
  })
@@ -68,10 +68,19 @@ describe('semantic text on the surface planes (dark)', () => {
68
68
  it('keeps primary > secondary > tertiary in contrast on every plane', () => {
69
69
  for (const plane of PLANES) {
70
70
  const [pri, sec, ter] = TEXT_ROLES.map(({ role }) =>
71
- contrast(semanticColorsDark[role as keyof typeof semanticColorsDark] as string, greyRamp[plane]),
71
+ contrast(
72
+ semanticColorsDark[role as keyof typeof semanticColorsDark] as string,
73
+ greyRamp[plane]
74
+ )
72
75
  )
73
- expect(pri, `on grey-${plane}: primary ${pri.toFixed(2)} vs secondary ${sec.toFixed(2)}`).toBeGreaterThan(sec)
74
- expect(sec, `on grey-${plane}: secondary ${sec.toFixed(2)} vs tertiary ${ter.toFixed(2)}`).toBeGreaterThan(ter)
76
+ expect(
77
+ pri,
78
+ `on grey-${plane}: primary ${pri.toFixed(2)} vs secondary ${sec.toFixed(2)}`
79
+ ).toBeGreaterThan(sec)
80
+ expect(
81
+ sec,
82
+ `on grey-${plane}: secondary ${sec.toFixed(2)} vs tertiary ${ter.toFixed(2)}`
83
+ ).toBeGreaterThan(ter)
75
84
  }
76
85
  })
77
86
 
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Status tokens that mean different things must LOOK different.
3
+ *
4
+ * `status-error-vivid` existed to separate "drop everything" from "serious",
5
+ * and it pointed at `ramp.red[600]` — the exact value of `status-error`. The two
6
+ * had been rendering as one colour for as long as the token had existed, in both
7
+ * themes. Three other places in the system already disagreed with it and said
8
+ * vivid was `#FF4757`: `--color-status-error-vivid-rgb`, the `-subtle` rgba, and
9
+ * the `glow` shadow in `Indicator`. Only the value anyone actually rendered was
10
+ * wrong.
11
+ *
12
+ * Nothing caught it because every existing token test asks whether a value is
13
+ * PRESENT and parity-matched across config.ts and global.css — a collapsed token
14
+ * is present and matches perfectly. Distinctness is a different question, so it
15
+ * gets its own gate.
16
+ *
17
+ * Found 2026-08-31 by measuring a rendered Storybook story, not by reading the
18
+ * tokens: `SeverityLabel`'s Critical and High dots came back the same RGB.
19
+ */
20
+ import { describe, it, expect } from 'vitest'
21
+ import { semanticColorsDark, semanticColorsLight } from './tokens/semantic'
22
+ import { alertRedVivid } from './tokens/primitives'
23
+
24
+ type SemanticMap = Record<string, string>
25
+
26
+ const THEMES: [string, SemanticMap][] = [
27
+ ['dark', semanticColorsDark as SemanticMap],
28
+ ['light', semanticColorsLight as SemanticMap],
29
+ ]
30
+
31
+ /**
32
+ * Token pairs a viewer is expected to tell apart at a glance. Each pair carries
33
+ * the reason, so a future retune can weigh it rather than just delete the row.
34
+ */
35
+ const MUST_DIFFER: [string, string, string][] = [
36
+ [
37
+ 'status-error',
38
+ 'status-error-vivid',
39
+ 'severity Critical vs High, and DeviceRow/DeviceIndicator "lost" vs a plain error',
40
+ ],
41
+ ['status-error', 'status-warning', 'error vs warning is the most common status pairing'],
42
+ ['status-warning', 'status-info', 'warning vs informational'],
43
+ ['status-success', 'status-info', 'succeeded vs merely reporting'],
44
+ ]
45
+
46
+ describe('semantically distinct status tokens render distinct values', () => {
47
+ for (const [theme, colors] of THEMES) {
48
+ for (const [a, b, why] of MUST_DIFFER) {
49
+ it(`${theme}: ${a} !== ${b} — ${why}`, () => {
50
+ // Both must exist, or the assertion passes vacuously on a typo.
51
+ expect(colors[a], `${a} missing from ${theme}`).toBeTruthy()
52
+ expect(colors[b], `${b} missing from ${theme}`).toBeTruthy()
53
+ expect(colors[a]!.toLowerCase()).not.toBe(colors[b]!.toLowerCase())
54
+ })
55
+ }
56
+ }
57
+ })
58
+
59
+ describe('status-error-vivid is the vivid red the rest of the system already assumes', () => {
60
+ for (const [theme, colors] of THEMES) {
61
+ it(`${theme}: is the alertRedVivid primitive that -rgb, -subtle and Indicator's glow encode`, () => {
62
+ expect(colors['status-error-vivid']).toBe(alertRedVivid)
63
+ expect(alertRedVivid.toLowerCase()).toBe('#ff4757')
64
+ })
65
+ }
66
+ })
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { PLANE_ORDER, pressedLevel, raisedLevel, surfaceBackground } from './surface-planes'
3
+ import { getSemanticColors } from './tokens/semantic'
4
+
5
+ describe('surface planes', () => {
6
+ it('orders the six planes darkest first', () => {
7
+ expect(PLANE_ORDER).toEqual(['frame', 'background', 'base', 'elevated', 'raised', 'overlay'])
8
+ })
9
+
10
+ it('resolves each plane to its semantic token, literal hex', () => {
11
+ const dark = getSemanticColors('dark')
12
+ expect(surfaceBackground('frame', 'dark')).toBe(dark['background-frame'])
13
+ expect(surfaceBackground('base', 'dark')).toBe(dark['surface-base'])
14
+ expect(surfaceBackground('overlay', 'dark')).toMatch(/^#[0-9A-F]{6}$/i)
15
+ })
16
+
17
+ it.each([
18
+ ['base', 1, 'elevated'],
19
+ ['base', 2, 'raised'],
20
+ ['base', 3, 'overlay'],
21
+ ['elevated', 2, 'overlay'],
22
+ ['raised', 3, 'overlay'],
23
+ ['overlay', 1, 'overlay'],
24
+ ] as const)('raisedLevel(%s, %i) → %s, clamped at overlay', (from, steps, expected) => {
25
+ expect(raisedLevel(from, steps)).toBe(expected)
26
+ })
27
+
28
+ it('raisedLevel with zero steps stays put', () => {
29
+ expect(raisedLevel('raised', 0)).toBe('raised')
30
+ })
31
+
32
+ it('pressedLevel steps down and clamps at the frame', () => {
33
+ expect(pressedLevel('base')).toBe('background')
34
+ expect(pressedLevel('background')).toBe('frame')
35
+ expect(pressedLevel('frame')).toBe('frame')
36
+ })
37
+ })