@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
|
@@ -12,7 +12,7 @@ export interface SupersetWrapperProps {
|
|
|
12
12
|
const DEFAULTS = {
|
|
13
13
|
label: 'SS',
|
|
14
14
|
color: getSemanticColors('dark')['brand-primary'],
|
|
15
|
-
bgBase: '
|
|
15
|
+
bgBase: getSemanticColors('dark')['background-base'],
|
|
16
16
|
} as const
|
|
17
17
|
|
|
18
18
|
export function SupersetWrapper({
|
|
@@ -84,11 +84,7 @@ function activeNumberTone(elapsedMs: number, targetMs: number | null): string {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/** The active number: `countup` elapsed (→ target) or `countdown` remaining (→ 0.0, then −). */
|
|
87
|
-
function liveReadoutText(
|
|
88
|
-
elapsedMs: number,
|
|
89
|
-
targetMs: number,
|
|
90
|
-
readout: TempoLiveReadout,
|
|
91
|
-
): string {
|
|
87
|
+
function liveReadoutText(elapsedMs: number, targetMs: number, readout: TempoLiveReadout): string {
|
|
92
88
|
const seconds = readout === 'countup' ? elapsedMs / 1000 : (targetMs - elapsedMs) / 1000
|
|
93
89
|
return seconds.toFixed(1)
|
|
94
90
|
}
|
|
@@ -308,7 +304,7 @@ export function TempoDisplay({
|
|
|
308
304
|
style={{
|
|
309
305
|
flexDirection: 'row',
|
|
310
306
|
alignItems: 'center',
|
|
311
|
-
backgroundColor: '
|
|
307
|
+
backgroundColor: t['surface-raised'],
|
|
312
308
|
paddingHorizontal: chromePadX,
|
|
313
309
|
paddingVertical: chromePadY,
|
|
314
310
|
borderRadius: chromeRadius,
|
|
@@ -378,7 +374,7 @@ export function TempoDisplay({
|
|
|
378
374
|
>
|
|
379
375
|
<View
|
|
380
376
|
style={{
|
|
381
|
-
backgroundColor: '
|
|
377
|
+
backgroundColor: t['surface-overlay'],
|
|
382
378
|
borderRadius: 6,
|
|
383
379
|
paddingVertical: 8,
|
|
384
380
|
paddingHorizontal: 12,
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { VelocityStrip } from './VelocityStrip'
|
|
3
|
+
import {
|
|
4
|
+
Sheet,
|
|
5
|
+
Note,
|
|
6
|
+
ScenarioPair,
|
|
7
|
+
SetTypeBoard,
|
|
8
|
+
RepTypeBoard,
|
|
9
|
+
REP_SET,
|
|
10
|
+
REP_SET_LAGGING,
|
|
11
|
+
FATIGUE_SET,
|
|
12
|
+
FATIGUE_SET_LAGGING,
|
|
13
|
+
} from './velocity-story-kit'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `compact` — the flat resting strip. SetBarChart in flat mode: uniform short
|
|
17
|
+
* bars, no labels, no axis. The glance used in the SetRow table and on cards,
|
|
18
|
+
* where the bar is colour-encoded rather than height-encoded.
|
|
19
|
+
*
|
|
20
|
+
* The compact dual FOLDS into one footprint rather than stacking to 2x: each
|
|
21
|
+
* rep column splits at the centre into an L top pill and an R bottom pill.
|
|
22
|
+
* Because resting bars carry their meaning in colour, the pair costs the same
|
|
23
|
+
* vertical space as a single — 2x is only forced for the value-height expanded.
|
|
24
|
+
*/
|
|
25
|
+
const meta: Meta<typeof VelocityStrip> = {
|
|
26
|
+
title: 'Workout/DataViz/VelocityStrip/Compact',
|
|
27
|
+
component: VelocityStrip,
|
|
28
|
+
tags: ['autodocs'],
|
|
29
|
+
}
|
|
30
|
+
export default meta
|
|
31
|
+
type Story = StoryObj<typeof VelocityStrip>
|
|
32
|
+
|
|
33
|
+
export const Default: Story = {
|
|
34
|
+
name: 'Default',
|
|
35
|
+
render: () => (
|
|
36
|
+
<Sheet>
|
|
37
|
+
<Note>
|
|
38
|
+
The representative set at rest. Single fills its plot; the dual folds the same reps into
|
|
39
|
+
rounded L/R pills sharing that footprint.
|
|
40
|
+
</Note>
|
|
41
|
+
<ScenarioPair
|
|
42
|
+
view="compact"
|
|
43
|
+
title="Default"
|
|
44
|
+
single={REP_SET}
|
|
45
|
+
left={REP_SET}
|
|
46
|
+
right={REP_SET_LAGGING}
|
|
47
|
+
/>
|
|
48
|
+
</Sheet>
|
|
49
|
+
),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const SetTypes: Story = {
|
|
53
|
+
name: 'Set Types',
|
|
54
|
+
render: () => (
|
|
55
|
+
<Sheet>
|
|
56
|
+
<Note>
|
|
57
|
+
The full set-type vocabulary at resting scale. The chunk-notch still separates drop
|
|
58
|
+
sub-loads even when every bar is the same height.
|
|
59
|
+
</Note>
|
|
60
|
+
<SetTypeBoard view="compact" />
|
|
61
|
+
</Sheet>
|
|
62
|
+
),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const FatigueDecline: Story = {
|
|
66
|
+
name: 'Fatigue Decline',
|
|
67
|
+
render: () => (
|
|
68
|
+
<Sheet>
|
|
69
|
+
<Note>
|
|
70
|
+
Loss colouring is the default: each bar is coloured by its drop from the set's own
|
|
71
|
+
best, so a fatiguing set reads green-to-red regardless of absolute speed. At compact scale
|
|
72
|
+
this colour IS the signal — there is no height to read.
|
|
73
|
+
</Note>
|
|
74
|
+
<ScenarioPair
|
|
75
|
+
view="compact"
|
|
76
|
+
title="Fatigue decline"
|
|
77
|
+
single={FATIGUE_SET}
|
|
78
|
+
left={FATIGUE_SET}
|
|
79
|
+
right={FATIGUE_SET_LAGGING}
|
|
80
|
+
barColor="loss"
|
|
81
|
+
/>
|
|
82
|
+
</Sheet>
|
|
83
|
+
),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const EmptyColdBoot: Story = {
|
|
87
|
+
name: 'Empty / Cold Boot',
|
|
88
|
+
render: () => (
|
|
89
|
+
<Sheet>
|
|
90
|
+
<Note>
|
|
91
|
+
Before the first rep lands. A planned set draws its upcoming reps, so the strip holds both
|
|
92
|
+
its footprint AND its column count — nothing reflows when rep one arrives.
|
|
93
|
+
</Note>
|
|
94
|
+
<ScenarioPair
|
|
95
|
+
view="compact"
|
|
96
|
+
title="Cold boot · 6 planned, none logged"
|
|
97
|
+
singleSet={{ type: 'straight', velocities: [], planned: 6 }}
|
|
98
|
+
leftSet={{ type: 'straight', velocities: [], planned: 6 }}
|
|
99
|
+
rightSet={{ type: 'straight', velocities: [], planned: 6 }}
|
|
100
|
+
/>
|
|
101
|
+
<ScenarioPair
|
|
102
|
+
view="compact"
|
|
103
|
+
title="Truly empty · no plan to draw"
|
|
104
|
+
note="No planned count either — the only case where there is nothing to hold."
|
|
105
|
+
single={[]}
|
|
106
|
+
left={[]}
|
|
107
|
+
right={[]}
|
|
108
|
+
/>
|
|
109
|
+
</Sheet>
|
|
110
|
+
),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const DualLaggingSide: Story = {
|
|
114
|
+
name: 'Dual · Lagging Side',
|
|
115
|
+
render: () => (
|
|
116
|
+
<Sheet>
|
|
117
|
+
<Note>
|
|
118
|
+
Index-locked: the right slot has logged fewer reps, so its trailing columns render as faint
|
|
119
|
+
empties directly under the left's. Columns never shift — the next rep a lagging side
|
|
120
|
+
performs lands in its own column.
|
|
121
|
+
</Note>
|
|
122
|
+
<ScenarioPair
|
|
123
|
+
view="compact"
|
|
124
|
+
title="Equal counts · the common case"
|
|
125
|
+
note="Both slots logged the same reps. This is what most of a session looks like."
|
|
126
|
+
single={REP_SET}
|
|
127
|
+
left={REP_SET}
|
|
128
|
+
right={[0.93, 0.88, 0.84, 0.78, 0.7]}
|
|
129
|
+
/>
|
|
130
|
+
<ScenarioPair
|
|
131
|
+
view="compact"
|
|
132
|
+
title="Missed rep · lower slot"
|
|
133
|
+
note="Rep 3 missing from the LOWER slot — its column holds as a faint empty."
|
|
134
|
+
single={REP_SET}
|
|
135
|
+
left={REP_SET}
|
|
136
|
+
right={[0.93, 0.88, 0.78, 0.7]}
|
|
137
|
+
/>
|
|
138
|
+
<ScenarioPair
|
|
139
|
+
view="compact"
|
|
140
|
+
title="Missed rep · upper slot"
|
|
141
|
+
note="The mirror: the UPPER slot is short a rep."
|
|
142
|
+
single={REP_SET}
|
|
143
|
+
left={[0.95, 0.9, 0.8, 0.72]}
|
|
144
|
+
right={[0.93, 0.88, 0.84, 0.78, 0.7]}
|
|
145
|
+
/>
|
|
146
|
+
<ScenarioPair
|
|
147
|
+
view="compact"
|
|
148
|
+
title="One slot well behind"
|
|
149
|
+
single={REP_SET}
|
|
150
|
+
left={REP_SET}
|
|
151
|
+
right={REP_SET_LAGGING.slice(0, 2)}
|
|
152
|
+
/>
|
|
153
|
+
</Sheet>
|
|
154
|
+
),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const Responsive: Story = {
|
|
158
|
+
name: 'Responsive',
|
|
159
|
+
render: () => (
|
|
160
|
+
<Sheet width={520}>
|
|
161
|
+
<Note>
|
|
162
|
+
Compact degrades by WIDTH, not height — the bar layout is shared geometry, so bars and gaps
|
|
163
|
+
scale together and the chunk-notch stays proportional.
|
|
164
|
+
</Note>
|
|
165
|
+
{[440, 300, 180, 110].map((w) => (
|
|
166
|
+
<ScenarioPair
|
|
167
|
+
key={w}
|
|
168
|
+
view="compact"
|
|
169
|
+
title={`${w}px`}
|
|
170
|
+
width={w}
|
|
171
|
+
single={REP_SET}
|
|
172
|
+
left={REP_SET}
|
|
173
|
+
right={REP_SET_LAGGING}
|
|
174
|
+
/>
|
|
175
|
+
))}
|
|
176
|
+
</Sheet>
|
|
177
|
+
),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const RepTypes: Story = {
|
|
181
|
+
name: 'Rep Types',
|
|
182
|
+
render: () => (
|
|
183
|
+
<Sheet>
|
|
184
|
+
<Note>
|
|
185
|
+
Every state a rep column can be in at resting scale. Colour is the only channel compact has,
|
|
186
|
+
so a state that needs height to read must find another way here.
|
|
187
|
+
</Note>
|
|
188
|
+
<RepTypeBoard view="compact" />
|
|
189
|
+
</Sheet>
|
|
190
|
+
),
|
|
191
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { DualVelocityStrip } from './VelocityStrip'
|
|
3
|
+
import {
|
|
4
|
+
LEFT_SLOT,
|
|
5
|
+
RIGHT_SLOT,
|
|
6
|
+
REP_SET,
|
|
7
|
+
REP_SET_LAGGING,
|
|
8
|
+
IN_PROGRESS_SET,
|
|
9
|
+
IN_PROGRESS_LAGGING,
|
|
10
|
+
dualOf,
|
|
11
|
+
wallDecorator,
|
|
12
|
+
} from './velocity-story-kit'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `DualVelocityStrip` — the strip when the exercise used TWO Voltras.
|
|
16
|
+
*
|
|
17
|
+
* Dual is **not a fourth view**, so its scenario coverage lives in the
|
|
18
|
+
* per-view groups, where every scenario renders single ABOVE dual on one
|
|
19
|
+
* dataset:
|
|
20
|
+
* [Compact](?path=/docs/workout-dataviz-velocitystrip-compact--docs) ·
|
|
21
|
+
* [Expanded](?path=/docs/workout-dataviz-velocitystrip-expanded--docs) ·
|
|
22
|
+
* [Hero](?path=/docs/workout-dataviz-velocitystrip-hero--docs).
|
|
23
|
+
*
|
|
24
|
+
* This group exists for the two things a pairing cannot carry: the exported
|
|
25
|
+
* **API** (`DualVelocityStripProps`, in the args table below) and the layout
|
|
26
|
+
* **extremes** — heights at which the labels have to degrade, and mismatched
|
|
27
|
+
* rep counts that must stay index-locked.
|
|
28
|
+
*
|
|
29
|
+
* The `hero` variant is literally two composed `VelocityStrip` heroes — an
|
|
30
|
+
* `orientation="up"` hero over a mirrored `orientation="down"` one, sharing ONE
|
|
31
|
+
* height scale and meeting at one centre axis — so every hero improvement
|
|
32
|
+
* reaches the dual for free. The up wing grows UP and the down wing grows DOWN,
|
|
33
|
+
* so asymmetry reads pre-attentively as the silhouette: a stronger side reads
|
|
34
|
+
* TALLER, while each wing still colours by its OWN best. **Side is POSITION
|
|
35
|
+
* only, never hue.** Each side takes a `DualVelocityStream` (`velocities` OR a
|
|
36
|
+
* structured `set`, plus an optional `label`); the vertical edge label is DATA,
|
|
37
|
+
* not a hardcoded side. Single-slot sets keep using `VelocityStrip`.
|
|
38
|
+
*/
|
|
39
|
+
const meta: Meta<typeof DualVelocityStrip> = {
|
|
40
|
+
title: 'Workout/DataViz/VelocityStrip/Dual',
|
|
41
|
+
component: DualVelocityStrip,
|
|
42
|
+
tags: ['autodocs'],
|
|
43
|
+
decorators: [wallDecorator],
|
|
44
|
+
argTypes: {
|
|
45
|
+
left: {
|
|
46
|
+
control: 'object',
|
|
47
|
+
description:
|
|
48
|
+
'Up-wing stream — `{ velocities | set, label }`. Edit `label` here to change the slot name.',
|
|
49
|
+
},
|
|
50
|
+
right: {
|
|
51
|
+
control: 'object',
|
|
52
|
+
description:
|
|
53
|
+
'Down-wing stream — `{ velocities | set, label }`. Edit `label` here to change the slot name.',
|
|
54
|
+
},
|
|
55
|
+
variant: {
|
|
56
|
+
control: 'inline-radio',
|
|
57
|
+
options: ['hero', 'compact', 'rail'],
|
|
58
|
+
description:
|
|
59
|
+
'hero (wall: labels + reference lines), compact (flat resting fold), or rail (lean, neither)',
|
|
60
|
+
},
|
|
61
|
+
scale: {
|
|
62
|
+
control: 'inline-radio',
|
|
63
|
+
options: ['peak', 'fixed'],
|
|
64
|
+
description:
|
|
65
|
+
'bar scaling, shared across both wings: peak (pair max) or fixed (cross-set ceiling)',
|
|
66
|
+
},
|
|
67
|
+
barColor: {
|
|
68
|
+
control: 'inline-radio',
|
|
69
|
+
options: ['zone', 'loss'],
|
|
70
|
+
description: 'per-wing loss (default) or the shared absolute zone scale; never encodes side',
|
|
71
|
+
},
|
|
72
|
+
targetReps: {
|
|
73
|
+
control: 'number',
|
|
74
|
+
description:
|
|
75
|
+
'planned rep count — reps beyond a side’s done count draw as mirrored dashed stubs',
|
|
76
|
+
},
|
|
77
|
+
liveRepIndex: {
|
|
78
|
+
control: 'number',
|
|
79
|
+
description: 'index of the newest rep; that mirrored pair animates in (hero only)',
|
|
80
|
+
},
|
|
81
|
+
height: {
|
|
82
|
+
control: { type: 'number', min: 60, max: 260, step: 4 },
|
|
83
|
+
description: 'total plot height (px), split evenly into the up (L) and down (R) wings',
|
|
84
|
+
},
|
|
85
|
+
zones: { control: false, description: 'Velocity-zone bands (WA); default scale when absent' },
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
export default meta
|
|
89
|
+
type Story = StoryObj<typeof DualVelocityStrip>
|
|
90
|
+
|
|
91
|
+
// ── The API surface ─────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/** Controls-driven: flip `variant` / `scale` / `barColor` / `targetReps` / `height`, edit each stream's `label` + data. */
|
|
94
|
+
export const Playground: Story = {
|
|
95
|
+
args: {
|
|
96
|
+
left: dualOf(REP_SET, LEFT_SLOT),
|
|
97
|
+
right: dualOf(REP_SET_LAGGING, RIGHT_SLOT),
|
|
98
|
+
variant: 'hero',
|
|
99
|
+
scale: 'peak',
|
|
100
|
+
height: 200,
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── The two canonical states ────────────────────────────────────────────────
|
|
105
|
+
// Deliberately minimal — the scenario pairs in the view groups carry breadth.
|
|
106
|
+
|
|
107
|
+
/** Both sides done, the lagging side one rep short — the everyday finished dual set. */
|
|
108
|
+
export const Default: Story = {
|
|
109
|
+
args: {
|
|
110
|
+
left: dualOf(REP_SET, LEFT_SLOT),
|
|
111
|
+
right: dualOf(REP_SET_LAGGING, RIGHT_SLOT),
|
|
112
|
+
variant: 'hero',
|
|
113
|
+
scale: 'fixed',
|
|
114
|
+
height: 200,
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Mid-set and live: 4 of 8 planned on the up wing, 3 on the down, newest rep
|
|
120
|
+
* animating in. The remainder draws as mirrored dashed stubs on both wings, so
|
|
121
|
+
* the columns are reserved and the set never reflows as reps land — which is
|
|
122
|
+
* also why a live dual should run `scale="fixed"`.
|
|
123
|
+
*/
|
|
124
|
+
export const InProgress: Story = {
|
|
125
|
+
args: {
|
|
126
|
+
left: dualOf(IN_PROGRESS_SET, LEFT_SLOT),
|
|
127
|
+
right: dualOf(IN_PROGRESS_LAGGING, RIGHT_SLOT),
|
|
128
|
+
variant: 'hero',
|
|
129
|
+
scale: 'fixed',
|
|
130
|
+
targetReps: 8,
|
|
131
|
+
liveRepIndex: 3,
|
|
132
|
+
height: 200,
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Layout extremes ─────────────────────────────────────────────────────────
|
|
137
|
+
// Recovered from the pre-reorg DualVelocityStrip stories. Unit tests cover the
|
|
138
|
+
// behaviour; these cover the SURFACE — label placement and collision at full
|
|
139
|
+
// width is a class of bug the tests have already missed once (TD-07.10).
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Symmetric index-lock — the up wing logs 5 reps, the down wing 3, same
|
|
143
|
+
* structure. The dual renders 5 aligned columns and the down side's reps 4–5
|
|
144
|
+
* render as EMPTY cells directly under the up side's (index-locked, never
|
|
145
|
+
* shifted). The next rep a lagging side performs lands at its own column.
|
|
146
|
+
*/
|
|
147
|
+
export const SymmetricEmpties: Story = {
|
|
148
|
+
args: {
|
|
149
|
+
left: dualOf([0.9, 0.88, 0.86, 0.84, 0.82], LEFT_SLOT),
|
|
150
|
+
right: dualOf([0.85, 0.8, 0.75], RIGHT_SLOT),
|
|
151
|
+
variant: 'hero',
|
|
152
|
+
scale: 'fixed',
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Small height (120px) — each wing gets ~59px rather than the full hero
|
|
158
|
+
* sentinel, and the responsive text DEGRADES: the per-side slot names collapse
|
|
159
|
+
* to initials ("Left"/"Right" → "L"/"R") and the VL20 / VL30 band labels drop
|
|
160
|
+
* entirely, while the dashed lines + washes stay.
|
|
161
|
+
*/
|
|
162
|
+
export const SmallHeightLabels: Story = {
|
|
163
|
+
args: {
|
|
164
|
+
left: dualOf([0.96, 0.9, 0.83, 0.72], LEFT_SLOT),
|
|
165
|
+
right: dualOf([0.9, 0.82, 0.71, 0.6], RIGHT_SLOT),
|
|
166
|
+
variant: 'hero',
|
|
167
|
+
scale: 'fixed',
|
|
168
|
+
height: 120,
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Very small height (72px) — the floor of graceful degradation. Below the
|
|
174
|
+
* thresholds the VL20 / VL30 labels are gone and the slot names are down to
|
|
175
|
+
* initials (a multi-word name would show its initials, e.g. "L A"). No
|
|
176
|
+
* shrinking into an illegible smear.
|
|
177
|
+
*/
|
|
178
|
+
export const TinyHeight: Story = {
|
|
179
|
+
args: {
|
|
180
|
+
left: dualOf([0.96, 0.9, 0.83, 0.72], LEFT_SLOT),
|
|
181
|
+
right: dualOf([0.9, 0.82, 0.71, 0.6], RIGHT_SLOT),
|
|
182
|
+
variant: 'hero',
|
|
183
|
+
scale: 'fixed',
|
|
184
|
+
height: 72,
|
|
185
|
+
},
|
|
186
|
+
}
|