@titan-design/react-ui 0.6.0 → 0.8.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 (258) hide show
  1. package/dist/bodymap.js +12 -7
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +12 -7
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +956 -128
  6. package/dist/index.d.ts +956 -128
  7. package/dist/index.js +2873 -981
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2826 -982
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/{pages-MO0JCySL.d.mts → pages-DCFBqp79.d.mts} +289 -57
  12. package/dist/{pages-D7Jlssvp.d.ts → pages-_fI3-x7Z.d.ts} +289 -57
  13. package/dist/pages.d.mts +1 -1
  14. package/dist/pages.d.ts +1 -1
  15. package/dist/pages.js +1659 -799
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1660 -800
  18. package/dist/pages.mjs.map +1 -1
  19. package/dist/theme/index.d.mts +16 -82
  20. package/dist/theme/index.d.ts +16 -82
  21. package/dist/theme/index.js +13 -115
  22. package/dist/theme/index.js.map +1 -1
  23. package/dist/theme/index.mjs +13 -115
  24. package/dist/theme/index.mjs.map +1 -1
  25. package/dist/theme/tokens-css.js +14 -6
  26. package/dist/theme/tokens-css.js.map +1 -1
  27. package/dist/theme/tokens-css.mjs +14 -6
  28. package/dist/theme/tokens-css.mjs.map +1 -1
  29. package/package.json +1 -1
  30. package/src/arch/Architecture.stories.tsx +870 -0
  31. package/src/arch/annotations.json +48 -0
  32. package/src/arch/arch-graph.json +4301 -0
  33. package/src/components/custom/CircularTimer/CircularTimer.stories.tsx +90 -0
  34. package/src/components/custom/CircularTimer/CircularTimer.test.tsx +62 -0
  35. package/src/components/custom/CircularTimer/CircularTimer.tsx +112 -0
  36. package/src/components/custom/CircularTimer/index.ts +1 -0
  37. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  38. package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
  39. package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
  40. package/src/components/custom/Metric/Metric.stories.tsx +1 -1
  41. package/src/components/custom/Scatter/Scatter.stories.tsx +1 -1
  42. package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
  43. package/src/components/custom/Table/Table.stories.tsx +1 -1
  44. package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +62 -0
  45. package/src/components/custom/TimerReadout/TimerReadout.test.tsx +72 -0
  46. package/src/components/custom/TimerReadout/TimerReadout.tsx +75 -0
  47. package/src/components/custom/TimerReadout/index.ts +1 -0
  48. package/src/components/custom/Treemap/Treemap.stories.tsx +1 -1
  49. package/src/components/custom/Typography/Typography.stories.tsx +1 -1
  50. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +21 -13
  51. package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +16 -13
  52. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +9 -15
  53. package/src/components/custom/Workout/BaseBadge.stories.tsx +1 -1
  54. package/src/components/custom/Workout/BodyMap.stories.tsx +1 -1
  55. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
  56. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
  57. package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
  58. package/src/components/custom/Workout/ExerciseCard.stories.tsx +51 -66
  59. package/src/components/custom/Workout/ExerciseCard.test.tsx +146 -143
  60. package/src/components/custom/Workout/ExerciseCard.tsx +176 -198
  61. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +99 -0
  62. package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +72 -0
  63. package/src/components/custom/Workout/ExerciseCardHeading.tsx +43 -0
  64. package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +151 -0
  65. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +5 -3
  66. package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +4 -4
  67. package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -5
  68. package/src/components/custom/Workout/ExerciseHeading.stories.tsx +81 -0
  69. package/src/components/custom/Workout/ExerciseHeading.test.tsx +77 -0
  70. package/src/components/custom/Workout/ExerciseHeading.tsx +85 -0
  71. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +110 -0
  72. package/src/components/custom/Workout/ExerciseIndicator.test.tsx +93 -0
  73. package/src/components/custom/Workout/ExerciseIndicator.tsx +120 -0
  74. package/src/components/custom/Workout/FatigueMeter.stories.tsx +51 -0
  75. package/src/components/custom/Workout/FatigueMeter.test.tsx +85 -0
  76. package/src/components/custom/Workout/FatigueMeter.tsx +86 -0
  77. package/src/components/custom/Workout/InputBar.stories.tsx +1 -1
  78. package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
  79. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +153 -0
  80. package/src/components/custom/Workout/LiveAuraFrame.test.tsx +74 -0
  81. package/src/components/custom/Workout/LiveAuraFrame.tsx +105 -0
  82. package/src/components/custom/Workout/MesoCard.stories.tsx +1 -1
  83. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
  84. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
  85. package/src/components/custom/Workout/MetricTiles.stories.tsx +60 -0
  86. package/src/components/custom/Workout/MetricTiles.test.tsx +58 -0
  87. package/src/components/custom/Workout/MetricTiles.tsx +48 -0
  88. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +1 -1
  89. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
  90. package/src/components/custom/Workout/PrBadge.stories.tsx +1 -1
  91. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
  92. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
  93. package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -6
  94. package/src/components/custom/Workout/README.md +193 -4
  95. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
  96. package/src/components/custom/Workout/RestTimer.stories.tsx +133 -3
  97. package/src/components/custom/Workout/RestTimer.test.tsx +73 -0
  98. package/src/components/custom/Workout/RestTimer.tsx +155 -59
  99. package/src/components/custom/Workout/RowInventory.stories.tsx +405 -0
  100. package/src/components/custom/Workout/S3FullRail.stories.tsx +34 -0
  101. package/src/components/custom/Workout/S3SessionPace.stories.tsx +95 -0
  102. package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +61 -0
  103. package/src/components/custom/Workout/S3SetTypes.stories.tsx +176 -0
  104. package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +240 -0
  105. package/src/components/custom/Workout/ScheduleTiles.stories.tsx +40 -0
  106. package/src/components/custom/Workout/ScheduleTiles.test.tsx +34 -0
  107. package/src/components/custom/Workout/ScheduleTiles.tsx +70 -0
  108. package/src/components/custom/Workout/SegmentedBar.stories.tsx +99 -0
  109. package/src/components/custom/Workout/SegmentedBar.test.tsx +77 -0
  110. package/src/components/custom/Workout/SegmentedBar.tsx +172 -0
  111. package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +50 -0
  112. package/src/components/custom/Workout/SegmentedProgressBar.test.tsx +80 -0
  113. package/src/components/custom/Workout/SegmentedProgressBar.tsx +67 -0
  114. package/src/components/custom/Workout/SessionHeader.stories.tsx +121 -0
  115. package/src/components/custom/Workout/SessionHeader.test.tsx +122 -0
  116. package/src/components/custom/Workout/SessionHeader.tsx +152 -0
  117. package/src/components/custom/Workout/SessionRail.stories.tsx +153 -0
  118. package/src/components/custom/Workout/SessionRail.test.tsx +86 -0
  119. package/src/components/custom/Workout/SessionRail.tsx +126 -0
  120. package/src/components/custom/Workout/SetBar.stories.tsx +124 -0
  121. package/src/components/custom/Workout/SetBar.test.tsx +123 -0
  122. package/src/components/custom/Workout/SetBar.tsx +171 -0
  123. package/src/components/custom/Workout/SetRow.stories.tsx +50 -100
  124. package/src/components/custom/Workout/SetRow.test.tsx +94 -166
  125. package/src/components/custom/Workout/SetRow.tsx +169 -232
  126. package/src/components/custom/Workout/SetStrip.stories.tsx +154 -0
  127. package/src/components/custom/Workout/SetStrip.test.tsx +109 -0
  128. package/src/components/custom/Workout/SetStrip.tsx +71 -0
  129. package/src/components/custom/Workout/SetTableHeader.stories.tsx +66 -0
  130. package/src/components/custom/Workout/SetTableHeader.test.tsx +47 -0
  131. package/src/components/custom/Workout/SetTableHeader.tsx +88 -0
  132. package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +47 -0
  133. package/src/components/custom/Workout/SetsRepsLoad.test.tsx +44 -0
  134. package/src/components/custom/Workout/SetsRepsLoad.tsx +52 -0
  135. package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
  136. package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
  137. package/src/components/custom/Workout/StatusPill.stories.tsx +59 -0
  138. package/src/components/custom/Workout/StatusPill.test.tsx +63 -0
  139. package/src/components/custom/Workout/StatusPill.tsx +88 -0
  140. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +1 -1
  141. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
  142. package/src/components/custom/Workout/TempoBar.stories.tsx +70 -1
  143. package/src/components/custom/Workout/TempoBar.test.tsx +119 -4
  144. package/src/components/custom/Workout/TempoBar.tsx +107 -22
  145. package/src/components/custom/Workout/TempoDisplay.stories.tsx +12 -1
  146. package/src/components/custom/Workout/TempoDisplay.tsx +23 -33
  147. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +1 -1
  148. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +254 -0
  149. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +145 -0
  150. package/src/components/custom/Workout/VelocityStrip.stories.tsx +262 -56
  151. package/src/components/custom/Workout/VelocityStrip.test.tsx +440 -141
  152. package/src/components/custom/Workout/VelocityStrip.tsx +776 -141
  153. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +73 -0
  154. package/src/components/custom/Workout/VolumeLandmarkBar.test.tsx +119 -0
  155. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +154 -0
  156. package/src/components/custom/Workout/WeekRow.stories.tsx +1 -1
  157. package/src/components/custom/Workout/WeightBadge.stories.tsx +1 -1
  158. package/src/components/custom/Workout/WorkoutCard.stories.tsx +2 -6
  159. package/src/components/custom/Workout/WorkoutCard.test.tsx +13 -46
  160. package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
  161. package/src/components/custom/Workout/ZoneTrack.stories.tsx +139 -0
  162. package/src/components/custom/Workout/ZoneTrack.test.tsx +234 -0
  163. package/src/components/custom/Workout/ZoneTrack.tsx +373 -0
  164. package/src/components/custom/Workout/index.ts +55 -2
  165. package/src/components/custom/Workout/metricText.stories.tsx +69 -0
  166. package/src/components/custom/Workout/metricText.tsx +34 -0
  167. package/src/components/custom/Workout/paceTone.test.ts +29 -0
  168. package/src/components/custom/Workout/paceTone.ts +27 -0
  169. package/src/components/custom/Workout/setHeadingKit.tsx +177 -0
  170. package/src/components/custom/index.ts +2 -0
  171. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  172. package/src/components/icons/icons.stories.tsx +67 -2
  173. package/src/components/icons/icons.test.tsx +32 -2
  174. package/src/components/icons/icons.tsx +108 -0
  175. package/src/components/shell/BrandLockup.stories.tsx +3 -3
  176. package/src/components/shell/DashboardShell.stories.tsx +65 -0
  177. package/src/components/shell/DashboardShell.test.tsx +42 -0
  178. package/src/components/shell/DashboardShell.tsx +74 -0
  179. package/src/components/shell/DeviceIndicator.stories.tsx +2 -2
  180. package/src/components/shell/DeviceMenu.stories.tsx +5 -5
  181. package/src/components/shell/DeviceRow.stories.tsx +3 -3
  182. package/src/components/shell/NavItem.stories.tsx +52 -0
  183. package/src/components/shell/NavItem.test.tsx +35 -0
  184. package/src/components/shell/NavItem.tsx +75 -0
  185. package/src/components/shell/README.md +30 -0
  186. package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
  187. package/src/components/shell/SessionStatePill.stories.tsx +3 -3
  188. package/src/components/shell/SessionStatePill.tsx +1 -1
  189. package/src/components/shell/SideNav.stories.tsx +81 -0
  190. package/src/components/shell/SideNav.test.tsx +51 -0
  191. package/src/components/shell/SideNav.tsx +74 -0
  192. package/src/components/shell/TopBar.stories.tsx +5 -5
  193. package/src/components/shell/index.ts +5 -0
  194. package/src/components/ui/alert/Alert.stories.tsx +86 -3
  195. package/src/components/ui/alert/Alert.test.tsx +52 -0
  196. package/src/components/ui/alert/Alert.tsx +54 -20
  197. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  198. package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
  199. package/src/components/ui/badge/Badge.stories.tsx +1 -1
  200. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  201. package/src/components/ui/button/Button.stories.tsx +1 -1
  202. package/src/components/ui/card/Card.stories.tsx +1 -1
  203. package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
  204. package/src/components/ui/chip/Chip.stories.tsx +1 -1
  205. package/src/components/ui/collapse/Collapse.stories.tsx +1 -1
  206. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  207. package/src/components/ui/divider/Divider.stories.tsx +1 -1
  208. package/src/components/ui/drawer/Drawer.stories.tsx +1 -1
  209. package/src/components/ui/form-field/FormField.stories.tsx +1 -1
  210. package/src/components/ui/help-tip/HelpTip.stories.tsx +1 -1
  211. package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
  212. package/src/components/ui/index.ts +1 -0
  213. package/src/components/ui/indicator/Indicator.stories.tsx +2 -2
  214. package/src/components/ui/indicator/Indicator.test.tsx +1 -0
  215. package/src/components/ui/indicator/Indicator.tsx +6 -1
  216. package/src/components/ui/input/Input.stories.tsx +1 -1
  217. package/src/components/ui/link/Link.stories.tsx +1 -1
  218. package/src/components/ui/list-item/ListItem.stories.tsx +1 -1
  219. package/src/components/ui/menu/Menu.stories.tsx +1 -1
  220. package/src/components/ui/modal/Modal.stories.tsx +1 -1
  221. package/src/components/ui/pill/Pill.stories.tsx +1 -1
  222. package/src/components/ui/popover/Popover.stories.tsx +1 -1
  223. package/src/components/ui/progress/Progress.stories.tsx +1 -1
  224. package/src/components/ui/progress/Progress.tsx +47 -22
  225. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  226. package/src/components/ui/section/Section.stories.tsx +1 -1
  227. package/src/components/ui/select/Select.stories.tsx +1 -1
  228. package/src/components/ui/skeleton/Skeleton.stories.tsx +1 -1
  229. package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
  230. package/src/components/ui/stack/Stack.stories.tsx +1 -1
  231. package/src/components/ui/surface/Surface.stories.tsx +1 -1
  232. package/src/components/ui/switch/Switch.stories.tsx +1 -1
  233. package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
  234. package/src/components/ui/tile/Tile.stories.tsx +79 -0
  235. package/src/components/ui/tile/Tile.test.tsx +48 -0
  236. package/src/components/ui/tile/Tile.tsx +64 -0
  237. package/src/components/ui/tile/index.ts +2 -0
  238. package/src/components/ui/toast/Toast.stories.tsx +1 -1
  239. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
  240. package/src/components/ui/tooltip/Tooltip.stories.tsx +1 -1
  241. package/src/examples/react-hook-form/ReactHookForm.stories.tsx +1 -1
  242. package/src/hooks/index.ts +2 -0
  243. package/src/hooks/useTimer.test.ts +150 -0
  244. package/src/hooks/useTimer.ts +77 -0
  245. package/src/index.ts +3 -0
  246. package/src/pages.ts +1 -1
  247. package/src/stories/PresetShowcase.stories.tsx +1 -1
  248. package/src/stories/ThemePresets.stories.tsx +1 -1
  249. package/src/theme/ColorSystem.stories.tsx +1 -1
  250. package/src/theme/Colors.stories.tsx +5 -8
  251. package/src/theme/config.ts +4 -0
  252. package/src/theme/elevation.stories.tsx +1 -1
  253. package/src/theme/global.css +5 -1
  254. package/src/theme/shadows.stories.tsx +1 -1
  255. package/src/theme/tokens/primitives.ts +11 -107
  256. package/src/theme/tokens/semantic.ts +8 -0
  257. package/src/theme/workout-tokens.ts +11 -7
  258. package/tailwind.config.js +5 -0
