@titan-design/react-ui 0.10.0 → 0.12.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 (190) hide show
  1. package/dist/bodymap.js +173 -343
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +173 -343
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +416 -18
  6. package/dist/index.d.ts +416 -18
  7. package/dist/index.js +2512 -1307
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2480 -1299
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/{pages-DyEx-lBf.d.ts → pages-B1hqnGIC.d.ts} +149 -15
  12. package/dist/{pages-D7hOD4Vj.d.mts → pages-CvsFqNtx.d.mts} +149 -15
  13. package/dist/pages.d.mts +1 -1
  14. package/dist/pages.d.ts +1 -1
  15. package/dist/pages.js +1644 -1575
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1645 -1576
  18. package/dist/pages.mjs.map +1 -1
  19. package/dist/theme/index.d.mts +409 -415
  20. package/dist/theme/index.d.ts +409 -415
  21. package/dist/theme/index.js +234 -408
  22. package/dist/theme/index.js.map +1 -1
  23. package/dist/theme/index.mjs +227 -399
  24. package/dist/theme/index.mjs.map +1 -1
  25. package/dist/theme/tokens-css.js +141 -140
  26. package/dist/theme/tokens-css.js.map +1 -1
  27. package/dist/theme/tokens-css.mjs +141 -140
  28. package/dist/theme/tokens-css.mjs.map +1 -1
  29. package/package.json +8 -3
  30. package/src/arch/Architecture.stories.tsx +9 -9
  31. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  32. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
  33. package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
  34. package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
  35. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
  36. package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
  37. package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
  38. package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
  39. package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
  40. package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
  41. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
  42. package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
  43. package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
  44. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
  45. package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
  46. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
  47. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
  48. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
  49. package/src/components/custom/Fatigue/README.md +93 -0
  50. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
  51. package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
  52. package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
  53. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
  54. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
  55. package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
  56. package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
  57. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
  58. package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
  59. package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
  60. package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
  61. package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
  62. package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
  63. package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
  64. package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
  65. package/src/components/custom/Fatigue/index.ts +41 -0
  66. package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
  67. package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
  68. package/src/components/custom/Sidebar/Sidebar.tsx +4 -4
  69. package/src/components/custom/Table/Table.tsx +2 -2
  70. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +3 -3
  71. package/src/components/custom/Workout/BaseBadge.test.tsx +6 -1
  72. package/src/components/custom/Workout/BaseBadge.tsx +1 -1
  73. package/src/components/custom/Workout/BodyMap.tsx +2 -2
  74. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +2 -2
  75. package/src/components/custom/Workout/DeviationBar.test.tsx +2 -1
  76. package/src/components/custom/Workout/DeviationBar.tsx +3 -2
  77. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
  78. package/src/components/custom/Workout/ExerciseCard.test.tsx +7 -7
  79. package/src/components/custom/Workout/ExerciseCard.tsx +5 -3
  80. package/src/components/custom/Workout/ExerciseDetailPage.tsx +11 -5
  81. package/src/components/custom/Workout/InputBar.tsx +2 -2
  82. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +1 -1
  83. package/src/components/custom/Workout/LiveAuraFrame.test.tsx +1 -1
  84. package/src/components/custom/Workout/LiveAuraFrame.tsx +3 -3
  85. package/src/components/custom/Workout/MesoCard.tsx +3 -1
  86. package/src/components/custom/Workout/MuscleGroupChip.tsx +4 -3
  87. package/src/components/custom/Workout/PrHistoryModal.tsx +2 -2
  88. package/src/components/custom/Workout/ProgramPlanningPage.tsx +1 -1
  89. package/src/components/custom/Workout/README.md +62 -16
  90. package/src/components/custom/Workout/ReadinessCheck.tsx +2 -2
  91. package/src/components/custom/Workout/RestTimer.tsx +2 -2
  92. package/src/components/custom/Workout/S3SetTypes.stories.tsx +3 -3
  93. package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
  94. package/src/components/custom/Workout/SessionHeader.stories.tsx +2 -2
  95. package/src/components/custom/Workout/SessionHeader.tsx +1 -1
  96. package/src/components/custom/Workout/SessionRail.stories.tsx +2 -2
  97. package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
  98. package/src/components/custom/Workout/SessionRail.tsx +63 -18
  99. package/src/components/custom/Workout/SetBar.tsx +3 -3
  100. package/src/components/custom/Workout/SetRow.test.tsx +12 -9
  101. package/src/components/custom/Workout/SetRow.tsx +9 -2
  102. package/src/components/custom/Workout/StatusDot.tsx +5 -4
  103. package/src/components/custom/Workout/StrengthTrendChart.tsx +1 -1
  104. package/src/components/custom/Workout/TrainingStatusPage.tsx +2 -2
  105. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
  106. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
  107. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
  108. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
  109. package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
  110. package/src/components/custom/Workout/VelocityStrip.test.tsx +199 -79
  111. package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
  112. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +3 -3
  113. package/src/components/custom/Workout/WeightBadge.test.tsx +6 -1
  114. package/src/components/custom/Workout/WeightBadge.tsx +2 -1
  115. package/src/components/custom/Workout/WorkoutCard.tsx +3 -1
  116. package/src/components/custom/Workout/WorkoutPill.tsx +5 -2
  117. package/src/components/custom/Workout/ZoneTrack.tsx +4 -4
  118. package/src/components/custom/Workout/index.ts +3 -0
  119. package/src/components/custom/Workout/setHeadingKit.tsx +14 -8
  120. package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
  121. package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
  122. package/src/components/custom/charts/SetBarChart.tsx +651 -0
  123. package/src/components/custom/charts/live-rep-growth.ts +91 -0
  124. package/src/components/custom/index.ts +1 -0
  125. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  126. package/src/components/custom/stepper/Stepper.tsx +3 -3
  127. package/src/components/shell/DeviceMenu.tsx +2 -2
  128. package/src/components/shell/SideNav.tsx +1 -1
  129. package/src/components/shell/TopBar.tsx +1 -1
  130. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  131. package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
  132. package/src/components/ui/avatar/Avatar.tsx +2 -2
  133. package/src/components/ui/badge/Badge.tsx +2 -2
  134. package/src/components/ui/card/Card.tsx +3 -3
  135. package/src/components/ui/checkbox/Checkbox.tsx +1 -1
  136. package/src/components/ui/collapse/Collapse.tsx +1 -1
  137. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  138. package/src/components/ui/drawer/Drawer.tsx +3 -3
  139. package/src/components/ui/form-field/FormField.tsx +1 -1
  140. package/src/components/ui/help-tip/HelpTip.tsx +1 -1
  141. package/src/components/ui/menu/Menu.tsx +2 -2
  142. package/src/components/ui/pill/Pill.tsx +2 -2
  143. package/src/components/ui/popover/Popover.stories.tsx +2 -2
  144. package/src/components/ui/popover/Popover.tsx +1 -1
  145. package/src/components/ui/progress/Progress.tsx +3 -3
  146. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  147. package/src/components/ui/radio/Radio.tsx +4 -4
  148. package/src/components/ui/select/Select.tsx +3 -3
  149. package/src/components/ui/surface/Surface.stories.tsx +3 -3
  150. package/src/components/ui/surface/Surface.test.tsx +32 -19
  151. package/src/components/ui/surface/Surface.tsx +3 -3
  152. package/src/components/ui/surface/SurfaceContext.ts +19 -29
  153. package/src/components/ui/surface/surface.contract.test.ts +57 -44
  154. package/src/components/ui/switch/Switch.tsx +1 -1
  155. package/src/components/ui/tabs/Tabs.tsx +2 -2
  156. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +5 -5
  157. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +1 -1
  158. package/src/components/ui/toolbar-button/ToolbarButton.tsx +36 -22
  159. package/src/stories/PresetShowcase.stories.tsx +21 -21
  160. package/src/test/raw-color-patterns.test.ts +71 -0
  161. package/src/theme/ColorPalettes.stories.tsx +874 -0
  162. package/src/theme/ColorPrimitives.stories.tsx +348 -0
  163. package/src/theme/Depth.stories.tsx +312 -0
  164. package/src/theme/DepthCalibration.stories.tsx +655 -0
  165. package/src/theme/ThemeProvider.tsx +1 -1
  166. package/src/theme/barrel.ts +7 -14
  167. package/src/theme/color-stories.coverage.test.ts +125 -0
  168. package/src/theme/color-story-kit.tsx +170 -0
  169. package/src/theme/color-utils.ts +197 -0
  170. package/src/theme/config.ts +0 -8
  171. package/src/theme/depth-calibration.test.tsx +97 -0
  172. package/src/theme/elevation.test.ts +62 -1
  173. package/src/theme/elevation.ts +34 -8
  174. package/src/theme/global.css +47 -53
  175. package/src/theme/grey-ramp.test.ts +166 -0
  176. package/src/theme/materials.test.ts +106 -0
  177. package/src/theme/materials.ts +211 -0
  178. package/src/theme/presets/audiobook.ts +4 -4
  179. package/src/theme/presets/types.ts +3 -3
  180. package/src/theme/semantic-contrast.test.ts +83 -0
  181. package/src/theme/tokens/primitives.ts +62 -115
  182. package/src/theme/tokens/semantic.ts +130 -82
  183. package/src/utils/colors.ts +1 -1
  184. package/tailwind.config.js +2 -5
  185. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
  186. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
  187. package/src/theme/ColorSystem.stories.tsx +0 -508
  188. package/src/theme/Colors.stories.tsx +0 -323
  189. package/src/theme/shadows.stories.tsx +0 -523
  190. package/src/theme/shadows.ts +0 -439
