@titan-design/react-ui 0.6.0 → 0.7.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 +12 -7
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +12 -7
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +855 -124
- package/dist/index.d.ts +855 -124
- package/dist/index.js +2276 -801
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2230 -802
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-MO0JCySL.d.mts → pages-8u_4WbL-.d.mts} +280 -57
- package/dist/{pages-D7Jlssvp.d.ts → pages-DaWiBOGa.d.ts} +280 -57
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +1255 -798
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1255 -798
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +16 -82
- package/dist/theme/index.d.ts +16 -82
- package/dist/theme/index.js +13 -115
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +13 -115
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +14 -6
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +14 -6
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +1 -1
- package/src/arch/Architecture.stories.tsx +870 -0
- package/src/arch/annotations.json +48 -0
- package/src/arch/arch-graph.json +4301 -0
- package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
- package/src/components/custom/Metric/Metric.stories.tsx +1 -1
- package/src/components/custom/Scatter/Scatter.stories.tsx +1 -1
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
- package/src/components/custom/Table/Table.stories.tsx +1 -1
- package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +62 -0
- package/src/components/custom/TimerReadout/TimerReadout.test.tsx +72 -0
- package/src/components/custom/TimerReadout/TimerReadout.tsx +75 -0
- package/src/components/custom/TimerReadout/index.ts +1 -0
- package/src/components/custom/Treemap/Treemap.stories.tsx +1 -1
- package/src/components/custom/Typography/Typography.stories.tsx +1 -1
- package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +21 -13
- package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +16 -13
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +9 -15
- package/src/components/custom/Workout/BaseBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/BodyMap.stories.tsx +1 -1
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
- package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
- package/src/components/custom/Workout/ExerciseCard.stories.tsx +51 -66
- package/src/components/custom/Workout/ExerciseCard.test.tsx +146 -143
- package/src/components/custom/Workout/ExerciseCard.tsx +176 -198
- package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +99 -0
- package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +72 -0
- package/src/components/custom/Workout/ExerciseCardHeading.tsx +43 -0
- package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +151 -0
- package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +5 -3
- package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +4 -4
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -5
- package/src/components/custom/Workout/ExerciseHeading.stories.tsx +81 -0
- package/src/components/custom/Workout/ExerciseHeading.test.tsx +77 -0
- package/src/components/custom/Workout/ExerciseHeading.tsx +85 -0
- package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +110 -0
- package/src/components/custom/Workout/ExerciseIndicator.test.tsx +93 -0
- package/src/components/custom/Workout/ExerciseIndicator.tsx +120 -0
- package/src/components/custom/Workout/FatigueMeter.stories.tsx +51 -0
- package/src/components/custom/Workout/FatigueMeter.test.tsx +73 -0
- package/src/components/custom/Workout/FatigueMeter.tsx +82 -0
- package/src/components/custom/Workout/InputBar.stories.tsx +1 -1
- package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
- package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +153 -0
- package/src/components/custom/Workout/LiveAuraFrame.test.tsx +74 -0
- package/src/components/custom/Workout/LiveAuraFrame.tsx +105 -0
- package/src/components/custom/Workout/MesoCard.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
- package/src/components/custom/Workout/MetricTiles.stories.tsx +60 -0
- package/src/components/custom/Workout/MetricTiles.test.tsx +58 -0
- package/src/components/custom/Workout/MetricTiles.tsx +48 -0
- package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +1 -1
- package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
- package/src/components/custom/Workout/PrBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
- package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -6
- package/src/components/custom/Workout/README.md +146 -4
- package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
- package/src/components/custom/Workout/RestTimer.stories.tsx +1 -1
- package/src/components/custom/Workout/RestTimer.tsx +15 -10
- package/src/components/custom/Workout/RowInventory.stories.tsx +405 -0
- package/src/components/custom/Workout/S3FullRail.stories.tsx +34 -0
- package/src/components/custom/Workout/S3SessionPace.stories.tsx +95 -0
- package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +61 -0
- package/src/components/custom/Workout/S3SetTypes.stories.tsx +176 -0
- package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +240 -0
- package/src/components/custom/Workout/ScheduleTiles.stories.tsx +40 -0
- package/src/components/custom/Workout/ScheduleTiles.test.tsx +34 -0
- package/src/components/custom/Workout/ScheduleTiles.tsx +70 -0
- package/src/components/custom/Workout/SegmentedBar.stories.tsx +99 -0
- package/src/components/custom/Workout/SegmentedBar.test.tsx +77 -0
- package/src/components/custom/Workout/SegmentedBar.tsx +172 -0
- package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +50 -0
- package/src/components/custom/Workout/SegmentedProgressBar.test.tsx +80 -0
- package/src/components/custom/Workout/SegmentedProgressBar.tsx +67 -0
- package/src/components/custom/Workout/SessionHeader.stories.tsx +121 -0
- package/src/components/custom/Workout/SessionHeader.test.tsx +122 -0
- package/src/components/custom/Workout/SessionHeader.tsx +152 -0
- package/src/components/custom/Workout/SessionRail.stories.tsx +153 -0
- package/src/components/custom/Workout/SessionRail.test.tsx +86 -0
- package/src/components/custom/Workout/SessionRail.tsx +126 -0
- package/src/components/custom/Workout/SetBar.stories.tsx +124 -0
- package/src/components/custom/Workout/SetBar.test.tsx +123 -0
- package/src/components/custom/Workout/SetBar.tsx +171 -0
- package/src/components/custom/Workout/SetRow.stories.tsx +50 -100
- package/src/components/custom/Workout/SetRow.test.tsx +94 -166
- package/src/components/custom/Workout/SetRow.tsx +169 -232
- package/src/components/custom/Workout/SetStrip.stories.tsx +154 -0
- package/src/components/custom/Workout/SetStrip.test.tsx +109 -0
- package/src/components/custom/Workout/SetStrip.tsx +71 -0
- package/src/components/custom/Workout/SetTableHeader.stories.tsx +66 -0
- package/src/components/custom/Workout/SetTableHeader.test.tsx +47 -0
- package/src/components/custom/Workout/SetTableHeader.tsx +88 -0
- package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +47 -0
- package/src/components/custom/Workout/SetsRepsLoad.test.tsx +44 -0
- package/src/components/custom/Workout/SetsRepsLoad.tsx +52 -0
- package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
- package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
- package/src/components/custom/Workout/StatusPill.stories.tsx +59 -0
- package/src/components/custom/Workout/StatusPill.test.tsx +63 -0
- package/src/components/custom/Workout/StatusPill.tsx +88 -0
- package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +1 -1
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
- package/src/components/custom/Workout/TempoBar.stories.tsx +1 -1
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +12 -1
- package/src/components/custom/Workout/TempoDisplay.tsx +23 -33
- package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +1 -1
- package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +254 -0
- package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +145 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +129 -66
- package/src/components/custom/Workout/VelocityStrip.test.tsx +376 -141
- package/src/components/custom/Workout/VelocityStrip.tsx +508 -120
- package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +73 -0
- package/src/components/custom/Workout/VolumeLandmarkBar.test.tsx +119 -0
- package/src/components/custom/Workout/VolumeLandmarkBar.tsx +154 -0
- package/src/components/custom/Workout/WeekRow.stories.tsx +1 -1
- package/src/components/custom/Workout/WeightBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/WorkoutCard.stories.tsx +2 -6
- package/src/components/custom/Workout/WorkoutCard.test.tsx +13 -46
- package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
- package/src/components/custom/Workout/ZoneTrack.stories.tsx +140 -0
- package/src/components/custom/Workout/ZoneTrack.test.tsx +200 -0
- package/src/components/custom/Workout/ZoneTrack.tsx +315 -0
- package/src/components/custom/Workout/index.ts +55 -2
- package/src/components/custom/Workout/metricText.stories.tsx +69 -0
- package/src/components/custom/Workout/metricText.tsx +34 -0
- package/src/components/custom/Workout/paceTone.test.ts +29 -0
- package/src/components/custom/Workout/paceTone.ts +27 -0
- package/src/components/custom/Workout/setHeadingKit.tsx +177 -0
- package/src/components/custom/index.ts +1 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
- package/src/components/icons/icons.stories.tsx +67 -2
- package/src/components/icons/icons.test.tsx +32 -2
- package/src/components/icons/icons.tsx +108 -0
- package/src/components/shell/BrandLockup.stories.tsx +3 -3
- package/src/components/shell/DashboardShell.stories.tsx +65 -0
- package/src/components/shell/DashboardShell.test.tsx +42 -0
- package/src/components/shell/DashboardShell.tsx +74 -0
- package/src/components/shell/DeviceIndicator.stories.tsx +2 -2
- package/src/components/shell/DeviceMenu.stories.tsx +5 -5
- package/src/components/shell/DeviceRow.stories.tsx +3 -3
- package/src/components/shell/NavItem.stories.tsx +52 -0
- package/src/components/shell/NavItem.test.tsx +35 -0
- package/src/components/shell/NavItem.tsx +75 -0
- package/src/components/shell/README.md +30 -0
- package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
- package/src/components/shell/SessionStatePill.stories.tsx +3 -3
- package/src/components/shell/SessionStatePill.tsx +1 -1
- package/src/components/shell/SideNav.stories.tsx +81 -0
- package/src/components/shell/SideNav.test.tsx +51 -0
- package/src/components/shell/SideNav.tsx +74 -0
- package/src/components/shell/TopBar.stories.tsx +5 -5
- package/src/components/shell/index.ts +5 -0
- package/src/components/ui/alert/Alert.stories.tsx +1 -1
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
- package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
- package/src/components/ui/badge/Badge.stories.tsx +1 -1
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
- package/src/components/ui/button/Button.stories.tsx +1 -1
- package/src/components/ui/card/Card.stories.tsx +1 -1
- package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
- package/src/components/ui/chip/Chip.stories.tsx +1 -1
- package/src/components/ui/collapse/Collapse.stories.tsx +1 -1
- package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
- package/src/components/ui/divider/Divider.stories.tsx +1 -1
- package/src/components/ui/drawer/Drawer.stories.tsx +1 -1
- package/src/components/ui/form-field/FormField.stories.tsx +1 -1
- package/src/components/ui/help-tip/HelpTip.stories.tsx +1 -1
- package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/indicator/Indicator.stories.tsx +2 -2
- package/src/components/ui/indicator/Indicator.test.tsx +1 -0
- package/src/components/ui/indicator/Indicator.tsx +6 -1
- package/src/components/ui/input/Input.stories.tsx +1 -1
- package/src/components/ui/link/Link.stories.tsx +1 -1
- package/src/components/ui/list-item/ListItem.stories.tsx +1 -1
- package/src/components/ui/menu/Menu.stories.tsx +1 -1
- package/src/components/ui/modal/Modal.stories.tsx +1 -1
- package/src/components/ui/pill/Pill.stories.tsx +1 -1
- package/src/components/ui/popover/Popover.stories.tsx +1 -1
- package/src/components/ui/progress/Progress.stories.tsx +1 -1
- package/src/components/ui/radio/Radio.stories.tsx +1 -1
- package/src/components/ui/section/Section.stories.tsx +1 -1
- package/src/components/ui/select/Select.stories.tsx +1 -1
- package/src/components/ui/skeleton/Skeleton.stories.tsx +1 -1
- package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
- package/src/components/ui/stack/Stack.stories.tsx +1 -1
- package/src/components/ui/surface/Surface.stories.tsx +1 -1
- package/src/components/ui/switch/Switch.stories.tsx +1 -1
- package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
- package/src/components/ui/tile/Tile.stories.tsx +79 -0
- package/src/components/ui/tile/Tile.test.tsx +48 -0
- package/src/components/ui/tile/Tile.tsx +64 -0
- package/src/components/ui/tile/index.ts +2 -0
- package/src/components/ui/toast/Toast.stories.tsx +1 -1
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
- package/src/components/ui/tooltip/Tooltip.stories.tsx +1 -1
- package/src/examples/react-hook-form/ReactHookForm.stories.tsx +1 -1
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useTimer.test.ts +150 -0
- package/src/hooks/useTimer.ts +77 -0
- package/src/index.ts +3 -0
- package/src/pages.ts +1 -1
- package/src/stories/PresetShowcase.stories.tsx +1 -1
- package/src/stories/ThemePresets.stories.tsx +1 -1
- package/src/theme/ColorSystem.stories.tsx +1 -1
- package/src/theme/Colors.stories.tsx +5 -8
- package/src/theme/config.ts +4 -0
- package/src/theme/elevation.stories.tsx +1 -1
- package/src/theme/global.css +5 -1
- package/src/theme/shadows.stories.tsx +1 -1
- package/src/theme/tokens/primitives.ts +11 -107
- package/src/theme/tokens/semantic.ts +8 -0
- package/src/theme/workout-tokens.ts +11 -7
- package/tailwind.config.js +5 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3 · Session Rail — SPECIMEN (rapid-prototype, not yet a hardened component).
|
|
3
|
+
*
|
|
4
|
+
* A faithful React port of the R2/C3 converged rail
|
|
5
|
+
* (coordination/design-explorations/drilldown-pass/r2-synthesis/gallery.html), brought into
|
|
6
|
+
* titan Storybook so we can iterate with the token system + real titan components + side-by-side.
|
|
7
|
+
* Raw hex lives here on purpose — it mirrors the gallery 1:1; tokenization happens at harden.
|
|
8
|
+
*
|
|
9
|
+
* Stories:
|
|
10
|
+
* - R2Baseline — the converged rail exactly as designed (live-session context)
|
|
11
|
+
* - ContextSwap — the same rail across live / historical / idle (NAV-D02)
|
|
12
|
+
* - Directions — DIRECTION A (this faithful rail) vs DIRECTION B (real titan ExerciseCard)
|
|
13
|
+
* rendered at the real rail width, to ground the "what is a rail item?" call.
|
|
14
|
+
*/
|
|
15
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
16
|
+
import { useState } from 'react'
|
|
17
|
+
import { ExerciseCard, type ExerciseCardProps } from '../custom/Workout/ExerciseCard'
|
|
18
|
+
import type { SetRowProps } from '../custom/Workout/SetRow'
|
|
19
|
+
|
|
20
|
+
// ----------------------------------------------------------------------------- specimen data
|
|
21
|
+
// Transcribed from the r2-synthesis gallery `EX` model (cable pull/push session, "Pull A").
|
|
22
|
+
type SpecSet = { reps: number; meanV: number; loss: number; live?: boolean }
|
|
23
|
+
type SpecExercise = {
|
|
24
|
+
id: string
|
|
25
|
+
name: string
|
|
26
|
+
wt: number
|
|
27
|
+
e1rm: number
|
|
28
|
+
pr?: boolean
|
|
29
|
+
cat: string
|
|
30
|
+
sets: SpecSet[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SESSION: SpecExercise[] = [
|
|
34
|
+
{
|
|
35
|
+
id: 'cable-row',
|
|
36
|
+
name: 'Seated Cable Row',
|
|
37
|
+
wt: 145,
|
|
38
|
+
e1rm: 205,
|
|
39
|
+
pr: true,
|
|
40
|
+
cat: 'Back',
|
|
41
|
+
sets: [
|
|
42
|
+
{ reps: 8, meanV: 0.56, loss: 11 },
|
|
43
|
+
{ reps: 8, meanV: 0.54, loss: 15 },
|
|
44
|
+
{ reps: 8, meanV: 0.51, loss: 19 },
|
|
45
|
+
{ reps: 8, meanV: 0.49, loss: 0, live: true },
|
|
46
|
+
{ reps: 7, meanV: 0.47, loss: 26 },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'lat-pull',
|
|
51
|
+
name: 'Lat Pulldown',
|
|
52
|
+
wt: 130,
|
|
53
|
+
e1rm: 188,
|
|
54
|
+
cat: 'Back',
|
|
55
|
+
sets: [
|
|
56
|
+
{ reps: 10, meanV: 0.64, loss: 9 },
|
|
57
|
+
{ reps: 10, meanV: 0.61, loss: 14 },
|
|
58
|
+
{ reps: 9, meanV: 0.58, loss: 17 },
|
|
59
|
+
{ reps: 8, meanV: 0.55, loss: 22 },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'cable-press',
|
|
64
|
+
name: 'Cable Chest Press',
|
|
65
|
+
wt: 90,
|
|
66
|
+
e1rm: 150,
|
|
67
|
+
cat: 'Chest',
|
|
68
|
+
sets: [
|
|
69
|
+
{ reps: 10, meanV: 0.58, loss: 10 },
|
|
70
|
+
{ reps: 9, meanV: 0.55, loss: 15 },
|
|
71
|
+
{ reps: 8, meanV: 0.51, loss: 20 },
|
|
72
|
+
{ reps: 8, meanV: 0.48, loss: 24 },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'cable-fly',
|
|
77
|
+
name: 'Cable Fly',
|
|
78
|
+
wt: 45,
|
|
79
|
+
e1rm: 78,
|
|
80
|
+
cat: 'Chest',
|
|
81
|
+
sets: [
|
|
82
|
+
{ reps: 12, meanV: 0.74, loss: 8 },
|
|
83
|
+
{ reps: 12, meanV: 0.71, loss: 12 },
|
|
84
|
+
{ reps: 11, meanV: 0.68, loss: 16 },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'face-pull',
|
|
89
|
+
name: 'Face Pull',
|
|
90
|
+
wt: 35,
|
|
91
|
+
e1rm: 60,
|
|
92
|
+
cat: 'Rear Delt',
|
|
93
|
+
sets: [
|
|
94
|
+
{ reps: 15, meanV: 0.78, loss: 6 },
|
|
95
|
+
{ reps: 15, meanV: 0.74, loss: 10 },
|
|
96
|
+
{ reps: 14, meanV: 0.7, loss: 14 },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
const LIVE_EX = 'cable-row'
|
|
102
|
+
const LIVE_SET_IDX = 3
|
|
103
|
+
const LIVE_TARGET = 8
|
|
104
|
+
const LIVE_REP = 5 // current rep of the running set
|
|
105
|
+
|
|
106
|
+
type RailContext = 'live' | 'historical' | 'idle'
|
|
107
|
+
|
|
108
|
+
// ----------------------------------------------------------------------------- helpers (gallery)
|
|
109
|
+
const velColor = (v: number): string =>
|
|
110
|
+
v < 0.5 ? '#ff4757' : v < 0.75 ? '#ffa502' : v < 1.0 ? '#ffd43b' : '#2ed573'
|
|
111
|
+
const f2 = (v: number): string => v.toFixed(2)
|
|
112
|
+
const lossClass = (l: number): string => (l >= 25 ? 'bad' : l >= 18 ? 'warn' : 'ok')
|
|
113
|
+
|
|
114
|
+
// ----------------------------------------------------------------------------- faithful CSS
|
|
115
|
+
// All selectors scoped under `.s3spec` so short class names never leak into the RNW canvas.
|
|
116
|
+
const RAIL_CSS = `
|
|
117
|
+
.s3spec{--brand:#FF7900;--brand-soft:rgba(255,121,0,.14);--spine:#131418;--stage:#0f1012;
|
|
118
|
+
--surface:#17181c;--raised:#1e2025;--border:#24262d;--border-strong:#31343d;
|
|
119
|
+
--hover:rgba(255,255,255,.04);--text:#E8EAED;--muted:#8D95A3;--dim:#5b616d;
|
|
120
|
+
--amber:#FFB020;--sky:#2196F3;--v-green:#2ed573;
|
|
121
|
+
--mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;--ease:cubic-bezier(.22,1,.36,1);
|
|
122
|
+
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;color:var(--text)}
|
|
123
|
+
.s3spec .rail{width:246px;flex:0 0 246px;background:var(--spine);border-right:1px solid var(--border);
|
|
124
|
+
display:flex;flex-direction:column;min-height:0;height:100%}
|
|
125
|
+
.s3spec .railhead{padding:11px 13px 9px;border-bottom:1px solid var(--border)}
|
|
126
|
+
.s3spec .railctx{font-size:9px;font-weight:800;letter-spacing:1px;text-transform:uppercase;color:var(--dim);display:flex;align-items:center;gap:5px}
|
|
127
|
+
.s3spec .railctx .d{width:6px;height:6px;border-radius:50%;background:var(--v-green)}
|
|
128
|
+
.s3spec .railctx.hist .d{background:var(--sky)}
|
|
129
|
+
.s3spec .railtitle{font-size:14px;font-weight:800;margin-top:3px;letter-spacing:-.2px}
|
|
130
|
+
.s3spec .railmeta{font-size:10px;color:var(--muted);margin-top:2px;font-family:var(--mono)}
|
|
131
|
+
.s3spec .railscroll{flex:1;overflow-y:auto;overflow-x:hidden;padding:6px 8px}
|
|
132
|
+
.s3spec .railscroll::-webkit-scrollbar{width:6px}
|
|
133
|
+
.s3spec .railscroll::-webkit-scrollbar-thumb{background:#2a2d34;border-radius:3px}
|
|
134
|
+
.s3spec .rex{border-radius:9px;margin-bottom:3px;border:1px solid transparent;transition:background .15s}
|
|
135
|
+
.s3spec .rex.active{background:var(--brand-soft);border-color:rgba(255,121,0,.28)}
|
|
136
|
+
.s3spec .rexhead{padding:8px 9px;cursor:pointer;display:flex;flex-direction:column;gap:6px;border-radius:9px;transition:background .15s}
|
|
137
|
+
.s3spec .rexhead:hover{background:var(--hover)}
|
|
138
|
+
.s3spec .rextop{display:flex;align-items:center;gap:6px}
|
|
139
|
+
.s3spec .rexidx{font-family:var(--mono);font-size:10px;color:var(--dim);width:13px;flex:0 0 auto}
|
|
140
|
+
.s3spec .rexname{font-size:12px;font-weight:700;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
141
|
+
.s3spec .rextag{font-size:8px;font-family:var(--mono);text-transform:uppercase;letter-spacing:.5px;color:var(--v-green)}
|
|
142
|
+
.s3spec .rexe1{font-family:var(--mono);font-size:10px;color:var(--dim)}
|
|
143
|
+
.s3spec .rexchev{width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid var(--dim);transition:transform .3s var(--ease)}
|
|
144
|
+
.s3spec .rex.open .rexchev{transform:rotate(180deg)}
|
|
145
|
+
.s3spec .rticks{display:flex;gap:3px;padding-left:19px}
|
|
146
|
+
.s3spec .rtick{flex:1;height:15px;border-radius:3px;background:#22252c;position:relative;overflow:hidden;transition:transform .12s}
|
|
147
|
+
.s3spec .rtick.up{background:none;border:1px dashed #3a3d44;opacity:.5}
|
|
148
|
+
.s3spec .rtick.live{box-shadow:0 0 0 2px var(--brand),0 0 10px rgba(255,121,0,.5);animation:s3tickpulse 1.6s infinite}
|
|
149
|
+
@keyframes s3tickpulse{0%,100%{box-shadow:0 0 0 2px var(--brand),0 0 3px rgba(255,121,0,.3)}50%{box-shadow:0 0 0 2px var(--brand),0 0 14px rgba(255,121,0,.7)}}
|
|
150
|
+
.s3spec .rtick .rf{position:absolute;left:0;right:0;bottom:0;border-radius:3px}
|
|
151
|
+
.s3spec .rtlbl{position:absolute;top:1px;left:0;right:0;text-align:center;font-family:var(--mono);font-size:7px;color:rgba(255,255,255,.6);z-index:2}
|
|
152
|
+
.s3spec .rexbody{overflow:hidden;transition:max-height .4s var(--ease),opacity .3s}
|
|
153
|
+
.s3spec .rsrow{display:flex;align-items:center;gap:7px;padding:6px 9px 6px 22px;font-family:var(--mono);font-size:11px;border-radius:7px}
|
|
154
|
+
.s3spec .rsrow .sn{color:var(--muted);width:30px}
|
|
155
|
+
.s3spec .rsrow .sv{font-weight:800}
|
|
156
|
+
.s3spec .rsrow .sl{margin-left:auto;font-size:10px}
|
|
157
|
+
.s3spec .rsrow .sl.ok{color:var(--v-green)}
|
|
158
|
+
.s3spec .rsrow .sl.warn{color:var(--amber)}
|
|
159
|
+
.s3spec .rsrow .sl.bad{color:#ff4757}
|
|
160
|
+
.s3spec .rsrow.livenow{color:var(--v-green)}
|
|
161
|
+
.s3spec .pace{border-top:1px solid var(--border);padding:11px 13px;background:linear-gradient(180deg,#141519,#101113)}
|
|
162
|
+
.s3spec .pacehead{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
|
|
163
|
+
.s3spec .pacetitle{font-size:10px;font-weight:800;letter-spacing:.6px;text-transform:uppercase;color:var(--brand)}
|
|
164
|
+
.s3spec .pacetoggle{font-size:8px;font-family:var(--mono);color:var(--dim);border:1px solid var(--border-strong);border-radius:5px;padding:2px 5px;cursor:pointer;letter-spacing:.3px}
|
|
165
|
+
.s3spec .pacetime{display:flex;align-items:baseline;gap:6px;font-family:var(--mono)}
|
|
166
|
+
.s3spec .pacetime .big{font-size:20px;font-weight:800;letter-spacing:-.5px}
|
|
167
|
+
.s3spec .pacetime .of{font-size:11px;color:var(--muted)}
|
|
168
|
+
.s3spec .pacebar{height:6px;border-radius:3px;background:#22252c;margin:7px 0 3px;overflow:hidden;position:relative}
|
|
169
|
+
.s3spec .pacebar .pf{position:absolute;left:0;top:0;bottom:0;border-radius:3px}
|
|
170
|
+
.s3spec .pacebar .budget{position:absolute;top:-2px;bottom:-2px;width:2px;background:var(--text);opacity:.7}
|
|
171
|
+
.s3spec .pacemetrics{display:flex;gap:4px;margin:8px 0}
|
|
172
|
+
.s3spec .pm{flex:1;text-align:center;background:var(--raised);border-radius:7px;padding:6px 3px}
|
|
173
|
+
.s3spec .pm .k{font-size:8px;text-transform:uppercase;letter-spacing:.4px;color:var(--dim);font-weight:800}
|
|
174
|
+
.s3spec .pm .v{font-family:var(--mono);font-size:13px;font-weight:800;margin-top:2px}
|
|
175
|
+
.s3spec .pacesuggest{font-size:10.5px;line-height:1.4;border-radius:8px;padding:8px 9px;display:flex;gap:7px;align-items:flex-start}
|
|
176
|
+
.s3spec .pacesuggest.behind{background:rgba(255,176,32,.12);border:1px solid rgba(255,176,32,.35);color:var(--amber)}
|
|
177
|
+
.s3spec .pacesuggest.ahead{background:rgba(46,213,115,.1);border:1px solid rgba(46,213,115,.3);color:var(--v-green)}
|
|
178
|
+
.s3spec .pacesuggest .ic{flex:0 0 auto;font-size:12px}
|
|
179
|
+
.s3spec .pacesuggest b{color:var(--text)}
|
|
180
|
+
.s3spec .speccol{display:flex;flex-direction:column;height:100vh}
|
|
181
|
+
/* Inside a flex-COLUMN the rail flex-basis would set HEIGHT; override to fill height, keep 246 width. */
|
|
182
|
+
.s3spec .speccol > .rail{flex:1 1 auto;min-height:0}
|
|
183
|
+
.s3spec .speclabel{font-family:var(--mono);font-size:10px;color:var(--dim);letter-spacing:.4px;padding:6px 0 8px 2px;flex:0 0 auto}
|
|
184
|
+
`
|
|
185
|
+
|
|
186
|
+
// ----------------------------------------------------------------------------- render pieces
|
|
187
|
+
function TickRow({ ex, live }: { ex: SpecExercise; live: boolean }) {
|
|
188
|
+
const isLiveEx = live && ex.id === LIVE_EX
|
|
189
|
+
return (
|
|
190
|
+
<div className="rticks">
|
|
191
|
+
{ex.sets.map((st, s) => {
|
|
192
|
+
if (isLiveEx && s > LIVE_SET_IDX) return <div key={s} className="rtick up" />
|
|
193
|
+
if (isLiveEx && s === LIVE_SET_IDX && st.live) {
|
|
194
|
+
const pct = Math.round((LIVE_REP / LIVE_TARGET) * 100)
|
|
195
|
+
return (
|
|
196
|
+
<div key={s} className="rtick live">
|
|
197
|
+
<span className="rtlbl">
|
|
198
|
+
{LIVE_REP}/{LIVE_TARGET}
|
|
199
|
+
</span>
|
|
200
|
+
<div className="rf" style={{ height: `${pct}%`, background: 'var(--brand)' }} />
|
|
201
|
+
</div>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
return (
|
|
205
|
+
<div key={s} className="rtick">
|
|
206
|
+
<span className="rtlbl">{f2(st.meanV)}</span>
|
|
207
|
+
<div
|
|
208
|
+
className="rf"
|
|
209
|
+
style={{ height: '100%', background: velColor(st.meanV), opacity: 0.85 }}
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
)
|
|
213
|
+
})}
|
|
214
|
+
</div>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function SetRows({ ex, live }: { ex: SpecExercise; live: boolean }) {
|
|
219
|
+
const isLiveEx = live && ex.id === LIVE_EX
|
|
220
|
+
return (
|
|
221
|
+
<>
|
|
222
|
+
{ex.sets.map((s, i) => {
|
|
223
|
+
if (isLiveEx && i > LIVE_SET_IDX)
|
|
224
|
+
return (
|
|
225
|
+
<div key={i} className="rsrow" style={{ opacity: 0.5 }}>
|
|
226
|
+
<span className="sn">S{i + 1}</span>
|
|
227
|
+
<span>upcoming</span>
|
|
228
|
+
</div>
|
|
229
|
+
)
|
|
230
|
+
if (isLiveEx && i === LIVE_SET_IDX && s.live)
|
|
231
|
+
return (
|
|
232
|
+
<div key={i} className="rsrow livenow">
|
|
233
|
+
<span className="sn">S{i + 1}</span>
|
|
234
|
+
<span className="sv">
|
|
235
|
+
{LIVE_REP}/{LIVE_TARGET}
|
|
236
|
+
</span>
|
|
237
|
+
<span className="sl">live</span>
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
return (
|
|
241
|
+
<div key={i} className="rsrow">
|
|
242
|
+
<span className="sn">S{i + 1}</span>
|
|
243
|
+
<span className="sv">{f2(s.meanV)}</span>
|
|
244
|
+
<span style={{ color: 'var(--muted)' }}>
|
|
245
|
+
{s.reps}×{ex.wt}
|
|
246
|
+
</span>
|
|
247
|
+
<span className={`sl ${lossClass(s.loss)}`}>−{s.loss}%</span>
|
|
248
|
+
</div>
|
|
249
|
+
)
|
|
250
|
+
})}
|
|
251
|
+
</>
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function RailItem({
|
|
256
|
+
ex,
|
|
257
|
+
idx,
|
|
258
|
+
live,
|
|
259
|
+
open,
|
|
260
|
+
onToggle,
|
|
261
|
+
}: {
|
|
262
|
+
ex: SpecExercise
|
|
263
|
+
idx: number
|
|
264
|
+
live: boolean
|
|
265
|
+
open: boolean
|
|
266
|
+
onToggle: () => void
|
|
267
|
+
}) {
|
|
268
|
+
const isLiveEx = live && ex.id === LIVE_EX
|
|
269
|
+
const cls = ['rex']
|
|
270
|
+
if (open) cls.push('open')
|
|
271
|
+
if (isLiveEx) cls.push('active')
|
|
272
|
+
return (
|
|
273
|
+
<div className={cls.join(' ')}>
|
|
274
|
+
<div className="rexhead" onClick={onToggle}>
|
|
275
|
+
<div className="rextop">
|
|
276
|
+
<span className="rexidx">{idx + 1}</span>
|
|
277
|
+
<span className="rexname">{ex.name}</span>
|
|
278
|
+
{isLiveEx ? (
|
|
279
|
+
<span className="rextag">live</span>
|
|
280
|
+
) : (
|
|
281
|
+
<span className="rexe1">{ex.e1rm} e1RM</span>
|
|
282
|
+
)}
|
|
283
|
+
<span className="rexchev" />
|
|
284
|
+
</div>
|
|
285
|
+
<TickRow ex={ex} live={live} />
|
|
286
|
+
</div>
|
|
287
|
+
<div className="rexbody" style={{ maxHeight: open ? 400 : 0, opacity: open ? 1 : 0.4 }}>
|
|
288
|
+
<SetRows ex={ex} live={live} />
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function PaceFooter({
|
|
295
|
+
context,
|
|
296
|
+
ahead,
|
|
297
|
+
onToggle,
|
|
298
|
+
}: {
|
|
299
|
+
context: RailContext
|
|
300
|
+
ahead: boolean
|
|
301
|
+
onToggle: () => void
|
|
302
|
+
}) {
|
|
303
|
+
if (context === 'idle') {
|
|
304
|
+
return (
|
|
305
|
+
<div className="pace">
|
|
306
|
+
<div className="pacehead">
|
|
307
|
+
<span className="pacetitle">Next session budget</span>
|
|
308
|
+
</div>
|
|
309
|
+
<div className="pacetime">
|
|
310
|
+
<span className="big">~52:00</span>
|
|
311
|
+
<span className="of">planned · Lower B</span>
|
|
312
|
+
</div>
|
|
313
|
+
<div className="pacesuggest ahead">
|
|
314
|
+
<span className="ic">◷</span>
|
|
315
|
+
<div>6 exercises · ~20 sets. Wall wakes to Live when the first set streams.</div>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
const behind = !ahead
|
|
321
|
+
return (
|
|
322
|
+
<div className="pace">
|
|
323
|
+
<div className="pacehead">
|
|
324
|
+
<span className="pacetitle">Session Pace</span>
|
|
325
|
+
<span className="pacetoggle" onClick={onToggle}>
|
|
326
|
+
{behind ? 'show ahead ▸' : 'show behind ▸'}
|
|
327
|
+
</span>
|
|
328
|
+
</div>
|
|
329
|
+
<div className="pacetime">
|
|
330
|
+
<span className="big">42:18</span>
|
|
331
|
+
<span className="of">of 60:00 budget</span>
|
|
332
|
+
</div>
|
|
333
|
+
<div className="pacebar">
|
|
334
|
+
<div
|
|
335
|
+
className="pf"
|
|
336
|
+
style={{ width: '70%', background: behind ? 'var(--amber)' : 'var(--v-green)' }}
|
|
337
|
+
/>
|
|
338
|
+
<div className="budget" style={{ left: '100%' }} />
|
|
339
|
+
</div>
|
|
340
|
+
<div className="pacemetrics">
|
|
341
|
+
<div className="pm">
|
|
342
|
+
<div className="k">Volume</div>
|
|
343
|
+
<div className="v">76%</div>
|
|
344
|
+
</div>
|
|
345
|
+
<div className="pm">
|
|
346
|
+
<div className="k">Load</div>
|
|
347
|
+
<div className="v">7.3k</div>
|
|
348
|
+
</div>
|
|
349
|
+
<div className="pm">
|
|
350
|
+
<div className="k">Fatigue</div>
|
|
351
|
+
<div className="v" style={{ color: 'var(--amber)' }}>
|
|
352
|
+
MOD
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
{behind ? (
|
|
357
|
+
<div className="pacesuggest behind">
|
|
358
|
+
<span className="ic">▲</span>
|
|
359
|
+
<div>
|
|
360
|
+
<b>Behind pace</b> — ~9 min over budget at this rate. Drop <b>Face Pull</b> to 2 sets to
|
|
361
|
+
finish on time.
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
) : (
|
|
365
|
+
<div className="pacesuggest ahead">
|
|
366
|
+
<span className="ic">+</span>
|
|
367
|
+
<div>
|
|
368
|
+
<b>Ahead of pace</b> — ~7 min of headroom. Room to add a <b>Cable Fly</b> drop-set or a
|
|
369
|
+
back-off set on Row.
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
)}
|
|
373
|
+
</div>
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Shared rail chrome header (context dot + title + meta) — reused by both the adapted rail and the
|
|
378
|
+
// "ExerciseCard exactly" comparison so only the item body differs.
|
|
379
|
+
function RailHeader({ context, meta }: { context: RailContext; meta?: string }) {
|
|
380
|
+
const hist = context === 'historical'
|
|
381
|
+
const ctxLabel =
|
|
382
|
+
context === 'historical'
|
|
383
|
+
? 'Reviewing · session Mar 11'
|
|
384
|
+
: context === 'idle'
|
|
385
|
+
? 'Last session'
|
|
386
|
+
: 'Live session'
|
|
387
|
+
const title = context === 'idle' ? 'Lower B · Volume (next)' : 'Pull A · Intensification'
|
|
388
|
+
const resolvedMeta =
|
|
389
|
+
meta ??
|
|
390
|
+
(context === 'historical'
|
|
391
|
+
? '22 sets · 48 min · complete'
|
|
392
|
+
: context === 'idle'
|
|
393
|
+
? 'ended 3 days ago'
|
|
394
|
+
: 'ex 1/5 · set 4 live')
|
|
395
|
+
return (
|
|
396
|
+
<div className="railhead">
|
|
397
|
+
<div className={`railctx${hist ? ' hist' : ''}`}>
|
|
398
|
+
<span className="d" />
|
|
399
|
+
{ctxLabel}
|
|
400
|
+
</div>
|
|
401
|
+
<div className="railtitle">{title}</div>
|
|
402
|
+
<div className="railmeta">{resolvedMeta}</div>
|
|
403
|
+
</div>
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// PIECE 1 · Live Session Plan — the context header + expandable exercise skeleton (set-tick progress).
|
|
408
|
+
function SessionPlan({ context = 'live' as RailContext }: { context?: RailContext }) {
|
|
409
|
+
const live = context === 'live'
|
|
410
|
+
const [open, setOpen] = useState<Record<string, boolean>>({ 'cable-row': true })
|
|
411
|
+
return (
|
|
412
|
+
<>
|
|
413
|
+
<RailHeader context={context} />
|
|
414
|
+
<div className="railscroll">
|
|
415
|
+
{SESSION.map((ex, i) => (
|
|
416
|
+
<RailItem
|
|
417
|
+
key={ex.id}
|
|
418
|
+
ex={ex}
|
|
419
|
+
idx={i}
|
|
420
|
+
live={live}
|
|
421
|
+
open={!!open[ex.id]}
|
|
422
|
+
onToggle={() => setOpen((o) => ({ ...o, [ex.id]: !o[ex.id] }))}
|
|
423
|
+
/>
|
|
424
|
+
))}
|
|
425
|
+
</div>
|
|
426
|
+
</>
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// The assembled rail = Live Session Plan (piece 1) + Session Pace (piece 2).
|
|
431
|
+
function R2Rail({ context = 'live' as RailContext }: { context?: RailContext }) {
|
|
432
|
+
const [ahead, setAhead] = useState(false)
|
|
433
|
+
return (
|
|
434
|
+
<div className="rail">
|
|
435
|
+
<SessionPlan context={context} />
|
|
436
|
+
<PaceFooter context={context} ahead={ahead} onToggle={() => setAhead((a) => !a)} />
|
|
437
|
+
</div>
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ----------------------------------------------------------------------------- ExerciseCard rail (state mix)
|
|
442
|
+
const perRepVel = (s: SpecSet): number[] =>
|
|
443
|
+
Array.from({ length: s.reps }, (_, r) => +(s.meanV + 0.04 - r * 0.01).toFixed(2))
|
|
444
|
+
|
|
445
|
+
type CardStatus = 'finished' | 'active' | 'upcoming'
|
|
446
|
+
|
|
447
|
+
// index-aligned to SESSION: 2 finished · 1 active · 2 upcoming
|
|
448
|
+
const CARD_MIX: { ex: SpecExercise; status: CardStatus }[] = SESSION.map((ex, i) => ({
|
|
449
|
+
ex,
|
|
450
|
+
status: (i < 2 ? 'finished' : i === 2 ? 'active' : 'upcoming') as CardStatus,
|
|
451
|
+
}))
|
|
452
|
+
|
|
453
|
+
const completedSet = (ex: SpecExercise, i: number, s: SpecSet): SetRowProps => ({
|
|
454
|
+
state: 'done',
|
|
455
|
+
setNumber: i + 1,
|
|
456
|
+
reps: s.reps,
|
|
457
|
+
weight: ex.wt,
|
|
458
|
+
rpe: 7 + i * 0.5,
|
|
459
|
+
unit: 'lbs',
|
|
460
|
+
velocities: perRepVel(s),
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
// The active exercise's 3-set mix: done · LIVE · todo. The live set is now a
|
|
464
|
+
// first-class SetRow state — the compact velocity-height spotlight strip, brightened.
|
|
465
|
+
const activeSets = (ex: SpecExercise): SetRowProps[] => [
|
|
466
|
+
completedSet(ex, 0, ex.sets[0]),
|
|
467
|
+
{
|
|
468
|
+
state: 'live',
|
|
469
|
+
setNumber: 2,
|
|
470
|
+
unit: 'lbs',
|
|
471
|
+
target: { reps: 8, weight: ex.wt },
|
|
472
|
+
reps: 8,
|
|
473
|
+
weight: ex.wt,
|
|
474
|
+
velocities: [0.58, 0.55, 0.53, 0.5, 0.48],
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
state: 'todo',
|
|
478
|
+
setNumber: 3,
|
|
479
|
+
unit: 'lbs',
|
|
480
|
+
target: { reps: 8, weight: ex.wt },
|
|
481
|
+
},
|
|
482
|
+
]
|
|
483
|
+
|
|
484
|
+
// Real ExerciseCard props for an exercise given its status + whether it's open (onToggle added by caller).
|
|
485
|
+
function cardProps(
|
|
486
|
+
ex: SpecExercise,
|
|
487
|
+
status: CardStatus,
|
|
488
|
+
open: boolean
|
|
489
|
+
): Omit<ExerciseCardProps, 'onExpandedChange'> {
|
|
490
|
+
const base = { name: ex.name, isPR: ex.pr }
|
|
491
|
+
const summary = {
|
|
492
|
+
sets: status === 'active' ? 3 : ex.sets.length,
|
|
493
|
+
reps: ex.sets[0].reps,
|
|
494
|
+
weight: ex.wt,
|
|
495
|
+
unit: 'lbs' as const,
|
|
496
|
+
}
|
|
497
|
+
if (status === 'upcoming') {
|
|
498
|
+
// Closed → the dimmed upcoming row; open → an expanded card of planned (todo) sets.
|
|
499
|
+
if (!open) {
|
|
500
|
+
return {
|
|
501
|
+
...base,
|
|
502
|
+
upcoming: true,
|
|
503
|
+
prescription: `${ex.sets.length} × ${ex.sets[0].reps} @ RPE 8`,
|
|
504
|
+
previousBest: `${ex.sets[0].reps} × ${ex.wt} lb`,
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
...base,
|
|
509
|
+
expanded: true,
|
|
510
|
+
summary,
|
|
511
|
+
tempo: [3, 1, 2, 0],
|
|
512
|
+
sets: ex.sets.map((s, i) => ({
|
|
513
|
+
state: 'todo',
|
|
514
|
+
setNumber: i + 1,
|
|
515
|
+
unit: 'lbs',
|
|
516
|
+
target: { reps: s.reps, weight: ex.wt },
|
|
517
|
+
})),
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (open) {
|
|
521
|
+
return {
|
|
522
|
+
...base,
|
|
523
|
+
expanded: true,
|
|
524
|
+
summary,
|
|
525
|
+
tempo: [3, 1, 2, 0],
|
|
526
|
+
sets: status === 'active' ? activeSets(ex) : ex.sets.map((s, i) => completedSet(ex, i, s)),
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const doneSets = status === 'active' ? ex.sets.slice(0, 1) : ex.sets
|
|
530
|
+
return {
|
|
531
|
+
...base,
|
|
532
|
+
expanded: false,
|
|
533
|
+
summary,
|
|
534
|
+
totalPlannedSets: status === 'active' ? 3 : ex.sets.length,
|
|
535
|
+
setVelocities: doneSets.map(perRepVel),
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// The session drawn with the REAL (responsive) ExerciseCard — interactive expand/collapse.
|
|
540
|
+
function ExerciseCardPlan() {
|
|
541
|
+
const [open, setOpen] = useState<Record<string, boolean>>({ [SESSION[2].id]: true })
|
|
542
|
+
return (
|
|
543
|
+
<>
|
|
544
|
+
<RailHeader context="live" meta="ex 3/5 · set 2 live" />
|
|
545
|
+
<div className="railscroll" style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
546
|
+
{CARD_MIX.map(({ ex, status }) => (
|
|
547
|
+
<ExerciseCard
|
|
548
|
+
key={ex.id}
|
|
549
|
+
{...cardProps(ex, status, !!open[ex.id])}
|
|
550
|
+
onExpandedChange={() => setOpen((o) => ({ ...o, [ex.id]: !o[ex.id] }))}
|
|
551
|
+
/>
|
|
552
|
+
))}
|
|
553
|
+
</div>
|
|
554
|
+
</>
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// A proper, standalone ExerciseCard at native width — reference point (interactive).
|
|
559
|
+
function ReferenceCard() {
|
|
560
|
+
const [open, setOpen] = useState(true)
|
|
561
|
+
return (
|
|
562
|
+
<ExerciseCard
|
|
563
|
+
{...cardProps(SESSION[0], 'finished', open)}
|
|
564
|
+
onExpandedChange={() => setOpen((v) => !v)}
|
|
565
|
+
/>
|
|
566
|
+
)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ----------------------------------------------------------------------------- stories
|
|
570
|
+
function Frame({ children }: { children: React.ReactNode }) {
|
|
571
|
+
return (
|
|
572
|
+
<div
|
|
573
|
+
className="s3spec"
|
|
574
|
+
style={{ display: 'flex', height: '100vh', background: 'var(--stage)' }}
|
|
575
|
+
>
|
|
576
|
+
<style>{RAIL_CSS}</style>
|
|
577
|
+
{children}
|
|
578
|
+
</div>
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const meta: Meta = {
|
|
583
|
+
title: 'Lab/Specimens/Session Rail',
|
|
584
|
+
parameters: { layout: 'fullscreen' },
|
|
585
|
+
}
|
|
586
|
+
export default meta
|
|
587
|
+
type Story = StoryObj
|
|
588
|
+
|
|
589
|
+
/** The converged R2/C3 rail, live-session context — the "version we have", now in React. */
|
|
590
|
+
export const R2Baseline: Story = {
|
|
591
|
+
render: () => (
|
|
592
|
+
<Frame>
|
|
593
|
+
<R2Rail context="live" />
|
|
594
|
+
<div
|
|
595
|
+
style={{
|
|
596
|
+
flex: 1,
|
|
597
|
+
display: 'flex',
|
|
598
|
+
alignItems: 'center',
|
|
599
|
+
justifyContent: 'center',
|
|
600
|
+
color: 'var(--dim)',
|
|
601
|
+
fontFamily: 'var(--mono)',
|
|
602
|
+
fontSize: 12,
|
|
603
|
+
}}
|
|
604
|
+
>
|
|
605
|
+
main stage
|
|
606
|
+
</div>
|
|
607
|
+
</Frame>
|
|
608
|
+
),
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** NAV-D02 context-swap: the same rail across live / historical / idle, side by side. */
|
|
612
|
+
export const ContextSwap: Story = {
|
|
613
|
+
render: () => (
|
|
614
|
+
<Frame>
|
|
615
|
+
<div className="speccol">
|
|
616
|
+
<div className="speclabel">live</div>
|
|
617
|
+
<R2Rail context="live" />
|
|
618
|
+
</div>
|
|
619
|
+
<div className="speccol">
|
|
620
|
+
<div className="speclabel">historical (Review)</div>
|
|
621
|
+
<R2Rail context="historical" />
|
|
622
|
+
</div>
|
|
623
|
+
<div className="speccol">
|
|
624
|
+
<div className="speclabel">idle</div>
|
|
625
|
+
<R2Rail context="idle" />
|
|
626
|
+
</div>
|
|
627
|
+
</Frame>
|
|
628
|
+
),
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const RAIL_DELTAS: [string, string][] = [
|
|
632
|
+
[
|
|
633
|
+
'Width / fit ✓ DONE',
|
|
634
|
+
'FIXED in the real component: SetRow fixed columns now flexShrink + PREV minWidth → the 5-col table renders correctly at 246px (was: "8×145" mangled per-char). No-op at 390px. This is the whole "make it reactive" ask.',
|
|
635
|
+
],
|
|
636
|
+
[
|
|
637
|
+
'Tick row',
|
|
638
|
+
'REUSE ExerciseCard’s collapsed strip-row (mini VelocityStrips + PlaceholderStrips) AS the set-tick row — already the same concept (see collapsed items in the left column).',
|
|
639
|
+
],
|
|
640
|
+
[
|
|
641
|
+
'Live set marker',
|
|
642
|
+
'ADD an in-progress tick (pulsing + “5/8” rep count). ExerciseCard only knows completed strips + placeholders — no live state.',
|
|
643
|
+
],
|
|
644
|
+
[
|
|
645
|
+
'Expanded body',
|
|
646
|
+
'OPTIONAL: swap the (now-fitting) 5-col input table for a read-only compact list (mean-con · reps×wt · loss%). Rail is read-only — density preference, no longer a fit blocker.',
|
|
647
|
+
],
|
|
648
|
+
[
|
|
649
|
+
'States',
|
|
650
|
+
'Add context-swap (live/historical/idle) + a “live” header tag + leading index; drop input affordances.',
|
|
651
|
+
],
|
|
652
|
+
]
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* ExerciseCard EXACTLY AS-IS vs ExerciseCard ADAPTED for the rail, both full 246px rails with pace.
|
|
656
|
+
* The middle panel lists the specific deltas. This is the reuse question made concrete.
|
|
657
|
+
*/
|
|
658
|
+
export const Directions: Story = {
|
|
659
|
+
name: 'ExerciseCard · exact vs adapted',
|
|
660
|
+
render: () => (
|
|
661
|
+
<Frame>
|
|
662
|
+
<div className="speccol">
|
|
663
|
+
<div className="speclabel">
|
|
664
|
+
ExerciseCard rail — state mix (2 finished · 1 active · 2 upcoming)
|
|
665
|
+
</div>
|
|
666
|
+
<div className="rail">
|
|
667
|
+
<ExerciseCardPlan />
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
<div className="speccol">
|
|
671
|
+
<div className="speclabel">old rail — adapted item (reference)</div>
|
|
672
|
+
<div className="rail">
|
|
673
|
+
<SessionPlan context="live" />
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
<div className="speccol" style={{ width: 340, padding: '6px 4px 0 14px', overflowY: 'auto' }}>
|
|
677
|
+
<div className="speclabel">reference · real ExerciseCard (native width)</div>
|
|
678
|
+
<div style={{ marginBottom: 22 }}>
|
|
679
|
+
<ReferenceCard />
|
|
680
|
+
</div>
|
|
681
|
+
<div className="speclabel">the rail modifications</div>
|
|
682
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, paddingRight: 8 }}>
|
|
683
|
+
{RAIL_DELTAS.map(([k, v], i) => (
|
|
684
|
+
<div key={i} style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
685
|
+
<span
|
|
686
|
+
style={{
|
|
687
|
+
fontSize: 11,
|
|
688
|
+
fontWeight: 800,
|
|
689
|
+
letterSpacing: 0.4,
|
|
690
|
+
textTransform: 'uppercase',
|
|
691
|
+
color: '#FF7900',
|
|
692
|
+
}}
|
|
693
|
+
>
|
|
694
|
+
{k}
|
|
695
|
+
</span>
|
|
696
|
+
<span style={{ fontSize: 12, lineHeight: 1.45, color: '#B8BEC9' }}>{v}</span>
|
|
697
|
+
</div>
|
|
698
|
+
))}
|
|
699
|
+
</div>
|
|
700
|
+
</div>
|
|
701
|
+
</Frame>
|
|
702
|
+
),
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ============================================================ decomposed pieces (for review)
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* PIECE 1 · LIVE SESSION PLAN — the context header + expandable exercise skeleton with per-set
|
|
709
|
+
* tick progress, reviewed in isolation across live / historical / idle (no pace footer).
|
|
710
|
+
*/
|
|
711
|
+
export const SessionPlanPiece: Story = {
|
|
712
|
+
name: 'Piece · Live Session Plan',
|
|
713
|
+
render: () => (
|
|
714
|
+
<Frame>
|
|
715
|
+
{(['live', 'historical', 'idle'] as RailContext[]).map((ctx) => (
|
|
716
|
+
<div className="speccol" key={ctx}>
|
|
717
|
+
<div className="speclabel">{ctx}</div>
|
|
718
|
+
<div className="rail">
|
|
719
|
+
<SessionPlan context={ctx} />
|
|
720
|
+
</div>
|
|
721
|
+
</div>
|
|
722
|
+
))}
|
|
723
|
+
</Frame>
|
|
724
|
+
),
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* PIECE 2 · SESSION PACE — the net-new pace tile in isolation, all states: behind / ahead / idle.
|
|
729
|
+
* VMCP-primary (NAV-D05): plan-aware trim (behind) / add (ahead); idle → next-session budget.
|
|
730
|
+
*/
|
|
731
|
+
export const SessionPacePiece: Story = {
|
|
732
|
+
name: 'Piece · Session Pace',
|
|
733
|
+
render: () => (
|
|
734
|
+
<Frame>
|
|
735
|
+
{(
|
|
736
|
+
[
|
|
737
|
+
['behind', 'live', false],
|
|
738
|
+
['ahead', 'live', true],
|
|
739
|
+
['idle', 'idle', false],
|
|
740
|
+
] as [string, RailContext, boolean][]
|
|
741
|
+
).map(([label, ctx, ahead]) => (
|
|
742
|
+
<div className="speccol" key={label} style={{ height: 'auto' }}>
|
|
743
|
+
<div className="speclabel">{label}</div>
|
|
744
|
+
<div
|
|
745
|
+
style={{ width: 246, background: 'var(--spine)', border: '1px solid var(--border)' }}
|
|
746
|
+
>
|
|
747
|
+
<PaceFooter context={ctx} ahead={ahead} onToggle={() => {}} />
|
|
748
|
+
</div>
|
|
749
|
+
</div>
|
|
750
|
+
))}
|
|
751
|
+
</Frame>
|
|
752
|
+
),
|
|
753
|
+
}
|