@titan-design/react-ui 0.6.0 → 0.7.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 (248) 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 +855 -124
  6. package/dist/index.d.ts +855 -124
  7. package/dist/index.js +2276 -801
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2230 -802
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/{pages-MO0JCySL.d.mts → pages-8u_4WbL-.d.mts} +280 -57
  12. package/dist/{pages-D7Jlssvp.d.ts → pages-DaWiBOGa.d.ts} +280 -57
  13. package/dist/pages.d.mts +1 -1
  14. package/dist/pages.d.ts +1 -1
  15. package/dist/pages.js +1255 -798
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1255 -798
  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/DateTime/DateTime.stories.tsx +1 -1
  34. package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
  35. package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
  36. package/src/components/custom/Metric/Metric.stories.tsx +1 -1
  37. package/src/components/custom/Scatter/Scatter.stories.tsx +1 -1
  38. package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
  39. package/src/components/custom/Table/Table.stories.tsx +1 -1
  40. package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +62 -0
  41. package/src/components/custom/TimerReadout/TimerReadout.test.tsx +72 -0
  42. package/src/components/custom/TimerReadout/TimerReadout.tsx +75 -0
  43. package/src/components/custom/TimerReadout/index.ts +1 -0
  44. package/src/components/custom/Treemap/Treemap.stories.tsx +1 -1
  45. package/src/components/custom/Typography/Typography.stories.tsx +1 -1
  46. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +21 -13
  47. package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +16 -13
  48. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +9 -15
  49. package/src/components/custom/Workout/BaseBadge.stories.tsx +1 -1
  50. package/src/components/custom/Workout/BodyMap.stories.tsx +1 -1
  51. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
  52. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
  53. package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
  54. package/src/components/custom/Workout/ExerciseCard.stories.tsx +51 -66
  55. package/src/components/custom/Workout/ExerciseCard.test.tsx +146 -143
  56. package/src/components/custom/Workout/ExerciseCard.tsx +176 -198
  57. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +99 -0
  58. package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +72 -0
  59. package/src/components/custom/Workout/ExerciseCardHeading.tsx +43 -0
  60. package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +151 -0
  61. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +5 -3
  62. package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +4 -4
  63. package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -5
  64. package/src/components/custom/Workout/ExerciseHeading.stories.tsx +81 -0
  65. package/src/components/custom/Workout/ExerciseHeading.test.tsx +77 -0
  66. package/src/components/custom/Workout/ExerciseHeading.tsx +85 -0
  67. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +110 -0
  68. package/src/components/custom/Workout/ExerciseIndicator.test.tsx +93 -0
  69. package/src/components/custom/Workout/ExerciseIndicator.tsx +120 -0
  70. package/src/components/custom/Workout/FatigueMeter.stories.tsx +51 -0
  71. package/src/components/custom/Workout/FatigueMeter.test.tsx +73 -0
  72. package/src/components/custom/Workout/FatigueMeter.tsx +82 -0
  73. package/src/components/custom/Workout/InputBar.stories.tsx +1 -1
  74. package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
  75. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +153 -0
  76. package/src/components/custom/Workout/LiveAuraFrame.test.tsx +74 -0
  77. package/src/components/custom/Workout/LiveAuraFrame.tsx +105 -0
  78. package/src/components/custom/Workout/MesoCard.stories.tsx +1 -1
  79. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
  80. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
  81. package/src/components/custom/Workout/MetricTiles.stories.tsx +60 -0
  82. package/src/components/custom/Workout/MetricTiles.test.tsx +58 -0
  83. package/src/components/custom/Workout/MetricTiles.tsx +48 -0
  84. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +1 -1
  85. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
  86. package/src/components/custom/Workout/PrBadge.stories.tsx +1 -1
  87. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
  88. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
  89. package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -6
  90. package/src/components/custom/Workout/README.md +146 -4
  91. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
  92. package/src/components/custom/Workout/RestTimer.stories.tsx +1 -1
  93. package/src/components/custom/Workout/RestTimer.tsx +15 -10
  94. package/src/components/custom/Workout/RowInventory.stories.tsx +405 -0
  95. package/src/components/custom/Workout/S3FullRail.stories.tsx +34 -0
  96. package/src/components/custom/Workout/S3SessionPace.stories.tsx +95 -0
  97. package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +61 -0
  98. package/src/components/custom/Workout/S3SetTypes.stories.tsx +176 -0
  99. package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +240 -0
  100. package/src/components/custom/Workout/ScheduleTiles.stories.tsx +40 -0
  101. package/src/components/custom/Workout/ScheduleTiles.test.tsx +34 -0
  102. package/src/components/custom/Workout/ScheduleTiles.tsx +70 -0
  103. package/src/components/custom/Workout/SegmentedBar.stories.tsx +99 -0
  104. package/src/components/custom/Workout/SegmentedBar.test.tsx +77 -0
  105. package/src/components/custom/Workout/SegmentedBar.tsx +172 -0
  106. package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +50 -0
  107. package/src/components/custom/Workout/SegmentedProgressBar.test.tsx +80 -0
  108. package/src/components/custom/Workout/SegmentedProgressBar.tsx +67 -0
  109. package/src/components/custom/Workout/SessionHeader.stories.tsx +121 -0
  110. package/src/components/custom/Workout/SessionHeader.test.tsx +122 -0
  111. package/src/components/custom/Workout/SessionHeader.tsx +152 -0
  112. package/src/components/custom/Workout/SessionRail.stories.tsx +153 -0
  113. package/src/components/custom/Workout/SessionRail.test.tsx +86 -0
  114. package/src/components/custom/Workout/SessionRail.tsx +126 -0
  115. package/src/components/custom/Workout/SetBar.stories.tsx +124 -0
  116. package/src/components/custom/Workout/SetBar.test.tsx +123 -0
  117. package/src/components/custom/Workout/SetBar.tsx +171 -0
  118. package/src/components/custom/Workout/SetRow.stories.tsx +50 -100
  119. package/src/components/custom/Workout/SetRow.test.tsx +94 -166
  120. package/src/components/custom/Workout/SetRow.tsx +169 -232
  121. package/src/components/custom/Workout/SetStrip.stories.tsx +154 -0
  122. package/src/components/custom/Workout/SetStrip.test.tsx +109 -0
  123. package/src/components/custom/Workout/SetStrip.tsx +71 -0
  124. package/src/components/custom/Workout/SetTableHeader.stories.tsx +66 -0
  125. package/src/components/custom/Workout/SetTableHeader.test.tsx +47 -0
  126. package/src/components/custom/Workout/SetTableHeader.tsx +88 -0
  127. package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +47 -0
  128. package/src/components/custom/Workout/SetsRepsLoad.test.tsx +44 -0
  129. package/src/components/custom/Workout/SetsRepsLoad.tsx +52 -0
  130. package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
  131. package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
  132. package/src/components/custom/Workout/StatusPill.stories.tsx +59 -0
  133. package/src/components/custom/Workout/StatusPill.test.tsx +63 -0
  134. package/src/components/custom/Workout/StatusPill.tsx +88 -0
  135. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +1 -1
  136. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
  137. package/src/components/custom/Workout/TempoBar.stories.tsx +1 -1
  138. package/src/components/custom/Workout/TempoDisplay.stories.tsx +12 -1
  139. package/src/components/custom/Workout/TempoDisplay.tsx +23 -33
  140. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +1 -1
  141. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +254 -0
  142. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +145 -0
  143. package/src/components/custom/Workout/VelocityStrip.stories.tsx +129 -66
  144. package/src/components/custom/Workout/VelocityStrip.test.tsx +376 -141
  145. package/src/components/custom/Workout/VelocityStrip.tsx +508 -120
  146. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +73 -0
  147. package/src/components/custom/Workout/VolumeLandmarkBar.test.tsx +119 -0
  148. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +154 -0
  149. package/src/components/custom/Workout/WeekRow.stories.tsx +1 -1
  150. package/src/components/custom/Workout/WeightBadge.stories.tsx +1 -1
  151. package/src/components/custom/Workout/WorkoutCard.stories.tsx +2 -6
  152. package/src/components/custom/Workout/WorkoutCard.test.tsx +13 -46
  153. package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
  154. package/src/components/custom/Workout/ZoneTrack.stories.tsx +140 -0
  155. package/src/components/custom/Workout/ZoneTrack.test.tsx +200 -0
  156. package/src/components/custom/Workout/ZoneTrack.tsx +315 -0
  157. package/src/components/custom/Workout/index.ts +55 -2
  158. package/src/components/custom/Workout/metricText.stories.tsx +69 -0
  159. package/src/components/custom/Workout/metricText.tsx +34 -0
  160. package/src/components/custom/Workout/paceTone.test.ts +29 -0
  161. package/src/components/custom/Workout/paceTone.ts +27 -0
  162. package/src/components/custom/Workout/setHeadingKit.tsx +177 -0
  163. package/src/components/custom/index.ts +1 -0
  164. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  165. package/src/components/icons/icons.stories.tsx +67 -2
  166. package/src/components/icons/icons.test.tsx +32 -2
  167. package/src/components/icons/icons.tsx +108 -0
  168. package/src/components/shell/BrandLockup.stories.tsx +3 -3
  169. package/src/components/shell/DashboardShell.stories.tsx +65 -0
  170. package/src/components/shell/DashboardShell.test.tsx +42 -0
  171. package/src/components/shell/DashboardShell.tsx +74 -0
  172. package/src/components/shell/DeviceIndicator.stories.tsx +2 -2
  173. package/src/components/shell/DeviceMenu.stories.tsx +5 -5
  174. package/src/components/shell/DeviceRow.stories.tsx +3 -3
  175. package/src/components/shell/NavItem.stories.tsx +52 -0
  176. package/src/components/shell/NavItem.test.tsx +35 -0
  177. package/src/components/shell/NavItem.tsx +75 -0
  178. package/src/components/shell/README.md +30 -0
  179. package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
  180. package/src/components/shell/SessionStatePill.stories.tsx +3 -3
  181. package/src/components/shell/SessionStatePill.tsx +1 -1
  182. package/src/components/shell/SideNav.stories.tsx +81 -0
  183. package/src/components/shell/SideNav.test.tsx +51 -0
  184. package/src/components/shell/SideNav.tsx +74 -0
  185. package/src/components/shell/TopBar.stories.tsx +5 -5
  186. package/src/components/shell/index.ts +5 -0
  187. package/src/components/ui/alert/Alert.stories.tsx +1 -1
  188. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  189. package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
  190. package/src/components/ui/badge/Badge.stories.tsx +1 -1
  191. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  192. package/src/components/ui/button/Button.stories.tsx +1 -1
  193. package/src/components/ui/card/Card.stories.tsx +1 -1
  194. package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
  195. package/src/components/ui/chip/Chip.stories.tsx +1 -1
  196. package/src/components/ui/collapse/Collapse.stories.tsx +1 -1
  197. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  198. package/src/components/ui/divider/Divider.stories.tsx +1 -1
  199. package/src/components/ui/drawer/Drawer.stories.tsx +1 -1
  200. package/src/components/ui/form-field/FormField.stories.tsx +1 -1
  201. package/src/components/ui/help-tip/HelpTip.stories.tsx +1 -1
  202. package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
  203. package/src/components/ui/index.ts +1 -0
  204. package/src/components/ui/indicator/Indicator.stories.tsx +2 -2
  205. package/src/components/ui/indicator/Indicator.test.tsx +1 -0
  206. package/src/components/ui/indicator/Indicator.tsx +6 -1
  207. package/src/components/ui/input/Input.stories.tsx +1 -1
  208. package/src/components/ui/link/Link.stories.tsx +1 -1
  209. package/src/components/ui/list-item/ListItem.stories.tsx +1 -1
  210. package/src/components/ui/menu/Menu.stories.tsx +1 -1
  211. package/src/components/ui/modal/Modal.stories.tsx +1 -1
  212. package/src/components/ui/pill/Pill.stories.tsx +1 -1
  213. package/src/components/ui/popover/Popover.stories.tsx +1 -1
  214. package/src/components/ui/progress/Progress.stories.tsx +1 -1
  215. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  216. package/src/components/ui/section/Section.stories.tsx +1 -1
  217. package/src/components/ui/select/Select.stories.tsx +1 -1
  218. package/src/components/ui/skeleton/Skeleton.stories.tsx +1 -1
  219. package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
  220. package/src/components/ui/stack/Stack.stories.tsx +1 -1
  221. package/src/components/ui/surface/Surface.stories.tsx +1 -1
  222. package/src/components/ui/switch/Switch.stories.tsx +1 -1
  223. package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
  224. package/src/components/ui/tile/Tile.stories.tsx +79 -0
  225. package/src/components/ui/tile/Tile.test.tsx +48 -0
  226. package/src/components/ui/tile/Tile.tsx +64 -0
  227. package/src/components/ui/tile/index.ts +2 -0
  228. package/src/components/ui/toast/Toast.stories.tsx +1 -1
  229. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
  230. package/src/components/ui/tooltip/Tooltip.stories.tsx +1 -1
  231. package/src/examples/react-hook-form/ReactHookForm.stories.tsx +1 -1
  232. package/src/hooks/index.ts +2 -0
  233. package/src/hooks/useTimer.test.ts +150 -0
  234. package/src/hooks/useTimer.ts +77 -0
  235. package/src/index.ts +3 -0
  236. package/src/pages.ts +1 -1
  237. package/src/stories/PresetShowcase.stories.tsx +1 -1
  238. package/src/stories/ThemePresets.stories.tsx +1 -1
  239. package/src/theme/ColorSystem.stories.tsx +1 -1
  240. package/src/theme/Colors.stories.tsx +5 -8
  241. package/src/theme/config.ts +4 -0
  242. package/src/theme/elevation.stories.tsx +1 -1
  243. package/src/theme/global.css +5 -1
  244. package/src/theme/shadows.stories.tsx +1 -1
  245. package/src/theme/tokens/primitives.ts +11 -107
  246. package/src/theme/tokens/semantic.ts +8 -0
  247. package/src/theme/workout-tokens.ts +11 -7
  248. package/tailwind.config.js +5 -0