@@ -1,7 +1,7 @@
1
1
  // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
2
  import { View, Text, type ViewProps } from 'react-native'
3
3
  import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
4
- import { primitiveColors } from '../../../theme/tokens/primitives'
4
+ import { greyRamp } from '../../../theme/tokens/primitives'
5
5
  import { ZoneTrack } from './ZoneTrack'
6
6
  import { DataRow } from '../../ui/data-row/DataRow'
7
7
  import type { VolumeLandmarks } from './muscleTaxonomy'
@@ -11,9 +11,9 @@ import type { VolumeLandmarks } from './muscleTaxonomy'
11
11
  // BodyMap / MesoProgressBar so a muscle reads identically across the app.
12
12
  const HEAT = WORKOUT_TOKENS.heatmap
13
13
 
14
- // The muted, un-reached track colour — the same charcoal step ZoneTrack defaults
14
+ // The muted, un-reached track colour — the same grey step ZoneTrack defaults
15
15
  // to, so the bar sits on the shared gauge-track surface.
16
- const NEUTRAL_TRACK = primitiveColors.charcoal[200]
16
+ const NEUTRAL_TRACK = greyRamp[800]
17
17
  const MONO = 'monospace'
18
18
 
19
19
  export type VolumeZone = 'under' | 'maintenance' | 'productive' | 'approaching' | 'over'
