@titan-design/react-ui 0.9.3 → 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.
- package/dist/bodymap.js +78 -24
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +78 -24
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +429 -11
- package/dist/index.d.ts +429 -11
- package/dist/index.js +2386 -885
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2306 -836
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-DyEx-lBf.d.ts → pages-B1hqnGIC.d.ts} +149 -15
- package/dist/{pages-D7hOD4Vj.d.mts → pages-CvsFqNtx.d.mts} +149 -15
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +1900 -1609
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1901 -1610
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +129 -34
- package/dist/theme/index.d.ts +129 -34
- package/dist/theme/index.js +95 -19
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +92 -20
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +88 -24
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +88 -24
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +8 -3
- package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
- package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
- package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
- package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
- package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
- package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
- package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
- package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
- package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
- package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
- package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
- package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
- package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
- package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
- package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
- package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
- package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
- package/src/components/custom/Fatigue/README.md +93 -0
- package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
- package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
- package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
- package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
- package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
- package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
- package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
- package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
- package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
- package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
- package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
- package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
- package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
- package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
- package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
- package/src/components/custom/Fatigue/index.ts +41 -0
- package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
- package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
- package/src/components/custom/Workout/BaseBadge.tsx +5 -5
- package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
- package/src/components/custom/Workout/ExerciseCard.test.tsx +4 -4
- package/src/components/custom/Workout/ExerciseCard.tsx +3 -1
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +7 -1
- package/src/components/custom/Workout/README.md +62 -16
- package/src/components/custom/Workout/RowInventory.stories.tsx +5 -2
- package/src/components/custom/Workout/S3FullRail.stories.tsx +13 -1
- package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
- package/src/components/custom/Workout/SessionHeader.tsx +2 -2
- package/src/components/custom/Workout/SessionRail.stories.tsx +4 -1
- package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
- package/src/components/custom/Workout/SessionRail.tsx +55 -18
- package/src/components/custom/Workout/SetRow.test.tsx +6 -6
- package/src/components/custom/Workout/SetRow.tsx +9 -2
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +16 -25
- package/src/components/custom/Workout/SupersetWrapper.tsx +1 -1
- package/src/components/custom/Workout/TempoDisplay.tsx +3 -7
- package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
- package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
- package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
- package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
- package/src/components/custom/Workout/VelocityStrip.test.tsx +196 -79
- package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
- package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +4 -1
- package/src/components/custom/Workout/WorkoutPill.tsx +2 -2
- package/src/components/custom/Workout/index.ts +3 -0
- package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
- package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
- package/src/components/custom/charts/SetBarChart.tsx +651 -0
- package/src/components/custom/charts/live-rep-growth.ts +91 -0
- package/src/components/custom/index.ts +1 -0
- package/src/components/ui/surface/Surface.stories.tsx +195 -1
- package/src/components/ui/surface/Surface.test.tsx +148 -9
- package/src/components/ui/surface/Surface.tsx +37 -9
- package/src/components/ui/surface/SurfaceContext.ts +48 -8
- package/src/components/ui/surface/index.ts +1 -0
- package/src/components/ui/surface/surface.contract.test.ts +283 -0
- package/src/test/raw-color-patterns.test.ts +71 -0
- package/src/theme/ColorPalettes.stories.tsx +871 -0
- package/src/theme/ColorPrimitives.stories.tsx +300 -0
- package/src/theme/bar-paper.ts +59 -0
- package/src/theme/color-stories.coverage.test.ts +130 -0
- package/src/theme/color-story-kit.tsx +170 -0
- package/src/theme/config.ts +12 -0
- package/src/theme/elevation.ts +90 -67
- package/src/theme/global.css +35 -9
- package/src/theme/tokens/primitives.ts +40 -0
- package/src/theme/tokens/semantic.ts +63 -14
- package/tailwind.config.js +8 -0
- package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
- package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
- package/src/theme/ColorSystem.stories.tsx +0 -323
- package/src/theme/Colors.stories.tsx +0 -323
|
@@ -1,34 +1,51 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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: ['
|
|
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
|
-
|
|
60
|
-
control: '
|
|
76
|
+
barColor: {
|
|
77
|
+
control: 'inline-radio',
|
|
78
|
+
options: ['zone', 'loss'],
|
|
61
79
|
description:
|
|
62
|
-
|
|
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:
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
187
|
-
|
|
204
|
+
{ state: 'todo', setNumber: 3, unit: 'lbs', target: { reps: 10, weight: 90 }, planned: 10 },
|
|
205
|
+
]
|
|
188
206
|
|
|
189
|
-
/**
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
238
|
-
}
|
|
247
|
+
]
|
|
239
248
|
|
|
240
|
-
/**
|
|
241
|
-
|
|
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={{
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
}
|