@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.
- package/dist/bodymap.js +173 -343
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +173 -343
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +416 -18
- package/dist/index.d.ts +416 -18
- package/dist/index.js +2512 -1307
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2480 -1299
- 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 +1644 -1575
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1645 -1576
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +409 -415
- package/dist/theme/index.d.ts +409 -415
- package/dist/theme/index.js +234 -408
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +227 -399
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +141 -140
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +141 -140
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +8 -3
- package/src/arch/Architecture.stories.tsx +9 -9
- package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
- 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/Sidebar/Sidebar.tsx +4 -4
- package/src/components/custom/Table/Table.tsx +2 -2
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +3 -3
- package/src/components/custom/Workout/BaseBadge.test.tsx +6 -1
- package/src/components/custom/Workout/BaseBadge.tsx +1 -1
- package/src/components/custom/Workout/BodyMap.tsx +2 -2
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +2 -2
- package/src/components/custom/Workout/DeviationBar.test.tsx +2 -1
- package/src/components/custom/Workout/DeviationBar.tsx +3 -2
- package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
- package/src/components/custom/Workout/ExerciseCard.test.tsx +7 -7
- package/src/components/custom/Workout/ExerciseCard.tsx +5 -3
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +11 -5
- package/src/components/custom/Workout/InputBar.tsx +2 -2
- package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +1 -1
- package/src/components/custom/Workout/LiveAuraFrame.test.tsx +1 -1
- package/src/components/custom/Workout/LiveAuraFrame.tsx +3 -3
- package/src/components/custom/Workout/MesoCard.tsx +3 -1
- package/src/components/custom/Workout/MuscleGroupChip.tsx +4 -3
- package/src/components/custom/Workout/PrHistoryModal.tsx +2 -2
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +1 -1
- package/src/components/custom/Workout/README.md +62 -16
- package/src/components/custom/Workout/ReadinessCheck.tsx +2 -2
- package/src/components/custom/Workout/RestTimer.tsx +2 -2
- package/src/components/custom/Workout/S3SetTypes.stories.tsx +3 -3
- package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
- package/src/components/custom/Workout/SessionHeader.stories.tsx +2 -2
- package/src/components/custom/Workout/SessionHeader.tsx +1 -1
- package/src/components/custom/Workout/SessionRail.stories.tsx +2 -2
- package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
- package/src/components/custom/Workout/SessionRail.tsx +63 -18
- package/src/components/custom/Workout/SetBar.tsx +3 -3
- package/src/components/custom/Workout/SetRow.test.tsx +12 -9
- package/src/components/custom/Workout/SetRow.tsx +9 -2
- package/src/components/custom/Workout/StatusDot.tsx +5 -4
- package/src/components/custom/Workout/StrengthTrendChart.tsx +1 -1
- package/src/components/custom/Workout/TrainingStatusPage.tsx +2 -2
- 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 +199 -79
- package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
- package/src/components/custom/Workout/VolumeLandmarkBar.tsx +3 -3
- package/src/components/custom/Workout/WeightBadge.test.tsx +6 -1
- package/src/components/custom/Workout/WeightBadge.tsx +2 -1
- package/src/components/custom/Workout/WorkoutCard.tsx +3 -1
- package/src/components/custom/Workout/WorkoutPill.tsx +5 -2
- package/src/components/custom/Workout/ZoneTrack.tsx +4 -4
- package/src/components/custom/Workout/index.ts +3 -0
- package/src/components/custom/Workout/setHeadingKit.tsx +14 -8
- 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/custom/stepper/Stepper.stories.tsx +1 -1
- package/src/components/custom/stepper/Stepper.tsx +3 -3
- package/src/components/shell/DeviceMenu.tsx +2 -2
- package/src/components/shell/SideNav.tsx +1 -1
- package/src/components/shell/TopBar.tsx +1 -1
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
- package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
- package/src/components/ui/avatar/Avatar.tsx +2 -2
- package/src/components/ui/badge/Badge.tsx +2 -2
- package/src/components/ui/card/Card.tsx +3 -3
- package/src/components/ui/checkbox/Checkbox.tsx +1 -1
- package/src/components/ui/collapse/Collapse.tsx +1 -1
- package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
- package/src/components/ui/drawer/Drawer.tsx +3 -3
- package/src/components/ui/form-field/FormField.tsx +1 -1
- package/src/components/ui/help-tip/HelpTip.tsx +1 -1
- package/src/components/ui/menu/Menu.tsx +2 -2
- package/src/components/ui/pill/Pill.tsx +2 -2
- package/src/components/ui/popover/Popover.stories.tsx +2 -2
- package/src/components/ui/popover/Popover.tsx +1 -1
- package/src/components/ui/progress/Progress.tsx +3 -3
- package/src/components/ui/radio/Radio.stories.tsx +1 -1
- package/src/components/ui/radio/Radio.tsx +4 -4
- package/src/components/ui/select/Select.tsx +3 -3
- package/src/components/ui/surface/Surface.stories.tsx +3 -3
- package/src/components/ui/surface/Surface.test.tsx +32 -19
- package/src/components/ui/surface/Surface.tsx +3 -3
- package/src/components/ui/surface/SurfaceContext.ts +19 -29
- package/src/components/ui/surface/surface.contract.test.ts +57 -44
- package/src/components/ui/switch/Switch.tsx +1 -1
- package/src/components/ui/tabs/Tabs.tsx +2 -2
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +5 -5
- package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +1 -1
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +36 -22
- package/src/stories/PresetShowcase.stories.tsx +21 -21
- package/src/test/raw-color-patterns.test.ts +71 -0
- package/src/theme/ColorPalettes.stories.tsx +874 -0
- package/src/theme/ColorPrimitives.stories.tsx +348 -0
- package/src/theme/Depth.stories.tsx +312 -0
- package/src/theme/DepthCalibration.stories.tsx +655 -0
- package/src/theme/ThemeProvider.tsx +1 -1
- package/src/theme/barrel.ts +7 -14
- package/src/theme/color-stories.coverage.test.ts +125 -0
- package/src/theme/color-story-kit.tsx +170 -0
- package/src/theme/color-utils.ts +197 -0
- package/src/theme/config.ts +0 -8
- package/src/theme/depth-calibration.test.tsx +97 -0
- package/src/theme/elevation.test.ts +62 -1
- package/src/theme/elevation.ts +34 -8
- package/src/theme/global.css +47 -53
- package/src/theme/grey-ramp.test.ts +166 -0
- package/src/theme/materials.test.ts +106 -0
- package/src/theme/materials.ts +211 -0
- package/src/theme/presets/audiobook.ts +4 -4
- package/src/theme/presets/types.ts +3 -3
- package/src/theme/semantic-contrast.test.ts +83 -0
- package/src/theme/tokens/primitives.ts +62 -115
- package/src/theme/tokens/semantic.ts +130 -82
- package/src/utils/colors.ts +1 -1
- package/tailwind.config.js +2 -5
- 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 -508
- package/src/theme/Colors.stories.tsx +0 -323
- package/src/theme/shadows.stories.tsx +0 -523
- package/src/theme/shadows.ts +0 -439
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
2
|
import { View, type ViewProps } from 'react-native'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { greyRamp } from '../../../theme/tokens/primitives'
|
|
4
|
+
import { insetWell } from '../../../theme/materials'
|
|
5
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
5
6
|
import { Surface } from '../../ui/surface'
|
|
6
7
|
import { ExerciseCardHeading } from './ExerciseCardHeading'
|
|
8
|
+
import { ExerciseCard } from './ExerciseCard'
|
|
9
|
+
import type { SetRowProps } from './SetRow'
|
|
7
10
|
import { SessionHeader } from './SessionHeader'
|
|
8
11
|
import type { MetricTileData } from './MetricTiles'
|
|
9
12
|
import type { SetStripSet } from './SetStrip'
|
|
@@ -15,7 +18,9 @@ import type { ExerciseIndicatorKind } from './ExerciseIndicator'
|
|
|
15
18
|
// recessed via its inner shadow yet paler than the header, so the transparent
|
|
16
19
|
// exercise headings on it never blend into the header plane. Surface owns both backgrounds,
|
|
17
20
|
// so the rail no longer hand-sets them.
|
|
18
|
-
const
|
|
21
|
+
const SEMANTIC = getSemanticColors('dark')
|
|
22
|
+
/** Row divider — one step up from the well it sits in so the line reads. */
|
|
23
|
+
const DIVIDER = greyRamp[875]
|
|
19
24
|
|
|
20
25
|
const DEFAULT_WIDTH = 246
|
|
21
26
|
|
|
@@ -38,6 +43,12 @@ export interface SessionRailExercise {
|
|
|
38
43
|
setStates: SetStripSet[]
|
|
39
44
|
/** Dim the row as a not-yet-reached exercise. */
|
|
40
45
|
upcoming?: boolean
|
|
46
|
+
/**
|
|
47
|
+
* Per-set rows for the expanded body. Only read when this row is the one named
|
|
48
|
+
* by `expandedIndex` — a rail row without them can never expand, which is the
|
|
49
|
+
* correct behaviour for an exercise whose sets aren't loaded.
|
|
50
|
+
*/
|
|
51
|
+
sets?: SetRowProps[]
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
export interface SessionRailProps extends ViewProps {
|
|
@@ -60,6 +71,16 @@ export interface SessionRailProps extends ViewProps {
|
|
|
60
71
|
stripHeight?: number
|
|
61
72
|
/** Rail width in px. Default 246. */
|
|
62
73
|
width?: number
|
|
74
|
+
/**
|
|
75
|
+
* Index of the exercise to render EXPANDED in place — its set table opens
|
|
76
|
+
* inside the rail rather than in a separate pane. The row still draws the same
|
|
77
|
+
* `ExerciseCardHeading` (an `ExerciseCard` composes one), so an expanded row
|
|
78
|
+
* differs from a collapsed one only by the revealed body.
|
|
79
|
+
*
|
|
80
|
+
* Ignored when the named exercise has no `sets`. Omit for an all-collapsed
|
|
81
|
+
* rail, which is the prior behaviour.
|
|
82
|
+
*/
|
|
83
|
+
expandedIndex?: number
|
|
63
84
|
onExercisePress?: (exercise: SessionRailExercise, index: number) => void
|
|
64
85
|
className?: string
|
|
65
86
|
}
|
|
@@ -81,6 +102,7 @@ export function SessionRail({
|
|
|
81
102
|
next,
|
|
82
103
|
stripHeight = 8,
|
|
83
104
|
width = DEFAULT_WIDTH,
|
|
105
|
+
expandedIndex,
|
|
84
106
|
onExercisePress,
|
|
85
107
|
className,
|
|
86
108
|
style,
|
|
@@ -105,9 +127,14 @@ export function SessionRail({
|
|
|
105
127
|
next={next}
|
|
106
128
|
/>
|
|
107
129
|
|
|
130
|
+
{/* The list is a WELL cut into the rail: one plane down from the rail's
|
|
131
|
+
`elevated`, recessed by the shared inset material rather than by a
|
|
132
|
+
neumorphic pressed shadow (which had no room to read at this
|
|
133
|
+
lightness). `level` follows the well's tone so on-surface text
|
|
134
|
+
resolves against what is actually painted. */}
|
|
108
135
|
<Surface
|
|
109
|
-
level="
|
|
110
|
-
style={[{ flex: 1 },
|
|
136
|
+
level="base"
|
|
137
|
+
style={[{ flex: 1 }, insetWell(SEMANTIC['surface-base'])]}
|
|
111
138
|
testID="session-rail-list"
|
|
112
139
|
>
|
|
113
140
|
{exercises.map((ex, i) => (
|
|
@@ -118,19 +145,37 @@ export function SessionRail({
|
|
|
118
145
|
borderBottomColor: DIVIDER,
|
|
119
146
|
}}
|
|
120
147
|
>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
148
|
+
{i === expandedIndex && ex.sets ? (
|
|
149
|
+
<ExerciseCard
|
|
150
|
+
name={ex.name}
|
|
151
|
+
expanded
|
|
152
|
+
summary={{
|
|
153
|
+
sets: ex.summary.sets,
|
|
154
|
+
reps: ex.summary.reps,
|
|
155
|
+
// ExerciseCard's summary takes a numeric load; a string load is
|
|
156
|
+
// an unset/discovery weight, which has no expanded form yet.
|
|
157
|
+
weight: typeof ex.summary.weight === 'number' ? ex.summary.weight : 0,
|
|
158
|
+
unit: ex.summary.unit,
|
|
159
|
+
}}
|
|
160
|
+
tempo={ex.tempo}
|
|
161
|
+
sets={ex.sets}
|
|
162
|
+
onExpandedChange={() => onExercisePress?.(ex, i)}
|
|
163
|
+
/>
|
|
164
|
+
) : (
|
|
165
|
+
<ExerciseCardHeading
|
|
166
|
+
name={ex.name}
|
|
167
|
+
sets={ex.summary.sets}
|
|
168
|
+
reps={ex.summary.reps}
|
|
169
|
+
load={ex.summary.weight}
|
|
170
|
+
unit={ex.summary.unit}
|
|
171
|
+
tempo={ex.tempo}
|
|
172
|
+
indicator={ex.indicator}
|
|
173
|
+
setStates={ex.setStates}
|
|
174
|
+
stripHeight={stripHeight}
|
|
175
|
+
dimmed={ex.upcoming}
|
|
176
|
+
onPress={() => onExercisePress?.(ex, i)}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
134
179
|
</View>
|
|
135
180
|
))}
|
|
136
181
|
</Surface>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
-
import {
|
|
2
|
+
import { primitiveRamps, greyRamp } from '../../../theme/tokens/primitives'
|
|
3
3
|
import { SegmentedBar, type SegmentedBarSegment } from './SegmentedBar'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -13,8 +13,8 @@ export const SET_STRIP_ZONES = {
|
|
|
13
13
|
fastest: primitiveRamps.green[300],
|
|
14
14
|
} as const
|
|
15
15
|
|
|
16
|
-
/** Grey fill for planned-but-unperformed reps / upcoming sets (
|
|
17
|
-
const TODO_COLOR =
|
|
16
|
+
/** Grey fill for planned-but-unperformed reps / upcoming sets (grey placeholder). */
|
|
17
|
+
const TODO_COLOR = greyRamp[900]
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* The learned "variable / unknown / opportunity" semantic (cyan-900, a real ramp
|
|
@@ -3,10 +3,13 @@ import { render, screen } from '@testing-library/react'
|
|
|
3
3
|
import { axe } from 'jest-axe'
|
|
4
4
|
import { View } from 'react-native'
|
|
5
5
|
import { SetRow, type SetRowProps } from './SetRow'
|
|
6
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
6
7
|
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
// Text treatments, read from the tokens rather than pinned as hexes. These
|
|
9
|
+
// used to be hand-copied literals and silently desynced when the greys moved
|
|
10
|
+
// to the warm ramp (TD-07.14). The assertion still bites — it checks the
|
|
11
|
+
// component reaches for the right ROLE — it just no longer restates the value.
|
|
12
|
+
const { 'text-primary': T_ACTIVE, 'text-secondary': T_MUTED } = getSemanticColors('dark')
|
|
10
13
|
|
|
11
14
|
const doneRow: SetRowProps = {
|
|
12
15
|
state: 'done',
|
|
@@ -55,9 +58,9 @@ describe('SetRow', () => {
|
|
|
55
58
|
expect(screen.getByText('8')).toHaveStyle({ color: T_MUTED })
|
|
56
59
|
})
|
|
57
60
|
|
|
58
|
-
it('renders
|
|
61
|
+
it('renders the flat compact strip (done reps, no todo stubs)', () => {
|
|
59
62
|
render(<SetRow {...doneRow} />)
|
|
60
|
-
expect(screen.getByTestId('velocity-strip-
|
|
63
|
+
expect(screen.getByTestId('velocity-strip-compact')).toBeInTheDocument()
|
|
61
64
|
expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(3)
|
|
62
65
|
expect(screen.queryByTestId('velocity-slot-todo')).not.toBeInTheDocument()
|
|
63
66
|
})
|
|
@@ -76,9 +79,9 @@ describe('SetRow', () => {
|
|
|
76
79
|
expect(screen.getByText('2')).toHaveStyle({ color: T_ACTIVE })
|
|
77
80
|
})
|
|
78
81
|
|
|
79
|
-
it('renders the
|
|
82
|
+
it('renders the velocity-height spotlight strip', () => {
|
|
80
83
|
render(<SetRow {...liveRow} />)
|
|
81
|
-
expect(screen.getByTestId('velocity-strip-
|
|
84
|
+
expect(screen.getByTestId('velocity-strip-spotlight')).toBeInTheDocument()
|
|
82
85
|
// 2 performed reps + grey stubs to the target (10).
|
|
83
86
|
expect(screen.getAllByTestId(/^velocity-bar-\d+$/)).toHaveLength(2)
|
|
84
87
|
expect(screen.getAllByTestId('velocity-slot-todo')).toHaveLength(8)
|
|
@@ -93,10 +96,10 @@ describe('SetRow', () => {
|
|
|
93
96
|
expect(screen.getByTestId('set-row-rpe')).toHaveTextContent('—')
|
|
94
97
|
})
|
|
95
98
|
|
|
96
|
-
it('is muted and renders an all-grey
|
|
99
|
+
it('is muted and renders an all-grey compact stub strip', () => {
|
|
97
100
|
render(<SetRow {...todoRow} />)
|
|
98
101
|
expect(screen.getByText('3')).toHaveStyle({ color: T_MUTED })
|
|
99
|
-
expect(screen.getByTestId('velocity-strip-
|
|
102
|
+
expect(screen.getByTestId('velocity-strip-compact')).toBeInTheDocument()
|
|
100
103
|
expect(screen.getAllByTestId('velocity-slot-todo')).toHaveLength(10)
|
|
101
104
|
expect(screen.queryByTestId(/^velocity-bar-\d+$/)).not.toBeInTheDocument()
|
|
102
105
|
})
|
|
@@ -120,7 +120,7 @@ function Cell({
|
|
|
120
120
|
)
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
/** The per-row velocity strip:
|
|
123
|
+
/** The per-row velocity strip: the velocity-height `expanded` spotlight for the live set, the flat `compact` strip otherwise. */
|
|
124
124
|
function RowStrip({ set }: { set: SetRowProps }) {
|
|
125
125
|
const zones = set.velocityZones
|
|
126
126
|
if (set.state === 'live') {
|
|
@@ -133,15 +133,22 @@ function RowStrip({ set }: { set: SetRowProps }) {
|
|
|
133
133
|
showInfo={false}
|
|
134
134
|
height={24}
|
|
135
135
|
scale="fixed"
|
|
136
|
+
// Grow the newest rep from the baseline as it lands (the live-set spotlight).
|
|
137
|
+
liveRepIndex={set.velocities.length - 1}
|
|
136
138
|
set={{ type: 'straight', velocities: set.velocities, planned: set.target.reps }}
|
|
137
139
|
zones={zones}
|
|
138
140
|
/>
|
|
139
141
|
)
|
|
140
142
|
}
|
|
141
143
|
const velocities = set.state === 'done' ? set.velocities : []
|
|
144
|
+
// Resting (done / todo) → the flat `compact` strip at the 8px SegmentedBar flat-bar language (the
|
|
145
|
+
// static compact bar fills its plot), so the resting per-rep strip matches the set-level segmented
|
|
146
|
+
// bars used elsewhere; the live spotlight above stays taller (24px).
|
|
142
147
|
return (
|
|
143
148
|
<VelocityStrip
|
|
144
|
-
variant="
|
|
149
|
+
variant="compact"
|
|
150
|
+
height={8}
|
|
151
|
+
hideBaseline
|
|
145
152
|
set={{ type: 'straight', velocities, planned: plannedReps(set) }}
|
|
146
153
|
zones={zones}
|
|
147
154
|
/>
|
|
@@ -1,6 +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 { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
4
|
+
import { greyRamp } from '../../../theme/tokens/primitives'
|
|
4
5
|
|
|
5
6
|
const t = getSemanticColors('dark')
|
|
6
7
|
|
|
@@ -28,7 +29,7 @@ const solidVariantColors: Record<string, string> = {
|
|
|
28
29
|
success: t['status-success'],
|
|
29
30
|
warning: t['status-warning'],
|
|
30
31
|
error: t['status-error'],
|
|
31
|
-
neutral:
|
|
32
|
+
neutral: greyRamp[500],
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
const ringVariantStyles: Record<string, Record<string, unknown>> = {
|
|
@@ -71,10 +72,10 @@ const iconColors: Record<StatusDotVariant, string> = {
|
|
|
71
72
|
success: '#0A5C52',
|
|
72
73
|
warning: '#6B4000',
|
|
73
74
|
error: '#5C1A1A',
|
|
74
|
-
neutral:
|
|
75
|
+
neutral: greyRamp[200],
|
|
75
76
|
'on-track': t['status-success'],
|
|
76
77
|
deviation: t['status-warning'],
|
|
77
|
-
future:
|
|
78
|
+
future: greyRamp[500],
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
const sizeDimensions = {
|
|
@@ -146,7 +147,7 @@ export function StatusDot({
|
|
|
146
147
|
>
|
|
147
148
|
<View accessibilityElementsHidden>{dot}</View>
|
|
148
149
|
<Text
|
|
149
|
-
style={{ fontSize: 12, lineHeight: 16, color:
|
|
150
|
+
style={{ fontSize: 12, lineHeight: 16, color: greyRamp[400] }}
|
|
150
151
|
accessibilityElementsHidden
|
|
151
152
|
>
|
|
152
153
|
{label}
|
|
@@ -477,7 +477,7 @@ export function StrengthTrendChart({
|
|
|
477
477
|
<View
|
|
478
478
|
testID="strength-trend-chart-tooltip"
|
|
479
479
|
accessibilityElementsHidden
|
|
480
|
-
className="bg-surface-elevated border-
|
|
480
|
+
className="bg-surface-elevated border-hairline-strong"
|
|
481
481
|
style={{
|
|
482
482
|
position: 'absolute',
|
|
483
483
|
left: Math.max(
|
|
@@ -114,7 +114,7 @@ function SummaryCards({ summary }: { summary: TrainingStatusSummary }) {
|
|
|
114
114
|
return (
|
|
115
115
|
<View
|
|
116
116
|
key={key}
|
|
117
|
-
className="bg-surface-elevated border-
|
|
117
|
+
className="bg-surface-elevated border-hairline"
|
|
118
118
|
style={{
|
|
119
119
|
flexGrow: 1,
|
|
120
120
|
flexBasis: '46%',
|
|
@@ -265,7 +265,7 @@ export function TrainingStatusPage({
|
|
|
265
265
|
<SummaryCards summary={summary} />
|
|
266
266
|
|
|
267
267
|
<View
|
|
268
|
-
className="bg-surface-elevated border-
|
|
268
|
+
className="bg-surface-elevated border-hairline"
|
|
269
269
|
style={{
|
|
270
270
|
borderWidth: 1,
|
|
271
271
|
borderRadius: 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
|
+
}
|