@@ -2,6 +2,11 @@ import { describe, it, expect, vi } from 'vitest'
2
2
  import { render, screen, fireEvent } from '@testing-library/react'
3
3
  import { axe } from 'jest-axe'
4
4
  import { WeightBadge } from './WeightBadge'
5
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
6
+
7
+ // Read from the token, not pinned: these used to be hand-copied hexes and
8
+ // silently desynced when borders became alpha hairlines (TD-07.16).
9
+ const T = getSemanticColors('dark')
5
10
 
6
11
  describe('WeightBadge', () => {
7
12
  it('renders value and default unit', () => {
@@ -71,7 +76,7 @@ describe('WeightBadge', () => {
71
76
  it('uses default border color when not a PR', () => {
72
77
  render(<WeightBadge value={225} />)
73
78
  expect(screen.getByTestId('weight-badge')).toHaveStyle({
74
- borderTopColor: '#1F1F1F',
79
+ borderTopColor: T['hairline-default'],
75
80
  })
76
81
  })
77
82
 
@@ -8,6 +8,7 @@ import {
8
8
  type BaseBadgeProps,
9
9
  } from './BaseBadge'
10
10
  import { getSemanticColors } from '../../../theme/tokens/semantic'
11
+ import { greyRamp } from '../../../theme/tokens/primitives'
11
12
 
12
13
  export type WeightBadgeSize = BaseBadgeSize
13
14
 
@@ -43,7 +44,7 @@ export function WeightBadge({
43
44
  ...props
44
45
  }: WeightBadgeProps) {
45
46
  const config = baseBadgeSizeConfig[size]
46
- const textColor = isPr ? getSemanticColors('dark')['brand-primary'] : '#9CA3AF'
47
+ const textColor = isPr ? getSemanticColors('dark')['brand-primary'] : greyRamp[400]
47
48
  // Dumbbell reads clearly as a weight/strength metric and is unit-agnostic
48
49
  // across estimated-1RM and N-rep-max contexts (vs. TrendingUp which implies
49
50
  // change/trend rather than a load value).
@@ -8,6 +8,8 @@ import {
8
8
  } from './MuscleGroupChip'
9
9
  import { getSemanticColors } from '../../../theme/tokens/semantic'
10
10
 
11
+ const HAIRLINE_DEFAULT = getSemanticColors('dark')['hairline-default']
12
+
11
13
  const t = getSemanticColors('dark')
12
14
 
13
15
  export type WorkoutStatus = 'completed' | 'today' | 'upcoming'
@@ -48,7 +50,7 @@ export interface WorkoutCardProps extends ViewProps {
48
50
  const COLORS = {
49
51
  statusSuccess: t['status-success'],
50
52
  brandPrimary: t['brand-primary'],
51
- borderDefault: '#1F1F1F',
53
+ borderDefault: HAIRLINE_DEFAULT,
52
54
  brandPrimarySubtle: 'rgba(255,121,0,0.06)',
53
55
  }
54
56
 
@@ -3,6 +3,9 @@ import { useEffect, useState } from 'react'
3
3
  import { View, Text, Pressable, Animated, Easing, type ViewProps } from 'react-native'
4
4
  import { getSemanticColors } from '../../../theme/tokens/semantic'
5
5
  import { WORKOUT_PILL_DELOAD } from '../../../theme/extracted-colors-dataviz'
6
+ import { greyRamp } from '../../../theme/tokens/primitives'
7
+
8
+ const HAIRLINE_DEFAULT = getSemanticColors('dark')['hairline-default']
6
9
 
7
10
  const t = getSemanticColors('dark')
8
11
 
@@ -35,7 +38,7 @@ const statusBorderStyles: Record<WorkoutPillStatus, Record<string, unknown>> = {
35
38
  completed: { borderWidth: 1, borderColor: 'rgba(46,213,115,0.3)' },
36
39
  current: { borderWidth: 1, borderColor: 'rgba(255,121,0,0.3)' },
37
40
  next: { borderWidth: 1, borderColor: 'rgba(255,121,0,0.4)' },
38
- upcoming: { borderWidth: 1, borderColor: '#1F1F1F' },
41
+ upcoming: { borderWidth: 1, borderColor: HAIRLINE_DEFAULT },
39
42
  missed: { borderWidth: 1, borderColor: 'rgba(209,67,67,0.25)' },
40
43
  deload: { borderWidth: 1, borderColor: 'rgba(186,41,150,0.3)' },
41
44
  }
@@ -44,7 +47,7 @@ const textColors: Record<WorkoutPillStatus, string> = {
44
47
  completed: t['status-success'],
45
48
  current: t['brand-primary'],
46
49
  next: t['brand-primary'],
47
- upcoming: '#6B7280',
50
+ upcoming: greyRamp[500],
48
51
  missed: 'rgba(209,67,67,0.7)',
49
52
  deload: WORKOUT_PILL_DELOAD,
50
53
  }
@@ -2,7 +2,7 @@
2
2
  import { useState } from 'react'
3
3
  import { View, Text, type ViewProps, type DimensionValue, type LayoutChangeEvent } from 'react-native'
4
4
  import { getSemanticColors } from '../../../theme/tokens/semantic'
5
- import { primitiveColors } from '../../../theme/tokens/primitives'
5
+ import { primitiveColors, greyRamp } from '../../../theme/tokens/primitives'
6
6
  import { alpha } from '../../../utils/colors'
7
7
  import { Tooltip } from '../../ui/tooltip/Tooltip'
8
8
 
@@ -13,8 +13,8 @@ function glowShadow(color: string): string {
13
13
  return `0 0 5px 1px ${alpha(color, 0.3)}, 0 0 10px 3px ${alpha(color, 0.12)}`
14
14
  }
15
15
 
16
- /** Muted, un-reached track colour — a charcoal step, matches the IntensityBar track family. */
17
- const DEFAULT_TRACK_COLOR = primitiveColors.charcoal[200]
16
+ /** Muted, un-reached track colour — a grey step, matches the IntensityBar track family. */
17
+ const DEFAULT_TRACK_COLOR = greyRamp[800]
18
18
  /** Default needle / fill-marker colour. */
19
19
  const DEFAULT_MARKER_COLOR = primitiveColors.white
20
20
  /** Tick mark + tick label colour. */
@@ -139,7 +139,7 @@ export interface ZoneTrackProps extends ViewProps {
139
139
  trackHeight?: number
140
140
  /** How far a needle marker overhangs the track top + bottom, in px. Overrides the `size` default (6 default / 9 wall). */
141
141
  needleOverhang?: number
142
- /** Muted colour of the track behind / beyond the fill. Default a charcoal step. */
142
+ /** Muted colour of the track behind / beyond the fill. Default a grey step. */
143
143
  trackColor?: string
144
144
  className?: string
145
145
  }
@@ -27,6 +27,9 @@ export {
27
27
  getVelocityZoneName,
28
28
  calculateVelocityLoss,
29
29
  calculateMeanVelocity,
30
+ DualVelocityStrip,
31
+ type DualVelocityStripProps,
32
+ type DualVelocityStream,
30
33
  } from './VelocityStrip'
31
34
  export { MuscleGroupChip, type MuscleGroupChipProps, type VolumeStatus } from './MuscleGroupChip'
32
35
  export { Sparkline, type SparklineProps } from './Sparkline'
@@ -5,19 +5,25 @@
5
5
  */
6
6
  import type { ReactNode } from 'react'
7
7
  import { TempoDisplay } from './TempoDisplay'
8
+ import { greyRamp } from '../../../theme/tokens/primitives'
9
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
8
10
 
9
11
  // ---- geometry
10
12
  export const RAIL_W = 232
11
13
  export const SET_GAP = 5
12
14
 
13
- // ---- surface + text tokens (dark theme; charcoal + neutral ramps)
14
- export const GREY = '#2C2C2C' // charcoal 300 upcoming/placeholder
15
- export const BORDER_SUBTLE = '#1C1C1C' // charcoal 500 row dividers
16
- export const T_PRIMARY = '#F3F4F6' // neutral 100
17
- export const T_SECONDARY = '#9CA3AF' // neutral 400
18
- export const T_TERTIARY = '#6B7280' // neutral 500
19
- export const RAISED = '#1F1F1F' // charcoal 400 — nav + heading raised plane
20
- export const INSET = '#131313' // charcoal 800 — sunk exercise-list column
15
+ // ---- surface + text tokens (dark theme)
16
+ // These were hand-copied hexes labelled with their grey/neutral steps, which
17
+ // is exactly how they silently stopped matching the ramps they named. They read
18
+ // from the tokens now, so a retune reaches them.
19
+ const T = getSemanticColors('dark')
20
+ export const GREY = greyRamp[900] // upcoming/placeholder
21
+ export const BORDER_SUBTLE = greyRamp[950] // row dividers
22
+ export const T_PRIMARY = T['text-primary']
23
+ export const T_SECONDARY = T['text-secondary']
24
+ export const T_TERTIARY = T['text-tertiary']
25
+ export const RAISED = greyRamp[925] // nav + heading raised plane
26
+ export const INSET = greyRamp[975] // sunk exercise-list column
21
27
  // depth lives on the sunk list: inner shadow from the heading (top) + nav (left). Raised plane is flat.
22
28
  export const INSET_SHADOW =
23
29
  'inset 0 9px 12px -6px rgba(0,0,0,0.85), inset 7px 0 10px -6px rgba(0,0,0,0.7), inset 0 -1px 0 rgba(255,255,255,0.02)'
@@ -0,0 +1,438 @@
1
+ /**
2
+ * Shared fixtures and framing for the VelocityStrip story groups.
3
+ *
4
+ * The strip's stories are split by VIEW (compact / expanded / hero), and each
5
+ * group walks the SAME scenario set against the SAME datasets. That only proves
6
+ * anything if every group is genuinely showing the same data — so the datasets,
7
+ * the single-above-dual pairing, and the set-type board all live here rather
8
+ * than being re-typed per file.
9
+ *
10
+ * Dual is not a fourth view. It is the same strip when the exercise used two
11
+ * Voltras, so every scenario renders single ABOVE dual on one dataset and the
12
+ * pairing itself carries the point: dual = single + a lower half.
13
+ *
14
+ * Not a `.stories` file on purpose — it exports no stories, and keeping it out
15
+ * of the story glob means the chrome below is held to the same token rules as
16
+ * component code.
17
+ */
18
+
19
+ import type { ReactNode } from 'react'
20
+ import type { Decorator } from '@storybook/react-vite'
21
+ import { View, Text } from 'react-native'
22
+ import { VelocityStrip, DualVelocityStrip, type VelocitySet } from './VelocityStrip'
23
+ import { greyRamp } from '../../../theme/tokens/primitives'
24
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
25
+
26
+ const t = getSemanticColors('dark')
27
+
28
+ /** The real dark surface-background plane — stories sit on the token, not an ad-hoc grey. */
29
+ export const SURFACE_BG = greyRamp[950]
30
+
31
+ export const VIEW_LABEL = t['text-secondary']
32
+ export const VIEW_NOTE = t['text-tertiary']
33
+ export const VIEW_HEADING = t['text-primary']
34
+
35
+ export const LEFT_SLOT = 'Left'
36
+ export const RIGHT_SLOT = 'Right'
37
+
38
+ // ── Datasets ────────────────────────────────────────────────────────────────
39
+ // One representative set reused across every view, so a drift in one view is
40
+ // visible as a mismatch against the others rather than hiding behind new data.
41
+
42
+ /** The representative set: a clean, mildly-declining working set. */
43
+ export const REP_SET = [0.95, 0.9, 0.86, 0.8, 0.72]
44
+ /** The lagging partner — 4 reps to REP_SET's 5, so the index-lock shows an empty. */
45
+ export const REP_SET_LAGGING = [0.9, 0.83, 0.75, 0.66]
46
+
47
+ /** A hard fatigue decline — the loss colouring and VL bands have something to say. */
48
+ export const FATIGUE_SET = [0.96, 0.91, 0.83, 0.72, 0.61, 0.5]
49
+ export const FATIGUE_SET_LAGGING = [0.94, 0.87, 0.76, 0.63, 0.48]
50
+
51
+ /** Mid-set, still going: 4 done of 8 planned, newest bar live. */
52
+ export const IN_PROGRESS_SET = [0.82, 0.79, 0.81, 0.76]
53
+ export const IN_PROGRESS_LAGGING = [0.8, 0.77, 0.75]
54
+
55
+ export const dualOf = (velocities: number[], label: string) => ({ velocities, label })
56
+
57
+ // ── Framing ─────────────────────────────────────────────────────────────────
58
+
59
+ export function ViewLabel({ text }: { text: string }) {
60
+ return (
61
+ <Text
62
+ style={{
63
+ color: VIEW_LABEL,
64
+ fontSize: 10,
65
+ fontWeight: '700',
66
+ letterSpacing: 1,
67
+ textTransform: 'uppercase',
68
+ }}
69
+ >
70
+ {text}
71
+ </Text>
72
+ )
73
+ }
74
+
75
+ export function Note({ children }: { children: ReactNode }) {
76
+ return <Text style={{ color: VIEW_NOTE, fontSize: 10, lineHeight: 15 }}>{children}</Text>
77
+ }
78
+
79
+ /** Page frame every scenario story sits in — one surface, one rhythm. */
80
+ export function Sheet({ children, width = 640 }: { children: ReactNode; width?: number }) {
81
+ return (
82
+ <View style={{ padding: 24, backgroundColor: SURFACE_BG, gap: 26, width }}>{children}</View>
83
+ )
84
+ }
85
+
86
+ /**
87
+ * Wall frame for hero stories. The eyebrow is ORGANISM chrome — the north-star
88
+ * live page renders it, the primitive does not. It sits here only so the hero
89
+ * is judged in the context it will live in.
90
+ */
91
+ export const wallDecorator: Decorator = (Story) => (
92
+ <View style={{ width: 620, padding: 28, backgroundColor: SURFACE_BG }}>
93
+ <Text
94
+ style={{
95
+ color: VIEW_NOTE,
96
+ fontSize: 10,
97
+ fontWeight: '700',
98
+ letterSpacing: 1,
99
+ marginBottom: 12,
100
+ }}
101
+ >
102
+ CONCENTRIC VELOCITY · THIS SET · (organism chrome — not the component)
103
+ </Text>
104
+ <Story />
105
+ </View>
106
+ )
107
+
108
+ // ── The single-above-dual pairing ───────────────────────────────────────────
109
+
110
+ /** Per-view heights. Dual expanded is taller because value-height genuinely needs 2×. */
111
+ export const VIEW_HEIGHT: Record<StripView, { single: number; dual: number }> = {
112
+ compact: { single: 11.5, dual: 11.5 },
113
+ expanded: { single: 60, dual: 64 },
114
+ hero: { single: 180, dual: 200 },
115
+ }
116
+
117
+ export type StripView = 'compact' | 'expanded' | 'hero'
118
+
119
+ /** The dual renderer for a view. `expanded`'s dual is the lean `rail` (a verified misnomer). */
120
+ export const dualVariantFor = (view: StripView) => (view === 'expanded' ? 'rail' : view)
121
+
122
+ /**
123
+ * One scenario, rendered single above dual on the same dataset.
124
+ *
125
+ * Takes either raw velocities or full `VelocitySet` descriptors, because the
126
+ * set-type scenarios need the descriptor form and the rest do not.
127
+ */
128
+ export function ScenarioPair({
129
+ view,
130
+ title,
131
+ note,
132
+ single,
133
+ left,
134
+ right,
135
+ singleSet,
136
+ leftSet,
137
+ rightSet,
138
+ scale = 'peak',
139
+ width,
140
+ ...rest
141
+ }: {
142
+ view: StripView
143
+ title: string
144
+ note?: string
145
+ single?: number[]
146
+ left?: number[]
147
+ right?: number[]
148
+ singleSet?: VelocitySet
149
+ leftSet?: VelocitySet
150
+ rightSet?: VelocitySet
151
+ scale?: 'peak' | 'fixed'
152
+ /** Constrains the pair, for the width-degrade ladders. Unset = fill the sheet. */
153
+ width?: number
154
+ targetReps?: number
155
+ liveRepIndex?: number
156
+ barColor?: 'zone' | 'loss'
157
+ }) {
158
+ const h = VIEW_HEIGHT[view]
159
+ const heroProps = view === 'hero' ? { label: 'Set' } : {}
160
+ const bareExpanded = view === 'expanded' ? { showNumbers: false, showInfo: false } : {}
161
+
162
+ return (
163
+ <View style={{ gap: 10, ...(width == null ? null : { width }) }}>
164
+ <View style={{ gap: 3 }}>
165
+ <ViewLabel text={title} />
166
+ {note ? <Note>{note}</Note> : null}
167
+ </View>
168
+
169
+ <ViewLabel text="single" />
170
+ <VelocityStrip
171
+ {...(singleSet ? { set: singleSet } : { velocities: single ?? [] })}
172
+ variant={view}
173
+ height={h.single}
174
+ scale={scale}
175
+ {...heroProps}
176
+ {...bareExpanded}
177
+ {...rest}
178
+ />
179
+
180
+ <ViewLabel text="dual" />
181
+ <DualVelocityStrip
182
+ left={leftSet ? { set: leftSet, label: LEFT_SLOT } : dualOf(left ?? [], LEFT_SLOT)}
183
+ right={rightSet ? { set: rightSet, label: RIGHT_SLOT } : dualOf(right ?? [], RIGHT_SLOT)}
184
+ variant={dualVariantFor(view)}
185
+ height={h.dual}
186
+ scale={scale}
187
+ {...rest}
188
+ />
189
+ </View>
190
+ )
191
+ }
192
+
193
+ // ── Set-type board ──────────────────────────────────────────────────────────
194
+ // The full set-type vocabulary, one row per type, each paired single-above-dual.
195
+ // Generalised over `view` so compact / expanded / hero all walk the same board
196
+ // instead of the hero owning a private copy.
197
+
198
+ export type BoardRow = {
199
+ type: string
200
+ note: string
201
+ single: VelocitySet
202
+ left: VelocitySet
203
+ right: VelocitySet
204
+ }
205
+
206
+ export const SET_TYPE_BOARD: BoardRow[] = [
207
+ {
208
+ type: 'straight',
209
+ note: 'done reps + solid to-do remainder to the planned count',
210
+ single: { type: 'straight', velocities: [0.9, 0.86, 0.82, 0.78], planned: 6 },
211
+ left: { type: 'straight', velocities: [0.9, 0.86, 0.82, 0.78], planned: 6 },
212
+ right: { type: 'straight', velocities: [0.84, 0.79], planned: 6 },
213
+ },
214
+ {
215
+ type: 'range',
216
+ note: 'committed rep bars + solid floor + the cyan variable window (floor..max)',
217
+ single: { type: 'range', velocities: [0.9, 0.86, 0.82, 0.8], floor: 6, max: 8 },
218
+ left: { type: 'range', velocities: [0.9, 0.86, 0.82, 0.8], floor: 6, max: 8 },
219
+ right: { type: 'range', velocities: [0.83, 0.78], floor: 6, max: 8 },
220
+ },
221
+ {
222
+ type: 'amrap',
223
+ note: 'performed reps + the trailing cyan-outline "continue" window',
224
+ single: { type: 'amrap', velocities: [0.9, 0.85, 0.8, 0.75] },
225
+ left: { type: 'amrap', velocities: [0.9, 0.85, 0.8, 0.75] },
226
+ right: { type: 'amrap', velocities: [0.82, 0.76] },
227
+ },
228
+ {
229
+ type: 'drop',
230
+ note: 'one bar per rep with WIDE chunk-notch gaps splitting each sub-load',
231
+ single: {
232
+ type: 'drop',
233
+ subloads: [
234
+ [0.95, 0.9],
235
+ [0.82, 0.76],
236
+ [0.7, 0.64],
237
+ ],
238
+ },
239
+ left: {
240
+ type: 'drop',
241
+ subloads: [
242
+ [0.95, 0.9],
243
+ [0.82, 0.76],
244
+ [0.7, 0.64],
245
+ ],
246
+ },
247
+ right: { type: 'drop', subloads: [[0.88, 0.83], [0.75, 0.68], [0.62]] },
248
+ },
249
+ ]
250
+
251
+ export function SetTypeBoard({ view }: { view: StripView }) {
252
+ const h = VIEW_HEIGHT[view]
253
+ return (
254
+ <View style={{ gap: 30 }}>
255
+ {SET_TYPE_BOARD.map((row) => (
256
+ <View key={row.type} style={{ gap: 8 }}>
257
+ <View style={{ flexDirection: 'row', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
258
+ <Text
259
+ style={{
260
+ color: VIEW_HEADING,
261
+ fontSize: 13,
262
+ fontWeight: '800',
263
+ letterSpacing: 1,
264
+ textTransform: 'uppercase',
265
+ }}
266
+ >
267
+ {row.type}
268
+ </Text>
269
+ <Note>{row.note}</Note>
270
+ </View>
271
+
272
+ <ViewLabel text="single" />
273
+ <VelocityStrip
274
+ variant={view}
275
+ set={row.single}
276
+ height={h.single}
277
+ scale="fixed"
278
+ {...(view === 'hero' ? { label: 'This Set' } : {})}
279
+ {...(view === 'expanded' ? { showNumbers: false, showInfo: false } : {})}
280
+ />
281
+
282
+ <ViewLabel text="dual · symmetric structure, right logs fewer → aligned empties" />
283
+ <DualVelocityStrip
284
+ left={{ set: row.left, label: LEFT_SLOT }}
285
+ right={{ set: row.right, label: RIGHT_SLOT }}
286
+ variant={dualVariantFor(view)}
287
+ height={h.dual}
288
+ scale="fixed"
289
+ />
290
+ </View>
291
+ ))}
292
+ </View>
293
+ )
294
+ }
295
+
296
+ // ── Rep-type coverage ───────────────────────────────────────────────────────
297
+ // Every state a single rep column can be in, in one board. The velocity zones
298
+ // are only three of them; the rest come from the set descriptor (planned
299
+ // remainder, the repeatable windows, drop sub-load splits) or from the dual's
300
+ // index-lock (a rep one side logged and the other did not).
301
+ //
302
+ // Generalised over `view` for the same reason the set-type board is: a state
303
+ // that renders on hero but not compact is exactly the class of bug this is
304
+ // meant to surface.
305
+
306
+ export type RepTypeRow = {
307
+ key: string
308
+ label: string
309
+ note: string
310
+ single: VelocitySet
311
+ left: VelocitySet
312
+ right: VelocitySet
313
+ }
314
+
315
+ /** Velocities chosen to land in each zone: fast, moderate, slow, grinding. */
316
+ const ZONE_SET = [1.15, 0.88, 0.66, 0.42]
317
+
318
+ export const REP_TYPE_ROWS: RepTypeRow[] = [
319
+ {
320
+ key: 'zones',
321
+ label: 'Logged reps · the velocity zones',
322
+ note: 'fast → moderate → slow → grinding. One bar per rep, coloured by the zone it landed in.',
323
+ single: { type: 'straight', velocities: ZONE_SET },
324
+ left: { type: 'straight', velocities: ZONE_SET },
325
+ right: { type: 'straight', velocities: ZONE_SET },
326
+ },
327
+ {
328
+ key: 'upcoming',
329
+ label: 'Upcoming · the planned remainder',
330
+ note: 'Two logged of six planned. The remainder holds its columns so the set never reflows as reps land.',
331
+ single: { type: 'straight', velocities: [0.9, 0.86], planned: 6 },
332
+ left: { type: 'straight', velocities: [0.9, 0.86], planned: 6 },
333
+ right: { type: 'straight', velocities: [0.88, 0.84], planned: 6 },
334
+ },
335
+ {
336
+ key: 'missed-bottom',
337
+ label: 'Missed rep · lower slot',
338
+ note: 'Both slots logged four, but the LOWER one missed rep 3. Its column stays as an index-locked empty, fainter than a planned rep — so the gap reads as missing, not upcoming.',
339
+ single: { type: 'straight', velocities: [0.9, 0.86, 0.82, 0.78] },
340
+ left: { type: 'straight', velocities: [0.9, 0.86, 0.82, 0.78] },
341
+ right: { type: 'straight', velocities: [0.88, 0.84, 0.76] },
342
+ },
343
+ {
344
+ key: 'missed-top',
345
+ label: 'Missed rep · upper slot',
346
+ note: 'The mirror case — the UPPER slot is the one short a rep. The empty sits above the axis instead of below it.',
347
+ single: { type: 'straight', velocities: [0.9, 0.86, 0.82, 0.78] },
348
+ left: { type: 'straight', velocities: [0.9, 0.86, 0.82] },
349
+ right: { type: 'straight', velocities: [0.88, 0.84, 0.8, 0.76] },
350
+ },
351
+ {
352
+ key: 'variable',
353
+ label: 'Variable · a rep range',
354
+ note: 'A range set: committed reps to the floor, then the variable window up to the max. Outlined, never filled — a rep that MIGHT happen must not read as one that did.',
355
+ single: { type: 'range', velocities: [0.9, 0.86, 0.82], floor: 4, max: 6 },
356
+ left: { type: 'range', velocities: [0.9, 0.86, 0.82], floor: 4, max: 6 },
357
+ right: { type: 'range', velocities: [0.88, 0.84, 0.8], floor: 4, max: 6 },
358
+ },
359
+ {
360
+ key: 'repeatable',
361
+ label: 'Repeatable · AMRAP / myo continue',
362
+ note: 'No planned ceiling — the trailing continue window says another rep is allowed at any time.',
363
+ single: { type: 'amrap', velocities: [0.9, 0.85, 0.8] },
364
+ left: { type: 'amrap', velocities: [0.9, 0.85, 0.8] },
365
+ right: { type: 'amrap', velocities: [0.88, 0.83, 0.78] },
366
+ },
367
+ {
368
+ key: 'drop',
369
+ label: 'Drop · sub-load splits',
370
+ note: 'One bar per rep, with a wide chunk-notch separating each sub-load. The notch is a gap in the columns, so it survives even at compact height.',
371
+ single: {
372
+ type: 'drop',
373
+ subloads: [
374
+ [0.95, 0.9],
375
+ [0.82, 0.76],
376
+ [0.7, 0.64],
377
+ ],
378
+ },
379
+ left: {
380
+ type: 'drop',
381
+ subloads: [
382
+ [0.95, 0.9],
383
+ [0.82, 0.76],
384
+ [0.7, 0.64],
385
+ ],
386
+ },
387
+ right: {
388
+ type: 'drop',
389
+ subloads: [
390
+ [0.93, 0.88],
391
+ [0.8, 0.74],
392
+ [0.68, 0.62],
393
+ ],
394
+ },
395
+ },
396
+ ]
397
+
398
+ export function RepTypeBoard({ view }: { view: StripView }) {
399
+ const h = VIEW_HEIGHT[view]
400
+ return (
401
+ <View style={{ gap: 30 }}>
402
+ {REP_TYPE_ROWS.map((row) => (
403
+ <View key={row.key} style={{ gap: 8 }}>
404
+ <Text
405
+ style={{
406
+ color: VIEW_HEADING,
407
+ fontSize: 13,
408
+ fontWeight: '800',
409
+ letterSpacing: 0.5,
410
+ }}
411
+ >
412
+ {row.label}
413
+ </Text>
414
+ <Note>{row.note}</Note>
415
+
416
+ <ViewLabel text="single" />
417
+ <VelocityStrip
418
+ variant={view}
419
+ set={row.single}
420
+ height={h.single}
421
+ scale="fixed"
422
+ {...(view === 'hero' ? { label: 'Set' } : {})}
423
+ {...(view === 'expanded' ? { showNumbers: false, showInfo: false } : {})}
424
+ />
425
+
426
+ <ViewLabel text="dual" />
427
+ <DualVelocityStrip
428
+ left={{ set: row.left, label: LEFT_SLOT }}
429
+ right={{ set: row.right, label: RIGHT_SLOT }}
430
+ variant={dualVariantFor(view)}
431
+ height={h.dual}
432
+ scale="fixed"
433
+ />
434
+ </View>
435
+ ))}
436
+ </View>
437
+ )
438
+ }