@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
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import { SessionStatePill } from './SessionStatePill'
3
3
 
4
4
  const meta: Meta<typeof SessionStatePill> = {
5
- title: 'Shell/TopBar/SessionStatePill',
5
+ title: 'Shell/SessionStatePill',
6
6
  component: SessionStatePill,
7
7
  tags: ['autodocs'],
8
8
  args: { state: 'live' },
@@ -15,8 +15,8 @@ const meta: Meta<typeof SessionStatePill> = {
15
15
  description: {
16
16
  component:
17
17
  '**Molecule** (= the ledger’s reusable StatusPill — also used by the Live-view header). Composes ' +
18
- '[Indicator](?path=/docs/components-indicator--docs) (pulse `ping` + vivid color for live) + ' +
19
- '[Typography](?path=/docs/custom-typography--docs) (`monoLabel`). Use the `state` control to switch.',
18
+ '[Indicator](?path=/docs/components-atoms-indicator--docs) (pulse `ping` + vivid color for live) + ' +
19
+ '[Typography](?path=/docs/foundations-typography--docs) (`monoLabel`). Use the `state` control to switch.',
20
20
  },
21
21
  },
22
22
  },
@@ -23,7 +23,7 @@ const stateConfig: Record<
23
23
  }
24
24
  > = {
25
25
  // live = vivid green with an expanding ring; rest = solid amber (no pulse — operator); idle is dim.
26
- live: { label: 'LIVE', color: 'success', pulse: 'ping', textColor: 'primary' },
26
+ live: { label: 'LIVE', color: 'live', pulse: 'ping', textColor: 'primary' },
27
27
  rest: { label: 'REST', color: 'warning', pulse: false, textColor: 'primary' },
28
28
  idle: { label: 'IDLE', color: 'default', pulse: false, textColor: 'secondary' },
29
29
  }
