@titan-design/react-ui 0.10.0 → 0.11.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 (84) hide show
  1. package/dist/index.d.mts +399 -4
  2. package/dist/index.d.ts +399 -4
  3. package/dist/index.js +2295 -895
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +2279 -905
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/{pages-DyEx-lBf.d.ts → pages-B1hqnGIC.d.ts} +149 -15
  8. package/dist/{pages-D7hOD4Vj.d.mts → pages-CvsFqNtx.d.mts} +149 -15
  9. package/dist/pages.d.mts +1 -1
  10. package/dist/pages.d.ts +1 -1
  11. package/dist/pages.js +1974 -1740
  12. package/dist/pages.js.map +1 -1
  13. package/dist/pages.mjs +1975 -1741
  14. package/dist/pages.mjs.map +1 -1
  15. package/package.json +8 -3
  16. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
  17. package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
  18. package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
  19. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
  20. package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
  21. package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
  22. package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
  23. package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
  24. package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
  25. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
  26. package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
  27. package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
  28. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
  29. package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
  30. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
  31. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
  32. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
  33. package/src/components/custom/Fatigue/README.md +93 -0
  34. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
  35. package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
  36. package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
  37. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
  38. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
  39. package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
  40. package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
  41. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
  42. package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
  43. package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
  44. package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
  45. package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
  46. package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
  47. package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
  48. package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
  49. package/src/components/custom/Fatigue/index.ts +41 -0
  50. package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
  51. package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
  52. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
  53. package/src/components/custom/Workout/ExerciseCard.test.tsx +4 -4
  54. package/src/components/custom/Workout/ExerciseCard.tsx +3 -1
  55. package/src/components/custom/Workout/ExerciseDetailPage.tsx +7 -1
  56. package/src/components/custom/Workout/README.md +62 -16
  57. package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
  58. package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
  59. package/src/components/custom/Workout/SessionRail.tsx +50 -13
  60. package/src/components/custom/Workout/SetRow.test.tsx +6 -6
  61. package/src/components/custom/Workout/SetRow.tsx +9 -2
  62. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
  63. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
  64. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
  65. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
  66. package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
  67. package/src/components/custom/Workout/VelocityStrip.test.tsx +196 -79
  68. package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
  69. package/src/components/custom/Workout/index.ts +3 -0
  70. package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
  71. package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
  72. package/src/components/custom/charts/SetBarChart.tsx +651 -0
  73. package/src/components/custom/charts/live-rep-growth.ts +91 -0
  74. package/src/components/custom/index.ts +1 -0
  75. package/src/test/raw-color-patterns.test.ts +71 -0
  76. package/src/theme/ColorPalettes.stories.tsx +871 -0
  77. package/src/theme/ColorPrimitives.stories.tsx +300 -0
  78. package/src/theme/bar-paper.ts +59 -0
  79. package/src/theme/color-stories.coverage.test.ts +130 -0
  80. package/src/theme/color-story-kit.tsx +170 -0
  81. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
  82. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
  83. package/src/theme/ColorSystem.stories.tsx +0 -508
  84. package/src/theme/Colors.stories.tsx +0 -323
@@ -1,34 +1,51 @@
1
- import { useState } from 'react'
2
- import type { Decorator, Meta, StoryObj } from '@storybook/react-vite'
3
- import { View, Text } from 'react-native'
4
- import { VelocityStrip } from './VelocityStrip'
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { VelocityStrip, DualVelocityStrip } from './VelocityStrip'
4
+ import { SessionRail, type SessionRailExercise } from './SessionRail'
5
+ import type { SetRowProps } from './SetRow'
6
+ import {
7
+ Note,
8
+ ViewLabel,
9
+ SURFACE_BG,
10
+ VIEW_HEIGHT,
11
+ dualVariantFor,
12
+ dualOf,
13
+ LEFT_SLOT,
14
+ RIGHT_SLOT,
15
+ REP_SET,
16
+ REP_SET_LAGGING,
17
+ type StripView,
18
+ } from './velocity-story-kit'
5
19
 