@@ -3,7 +3,7 @@ import { View } from 'react-native'
3
3
  import { Switch } from './Switch'
4
4
 
5
5
  const meta: Meta<typeof Switch> = {
6
- title: 'Components/Switch',
6
+ title: 'Components/Molecules/Switch',
7
7
  component: Switch,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -3,7 +3,7 @@ import { View, Text } from 'react-native'
3
3
  import { Tabs, TabList, Tab, TabPanels, TabPanel } from './Tabs'
4
4
 
5
5
  const meta: Meta<typeof Tabs> = {
6
- title: 'Components/Tabs',
6
+ title: 'Components/Molecules/Tabs',
7
7
  component: Tabs,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -0,0 +1,79 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { Tile } from './Tile'
4
+ import { getSemanticColors } from '../../../theme'
5
+
6
+ const ACCENT = getSemanticColors('dark')['status-warning']
7
+ const SUCCESS = getSemanticColors('dark')['status-success']
8
+
9
+ const meta: Meta<typeof Tile> = {
10
+ title: 'Components/Atoms/Tile',
11
+ component: Tile,
12
+ tags: ['autodocs'],
13
+ parameters: {
14
+ docs: {
15
+ description: {
16
+ component: '**Atom.** A compact card preset; grouped by SessionMetrics / ScheduleTiles.',
17
+ },
18
+ },
19
+ },
20
+ argTypes: {
21
+ label: { control: 'text', description: 'Uppercase micro-label' },
22
+ value: { control: 'text', description: 'Primary mono value' },
23
+ valueColor: { control: 'color', description: 'Tints the value' },
24
+ align: {
25
+ control: 'radio',
26
+ options: ['center', 'start'],
27
+ description: 'Content alignment',
28
+ },
29
+ },
30
+ }
31
+
32
+ export default meta
33
+ type Story = StoryObj<typeof Tile>
34
+
35
+ export const Default: Story = {
36
+ args: {
37
+ label: 'Volume',
38
+ value: '76%',
39
+ align: 'center',
40
+ },
41
+ }
42
+
43
+ export const Row: Story = {
44
+ render: () => (
45
+ <View style={{ flexDirection: 'row', gap: 4, width: 246 }}>
46
+ <Tile label="Volume" value="76%" />
47
+ <Tile label="Load" value="7.3k" />
48
+ <Tile label="Fatigue" value="MOD" valueColor={ACCENT} />
49
+ </View>
50
+ ),
51
+ }
52
+
53
+ export const ScheduleTiles: Story = {
54
+ render: () => (
55
+ <View style={{ flexDirection: 'row', gap: 4, width: 246 }}>
56
+ <Tile label="Date" value="Jul 10" />
57
+ <Tile label="Time" value="6:30 PM" />
58
+ <Tile label="Until" value="5h" valueColor={ACCENT} />
59
+ </View>
60
+ ),
61
+ }
62
+
63
+ export const ValueColors: Story = {
64
+ render: () => (
65
+ <View style={{ flexDirection: 'row', gap: 4, width: 246 }}>
66
+ <Tile label="Default" value="12" />
67
+ <Tile label="Success" value="OK" valueColor={SUCCESS} />
68
+ <Tile label="Warning" value="MOD" valueColor={ACCENT} />
69
+ </View>
70
+ ),
71
+ }
72
+
73
+ export const StartAligned: Story = {
74
+ args: {
75
+ label: 'Program',
76
+ value: 'Pull A',
77
+ align: 'start',
78
+ },
79
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { Tile } from './Tile'
5
+
6
+ describe('Tile', () => {
7
+ it('renders label and value', () => {
8
+ render(<Tile label="Volume" value="76%" />)
9
+ expect(screen.getByText('Volume')).toBeInTheDocument()
10
+ expect(screen.getByText('76%')).toBeInTheDocument()
11
+ })
12
+
13
+ it('applies valueColor to the value text', () => {
14
+ render(<Tile label="Fatigue" value="MOD" valueColor="#F5A623" />)
15
+ const value = screen.getByText('MOD')
16
+ expect(value).toHaveStyle({ color: '#F5A623' })
17
+ })
18
+
19
+ it('does not set an inline color when valueColor is omitted', () => {
20
+ render(<Tile label="Volume" value="76%" />)
21
+ const value = screen.getByText('76%')
22
+ expect(value.style.color).toBe('')
23
+ })
24
+
25
+ it('renders with start alignment', () => {
26
+ render(<Tile label="Program" value="Pull A" align="start" />)
27
+ expect(screen.getByText('Program')).toBeInTheDocument()
28
+ expect(screen.getByText('Pull A')).toBeInTheDocument()
29
+ })
30
+
31
+ it('applies custom className', () => {
32
+ const { container } = render(<Tile label="Load" value="7.3k" className="custom-class" />)
33
+ expect(container.firstChild).toBeInTheDocument()
34
+ })
35
+
36
+ it('passes additional props through', () => {
37
+ render(<Tile label="Load" value="7.3k" testID="tile-test" />)
38
+ expect(screen.getByText('7.3k')).toBeInTheDocument()
39
+ })
40
+
41
+ describe('accessibility', () => {
42
+ it('has no accessibility violations', async () => {
43
+ const { container } = render(<Tile label="Volume" value="76%" />)
44
+ const results = await axe(container)
45
+ expect(results).toHaveNoViolations()
46
+ })
47
+ })
48
+ })
@@ -0,0 +1,64 @@
1
+ import { View, Text, type ViewProps } from 'react-native'
2
+ import { cn } from '../../../utils/cn'
3
+
4
+ export interface TileProps extends ViewProps {
5
+ /** Uppercase micro-label shown above the value */
6
+ label: string
7
+ /** Primary value, rendered in a bold mono face */
8
+ value: string
9
+ /** Tints the value (status/accent hex or CSS color); defaults to primary text */
10
+ valueColor?: string
11
+ /** Content alignment within the tile (default: 'center') */
12
+ align?: 'center' | 'start'
13
+ /** Additional className */
14
+ className?: string
15
+ }
16
+
17
+ /**
18
+ * Tile — a compact label-over-value stat card.
19
+ *
20
+ * A rounded raised box with an uppercase micro-label stacked over a bold mono
21
+ * value. Fills its flex slot so a row of Tiles reads as an even HStack.
22
+ *
23
+ * @example
24
+ * <Tile label="Volume" value="76%" />
25
+ * <Tile label="Fatigue" value="MOD" valueColor="#F5A623" />
26
+ */
27
+ export function Tile({
28
+ label,
29
+ value,
30
+ valueColor,
31
+ align = 'center',
32
+ className,
33
+ ...props
34
+ }: TileProps) {
35
+ const isCenter = align === 'center'
36
+ return (
37
+ <View
38
+ className={cn(
39
+ 'flex-1 rounded-md bg-surface-raised px-1.5 py-2',
40
+ isCenter ? 'items-center' : 'items-start',
41
+ className
42
+ )}
43
+ {...props}
44
+ >
45
+ <Text
46
+ className={cn(
47
+ 'text-[10px] font-bold uppercase tracking-wider text-text-tertiary',
48
+ isCenter ? 'text-center' : 'text-left'
49
+ )}
50
+ >
51
+ {label}
52
+ </Text>
53
+ <Text
54
+ className={cn(
55
+ 'mt-0.5 font-mono text-sm font-bold text-text-primary',
56
+ isCenter ? 'text-center' : 'text-left'
57
+ )}
58
+ style={valueColor ? { color: valueColor } : undefined}
59
+ >
60
+ {value}
61
+ </Text>
62
+ </View>
63
+ )
64
+ }
@@ -0,0 +1,2 @@
1
+ export { Tile } from './Tile'
2
+ export type { TileProps } from './Tile'
@@ -4,7 +4,7 @@ import { Toast, ToastProvider, useToast } from './Toast'
4
4
  import { Button, ButtonText } from '../button'
5
5
 
6
6
  const meta: Meta<typeof Toast> = {
7
- title: 'Components/Toast',
7
+ title: 'Components/Molecules/Toast',
8
8
  component: Toast,
9
9
  tags: ['autodocs'],
10
10
  argTypes: {
@@ -4,7 +4,7 @@ import { View, Text } from 'react-native'
4
4
  import { ToolbarButton, ToolbarButtonGroup } from './ToolbarButton'
5
5
 
6
6
  const meta: Meta<typeof ToolbarButton> = {
7
- title: 'Components/ToolbarButton',
7
+ title: 'Components/Molecules/ToolbarButton',
8
8
  component: ToolbarButton,
9
9
  tags: ['autodocs'],
10
10
  argTypes: {
@@ -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