@@ -0,0 +1,81 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useArgs } from 'storybook/preview-api'
3
+ import { View } from 'react-native'
4
+ import { SideNav } from './SideNav'
5
+ import { Typography } from '../custom/Typography'
6
+
7
+ const meta: Meta<typeof SideNav> = {
8
+ title: 'Shell/SideNav',
9
+ component: SideNav,
10
+ tags: ['autodocs'],
11
+ args: { activeKey: 'live', liveKey: null },
12
+ argTypes: {
13
+ activeKey: { control: 'select', options: ['live', 'review', 'program', 'body'] },
14
+ liveKey: { control: 'select', options: [null, 'live', 'review', 'program', 'body'] },
15
+ onNavigate: { action: 'navigate' },
16
+ },
17
+ // Interactive AND Controls-synced: clicking a category writes `activeKey` back to the
18
+ // story args (canvas + Controls stay in lock-step) and still fires the `onNavigate`
19
+ // action. `useArgs` must be called directly in the story/render fn (not a child
20
+ // component), so the rules-of-hooks lint is disabled here — the idiomatic SB pattern.
21
+ render: (args) => {
22
+ // eslint-disable-next-line react-hooks/rules-of-hooks
23
+ const [, updateArgs] = useArgs()
24
+ return (
25
+ <SideNav
26
+ {...args}
27
+ onNavigate={(key) => {
28
+ updateArgs({ activeKey: key })
29
+ args.onNavigate?.(key)
30
+ }}
31
+ />
32
+ )
33
+ },
34
+ // Render in-context: full-height shell frame so the rail snaps to the left edge and
35
+ // stretches with the viewport (resize the canvas height to see it hold).
36
+ decorators: [
37
+ (Story) => (
38
+ <View className="min-h-screen flex-row bg-background-default">
39
+ <Story />
40
+ <View className="flex-1 items-center justify-center">
41
+ <Typography variant="body2" color="tertiary">
42
+ main viewport
43
+ </Typography>
44
+ </View>
45
+ </View>
46
+ ),
47
+ ],
48
+ parameters: {
49
+ layout: 'fullscreen',
50
+ docs: {
51
+ description: {
52
+ component:
53
+ '**Organism** (shell region). The persistent 60px left rail switching Live · Review · Plan · ' +
54
+ 'Body. Composes [NavItem](?path=/docs/shell-navitem--docs) × the four categories + the ' +
55
+ 'shared [icon set](?path=/docs/foundations-icons--docs). Presentational — drive it with `activeKey` / ' +
56
+ '`onNavigate` / `liveKey`. Active = left accent bar; `liveKey` (when not active) tints that ' +
57
+ 'label a muted green.\n\n' +
58
+ '**Try it:** stories render in a full-height shell frame — the rail pins to the left edge and ' +
59
+ 'stretches to the viewport, items staying top-aligned. **Resize the canvas** to see it hold at ' +
60
+ 'any height. Use the **Controls** to move `activeKey` / `liveKey`.',
61
+ },
62
+ },
63
+ },
64
+ }
65
+ export default meta
66
+ type Story = StoryObj<typeof SideNav>
67
+
68
+ export const Default: Story = {}
69
+
70
+ export const LiveElsewhere: Story = {
71
+ args: { activeKey: 'program', liveKey: 'live' },
72
+ parameters: {
73
+ docs: {
74
+ description: {
75
+ story:
76
+ 'A set runs on Live while the operator is on Plan → the Live item carries a quiet green label cue. ' +
77
+ '(Still clickable — try switching away and back.)',
78
+ },
79
+ },
80
+ },
81
+ }
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent, within } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { SideNav } from './SideNav'
5
+
6
+ describe('SideNav', () => {
7
+ it('renders the four default categories as tabs in a tablist', () => {
8
+ render(<SideNav activeKey="live" />)
9
+ expect(screen.getByRole('tablist')).toBeInTheDocument()
10
+ ;['Live', 'Review', 'Plan', 'Body'].forEach((name) => {
11
+ expect(screen.getByRole('tab', { name })).toBeInTheDocument()
12
+ })
13
+ })
14
+
15
+ it('shows the accent bar only on the active category', () => {
16
+ render(<SideNav activeKey="program" />)
17
+ expect(screen.getAllByTestId('nav-item-accent')).toHaveLength(1)
18
+ expect(
19
+ within(screen.getByRole('tab', { name: 'Plan' })).getByTestId('nav-item-accent')
20
+ ).toBeInTheDocument()
21
+ expect(
22
+ within(screen.getByRole('tab', { name: 'Live' })).queryByTestId('nav-item-accent')
23
+ ).toBeNull()
24
+ })
25
+
26
+ it('reports the tapped key via onNavigate', () => {
27
+ const onNavigate = vi.fn()
28
+ render(<SideNav activeKey="live" onNavigate={onNavigate} />)
29
+ fireEvent.click(screen.getByRole('tab', { name: 'Body' }))
30
+ expect(onNavigate).toHaveBeenCalledWith('body')
31
+ })
32
+
33
+ it('keeps the active item as active even when it is also the liveKey (no double-state)', () => {
34
+ render(<SideNav activeKey="live" liveKey="live" />)
35
+ // active wins: exactly one accent bar, on Live (the live cue never applies to the active view)
36
+ expect(screen.getAllByTestId('nav-item-accent')).toHaveLength(1)
37
+ expect(
38
+ within(screen.getByRole('tab', { name: 'Live' })).getByTestId('nav-item-accent')
39
+ ).toBeInTheDocument()
40
+ })
41
+
42
+ it('accepts custom items', () => {
43
+ render(<SideNav activeKey="a" items={[{ key: 'a', label: 'Alpha', icon: null }]} />)
44
+ expect(screen.getByRole('tab', { name: 'Alpha' })).toBeInTheDocument()
45
+ })
46
+
47
+ it('has no accessibility violations', async () => {
48
+ const { container } = render(<SideNav activeKey="live" />)
49
+ expect(await axe(container)).toHaveNoViolations()
50
+ })
51
+ })
@@ -0,0 +1,74 @@
1
+ import type { ReactNode } from 'react'
2
+ import { View } from 'react-native'
3
+ import { cn } from '../../utils/cn'
4
+ import { ActivityIcon, HistoryIcon, LayersIcon, PersonStandingIcon } from '../icons'
5
+ import { NavItem } from './NavItem'
6
+
7
+ export interface SideNavItem {
8
+ /** Stable category key — matches `activeKey` / `liveKey`. */
9
+ key: string
10
+ /** Micro-label shown under the glyph. */
11
+ label: string
12
+ /** The glyph node (rendered ~20px via `currentColor`). */
13
+ icon: ReactNode
14
+ }
15
+
16
+ export interface SideNavProps {
17
+ /** Category items, top → bottom. Defaults to the four dashboard categories. */
18
+ items?: SideNavItem[]
19
+ /** Key of the active category. */
20
+ activeKey: string
21
+ /** Called with the tapped item's key. */
22
+ onNavigate?: (key: string) => void
23
+ /**
24
+ * Key of a category with live activity while it is NOT active (e.g. a set
25
+ * running off its view) → a quiet green cue on that item's label.
26
+ */
27
+ liveKey?: string | null
28
+ className?: string
29
+ }
30
+
31
+ /** The four dashboard categories with their locked S2 glyphs (activity · history · layers · figure). */
32
+ export const defaultNavItems: SideNavItem[] = [
33
+ { key: 'live', label: 'Live', icon: <ActivityIcon size={20} color="currentColor" /> },
34
+ { key: 'review', label: 'Review', icon: <HistoryIcon size={20} color="currentColor" /> },
35
+ { key: 'program', label: 'Plan', icon: <LayersIcon size={20} color="currentColor" /> },
36
+ { key: 'body', label: 'Body', icon: <PersonStandingIcon size={20} color="currentColor" /> },
37
+ ]
38
+
39
+ /**
40
+ * Shell S2 · SideNav — the persistent 60px left rail that switches the main
41
+ * viewport between categories (Live · Review · Program · Body). Presentational:
42
+ * it renders `items` and reports taps via `onNavigate`; the app owns routing and
43
+ * which key is `live`. Active item shows a left accent bar; `liveKey` (when not
44
+ * the active view) tints that item's label a muted green. Fixed 60px at every
45
+ * width — labels sit under the glyph, so they never change the rail width.
46
+ */
47
+ export function SideNav({
48
+ items = defaultNavItems,
49
+ activeKey,
50
+ onNavigate,
51
+ liveKey = null,
52
+ className,
53
+ }: SideNavProps) {
54
+ return (
55
+ <View
56
+ accessibilityRole="tablist"
57
+ className={cn(
58
+ 'w-[60px] items-center gap-[6px] border-r border-border bg-background-base py-3',
59
+ className
60
+ )}
61
+ >
62
+ {items.map((item) => (
63
+ <NavItem
64
+ key={item.key}
65
+ icon={item.icon}
66
+ label={item.label}
67
+ active={item.key === activeKey}
68
+ live={item.key === liveKey && item.key !== activeKey}
69
+ onPress={() => onNavigate?.(item.key)}
70
+ />
71
+ ))}
72
+ </View>
73
+ )
74
+ }
@@ -29,11 +29,11 @@ const meta: Meta<typeof TopBar> = {
29
29
  component:
30
30
  '**Organism** — the persistent shell chrome band, and the root of the top-bar family ' +
31
31
  '(see `shell/README.md` for the full dependency map). Composes ' +
32
- '[BrandLockup](?path=/docs/shell-topbar-brandlockup--docs) + ' +
33
- '[SessionStatePill](?path=/docs/shell-topbar-sessionstatepill--docs) + ' +
34
- '[Divider](?path=/docs/components-divider--docs) (`bg-border-prominent`) + ' +
35
- '[DeviceMenu](?path=/docs/shell-topbar-devicemenu--docs) + ' +
36
- '[DateTime](?path=/docs/custom-datetime--docs) (`variant="mono"` live clock). ' +
32
+ '[BrandLockup](?path=/docs/shell-brandlockup--docs) + ' +
33
+ '[SessionStatePill](?path=/docs/shell-sessionstatepill--docs) + ' +
34
+ '[Divider](?path=/docs/components-atoms-divider--docs) (`bg-border-prominent`) + ' +
35
+ '[DeviceMenu](?path=/docs/shell-devicemenu--docs) + ' +
36
+ '[DateTime](?path=/docs/components-molecules-datetime--docs) (`variant="mono"` live clock). ' +
37
37
  'Background = the shared `surfaceGradient.chrome` primitive.\n\n' +
38
38
  '**Try it:** use the **Controls** to change `state`, edit `devices` (set one to `lost` to see the ' +
39
39
  'fault), or toggle `showSubtitle` / `showClock`. **Resize the canvas** to watch the ' +
@@ -6,3 +6,8 @@ export * from './DeviceIndicator'
6
6
  export * from './DeviceRow'
7
7
  export * from './DeviceMenu'
8
8
  export * from './TopBar'
9
+ // S2 · Side nav family.
10
+ export * from './NavItem'
11
+ export * from './SideNav'
12
+ // S3 · Dashboard shell — composes the nav + top-bar chrome over a content slot.
13
+ export * from './DashboardShell'
@@ -3,7 +3,7 @@ import { View } from 'react-native'
3
3
  import { Alert, AlertTitle, AlertDescription } from './Alert'
4
4
 
5
5
  const meta: Meta<typeof Alert> = {
6
- title: 'Components/Alert',
6
+ title: 'Components/Molecules/Alert',
7
7
  component: Alert,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -4,7 +4,7 @@ import { View, Text } from 'react-native'
4
4
  import { Autocomplete, AutocompleteOption } from './Autocomplete'
5
5
 
6
6
  const meta: Meta<typeof Autocomplete> = {
7
- title: 'Components/Autocomplete',
7
+ title: 'Components/Molecules/Autocomplete',
8
8
  component: Autocomplete,
9
9
  tags: ['autodocs'],
10
10
  argTypes: {
@@ -3,7 +3,7 @@ import { View } from 'react-native'
3
3
  import { Avatar, AvatarBadge, AvatarGroup } from './Avatar'
4
4
 
5
5
  const meta: Meta<typeof Avatar> = {
6
- title: 'Components/Avatar',
6
+ title: 'Components/Atoms/Avatar',
7
7
  component: Avatar,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -3,7 +3,7 @@ import { View } from 'react-native'
3
3
  import { Badge, BadgeText } from './Badge'
4
4
 
5
5
  const meta: Meta<typeof Badge> = {
6
- title: 'Components/Badge',
6
+ title: 'Components/Atoms/Badge',
7
7
  component: Badge,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -3,7 +3,7 @@ import { Text } from 'react-native'
3
3
  import { Breadcrumbs, BreadcrumbItem } from './Breadcrumbs'
4
4
 
5
5
  const meta: Meta<typeof Breadcrumbs> = {
6
- title: 'Components/Breadcrumbs',
6
+ title: 'Components/Molecules/Breadcrumbs',
7
7
  component: Breadcrumbs,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -3,7 +3,7 @@ import { View } from 'react-native'
3
3
  import { Button, ButtonText, ButtonIcon } from './Button'
4
4
 
5
5
  const meta: Meta<typeof Button> = {
6
- title: 'Components/Button',
6
+ title: 'Components/Molecules/Button',
7
7
  component: Button,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -13,7 +13,7 @@ import {
13
13
  import { Button, ButtonText } from '../button'
14
14
 
15
15
  const meta: Meta<typeof Card> = {
16
- title: 'Components/Card',
16
+ title: 'Components/Molecules/Card',
17
17
  component: Card,
18
18
  tags: ['autodocs'],
19
19
  argTypes: {
@@ -3,7 +3,7 @@ import { View } from 'react-native'
3
3
  import { Checkbox, CheckboxGroup } from './Checkbox'
4
4
 
5
5
  const meta: Meta<typeof Checkbox> = {
6
- title: 'Components/Checkbox',
6
+ title: 'Components/Molecules/Checkbox',
7
7
  component: Checkbox,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -3,7 +3,7 @@ import { View } from 'react-native'
3
3
  import { Chip } from './Chip'
4
4
 
5
5
  const meta: Meta<typeof Chip> = {
6
- title: 'Components/Chip',
6
+ title: 'Components/Atoms/Chip',
7
7
  component: Chip,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -12,7 +12,7 @@ import {
12
12
  } from './Collapse'
13
13
 
14
14
  const meta: Meta<typeof Accordion> = {
15
- title: 'Components/Accordion',
15
+ title: 'Components/Organisms/Accordion',
16
16
  component: Accordion,
17
17
  tags: ['autodocs'],
18
18
  argTypes: {
@@ -3,7 +3,7 @@ import { View, Text } from 'react-native'
3
3
  import { DataRow } from './DataRow'
4
4
 
5
5
  const meta: Meta<typeof DataRow> = {
6
- title: 'Components/DataRow',
6
+ title: 'Components/Molecules/DataRow',
7
7
  component: DataRow,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -3,7 +3,7 @@ import { View, Text } from 'react-native'
3
3
  import { Divider } from './Divider'
4
4
 
5
5
  const meta: Meta<typeof Divider> = {
6
- title: 'Components/Divider',
6
+ title: 'Components/Atoms/Divider',
7
7
  component: Divider,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -5,7 +5,7 @@ import { Drawer, DrawerBody, DrawerFooter } from './Drawer'
5
5
  import { Button, ButtonText } from '../button'
6
6
 
7
7
  const meta: Meta<typeof Drawer> = {
8
- title: 'Components/Drawer',
8
+ title: 'Components/Organisms/Drawer',
9
9
  component: Drawer,
10
10
  tags: ['autodocs'],
11
11
  argTypes: {
@@ -5,7 +5,7 @@ import { Button, ButtonText } from '../button'
5
5
  import { Input } from '../input'
6
6
 
7
7
  const meta: Meta<typeof FormField> = {
8
- title: 'Components/FormField',
8
+ title: 'Components/Molecules/FormField',
9
9
  component: FormField,
10
10
  tags: ['autodocs'],
11
11
  argTypes: {
@@ -3,7 +3,7 @@ import { View, Text } from 'react-native'
3
3
  import { HelpTip, LabelWithHelp } from './HelpTip'
4
4
 
5
5
  const meta: Meta<typeof HelpTip> = {
6
- title: 'Components/HelpTip',
6
+ title: 'Components/Molecules/HelpTip',
7
7
  component: HelpTip,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -53,7 +53,7 @@ function BoltIcon({ size = 20, className }: { size?: number; className?: string
53
53
  }
54
54
 
55
55
  const meta: Meta<typeof IconBox> = {
56
- title: 'Components/IconBox',
56
+ title: 'Components/Atoms/IconBox',
57
57
  component: IconBox,
58
58
  tags: ['autodocs'],
59
59
  argTypes: {
@@ -36,3 +36,4 @@ export * from './surface'
36
36
  export * from './list-item'
37
37
  export * from './indicator'
38
38
  export * from './pill'
39
+ export * from './tile'
@@ -3,14 +3,14 @@ import { View } from 'react-native'
3
3
  import { Indicator } from './Indicator'
4
4
 
5
5
  const meta: Meta<typeof Indicator> = {
6
- title: 'Components/Indicator',
6
+ title: 'Components/Atoms/Indicator',
7
7
  component: Indicator,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
10
10
  size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] },
11
11
  color: {
12
12
  control: 'select',
13
- options: ['default', 'primary', 'success', 'error', 'warning', 'info'],
13
+ options: ['default', 'primary', 'success', 'live', 'error', 'warning', 'info'],
14
14
  },
15
15
  glow: { control: 'boolean' },
16
16
  ring: { control: 'boolean' },
@@ -22,6 +22,7 @@ describe('Indicator', () => {
22
22
  'default',
23
23
  'primary',
24
24
  'success',
25
+ 'live',
25
26
  'error',
26
27
  'warning',
27
28
  'info',
@@ -6,6 +6,7 @@ export type IndicatorColor =
6
6
  | 'default'
7
7
  | 'primary'
8
8
  | 'success'
9
+ | 'live'
9
10
  | 'error'
10
11
  | 'warning'
11
12
  | 'info'
@@ -42,6 +43,7 @@ const colorStyles: Record<IndicatorColor, string> = {
42
43
  default: 'bg-text-tertiary',
43
44
  primary: 'bg-brand-primary',
44
45
  success: 'bg-status-success',
46
+ live: 'bg-status-live',
45
47
  error: 'bg-status-error',
46
48
  warning: 'bg-status-warning',
47
49
  info: 'bg-status-info',
@@ -98,7 +100,10 @@ export function Indicator({
98
100
  colorClass,
99
101
  pulseMode === 'opacity' && 'animate-pulse',
100
102
  ring && 'border-2 border-background-base',
101
- glow && !customColor && color === 'success' && 'shadow-[0_0_6px_rgba(46,213,115,0.6)]',
103
+ glow &&
104
+ !customColor &&
105
+ (color === 'success' || color === 'live') &&
106
+ 'shadow-[0_0_6px_rgba(46,213,115,0.6)]',
102
107
  glow && !customColor && color === 'error' && 'shadow-[0_0_6px_rgba(239,68,68,0.6)]',
103
108
  glow && !customColor && color === 'warning' && 'shadow-[0_0_6px_rgba(245,158,11,0.6)]',
104
109
  glow && !customColor && color === 'primary' && 'shadow-[0_0_6px_rgba(255,121,0,0.6)]',
@@ -3,7 +3,7 @@ import { View, Text } from 'react-native'
3
3
  import { Input, InputGroup, PasswordInput } from './Input'
4
4
 
5
5
  const meta: Meta<typeof Input> = {
6
- title: 'Components/Input',
6
+ title: 'Components/Molecules/Input',
7
7
  component: Input,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -3,7 +3,7 @@ import { View } from 'react-native'
3
3
  import { Link } from './Link'
4
4
 
5
5
  const meta: Meta<typeof Link> = {
6
- title: 'Components/Link',
6
+ title: 'Components/Atoms/Link',
7
7
  component: Link,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -45,7 +45,7 @@ function ChevronRight({ size = 16 }: { size?: number }) {
45
45
  }
46
46
 
47
47
  const meta: Meta<typeof ListItem> = {
48
- title: 'Components/ListItem',
48
+ title: 'Components/Molecules/ListItem',
49
49
  component: ListItem,
50
50
  tags: ['autodocs'],
51
51
  }
@@ -4,7 +4,7 @@ import { Menu, MenuTrigger, MenuList, MenuItem, MenuDivider, MenuGroup } from '.
4
4
  import { Button, ButtonText } from '../button/Button'
5
5
 
6
6
  const meta: Meta<typeof Menu> = {
7
- title: 'Components/Menu',
7
+ title: 'Components/Molecules/Menu',
8
8
  component: Menu,
9
9
  tags: ['autodocs'],
10
10
  argTypes: {
@@ -13,7 +13,7 @@ import {
13
13
  import { Button, ButtonText } from '../button/Button'
14
14
 
15
15
  const meta: Meta<typeof Modal> = {
16
- title: 'Components/Modal',
16
+ title: 'Components/Organisms/Modal',
17
17
  component: Modal,
18
18
  tags: ['autodocs'],
19
19
  argTypes: {
@@ -4,7 +4,7 @@ import { Pill } from './Pill'
4
4
  import { Indicator } from '../indicator'
5
5
 
6
6
  const meta: Meta<typeof Pill> = {
7
- title: 'Components/Pill',
7
+ title: 'Components/Atoms/Pill',
8
8
  component: Pill,
9
9
  tags: ['autodocs'],
10
10
  argTypes: {
@@ -4,7 +4,7 @@ import { Popover, PopoverTrigger, PopoverContent, PopoverCloseButton } from './P
4
4
  import { Button, ButtonText } from '../button/Button'
5
5
 
6
6
  const meta: Meta<typeof Popover> = {
7
- title: 'Components/Popover',
7
+ title: 'Components/Molecules/Popover',
8
8
  component: Popover,
9
9
  tags: ['autodocs'],
10
10
  argTypes: {
@@ -3,7 +3,7 @@ import { View, Text } from 'react-native'
3
3
  import { Progress, CircularProgress, ProgressSteps } from './Progress'
4
4
 
5
5
  const meta: Meta<typeof Progress> = {
6
- title: 'Components/Progress',
6
+ title: 'Components/Molecules/Progress',
7
7
  component: Progress,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -4,7 +4,7 @@ import { View, Text } from 'react-native'
4
4
  import { Radio, RadioGroup } from './Radio'
5
5
 
6
6
  const meta: Meta<typeof RadioGroup> = {
7
- title: 'Components/Radio',
7
+ title: 'Components/Molecules/Radio',
8
8
  component: RadioGroup,
9
9
  tags: ['autodocs'],
10
10
  argTypes: {
@@ -3,7 +3,7 @@ import { View, Text } from 'react-native'
3
3
  import { Section, SectionHeader, SectionContent } from './Section'
4
4
 
5
5
  const meta: Meta<typeof Section> = {
6
- title: 'Components/Section',
6
+ title: 'Components/Atoms/Section',
7
7
  component: Section,
8
8
  tags: ['autodocs'],
9
9
  }
@@ -12,7 +12,7 @@ const basicOptions: SelectOption[] = [
12
12
  ]
13
13
 
14
14
  const meta: Meta<typeof Select> = {
15
- title: 'Components/Select',
15
+ title: 'Components/Molecules/Select',
16
16
  component: Select,
17
17
  tags: ['autodocs'],
18
18
  argTypes: {
@@ -9,7 +9,7 @@ import {
9
9
  } from './Skeleton'
10
10
 
11
11
  const meta: Meta<typeof Skeleton> = {
12
- title: 'Components/Skeleton',
12
+ title: 'Components/Atoms/Skeleton',
13
13
  component: Skeleton,
14
14
  tags: ['autodocs'],
15
15
  argTypes: {
@@ -3,7 +3,7 @@ import { View } from 'react-native'
3
3
  import { Spinner } from './Spinner'
4
4
 
5
5
  const meta: Meta<typeof Spinner> = {
6
- title: 'Components/Spinner',
6
+ title: 'Components/Atoms/Spinner',
7
7
  component: Spinner,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -3,7 +3,7 @@ import { View, Text } from 'react-native'
3
3
  import { Stack, HStack, VStack } from './Stack'
4
4
 
5
5
  const meta: Meta<typeof Stack> = {
6
- title: 'Components/Stack',
6
+ title: 'Components/Atoms/Stack',
7
7
  component: Stack,
8
8
  tags: ['autodocs'],
9
9
  argTypes: {
@@ -4,7 +4,7 @@ import { Surface } from './Surface'
4
4
  import type { ElevationLevel } from '../../../theme/elevation'
5
5
 
6
6
  const meta: Meta<typeof Surface> = {
7
- title: 'Components/Surface',
7
+ title: 'Components/Atoms/Surface',
8
8
  component: Surface,
9
9
  tags: ['autodocs'],
10
10
  argTypes: {