@@ -4,7 +4,7 @@ import { Tooltip } from './Tooltip'
4
4
  import { Button, ButtonText } from '../button'
5
5
 
6
6
  const meta: Meta<typeof Tooltip> = {
7
- title: 'Components/Tooltip',
7
+ title: 'Components/Molecules/Tooltip',
8
8
  component: Tooltip,
9
9
  tags: ['autodocs'],
10
10
  argTypes: {
@@ -10,7 +10,7 @@ import { RhfContactForm, type ContactFormValues } from './RhfContactForm'
10
10
  * `Controller` render-prop idiom. See `ControlledFields.tsx` for the adapters.
11
11
  */
12
12
  const meta: Meta<typeof RhfContactForm> = {
13
- title: 'Examples/React Hook Form',
13
+ title: 'Lab/Recipes/React Hook Form',
14
14
  component: RhfContactForm,
15
15
  tags: ['autodocs'],
16
16
  parameters: {
@@ -0,0 +1,2 @@
1
+ export { useTimer, formatDuration } from './useTimer'
2
+ export type { UseTimerOptions, TimerState, TimerMode } from './useTimer'
@@ -0,0 +1,150 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest'
2
+ import { renderHook, act } from '@testing-library/react'
3
+ import { useTimer, formatDuration } from './useTimer'
4
+
5
+ describe('formatDuration', () => {
6
+ it('formats zero as 0:00', () => {
7
+ expect(formatDuration(0)).toBe('0:00')
8
+ })
9
+
10
+ it('pads seconds with a leading zero', () => {
11
+ expect(formatDuration(65000)).toBe('1:05')
12
+ })
13
+
14
+ it('rounds partial seconds up', () => {
15
+ expect(formatDuration(4500)).toBe('0:05')
16
+ })
17
+
18
+ it('clamps negative input to 0:00', () => {
19
+ expect(formatDuration(-1000)).toBe('0:00')
20
+ })
21
+
22
+ it('formats multi-minute durations', () => {
23
+ expect(formatDuration(150000)).toBe('2:30')
24
+ })
25
+ })
26
+
27
+ describe('useTimer (controlled, down)', () => {
28
+ it('derives remaining, progress, and label from elapsedMs', () => {
29
+ const { result } = renderHook(() =>
30
+ useTimer({ mode: 'down', durationMs: 100000, elapsedMs: 30000 })
31
+ )
32
+ expect(result.current.remainingMs).toBe(70000)
33
+ expect(result.current.progress).toBeCloseTo(0.3)
34
+ expect(result.current.label).toBe('1:10')
35
+ expect(result.current.done).toBe(false)
36
+ })
37
+
38
+ it('clamps remaining to 0 and progress to 1 when elapsed exceeds duration', () => {
39
+ const { result } = renderHook(() =>
40
+ useTimer({ mode: 'down', durationMs: 100000, elapsedMs: 200000 })
41
+ )
42
+ expect(result.current.remainingMs).toBe(0)
43
+ expect(result.current.progress).toBe(1)
44
+ expect(result.current.label).toBe('0:00')
45
+ expect(result.current.done).toBe(true)
46
+ })
47
+
48
+ it('reports progress 1 and done for a zero-duration countdown', () => {
49
+ const { result } = renderHook(() => useTimer({ mode: 'down', durationMs: 0, elapsedMs: 0 }))
50
+ expect(result.current.progress).toBe(1)
51
+ expect(result.current.done).toBe(true)
52
+ expect(result.current.label).toBe('0:00')
53
+ })
54
+ })
55
+
56
+ describe('useTimer (controlled, up)', () => {
57
+ it('counts elapsed upward and computes progress against duration', () => {
58
+ const { result } = renderHook(() =>
59
+ useTimer({ mode: 'up', durationMs: 120000, elapsedMs: 30000 })
60
+ )
61
+ expect(result.current.elapsedMs).toBe(30000)
62
+ expect(result.current.progress).toBeCloseTo(0.25)
63
+ expect(result.current.label).toBe('0:30')
64
+ })
65
+
66
+ it('reports progress 0 and never done without a duration', () => {
67
+ const { result } = renderHook(() => useTimer({ mode: 'up', elapsedMs: 45000 }))
68
+ expect(result.current.progress).toBe(0)
69
+ expect(result.current.done).toBe(false)
70
+ expect(result.current.label).toBe('0:45')
71
+ })
72
+ })
73
+
74
+ describe('useTimer (self-ticking)', () => {
75
+ afterEach(() => {
76
+ vi.useRealTimers()
77
+ })
78
+
79
+ it('advances elapsed on each interval while running', () => {
80
+ vi.useFakeTimers()
81
+ const { result } = renderHook(() =>
82
+ useTimer({ mode: 'up', autoTick: true, running: true, tickMs: 1000 })
83
+ )
84
+ expect(result.current.elapsedMs).toBe(0)
85
+ act(() => {
86
+ vi.advanceTimersByTime(3000)
87
+ })
88
+ expect(result.current.elapsedMs).toBe(3000)
89
+ })
90
+
91
+ it('counts a down-timer toward 0 and reports done', () => {
92
+ vi.useFakeTimers()
93
+ const { result } = renderHook(() =>
94
+ useTimer({ mode: 'down', durationMs: 3000, autoTick: true, running: true })
95
+ )
96
+ expect(result.current.label).toBe('0:03')
97
+ act(() => {
98
+ vi.advanceTimersByTime(3000)
99
+ })
100
+ expect(result.current.remainingMs).toBe(0)
101
+ expect(result.current.label).toBe('0:00')
102
+ expect(result.current.done).toBe(true)
103
+ })
104
+
105
+ it('does not tick when not running', () => {
106
+ vi.useFakeTimers()
107
+ const { result } = renderHook(() => useTimer({ mode: 'up', autoTick: true, running: false }))
108
+ act(() => {
109
+ vi.advanceTimersByTime(5000)
110
+ })
111
+ expect(result.current.elapsedMs).toBe(0)
112
+ })
113
+
114
+ it('does not tick when a controlled elapsedMs is supplied', () => {
115
+ vi.useFakeTimers()
116
+ const { result } = renderHook(() =>
117
+ useTimer({ mode: 'up', autoTick: true, running: true, elapsedMs: 10000 })
118
+ )
119
+ act(() => {
120
+ vi.advanceTimersByTime(5000)
121
+ })
122
+ expect(result.current.elapsedMs).toBe(10000)
123
+ })
124
+
125
+ it('clears the interval on unmount', () => {
126
+ vi.useFakeTimers()
127
+ const clearSpy = vi.spyOn(global, 'clearInterval')
128
+ const { unmount } = renderHook(() => useTimer({ mode: 'up', autoTick: true, running: true }))
129
+ unmount()
130
+ expect(clearSpy).toHaveBeenCalled()
131
+ clearSpy.mockRestore()
132
+ })
133
+
134
+ it('stops ticking after running flips to false', () => {
135
+ vi.useFakeTimers()
136
+ const { result, rerender } = renderHook(
137
+ ({ running }) => useTimer({ mode: 'up', autoTick: true, running }),
138
+ { initialProps: { running: true } }
139
+ )
140
+ act(() => {
141
+ vi.advanceTimersByTime(2000)
142
+ })
143
+ expect(result.current.elapsedMs).toBe(2000)
144
+ rerender({ running: false })
145
+ act(() => {
146
+ vi.advanceTimersByTime(5000)
147
+ })
148
+ expect(result.current.elapsedMs).toBe(2000)
149
+ })
150
+ })
@@ -0,0 +1,77 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ export type TimerMode = 'up' | 'down'
4
+
5
+ export interface UseTimerOptions {
6
+ /** 'up' counts elapsed upward; 'down' counts remaining toward 0. Default 'up'. */
7
+ mode?: TimerMode
8
+ /** Total duration in ms. Required for 'down' mode and for progress in 'up' mode. */
9
+ durationMs?: number
10
+ /** Controlled elapsed time in ms. When provided, the hook derives everything from it and never ticks. */
11
+ elapsedMs?: number
12
+ /** Self-tick only advances while running. */
13
+ running?: boolean
14
+ /** Enable the internal interval. Ignored when `elapsedMs` is controlled. */
15
+ autoTick?: boolean
16
+ /** Interval step in ms. Default 1000. */
17
+ tickMs?: number
18
+ }
19
+
20
+ export interface TimerState {
21
+ elapsedMs: number
22
+ remainingMs: number
23
+ /** 0..1, clamped. */
24
+ progress: number
25
+ /** mm:ss for the active direction (remaining when 'down', elapsed when 'up'). */
26
+ label: string
27
+ /** True once a down-timer reaches 0 (or an up-timer passes its duration). */
28
+ done: boolean
29
+ }
30
+
31
+ /** Format a millisecond duration as mm:ss, rounding partial seconds up. */
32
+ export function formatDuration(ms: number): string {
33
+ const totalSeconds = Math.max(0, Math.ceil(ms / 1000))
34
+ const minutes = Math.floor(totalSeconds / 60)
35
+ const seconds = totalSeconds % 60
36
+ return `${minutes}:${String(seconds).padStart(2, '0')}`
37
+ }
38
+
39
+ export function useTimer(options: UseTimerOptions = {}): TimerState {
40
+ const {
41
+ mode = 'up',
42
+ durationMs,
43
+ elapsedMs: controlledElapsedMs,
44
+ running = false,
45
+ autoTick = false,
46
+ tickMs = 1000,
47
+ } = options
48
+
49
+ const isControlled = controlledElapsedMs != null
50
+ const [internalElapsedMs, setInternalElapsedMs] = useState(0)
51
+
52
+ useEffect(() => {
53
+ if (isControlled || !autoTick || !running) return
54
+ const id = setInterval(() => {
55
+ setInternalElapsedMs((prev) => prev + tickMs)
56
+ }, tickMs)
57
+ return () => clearInterval(id)
58
+ }, [isControlled, autoTick, running, tickMs])
59
+
60
+ const elapsedMs = isControlled ? controlledElapsedMs : internalElapsedMs
61
+
62
+ const hasDuration = durationMs != null && durationMs > 0
63
+ const remainingMs = durationMs != null ? Math.max(0, durationMs - elapsedMs) : 0
64
+
65
+ let progress: number
66
+ if (hasDuration) {
67
+ progress = Math.min(1, Math.max(0, elapsedMs / durationMs))
68
+ } else {
69
+ // A zero/undefined duration is trivially complete for a countdown, empty for a stopwatch.
70
+ progress = mode === 'down' ? 1 : 0
71
+ }
72
+
73
+ const label = formatDuration(mode === 'down' ? remainingMs : elapsedMs)
74
+ const done = durationMs != null && elapsedMs >= durationMs
75
+
76
+ return { elapsedMs, remainingMs, progress, label, done }
77
+ }
package/src/index.ts CHANGED
@@ -9,3 +9,6 @@ export * from './theme/barrel'
9
9
 
10
10
  // Utilities
11
11
  export * from './utils'
12
+
13
+ // Hooks
14
+ export * from './hooks'
package/src/pages.ts CHANGED
@@ -15,7 +15,7 @@ export {
15
15
  countCompletedSets,
16
16
  deriveWorkoutProgress,
17
17
  findActiveExercise,
18
- statusToCardState,
18
+ isUpcomingExercise,
19
19
  toActiveCardProps,
20
20
  groupExercises,
21
21
  type ActiveWorkoutPageProps,
@@ -438,7 +438,7 @@ function Section({ title, children }: { title: string; children: React.ReactNode
438
438
  // ---------------------------------------------------------------------------
439
439
 
440
440
  const meta: Meta = {
441
- title: 'Theme/Preset Showcase',
441
+ title: 'Lab/Recipes/Theme Preset Showcase',
442
442
  parameters: {
443
443
  layout: 'fullscreen',
444
444
  },
@@ -66,7 +66,7 @@ function PresetShowcase({ preset }: { preset: typeof audiobookPreset }) {
66
66
  }
67
67
 
68
68
  const meta: Meta = {
69
- title: 'Theme/Presets',
69
+ title: 'Foundations/Theme Presets',
70
70
  parameters: {
71
71
  layout: 'fullscreen',
72
72
  },
@@ -13,7 +13,7 @@ import {
13
13
  } from './tokens/primitives'
14
14
 
15
15
  const meta: Meta = {
16
- title: 'Design Tokens/Color System',
16
+ title: 'Foundations/Color/System',
17
17
  tags: ['autodocs'],
18
18
  }
19
19
  export default meta
@@ -4,7 +4,7 @@ import { primitiveColors, discreteRainbow, primitiveRamps, categoricalPalette }
4
4
  import { semanticColorsLight, semanticColorsDark } from './tokens/semantic'
5
5
 
6
6
  const meta: Meta = {
7
- title: 'Design Tokens/Colors',
7
+ title: 'Foundations/Color/Palette',
8
8
  tags: ['autodocs'],
9
9
  }
10
10
 
@@ -79,16 +79,13 @@ export const PrimitiveColors: StoryObj = {
79
79
  Primitive Colors
80
80
  </Text>
81
81
  <Text className="text-text-secondary mb-6">
82
- Raw color values. Use semantic tokens instead when building components.
82
+ Raw color values. Chromatic hues live as OKLCH tonal ramps (see the Tonal Ramps story);
83
+ these are the achromatic and support scales. Use semantic tokens when building components.
83
84
  </Text>
84
85
 
85
- <ColorScale name="Orange (Primary Brand)" colors={primitiveColors.accent} />
86
- <ColorScale name="Steel (Secondary Brand)" colors={primitiveColors.steel} />
87
86
  <ColorScale name="Blue" colors={primitiveColors.blue} />
88
- <ColorScale name="Green" colors={primitiveColors.green} />
89
- <ColorScale name="Teal (Success)" colors={primitiveColors.teal} />
90
- <ColorScale name="Red (Error)" colors={primitiveColors.red} />
91
- <ColorScale name="Sky (Info)" colors={primitiveColors.sky} />
87
+ <ColorScale name="Red" colors={primitiveColors.red} />
88
+ <ColorScale name="Red Vivid" colors={primitiveColors.redVivid} />
92
89
  <ColorScale name="Neutral" colors={primitiveColors.neutral} />
93
90
  <ColorScale name="Charcoal (Dark Backgrounds)" colors={primitiveColors.charcoal} />
94
91
  </View>
@@ -53,6 +53,8 @@ export const lightThemeCSSVars = {
53
53
 
54
54
  '--color-status-success': semanticColorsLight['status-success'],
55
55
  '--color-status-success-subtle': semanticColorsLight['status-success-subtle'],
56
+ '--color-status-live': semanticColorsLight['status-live'],
57
+ '--color-status-live-muted': semanticColorsLight['status-live-muted'],
56
58
  '--color-status-error': semanticColorsLight['status-error'],
57
59
  '--color-status-error-subtle': semanticColorsLight['status-error-subtle'],
58
60
  '--color-status-warning': semanticColorsLight['status-warning'],
@@ -172,6 +174,8 @@ export const darkThemeCSSVars = {
172
174
 
173
175
  '--color-status-success': semanticColorsDark['status-success'],
174
176
  '--color-status-success-subtle': semanticColorsDark['status-success-subtle'],
177
+ '--color-status-live': semanticColorsDark['status-live'],
178
+ '--color-status-live-muted': semanticColorsDark['status-live-muted'],
175
179
  '--color-status-error': semanticColorsDark['status-error'],
176
180
  '--color-status-error-subtle': semanticColorsDark['status-error-subtle'],
177
181
  '--color-status-warning': semanticColorsDark['status-warning'],
@@ -21,7 +21,7 @@ import { useTheme } from '../utils/useTheme'
21
21
  import { Card, CardContent, CardInset } from '../components/ui/card/Card'
22
22
 
23
23
  const meta: Meta = {
24
- title: 'Theme/Elevation System',
24
+ title: 'Foundations/Elevation',
25
25
  tags: ['autodocs'],
26
26
  }
27
27
 
@@ -35,6 +35,8 @@
35
35
  --color-status-success-light: #58F69E;
36
36
  --color-status-success-dark: #298732;
37
37
  --color-status-success-subtle: rgba(46, 213, 115, 0.12);
38
+ --color-status-live: #2ED573;
39
+ --color-status-live-muted: #22A444;
38
40
  --color-status-error: #D14343;
39
41
  --color-status-error-light: #E05254;
40
42
  --color-status-error-dark: #A4221C;
@@ -170,6 +172,8 @@
170
172
  --color-status-success-light: #58F69E;
171
173
  --color-status-success-dark: #298732;
172
174
  --color-status-success-subtle: #E3FFEE;
175
+ --color-status-live: #2ED573;
176
+ --color-status-live-muted: #22A444;
173
177
  --color-status-error: #D14343;
174
178
  --color-status-error-light: #E05254;
175
179
  --color-status-error-dark: #A4221C;
@@ -369,7 +373,7 @@
369
373
  z-index: 0;
370
374
  background:
371
375
  radial-gradient(ellipse 140% 100% at 15% 5%, rgba(var(--color-brand-primary-rgb, 255, 121, 0), 0.06) 0%, transparent 45%),
372
- radial-gradient(ellipse 120% 80% at 85% 95%, rgba(var(--color-brand-secondary-rgb, 64, 109, 135), 0.04) 0%, transparent 45%);
376
+ radial-gradient(ellipse 120% 80% at 85% 95%, rgba(var(--color-brand-secondary-rgb, 48, 123, 155), 0.04) 0%, transparent 45%);
373
377
  }
374
378
 
375
379
  /* Film grain overlay for vintage texture */
@@ -8,7 +8,7 @@ import {
8
8
  } from './shadows'
9
9
 
10
10
  const meta: Meta = {
11
- title: 'Theme/Shadows',
11
+ title: 'Foundations/Shadows',
12
12
  }
13
13
 
14
14
  export default meta
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  export const primitiveColors = {
9
- // Blue scale (primary brand)
9
+ // Blue scale
10
10
  blue: {
11
11
  50: '#EFF6FF',
12
12
  100: '#DBEAFE',
@@ -14,110 +14,40 @@ export const primitiveColors = {
14
14
  300: '#93C5FD',
15
15
  400: '#60A5FA',
16
16
  500: '#3B82F6',
17
- 600: '#5048E5', // Primary main
18
- 700: '#3832A0', // Primary dark
17
+ 600: '#5048E5',
18
+ 700: '#3832A0',
19
19
  800: '#1E40AF',
20
20
  900: '#1E3A8A',
21
21
  },
22
22
 
23
- // Green scale (secondary brand)
24
- green: {
25
- 50: '#ECFDF5',
26
- 100: '#D1FAE5',
27
- 200: '#A7F3D0',
28
- 300: '#6EE7B7',
29
- 400: '#3FC79A', // Secondary light
30
- 500: '#10B981', // Secondary main
31
- 600: '#059669',
32
- 700: '#0B815A', // Secondary dark
33
- 800: '#065F46',
34
- 900: '#064E3B',
35
- },
36
-
37
- // Teal scale (success)
38
- teal: {
39
- 50: '#F0FDFA',
40
- 100: '#CCFBF1',
41
- 200: '#99F6E4',
42
- 300: '#5EEAD4',
43
- 400: '#43C6B7', // Success light
44
- 500: '#14B8A6', // Success main
45
- 600: '#0D9488',
46
- 700: '#0E8074', // Success dark
47
- 800: '#115E59',
48
- 900: '#134E4A',
49
- },
50
-
51
- // Red scale (error)
23
+ // Red scale
52
24
  red: {
53
25
  50: '#FEF2F2',
54
26
  100: '#FEE2E2',
55
27
  200: '#FECACA',
56
28
  300: '#FCA5A5',
57
- 400: '#DA6868', // Error light
29
+ 400: '#DA6868',
58
30
  500: '#EF4444',
59
- 600: '#D14343', // Error main
60
- 700: '#922E2E', // Error dark
31
+ 600: '#D14343',
32
+ 700: '#922E2E',
61
33
  800: '#991B1B',
62
34
  900: '#7F1D1D',
63
35
  },
64
36
 
65
- // Vivid green scale (live/go success)
66
- greenVivid: {
67
- 50: '#E9FBF0',
68
- 100: '#C8F7DA',
69
- 200: '#9FF0BF',
70
- 300: '#6BE79D',
71
- 400: '#47DE84', // Success vivid light
72
- 500: '#2ED573', // Success vivid main
73
- 600: '#22B85F',
74
- 700: '#1A9950', // Success vivid dark
75
- 800: '#157A40',
76
- 900: '#105C30',
77
- },
78
-
79
- // Vivid red scale (alert)
37
+ // Vivid red scale
80
38
  redVivid: {
81
39
  50: '#FFECEE',
82
40
  100: '#FFD6DB',
83
41
  200: '#FFB3BC',
84
42
  300: '#FF8593',
85
- 400: '#FF6070', // Error vivid light
86
- 500: '#FF4757', // Error vivid main
43
+ 400: '#FF6070',
44
+ 500: '#FF4757',
87
45
  600: '#E63548',
88
- 700: '#C42539', // Error vivid dark
46
+ 700: '#C42539',
89
47
  800: '#9E1C2C',
90
48
  900: '#7A1520',
91
49
  },
92
50
 
93
- // Amber scale (warning)
94
- amber: {
95
- 50: '#FFFBEB',
96
- 100: '#FEF3C7',
97
- 200: '#FDE68A',
98
- 300: '#FCD34D',
99
- 400: '#FFBF4C', // Warning light
100
- 500: '#FFB020', // Warning main
101
- 600: '#B27B16', // Warning dark
102
- 700: '#B45309',
103
- 800: '#92400E',
104
- 900: '#78350F',
105
- },
106
-
107
- // Sky/Blue scale (info)
108
- sky: {
109
- 50: '#F0F9FF',
110
- 100: '#E0F2FE',
111
- 200: '#BAE6FD',
112
- 300: '#7DD3FC',
113
- 400: '#64B6F7', // Info light
114
- 500: '#2196F3', // Info main
115
- 600: '#0284C7',
116
- 700: '#0B79D0', // Info dark
117
- 800: '#075985',
118
- 900: '#0C4A6E',
119
- },
120
-
121
51
  // Neutral/Gray scale
122
52
  neutral: {
123
53
  50: '#FAFAFA',
@@ -133,19 +63,6 @@ export const primitiveColors = {
133
63
  950: '#030712',
134
64
  },
135
65
 
136
- // Orange scale (accent/brand color)
137
- accent: {
138
- 100: '#FFA860',
139
- 200: '#FF9630',
140
- 300: '#FF8500',
141
- 400: '#FF7900',
142
- 500: '#F56C00',
143
- 600: '#E06D10',
144
- 700: '#D0620C',
145
- 800: '#B75500',
146
- 900: '#A34900',
147
- },
148
-
149
66
  // Charcoal scale (dark backgrounds)
150
67
  charcoal: {
151
68
  0: '#6E6E6E',
@@ -161,19 +78,6 @@ export const primitiveColors = {
161
78
  900: '#101010',
162
79
  },
163
80
 
164
- // Steel scale (cool accent)
165
- steel: {
166
- 100: '#8AA4B8',
167
- 200: '#7894A8',
168
- 300: '#678498',
169
- 400: '#557488',
170
- 500: '#406D87',
171
- 600: '#39617A',
172
- 700: '#32556D',
173
- 800: '#243D53',
174
- 900: '#1D3146',
175
- },
176
-
177
81
  // Pure colors
178
82
  white: '#FFFFFF',
179
83
  black: '#000000',
@@ -46,6 +46,10 @@ export const semanticColorsLight = {
46
46
  'status-success-dark': ramp.green[600],
47
47
  'status-success-subtle': ramp.green[50],
48
48
 
49
+ // Live-session accent — its OWN role, decoupled from success so the two can diverge
50
+ 'status-live': ramp.green[300],
51
+ 'status-live-muted': ramp.green[500],
52
+
49
53
  'status-error': ramp.red[600],
50
54
  'status-error-light': ramp.red[500],
51
55
  'status-error-dark': ramp.red[700],
@@ -176,6 +180,10 @@ export const semanticColorsDark = {
176
180
  'status-success-dark': ramp.green[600],
177
181
  'status-success-subtle': 'rgba(46, 213, 115, 0.12)',
178
182
 
183
+ // Live-session accent — its OWN role, decoupled from success so the two can diverge
184
+ 'status-live': ramp.green[300],
185
+ 'status-live-muted': ramp.green[500],
186
+
179
187
  'status-error': ramp.red[600],
180
188
  'status-error-light': ramp.red[500],
181
189
  'status-error-dark': ramp.red[700],
@@ -8,14 +8,18 @@ export const WORKOUT_TOKENS = {
8
8
  // Canonical 4-band performance scale — the single source for BOTH the
9
9
  // VelocityStrip zone bars and the SetRow RPE color (TD-03.43). The direction
10
10
  // is intentionally inverted between the two consumers: for velocity, green =
11
- // fastest/best; for RPE, green = easiest. This is the [0,1,2,4] subsample of
12
- // the canonical `sequentialEffort` primitive. The `orange` key is the band
13
- // label. Consumed inline (RN).
11
+ // fastest/best; for RPE, green = easiest. This is the [0,2,3,4] subsample of
12
+ // the canonical `sequentialEffort` primitive: an EVEN hue walk across the ramp
13
+ // (green gold → orange → red). The earlier [0,1,2,4] spent two of its four
14
+ // bands on adjacent yellows (amber-200 + amber-300) and skipped orange, reading
15
+ // yellow-heavy with an abrupt jump to red; sampling amber-300 + orange-400
16
+ // instead gives four cleanly separable bands that hold at 3px strip scale.
17
+ // Consumed inline (RN).
14
18
  scale: {
15
- green: sequentialEffort[0],
16
- yellow: sequentialEffort[1],
17
- orange: sequentialEffort[2], // gold band
18
- red: sequentialEffort[4],
19
+ green: sequentialEffort[0], // green-300
20
+ yellow: sequentialEffort[2], // amber-300 (gold)
21
+ orange: sequentialEffort[3], // orange-400 (true orange)
22
+ red: sequentialEffort[4], // red-600
19
23
  },
20
24
 
21
25
  // BodyMap volume heatmap — the canonical `divergingScale` (under → optimal →
@@ -38,6 +38,11 @@ module.exports = {
38
38
  dark: 'var(--color-status-success-dark)',
39
39
  subtle: 'var(--color-status-success-subtle)',
40
40
  },
41
+ // Live-session accent — own role, decoupled from success
42
+ live: {
43
+ DEFAULT: 'var(--color-status-live)',
44
+ muted: 'var(--color-status-live-muted)',
45
+ },
41
46
  error: {
42
47
  DEFAULT: 'var(--color-status-error)',
43
48
  light: 'var(--color-status-error-light)',