20
+ /**
21
+ * Per-rep velocity strip.
22
+ *
23
+ * Three views, each with its own story group:
24
+ * - **[Compact](?path=/docs/workout-dataviz-velocitystrip-compact--docs)** — the flat resting strip
25
+ * - **[Expanded](?path=/docs/workout-dataviz-velocitystrip-expanded--docs)** — the value-height chart
26
+ * - **[Hero](?path=/docs/workout-dataviz-velocitystrip-hero--docs)** — the across-the-room wall treatment
27
+ *
28
+ * Dual is NOT a fourth view. It is the same strip when the exercise used two
29
+ * Voltras, so every group pairs single above dual on one dataset.
30
+ *
31
+ * Feed either `velocities` or a `set` descriptor (set-type aware). `height` sets
32
+ * the plot height; `scale` is `peak` (to the set max) or `fixed` (a fixed
33
+ * ceiling, cross-set comparable — recommended for a live hero so bar heights
34
+ * never reflow as reps land). `barColor` picks the fill scale: `zone` is the
35
+ * absolute velocity zone, `loss` colours each bar by its drop from the set's own
36
+ * best, so a fatiguing set reads green-to-red regardless of absolute speed.
37
+ *
38
+ * This root group holds only the cross-cutting stories — the per-view scenarios
39
+ * live in the three group files above.
40
+ */
6
41
  const meta: Meta<typeof VelocityStrip> = {
7
42
  title: 'Workout/DataViz/VelocityStrip',
8
43
  component: VelocityStrip,
9
44
  tags: ['autodocs'],
10
- parameters: {
11
- docs: {
12
- description: {
13
- component:
14
- 'Per-rep velocity strip. Three variants: `mini` (a flat 3px static strip), `expanded` ' +
15
- '(the velocity-HEIGHT bar chart, rounded tops), and `hero` (the across-the-room, single-set ' +
16
- 'wall treatment — tall bars, per-bar value labels, a dashed running-best reference line, and ' +
17
- 'dashed placeholders for the reps still to come via `targetReps`). The `expanded` chrome is ' +
18
- 'prop-driven — with `showNumbers`/`showInfo` on it is the framed chart (raised surface, per-bar ' +
19
- 'm/s labels, mean/loss info row, interactive tap-to-expand); with both off it is a bare strip, ' +
20
- 'the active-set spotlight. `height` sets the plot height; `scale` is `peak` (to the set max) ' +
21
- 'or `fixed` (a fixed ceiling, cross-set-comparable — recommended for the live `hero` so bar ' +
22
- 'heights never reflow as reps land). Feed either `velocities` or a `set` ' +
23
- 'descriptor (set-type aware — see the ' +
24
- '[modalities](?path=/docs/workout-dataviz-velocitystrip-modalities--docs) sheet).',
25
- },
26
- },
27
- },
28
45
  argTypes: {
29
46
  variant: {
30
47
  control: 'select',
31
- options: ['mini', 'expanded', 'hero'],
48
+ options: ['compact', 'expanded', 'hero'],
32
49
  description: 'Display variant',
33
50
  },
34
51
  targetReps: {
@@ -56,28 +73,21 @@ const meta: Meta<typeof VelocityStrip> = {
56
73
  options: ['peak', 'fixed'],
57
74
  description: 'expanded bar scaling: peak (set max) or fixed (cross-set ceiling)',
58
75
  },
59
- liveRepIndex: {
60
- control: 'number',
76
+ barColor: {
77
+ control: 'inline-radio',
78
+ options: ['zone', 'loss'],
61
79
  description:
62
- 'expanded framed chart: index of the newest rep to animate (pop / new-peak bounce)',
80
+ "bar-fill scale: zone (default, absolute velocity) or loss (relative to set's own best)",
63
81
  },
64
- set: { control: false, description: 'Structured set descriptor (supersedes `velocities`)' },
65
- zones: { control: false, description: 'Velocity-zone bands (WA); default scale when absent' },
66
82
  },
67
83
  }
68
-
69
84
  export default meta
70
85
  type Story = StoryObj<typeof VelocityStrip>
71
86
 
72
- const fastSet = [1.15, 1.12, 1.08, 1.05, 1.02]
73
- const slowSet = [0.65, 0.58, 0.52, 0.48, 0.42]
74
- const mixedSet = [1.1, 0.95, 0.82, 0.68, 0.55, 0.45]
75
- const moderateSet = [0.88, 0.85, 0.82, 0.78, 0.76]
76
-
77
87
  /** Controls-driven: flip `variant` / `showNumbers` / `showInfo` / `height` / `scale` in the panel. */
78
88
  export const Playground: Story = {
79
89
  args: {
80
- velocities: moderateSet,
90
+ velocities: [0.88, 0.85, 0.82, 0.78, 0.76],
81
91
  variant: 'expanded',
82
92
  expanded: true,
83
93
  showNumbers: true,
@@ -88,244 +98,201 @@ export const Playground: Story = {
88
98
  },
89
99
  decorators: [
90
100
  (Story) => (
91
- <View style={{ width: 300, padding: 16 }}>
101
+ <View style={{ width: 300, padding: 16, backgroundColor: SURFACE_BG }}>
92
102
  <Story />
93
103
  </View>
94
104
  ),
95
105
  ],
96
106
  }
97
107
 
98
- /** The flat 3px static strip — the collapsed glance used in cards and rails. */
99
- export const Mini: Story = {
100
- args: { velocities: mixedSet, variant: 'mini' },
101
- }
108
+ // ── All views · the alignment matrix ────────────────────────────────────────
109
+ // ONE dataset across all three views, each in its single and dual form. The
110
+ // whole SetBarChart fold exists so these share ONE geometry; this is the story
111
+ // that catches drift at a glance.
102
112
 
103
- /** Framed chart with per-bar m/s labels + info row (numbers ON) — the rich readout. */
104
- export const ExpandedWithNumbers: Story = {
105
- args: { velocities: mixedSet, variant: 'expanded', expanded: true },
106
- decorators: [
107
- (Story) => (
108
- <View style={{ width: 300, padding: 16 }}>
109
- <Story />
113
+ const AV_VIEWS: { view: StripView; note: string }[] = [
114
+ { view: 'compact', note: 'single fills · dual folds to rounded L/R pills' },
115
+ { view: 'expanded', note: 'value-height bars' },
116
+ { view: 'hero', note: 'wall treatment' },
117
+ ]
118
+
119
+ /**
120
+ * The hero reserves a 34px slot-label gutter; compact and expanded are
121
+ * gutter-less in production. They are indented here ONLY so every row's bars
122
+ * line up under the hero's — a comparison affordance, not production layout.
123
+ */
124
+ const AV_GUTTER = 34
125
+
126
+ export const AllViews: Story = {
127
+ name: 'All Views · single × dual matrix',
128
+ render: () => (
129
+ <View style={{ padding: 24, backgroundColor: SURFACE_BG, gap: 26, width: 760 }}>
130
+ <View style={{ flexDirection: 'row', gap: 16 }}>
131
+ <View style={{ width: 92 }} />
132
+ <View style={{ flex: 1 }}>
133
+ <ViewLabel text="Single slot" />
134
+ </View>
135
+ <View style={{ flex: 1 }}>
136
+ <ViewLabel text="Dual · diverging" />
137
+ </View>
110
138
  </View>
111
- ),
112
- ],
139
+ <Note>
140
+ The SAME set across every row — compact is the flattened expanded (identical bar
141
+ x-positions, only the heights change). Shown at actual size.
142
+ </Note>
143
+ {AV_VIEWS.map(({ view, note }) => (
144
+ <View key={view} style={{ flexDirection: 'row', gap: 16, alignItems: 'center' }}>
145
+ <View style={{ width: 92, gap: 3 }}>
146
+ <ViewLabel text={view} />
147
+ <Note>{note}</Note>
148
+ </View>
149
+ <View style={{ flex: 1, paddingLeft: view === 'hero' ? 0 : AV_GUTTER }}>
150
+ <VelocityStrip
151
+ velocities={REP_SET}
152
+ variant={view}
153
+ height={VIEW_HEIGHT[view].single}
154
+ scale="peak"
155
+ {...(view === 'hero' ? { label: 'Set' } : {})}
156
+ {...(view === 'expanded' ? { showNumbers: false, showInfo: false } : {})}
157
+ />
158
+ </View>
159
+ <View style={{ flex: 1, paddingLeft: view === 'hero' ? 0 : AV_GUTTER }}>
160
+ <DualVelocityStrip
161
+ left={dualOf(REP_SET, LEFT_SLOT)}
162
+ right={dualOf(REP_SET_LAGGING, RIGHT_SLOT)}
163
+ variant={dualVariantFor(view)}
164
+ height={VIEW_HEIGHT[view].dual}
165
+ scale="peak"
166
+ />
167
+ </View>
168
+ </View>
169
+ ))}
170
+ </View>
171
+ ),
113
172
  }
114
173
 
115
- /** Bare velocity-height strip (numbers OFF, info OFF, fixed 24px) the active-set spotlight. */
116
- export const ExpandedBareSpotlight: Story = {
117
- args: {
118
- variant: 'expanded',
119
- showNumbers: false,
120
- showInfo: false,
121
- height: 24,
122
- scale: 'fixed',
123
- set: { type: 'straight', velocities: [0.95, 0.9, 0.86, 0.8, 0.72], planned: 10 },
124
- },
125
- }
174
+ // ── In context · the real session rail + expanded exercise ──────────────────
126
175
 
127
- // --- Hero variant ------------------------------------------------------------
128
- // The across-the-room, single-set wall treatment. Rendered on the north-star wall
129
- // background so the dashed reference line + placeholders read the way they will live.
176
+ const decay = (n: number, start: number, span = 0.4): number[] =>
177
+ Array.from({ length: n }, (_, r) => +(start - (span * r) / Math.max(1, n - 1)).toFixed(3))
130
178
 
131
179
  /**
132
- * Wall-background frame for the hero stories. The eyebrow label is ORGANISM chrome
133
- * (the north-star live page renders it), NOT part of the primitive it sits here only
134
- * to show the component in the context it will live in. The `VelocityStrip` hero variant
135
- * itself renders no title.
180
+ * The current exercise's set table. This is where BOTH strip views ship: a
181
+ * `done` row carries the flat compact strip, the `live` row carries the
182
+ * velocity-height spotlight, and `todo` rows carry a grey stub.
136
183
  */
137
- const heroDecorator: Decorator = (Story) => (
138
- <View style={{ width: 560, padding: 28, backgroundColor: '#0E0E0E' }}>
139
- <Text
140
- style={{
141
- color: '#5A5A5A',
142
- fontSize: 10,
143
- fontWeight: '700',
144
- letterSpacing: 1,
145
- marginBottom: 12,
146
- }}
147
- >
148
- CONCENTRIC VELOCITY · THIS SET · (organism chrome — not the component)
149
- </Text>
150
- <Story />
151
- </View>
152
- )
153
-
154
- const heroMidSet = [0.82, 0.79, 0.81, 0.76]
155
- const heroNearComplete = [0.82, 0.79, 0.81, 0.76, 0.74, 0.71, 0.68]
156
- const heroFatigue = [0.96, 0.91, 0.83, 0.72, 0.61, 0.5]
157
-
158
- /** Controls-driven hero: flip `velocities` / `targetReps` / `liveRepIndex` / `scale` / `height`. */
159
- export const HeroPlayground: Story = {
160
- args: {
161
- velocities: heroMidSet,
162
- variant: 'hero',
163
- targetReps: 8,
164
- liveRepIndex: 3,
165
- scale: 'fixed',
166
- height: 220,
184
+ const CURRENT_SETS: SetRowProps[] = [
185
+ {
186
+ state: 'done',
187
+ setNumber: 1,
188
+ unit: 'lbs',
189
+ reps: 10,
190
+ weight: 90,
191
+ rpe: 7,
192
+ velocities: decay(10, 0.72),
167
193
  },
168
- decorators: [heroDecorator],
169
- }
170
-
171
- /** Mid-set: 4 of 8 reps done, the newest bar popping, 4 dashed reps still to come. */
172
- export const HeroMidSet: Story = {
173
- args: { velocities: heroMidSet, variant: 'hero', targetReps: 8, liveRepIndex: 3, scale: 'fixed' },
174
- decorators: [heroDecorator],
175
- }
176
-
177
- /** Near complete: 7 of 8, one placeholder left; the running-best line sits at rep 3. */
178
- export const HeroNearComplete: Story = {
179
- args: {
180
- velocities: heroNearComplete,
181
- variant: 'hero',
182
- targetReps: 8,
183
- liveRepIndex: 6,
184
- scale: 'fixed',
194
+ {
195
+ state: 'live',
196
+ setNumber: 2,
197
+ unit: 'lbs',
198
+ target: { reps: 10, weight: 90 },
199
+ reps: 5,
200
+ weight: 90,
201
+ velocities: decay(5, 0.62),
202
+ liveRepIndex: 4,
185
203
  },
186
- decorators: [heroDecorator],
187
- }
204
+ { state: 'todo', setNumber: 3, unit: 'lbs', target: { reps: 10, weight: 90 }, planned: 10 },
205
+ ]
188
206
 
189
- /** Fatigue decline: velocity falling rep over rep the shape you read across the room. */
190
- export const HeroFatigueDecline: Story = {
191
- args: {
192
- velocities: heroFatigue,
193
- variant: 'hero',
194
- targetReps: 8,
195
- liveRepIndex: 5,
196
- scale: 'fixed',
207
+ /** A live session, mid-workout: two done, one active, one still upcoming. */
208
+ const RAIL_SESSION: SessionRailExercise[] = [
209
+ {
210
+ name: 'Seated Cable Row',
211
+ summary: { sets: 5, reps: 8, weight: 145, unit: 'lbs' },
212
+ tempo: [3, 1, 2, 0],
213
+ indicator: 'pr',
214
+ setStates: [1.0, 0.9, 0.8, 0.7, 0.6].map((v) => ({
215
+ status: 'done' as const,
216
+ velocities: decay(8, v),
217
+ })),
197
218
  },
198
- decorators: [heroDecorator],
199
- }
200
-
201
- /** Ends on the best rep: the running-best line lands on the last (live) bar's top. */
202
- export const HeroEndsOnBest: Story = {
203
- args: {
204
- velocities: [0.7, 0.76, 0.83, 0.9],
205
- variant: 'hero',
206
- targetReps: 4,
207
- liveRepIndex: 3,
208
- scale: 'peak',
219
+ {
220
+ name: 'Incline DB Press',
221
+ summary: { sets: 3, reps: 8, weight: 70, unit: 'lbs' },
222
+ tempo: [3, 0, 3, 0],
223
+ setStates: [0.72, 0.62, 0.54].map((v) => ({
224
+ status: 'done' as const,
225
+ velocities: decay(8, v),
226
+ })),
209
227
  },
210
- decorators: [heroDecorator],
211
- }
212
-
213
- /**
214
- * Set expansion — reps performed BEYOND `targetReps` (11 done vs 8 planned). Placeholders
215
- * are gone (target met) and the extra reps just render as more bars; `flex` bars narrow to
216
- * fit, so the chart absorbs the overflow with no clipping. The AMRAP "keep going" case.
217
- */
218
- export const HeroExceedsTarget: Story = {
219
- args: {
220
- velocities: [0.9, 0.87, 0.84, 0.8, 0.77, 0.74, 0.71, 0.68, 0.64, 0.6, 0.55],
221
- variant: 'hero',
222
- targetReps: 8,
223
- liveRepIndex: 10,
224
- scale: 'fixed',
228
+ {
229
+ name: 'Cable Chest Press',
230
+ summary: { sets: 3, reps: 10, weight: 90, unit: 'lbs' },
231
+ tempo: [2, 1, 2, 0],
232
+ indicator: 'info',
233
+ setStates: [
234
+ { status: 'done', velocities: decay(10, 0.72) },
235
+ { status: 'active', velocities: decay(5, 0.62), planned: 10 },
236
+ { status: 'todo', planned: 10 },
237
+ ],
238
+ sets: CURRENT_SETS,
225
239
  },
226
- decorators: [heroDecorator],
227
- }
228
-
229
- /** Set complete: 8 of 8, no placeholders. */
230
- export const HeroComplete: Story = {
231
- args: {
232
- velocities: [0.96, 0.91, 0.83, 0.79, 0.76, 0.72, 0.68, 0.61],
233
- variant: 'hero',
234
- targetReps: 8,
235
- scale: 'fixed',
240
+ {
241
+ name: 'Standing Calf Raise',
242
+ summary: { sets: 5, reps: 20, weight: 25, unit: 'lbs' },
243
+ tempo: [2, 1, 2, 0],
244
+ upcoming: true,
245
+ setStates: [1, 2, 3, 4, 5].map(() => ({ status: 'todo' as const, planned: 20 })),
236
246
  },
237
- decorators: [heroDecorator],
238
- }
247
+ ]
239
248
 
240
- /** The same set at three container widths — the hero is width-fluid (flex bars, capped width, fixed height). */
241
- export const HeroResponsive: Story = {
249
+ /**
250
+ * The strip in the place it actually ships, at production size, using the real
251
+ * components — no mock rows.
252
+ *
253
+ * Left: `SessionRail`, which draws SET-level bars (`SetStrip`, `stripHeight={8}`)
254
+ * — one bar per set, segmented per rep. Right: the current exercise opened as an
255
+ * `ExerciseCard`, whose set table is where BOTH strip views live in production —
256
+ * a `done` row renders `compact`, the `live` row renders the velocity-height
257
+ * spotlight (`expanded` at h=24), and a `todo` row renders a grey stub.
258
+ *
259
+ * That composition is the honest answer to "where does this component appear":
260
+ * the SPA renders `ExerciseCard` in its hero panel and rest view, so compact
261
+ * ships through this table rather than through any direct call site. Reading
262
+ * left to right is a zoom — set-level bars, then the same sets rep by rep — and
263
+ * a fork in the flat-bar language shows up here as a mismatch in bar height,
264
+ * radius or gap between the rail and the table.
265
+ */
266
+ export const InContext: Story = {
267
+ name: 'In Context · session rail',
268
+ parameters: { layout: 'fullscreen' },
242
269
  render: () => (
243
- <View style={{ gap: 24, padding: 24, backgroundColor: '#0E0E0E' }}>
244
- {[360, 560, 900].map((w) => (
245
- <View key={w} style={{ width: w }}>
246
- <Text style={{ color: '#5A5A5A', fontSize: 10, fontWeight: '700', marginBottom: 8 }}>
247
- {w}px
248
- </Text>
249
- <VelocityStrip
250
- velocities={heroFatigue}
251
- variant="hero"
252
- targetReps={8}
253
- liveRepIndex={5}
254
- scale="fixed"
255
- height={180}
256
- />
257
- </View>
258
- ))}
259
- </View>
260
- ),
261
- }
262
-
263
- /** A structured set descriptor (a drop set): the mini variant carries the set-type gap/color encoding. */
264
- export const SetTypeMini: Story = {
265
- args: {
266
- variant: 'mini',
267
- set: {
268
- type: 'drop',
269
- subloads: [
270
- [1.0, 0.92],
271
- [0.85, 0.78],
272
- [0.7, 0.62],
273
- ],
274
- },
275
- },
276
- }
277
-
278
- function InteractiveVelocityStrip() {
279
- const [open, setOpen] = useState(false)
280
- // Tap-to-expand: `onToggle` + the controlled `expanded` boolean. Tapping the strip
281
- // toggles both ways (collapsed 3px ↔ chart). No `onRepPress` here — per-bar rep
282
- // presses would intercept bar taps and block collapse; that's a separate mode.
283
- return (
284
- <View style={{ width: 300, padding: 16 }}>
285
- <VelocityStrip velocities={moderateSet} expanded={open} onToggle={() => setOpen((v) => !v)} />
286
- </View>
287
- )
288
- }
270
+ <View style={{ flexDirection: 'row', backgroundColor: SURFACE_BG, minHeight: 640 }}>
271
+ <SessionRail
272
+ title="Pull A · Intensification"
273
+ exercises={RAIL_SESSION}
274
+ stripHeight={8}
275
+ width={560}
276
+ expandedIndex={2}
277
+ setsDone={7.2}
278
+ elapsedMs={(42 * 60 + 18) * 1000}
279
+ budgetMs={60 * 60 * 1000}
280
+ metrics={[
281
+ { label: 'Volume', value: '76%' },
282
+ { label: 'Load', value: '7.3k' },
283
+ { label: 'Fatigue', value: 'MOD' },
284
+ ]}
285
+ />
289
286
 
290
- /** Tap to expand; tap again to collapse (3px ↔ chart, animated). */
291
- export const Interactive: Story = {
292
- render: () => <InteractiveVelocityStrip />,
293
- }
294
-
295
- /** The four zone profiles across the treatments, so the color scale reads at a glance. */
296
- export const Profiles: Story = {
297
- render: () => {
298
- const rows: [string, number[]][] = [
299
- ['Speed', fastSet],
300
- ['Power', moderateSet],
301
- ['Strength', slowSet],
302
- ['Mixed', mixedSet],
303
- ]
304
- return (
305
- <View style={{ gap: 20, padding: 16, width: 320 }}>
306
- {rows.map(([label, velocities]) => (
307
- <View key={label} style={{ gap: 6 }}>
308
- <Text style={{ fontFamily: 'Inter, sans-serif', fontSize: 11, color: '#9CA3AF' }}>
309
- {label}
310
- </Text>
311
- <VelocityStrip
312
- velocities={velocities}
313
- variant="expanded"
314
- showInfo={false}
315
- onToggle={() => {}}
316
- />
317
- <VelocityStrip
318
- variant="expanded"
319
- showNumbers={false}
320
- showInfo={false}
321
- height={24}
322
- scale="fixed"
323
- set={{ type: 'straight', velocities, planned: velocities.length }}
324
- />
325
- <VelocityStrip velocities={velocities} variant="mini" />
326
- </View>
327
- ))}
287
+ <View style={{ flex: 1, padding: 28, gap: 6 }}>
288
+ <ViewLabel text="reading the rail" />
289
+ <Note>
290
+ Collapsed rows draw SET-level bars — one per set, segmented per rep, at the same 8px
291
+ flat-bar language. The expanded row is the same exercise rep by rep: its `done` rows carry
292
+ the compact strip, the `live` row the velocity-height spotlight, `todo` rows a grey stub.
293
+ A zoom, not a repeat.
294
+ </Note>
328
295
  </View>
329
- )
330
- },
296
+ </View>
297
+ ),
331
298
  }