@titan-design/react-ui 0.6.0 → 0.8.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 +956 -128
- package/dist/index.d.ts +956 -128
- package/dist/index.js +2873 -981
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2826 -982
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-MO0JCySL.d.mts → pages-DCFBqp79.d.mts} +289 -57
- package/dist/{pages-D7Jlssvp.d.ts → pages-_fI3-x7Z.d.ts} +289 -57
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +1659 -799
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1660 -800
- 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/CircularTimer/CircularTimer.stories.tsx +90 -0
- package/src/components/custom/CircularTimer/CircularTimer.test.tsx +62 -0
- package/src/components/custom/CircularTimer/CircularTimer.tsx +112 -0
- package/src/components/custom/CircularTimer/index.ts +1 -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 +85 -0
- package/src/components/custom/Workout/FatigueMeter.tsx +86 -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 +193 -4
- package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
- package/src/components/custom/Workout/RestTimer.stories.tsx +133 -3
- package/src/components/custom/Workout/RestTimer.test.tsx +73 -0
- package/src/components/custom/Workout/RestTimer.tsx +155 -59
- 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 +70 -1
- package/src/components/custom/Workout/TempoBar.test.tsx +119 -4
- package/src/components/custom/Workout/TempoBar.tsx +107 -22
- 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 +262 -56
- package/src/components/custom/Workout/VelocityStrip.test.tsx +440 -141
- package/src/components/custom/Workout/VelocityStrip.tsx +776 -141
- 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 +139 -0
- package/src/components/custom/Workout/ZoneTrack.test.tsx +234 -0
- package/src/components/custom/Workout/ZoneTrack.tsx +373 -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 +2 -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 +86 -3
- package/src/components/ui/alert/Alert.test.tsx +52 -0
- package/src/components/ui/alert/Alert.tsx +54 -20
- 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/progress/Progress.tsx +47 -22
- 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,152 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import { View, Text, type ViewProps } from 'react-native'
|
|
3
|
+
import { primitiveColors } from '../../../theme/tokens/primitives'
|
|
4
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
5
|
+
import { Typography } from '../Typography'
|
|
6
|
+
import { TimerReadout } from '../TimerReadout'
|
|
7
|
+
import { SegmentedProgressBar } from './SegmentedProgressBar'
|
|
8
|
+
import { MetricTiles, type MetricTileData } from './MetricTiles'
|
|
9
|
+
import { ScheduleTiles } from './ScheduleTiles'
|
|
10
|
+
import { paceTone, paceToneColor } from './paceTone'
|
|
11
|
+
|
|
12
|
+
const t = getSemanticColors('dark')
|
|
13
|
+
|
|
14
|
+
// The raised charcoal heading plane (charcoal 400) — reads as one flat surface with
|
|
15
|
+
// the nav; depth lives on the sunk list below it, not here.
|
|
16
|
+
const RAISED = primitiveColors.charcoal[400] // #1F1F1F
|
|
17
|
+
|
|
18
|
+
// The chunked pace bar matches the SetStrip language but sits tighter than the default
|
|
19
|
+
// SetStrip gap (5) — the locked design uses a 3px gap and a 9px track.
|
|
20
|
+
const BAR_HEIGHT = 9
|
|
21
|
+
const BAR_GAP = 3
|
|
22
|
+
|
|
23
|
+
export interface SessionHeaderPlanEntry {
|
|
24
|
+
/** Exercise name (currently informational — chunk widths come from `sets`). */
|
|
25
|
+
name?: string
|
|
26
|
+
/** Planned set count — drives this exercise's chunk width (∝ sets). */
|
|
27
|
+
sets: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SessionHeaderProps extends ViewProps {
|
|
31
|
+
/** Session title — the live session's name, or the next session's name when `next` is set. */
|
|
32
|
+
title: string
|
|
33
|
+
/** Exercises in order → chunk widths (∝ sets). */
|
|
34
|
+
plan: SessionHeaderPlanEntry[]
|
|
35
|
+
/** Fractional sets completed (live). 0/undefined leaves the bar empty. */
|
|
36
|
+
setsDone?: number
|
|
37
|
+
/** Live elapsed time in ms — drives the ⏱ readout and the pace marker. */
|
|
38
|
+
elapsedMs?: number
|
|
39
|
+
/** Optional time budget in ms → adds the pace marker and the ` / budget` suffix. Omit → no marker, plain (steel) progress. */
|
|
40
|
+
budgetMs?: number
|
|
41
|
+
/** Elapsed readout goes live-green while true. Default true (live). */
|
|
42
|
+
running?: boolean
|
|
43
|
+
/** Live stat tiles (Volume / Load / Fatigue). Ignored when `next` is set. */
|
|
44
|
+
metrics?: MetricTileData[]
|
|
45
|
+
/** When set → UPCOMING layout: schedule tiles + empty bar; `title` is the next session's name. */
|
|
46
|
+
next?: number | Date
|
|
47
|
+
className?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The session-rail heading: the session title over a stat row and a chunked pace bar.
|
|
52
|
+
* Live, it shows Volume/Load/Fatigue tiles, a per-exercise progress bar (fill ∝ sets
|
|
53
|
+
* done, coloured green/amber against the elapsed-vs-budget pace, with a live marker),
|
|
54
|
+
* a mono `n/total sets` label and a ⏱ elapsed readout. Upcoming (`next` set), the tiles
|
|
55
|
+
* become Date/Time/Until, the bar is the empty plan shape, and the readout shows the
|
|
56
|
+
* planned duration. Rendered on the raised charcoal plane; presentational. Composes
|
|
57
|
+
* {@link MetricTiles} / {@link ScheduleTiles} + {@link SegmentedProgressBar} +
|
|
58
|
+
* {@link TimerReadout}.
|
|
59
|
+
*/
|
|
60
|
+
export function SessionHeader({
|
|
61
|
+
title,
|
|
62
|
+
plan,
|
|
63
|
+
setsDone = 0,
|
|
64
|
+
elapsedMs,
|
|
65
|
+
budgetMs,
|
|
66
|
+
running = true,
|
|
67
|
+
metrics = [],
|
|
68
|
+
next,
|
|
69
|
+
className,
|
|
70
|
+
style,
|
|
71
|
+
...props
|
|
72
|
+
}: SessionHeaderProps) {
|
|
73
|
+
const upcoming = next != null
|
|
74
|
+
const totalSets = plan.reduce((sum, e) => sum + e.sets, 0)
|
|
75
|
+
|
|
76
|
+
const hasPace = !upcoming && budgetMs != null && elapsedMs != null
|
|
77
|
+
const target = hasPace ? (elapsedMs as number) / (budgetMs as number) : undefined
|
|
78
|
+
|
|
79
|
+
const setsLabel = upcoming ? `${totalSets} sets` : `${Math.floor(setsDone)}/${totalSets} sets`
|
|
80
|
+
const setsLabelColor = upcoming
|
|
81
|
+
? t['text-secondary']
|
|
82
|
+
: paceToneColor(paceTone(totalSets > 0 ? setsDone / totalSets : 0, target))
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<View
|
|
86
|
+
className={className}
|
|
87
|
+
style={[
|
|
88
|
+
{
|
|
89
|
+
backgroundColor: RAISED,
|
|
90
|
+
paddingTop: 11,
|
|
91
|
+
paddingRight: 12,
|
|
92
|
+
paddingBottom: 12,
|
|
93
|
+
paddingLeft: 12,
|
|
94
|
+
zIndex: 2,
|
|
95
|
+
},
|
|
96
|
+
style,
|
|
97
|
+
]}
|
|
98
|
+
testID="session-rail-header"
|
|
99
|
+
{...props}
|
|
100
|
+
>
|
|
101
|
+
<Text
|
|
102
|
+
accessibilityRole="header"
|
|
103
|
+
style={{
|
|
104
|
+
fontSize: 16,
|
|
105
|
+
lineHeight: 18,
|
|
106
|
+
fontWeight: '700',
|
|
107
|
+
fontFamily: '"Space Grotesk", sans-serif',
|
|
108
|
+
color: t['text-primary'],
|
|
109
|
+
marginBottom: 10,
|
|
110
|
+
}}
|
|
111
|
+
testID="session-rail-title"
|
|
112
|
+
>
|
|
113
|
+
{title}
|
|
114
|
+
</Text>
|
|
115
|
+
|
|
116
|
+
{upcoming ? <ScheduleTiles when={next} /> : <MetricTiles metrics={metrics} />}
|
|
117
|
+
|
|
118
|
+
<View style={{ marginTop: 10 }}>
|
|
119
|
+
<SegmentedProgressBar
|
|
120
|
+
segments={plan.map((e) => ({ weight: e.sets }))}
|
|
121
|
+
value={upcoming ? 0 : setsDone}
|
|
122
|
+
target={target}
|
|
123
|
+
height={BAR_HEIGHT}
|
|
124
|
+
gap={BAR_GAP}
|
|
125
|
+
/>
|
|
126
|
+
<View
|
|
127
|
+
style={{
|
|
128
|
+
flexDirection: 'row',
|
|
129
|
+
justifyContent: 'space-between',
|
|
130
|
+
alignItems: 'baseline',
|
|
131
|
+
marginTop: 5,
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
<Typography
|
|
135
|
+
variant="mono"
|
|
136
|
+
style={{ fontSize: 10, color: setsLabelColor }}
|
|
137
|
+
testID="session-rail-sets"
|
|
138
|
+
>
|
|
139
|
+
{setsLabel}
|
|
140
|
+
</Typography>
|
|
141
|
+
<TimerReadout
|
|
142
|
+
mode="up"
|
|
143
|
+
elapsedMs={upcoming ? budgetMs : elapsedMs}
|
|
144
|
+
durationMs={budgetMs}
|
|
145
|
+
running={running && !upcoming}
|
|
146
|
+
showTotal={budgetMs != null && !upcoming}
|
|
147
|
+
/>
|
|
148
|
+
</View>
|
|
149
|
+
</View>
|
|
150
|
+
</View>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import { primitiveRamps } from '../../../theme/tokens/primitives'
|
|
4
|
+
import { SessionRail, type SessionRailExercise } from './SessionRail'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `SessionRail` (shell organism) — the live-workout exercise list: a flat raised
|
|
8
|
+
* `SessionHeader` glance (title, stat tiles, chunked pace bar) over a sunk, inset list
|
|
9
|
+
* of ExerciseCard rail headings. The header plan (chunk widths ∝ sets) is derived from
|
|
10
|
+
* `exercises`. Surfaces bind to the charcoal ramp; the list depth is a subtle neumorphic
|
|
11
|
+
* inset. Driven entirely by props.
|
|
12
|
+
*/
|
|
13
|
+
const meta: Meta<typeof SessionRail> = {
|
|
14
|
+
title: 'Shell/SessionRail',
|
|
15
|
+
component: SessionRail,
|
|
16
|
+
tags: ['autodocs'],
|
|
17
|
+
argTypes: {
|
|
18
|
+
stripHeight: { control: { type: 'range', min: 2, max: 16, step: 1 } },
|
|
19
|
+
onExercisePress: { action: 'exercise-press' },
|
|
20
|
+
},
|
|
21
|
+
decorators: [
|
|
22
|
+
(Story) => (
|
|
23
|
+
<View className="min-h-screen flex-row" style={{ backgroundColor: '#101010' }}>
|
|
24
|
+
<Story />
|
|
25
|
+
<View className="flex-1 items-center justify-center">
|
|
26
|
+
<Text style={{ color: '#6B7280', fontSize: 12 }}>main viewport</Text>
|
|
27
|
+
</View>
|
|
28
|
+
</View>
|
|
29
|
+
),
|
|
30
|
+
],
|
|
31
|
+
parameters: {
|
|
32
|
+
layout: 'fullscreen',
|
|
33
|
+
docs: {
|
|
34
|
+
description: {
|
|
35
|
+
component:
|
|
36
|
+
'**Organism** (shell S3). The live-workout exercise list: raised header glance over ' +
|
|
37
|
+
'a sunk inset list, subtle neumorphic depth (charcoal ramp + `neumorphicShadows`). ' +
|
|
38
|
+
'Composes ' +
|
|
39
|
+
'[SessionHeader](?path=/docs/shell-sessionrail-sessionheader--docs) + ' +
|
|
40
|
+
'[ExerciseCardHeading](?path=/docs/workout-exercisecardheading--docs) × N. ' +
|
|
41
|
+
'Sits beside [SideNav](?path=/docs/shell-sidenav--docs) in the dashboard shell.',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default meta
|
|
48
|
+
type Story = StoryObj<typeof SessionRail>
|
|
49
|
+
|
|
50
|
+
const AMBER = primitiveRamps.amber[300]
|
|
51
|
+
|
|
52
|
+
const decay = (n: number, start: number, span = 0.4): number[] =>
|
|
53
|
+
Array.from({ length: n }, (_, r) => +(start - (span * r) / Math.max(1, n - 1)).toFixed(3))
|
|
54
|
+
|
|
55
|
+
const SESSION: SessionRailExercise[] = [
|
|
56
|
+
{
|
|
57
|
+
name: 'Seated Cable Row',
|
|
58
|
+
summary: { sets: 5, reps: 8, weight: 145, unit: 'lbs' },
|
|
59
|
+
tempo: [3, 1, 2, 0],
|
|
60
|
+
indicator: 'pr',
|
|
61
|
+
setStates: [1.0, 0.9, 0.8, 0.7, 0.6].map((s) => ({ status: 'done', velocities: decay(8, s) })),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'Incline DB Press',
|
|
65
|
+
summary: { sets: 3, reps: 8, weight: 70, unit: 'lbs' },
|
|
66
|
+
tempo: [3, 0, 3, 0],
|
|
67
|
+
setStates: [0.72, 0.62, 0.54].map((s) => ({ status: 'done', velocities: decay(8, s) })),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'Cable Chest Press',
|
|
71
|
+
summary: { sets: 3, reps: 10, weight: 90, unit: 'lbs' },
|
|
72
|
+
tempo: [2, 1, 2, 0],
|
|
73
|
+
indicator: 'info',
|
|
74
|
+
setStates: [
|
|
75
|
+
{ status: 'done', velocities: decay(10, 0.72) },
|
|
76
|
+
{ status: 'active', velocities: decay(5, 0.62), planned: 10 },
|
|
77
|
+
{ status: 'todo', planned: 10 },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'Standing Calf Raise',
|
|
82
|
+
summary: { sets: 5, reps: 20, weight: 25, unit: 'lbs' },
|
|
83
|
+
tempo: [2, 1, 2, 0],
|
|
84
|
+
upcoming: true,
|
|
85
|
+
setStates: [1, 2, 3, 4, 5].map(() => ({ status: 'todo', planned: 20 })),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'Face Pull',
|
|
89
|
+
summary: { sets: 3, reps: '15-20', weight: 40, unit: 'lbs' },
|
|
90
|
+
tempo: [2, 1, 2, 0],
|
|
91
|
+
upcoming: true,
|
|
92
|
+
setStates: [1, 2, 3].map(() => ({ status: 'todo', planned: 18 })),
|
|
93
|
+
},
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
const LIVE_METRICS = [
|
|
97
|
+
{ label: 'Volume', value: '76%' },
|
|
98
|
+
{ label: 'Load', value: '7.3k' },
|
|
99
|
+
{ label: 'Fatigue', value: 'MOD', valueColor: AMBER },
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
const baseArgs = {
|
|
103
|
+
title: 'Pull A · Intensification',
|
|
104
|
+
exercises: SESSION,
|
|
105
|
+
stripHeight: 8,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const Default: Story = {
|
|
109
|
+
args: {
|
|
110
|
+
...baseArgs,
|
|
111
|
+
setsDone: 7.2,
|
|
112
|
+
elapsedMs: (42 * 60 + 18) * 1000,
|
|
113
|
+
budgetMs: 60 * 60 * 1000,
|
|
114
|
+
metrics: LIVE_METRICS,
|
|
115
|
+
},
|
|
116
|
+
parameters: {
|
|
117
|
+
docs: {
|
|
118
|
+
description: {
|
|
119
|
+
story: 'Live, behind pace — the header carries the session glance over the exercise list.',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const NoTimeTarget: Story = {
|
|
126
|
+
args: {
|
|
127
|
+
...baseArgs,
|
|
128
|
+
setsDone: 7.2,
|
|
129
|
+
elapsedMs: (42 * 60 + 18) * 1000,
|
|
130
|
+
metrics: LIVE_METRICS,
|
|
131
|
+
},
|
|
132
|
+
parameters: {
|
|
133
|
+
docs: {
|
|
134
|
+
description: { story: 'Live with no time budget — plain (steel) progress, ⏱ elapsed only.' },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const Upcoming: Story = {
|
|
140
|
+
args: {
|
|
141
|
+
...baseArgs,
|
|
142
|
+
title: 'Lower B · Volume',
|
|
143
|
+
budgetMs: 52 * 60 * 1000,
|
|
144
|
+
next: Date.now() + 2 * 24 * 60 * 60 * 1000,
|
|
145
|
+
},
|
|
146
|
+
parameters: {
|
|
147
|
+
docs: {
|
|
148
|
+
description: {
|
|
149
|
+
story: 'Upcoming session — the header shows Date / Time / Until + the empty plan bar.',
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { SessionRail, type SessionRailExercise } from './SessionRail'
|
|
5
|
+
|
|
6
|
+
const exercises: SessionRailExercise[] = [
|
|
7
|
+
{
|
|
8
|
+
name: 'Seated Cable Row',
|
|
9
|
+
summary: { sets: 5, reps: 8, weight: 145, unit: 'lbs' },
|
|
10
|
+
tempo: [3, 1, 2, 0],
|
|
11
|
+
indicator: 'pr',
|
|
12
|
+
setStates: [{ status: 'done', velocities: [1, 0.9] }],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'Cable Chest Press',
|
|
16
|
+
summary: { sets: 3, reps: 10, weight: 90, unit: 'lbs' },
|
|
17
|
+
setStates: [{ status: 'active', velocities: [0.6], planned: 10 }],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'Face Pull',
|
|
21
|
+
summary: { sets: 3, reps: '15-20', weight: 40, unit: 'lbs' },
|
|
22
|
+
upcoming: true,
|
|
23
|
+
setStates: [{ status: 'todo', planned: 18 }],
|
|
24
|
+
},
|
|
25
|
+
] // total = 11 sets
|
|
26
|
+
|
|
27
|
+
const baseProps = {
|
|
28
|
+
title: 'Pull A · Intensification',
|
|
29
|
+
exercises,
|
|
30
|
+
setsDone: 4,
|
|
31
|
+
elapsedMs: 20 * 60 * 1000,
|
|
32
|
+
budgetMs: 60 * 60 * 1000,
|
|
33
|
+
metrics: [
|
|
34
|
+
{ label: 'Volume', value: '40%' },
|
|
35
|
+
{ label: 'Load', value: '3.1k' },
|
|
36
|
+
{ label: 'Fatigue', value: 'LOW' },
|
|
37
|
+
],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('SessionRail', () => {
|
|
41
|
+
it('renders the session title', () => {
|
|
42
|
+
render(<SessionRail {...baseProps} />)
|
|
43
|
+
expect(screen.getByTestId('session-rail-title')).toHaveTextContent('Pull A · Intensification')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('derives the header sets label from the exercises', () => {
|
|
47
|
+
render(<SessionRail {...baseProps} />)
|
|
48
|
+
expect(screen.getByTestId('session-rail-sets')).toHaveTextContent('4/11 sets')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('feeds the live metric tiles into the header', () => {
|
|
52
|
+
render(<SessionRail {...baseProps} />)
|
|
53
|
+
expect(screen.getByText('Volume')).toBeInTheDocument()
|
|
54
|
+
expect(screen.getByText('Fatigue')).toBeInTheDocument()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('renders the upcoming glance when next is set', () => {
|
|
58
|
+
render(
|
|
59
|
+
<SessionRail title="Lower B" exercises={exercises} next={new Date('2026-07-11T18:30:00')} />
|
|
60
|
+
)
|
|
61
|
+
expect(screen.getByText('Date')).toBeInTheDocument()
|
|
62
|
+
expect(screen.getByText('Until')).toBeInTheDocument()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('renders one rail heading per exercise', () => {
|
|
66
|
+
render(<SessionRail {...baseProps} />)
|
|
67
|
+
expect(screen.getAllByTestId('exercise-card')).toHaveLength(3)
|
|
68
|
+
expect(screen.getByText('Seated Cable Row')).toBeInTheDocument()
|
|
69
|
+
expect(screen.getByText('Face Pull')).toBeInTheDocument()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('fires onExercisePress with the exercise and its index', () => {
|
|
73
|
+
const onExercisePress = vi.fn()
|
|
74
|
+
render(<SessionRail {...baseProps} onExercisePress={onExercisePress} />)
|
|
75
|
+
fireEvent.click(screen.getAllByTestId('exercise-card-header')[1])
|
|
76
|
+
expect(onExercisePress).toHaveBeenCalledWith(exercises[1], 1)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('accessibility', () => {
|
|
80
|
+
it('has no accessibility violations', async () => {
|
|
81
|
+
const { container } = render(<SessionRail {...baseProps} />)
|
|
82
|
+
const results = await axe(container)
|
|
83
|
+
expect(results).toHaveNoViolations()
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import { View, type ViewProps } from 'react-native'
|
|
3
|
+
import { primitiveColors } from '../../../theme/tokens/primitives'
|
|
4
|
+
import { neumorphicShadows } from '../../../theme/shadows'
|
|
5
|
+
import { ExerciseCardHeading } from './ExerciseCardHeading'
|
|
6
|
+
import { SessionHeader } from './SessionHeader'
|
|
7
|
+
import type { MetricTileData } from './MetricTiles'
|
|
8
|
+
import type { SetStripSet } from './SetStrip'
|
|
9
|
+
import type { ExerciseIndicatorKind } from './ExerciseIndicator'
|
|
10
|
+
|
|
11
|
+
// Charcoal-ramp surfaces (the locked S3 shell shades): the nav + the heading plane
|
|
12
|
+
// read as ONE flat raised surface; the exercise list is sunk. Depth comes only from
|
|
13
|
+
// the list's subtle inset shadow (top cast by the heading, faint left edge by the nav).
|
|
14
|
+
const INSET = primitiveColors.charcoal[800] // #131313 — sunk exercise list
|
|
15
|
+
const DIVIDER = primitiveColors.charcoal[500] // #1C1C1C — row divider
|
|
16
|
+
|
|
17
|
+
const DEFAULT_WIDTH = 246
|
|
18
|
+
|
|
19
|
+
export interface SessionRailExercise {
|
|
20
|
+
/** Stable key + press payload. Falls back to list index when absent. */
|
|
21
|
+
id?: string
|
|
22
|
+
name: string
|
|
23
|
+
summary: { sets: number; reps: number | string; weight: number; unit: 'lbs' | 'kg' }
|
|
24
|
+
tempo?: [number, number, number, number]
|
|
25
|
+
indicator?: ExerciseIndicatorKind
|
|
26
|
+
/** Per-set performance data driving the heading strip. */
|
|
27
|
+
setStates: SetStripSet[]
|
|
28
|
+
/** Dim the row as a not-yet-reached exercise. */
|
|
29
|
+
upcoming?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SessionRailProps extends ViewProps {
|
|
33
|
+
/** Session title — the live session's name, or the next session's name when `next` is set. */
|
|
34
|
+
title: string
|
|
35
|
+
exercises: SessionRailExercise[]
|
|
36
|
+
/** Fractional sets completed (live) — drives the header's pace bar + sets label. */
|
|
37
|
+
setsDone?: number
|
|
38
|
+
/** Live elapsed time in ms — drives the header's ⏱ readout and pace marker. */
|
|
39
|
+
elapsedMs?: number
|
|
40
|
+
/** Optional time budget in ms → header pace marker + ` / budget`. */
|
|
41
|
+
budgetMs?: number
|
|
42
|
+
/** Header elapsed readout goes live-green while true. Default true (live). */
|
|
43
|
+
running?: boolean
|
|
44
|
+
/** Live stat tiles (Volume / Load / Fatigue). Ignored when `next` is set. */
|
|
45
|
+
metrics?: MetricTileData[]
|
|
46
|
+
/** When set → the header renders the UPCOMING glance (Date/Time/Until + empty bar). */
|
|
47
|
+
next?: number | Date
|
|
48
|
+
/** Heading strip height in px, forwarded to each row. Default 8. */
|
|
49
|
+
stripHeight?: number
|
|
50
|
+
/** Rail width in px. Default 246. */
|
|
51
|
+
width?: number
|
|
52
|
+
onExercisePress?: (exercise: SessionRailExercise, index: number) => void
|
|
53
|
+
className?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The live-workout session rail: a flat raised {@link SessionHeader} glance (title,
|
|
58
|
+
* stat tiles, chunked pace bar) over a sunk, inset list of {@link ExerciseCardHeading}
|
|
59
|
+
* exercise headings. The header's plan (chunk widths ∝ sets) is derived from `exercises`.
|
|
60
|
+
* Presentational — driven entirely by props.
|
|
61
|
+
*/
|
|
62
|
+
export function SessionRail({
|
|
63
|
+
title,
|
|
64
|
+
exercises,
|
|
65
|
+
setsDone,
|
|
66
|
+
elapsedMs,
|
|
67
|
+
budgetMs,
|
|
68
|
+
running,
|
|
69
|
+
metrics,
|
|
70
|
+
next,
|
|
71
|
+
stripHeight = 8,
|
|
72
|
+
width = DEFAULT_WIDTH,
|
|
73
|
+
onExercisePress,
|
|
74
|
+
className,
|
|
75
|
+
style,
|
|
76
|
+
...props
|
|
77
|
+
}: SessionRailProps) {
|
|
78
|
+
return (
|
|
79
|
+
<View
|
|
80
|
+
className={className}
|
|
81
|
+
style={[{ width, flexDirection: 'column', backgroundColor: INSET }, style]}
|
|
82
|
+
testID="session-rail"
|
|
83
|
+
{...props}
|
|
84
|
+
>
|
|
85
|
+
<SessionHeader
|
|
86
|
+
title={title}
|
|
87
|
+
plan={exercises.map((ex) => ({ name: ex.name, sets: ex.summary.sets }))}
|
|
88
|
+
setsDone={setsDone}
|
|
89
|
+
elapsedMs={elapsedMs}
|
|
90
|
+
budgetMs={budgetMs}
|
|
91
|
+
running={running}
|
|
92
|
+
metrics={metrics}
|
|
93
|
+
next={next}
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<View
|
|
97
|
+
style={[{ flex: 1, backgroundColor: INSET }, neumorphicShadows.charcoal.pressed.subtle]}
|
|
98
|
+
testID="session-rail-list"
|
|
99
|
+
>
|
|
100
|
+
{exercises.map((ex, i) => (
|
|
101
|
+
<View
|
|
102
|
+
key={ex.id ?? i}
|
|
103
|
+
style={{
|
|
104
|
+
borderBottomWidth: i < exercises.length - 1 ? 1 : 0,
|
|
105
|
+
borderBottomColor: DIVIDER,
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<ExerciseCardHeading
|
|
109
|
+
name={ex.name}
|
|
110
|
+
sets={ex.summary.sets}
|
|
111
|
+
reps={ex.summary.reps}
|
|
112
|
+
load={ex.summary.weight}
|
|
113
|
+
unit={ex.summary.unit}
|
|
114
|
+
tempo={ex.tempo}
|
|
115
|
+
indicator={ex.indicator}
|
|
116
|
+
setStates={ex.setStates}
|
|
117
|
+
stripHeight={stripHeight}
|
|
118
|
+
dimmed={ex.upcoming}
|
|
119
|
+
onPress={() => onExercisePress?.(ex, i)}
|
|
120
|
+
/>
|
|
121
|
+
</View>
|
|
122
|
+
))}
|
|
123
|
+
</View>
|
|
124
|
+
</View>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { SetBar, type SetStripSet } from './SetBar'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `SetBar` — ONE set's multi-coloured bar: the butted per-rep colour segments for a
|
|
7
|
+
* single set. `done` = velocity-coloured reps · `active` = performed reps pulse with
|
|
8
|
+
* a grey remainder · `todo` = a solid grey bar. Colours are the real titan ramp pins
|
|
9
|
+
* (red-600 · orange-400 · amber-300 · green-300). {@link SetStrip} lays several side by side.
|
|
10
|
+
*/
|
|
11
|
+
const meta: Meta<typeof SetBar> = {
|
|
12
|
+
title: 'Workout/SetBar',
|
|
13
|
+
component: SetBar,
|
|
14
|
+
tags: ['autodocs'],
|
|
15
|
+
parameters: {
|
|
16
|
+
docs: {
|
|
17
|
+
description: {
|
|
18
|
+
component:
|
|
19
|
+
'**Atom.** ONE set’s multi-coloured bar — the per-rep colour segments for a ' +
|
|
20
|
+
'single set (done / active-with-pulse / todo). Colours are the real titan ramp pins. ' +
|
|
21
|
+
'Used-by ↑ [SetStrip](?path=/docs/workout-setstrip--docs).',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
argTypes: {
|
|
26
|
+
height: { control: { type: 'range', min: 2, max: 16, step: 1 } },
|
|
27
|
+
},
|
|
28
|
+
decorators: [
|
|
29
|
+
(Story) => (
|
|
30
|
+
<View style={{ width: 180, flexDirection: 'row', padding: 16, backgroundColor: '#131313' }}>
|
|
31
|
+
<Story />
|
|
32
|
+
</View>
|
|
33
|
+
),
|
|
34
|
+
],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default meta
|
|
38
|
+
type Story = StoryObj<typeof SetBar>
|
|
39
|
+
|
|
40
|
+
const decay = (n: number, start: number, span = 0.4): number[] =>
|
|
41
|
+
Array.from({ length: n }, (_, r) => +(start - (span * r) / Math.max(1, n - 1)).toFixed(3))
|
|
42
|
+
|
|
43
|
+
export const Done: Story = {
|
|
44
|
+
args: { height: 8, set: { status: 'done', velocities: decay(8, 0.9) } as SetStripSet },
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const Active: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
height: 8,
|
|
50
|
+
set: { status: 'active', velocities: decay(5, 0.62), planned: 10 } as SetStripSet,
|
|
51
|
+
},
|
|
52
|
+
parameters: {
|
|
53
|
+
docs: { description: { story: 'Performed reps pulse; the planned remainder is greyed.' } },
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const Todo: Story = {
|
|
58
|
+
args: { height: 8, set: { status: 'todo', planned: 10 } as SetStripSet },
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const Range: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
height: 8,
|
|
64
|
+
set: { status: 'range', floor: 15, max: 20, doneVels: decay(17, 0.85) } as SetStripSet,
|
|
65
|
+
},
|
|
66
|
+
parameters: {
|
|
67
|
+
docs: {
|
|
68
|
+
description: {
|
|
69
|
+
story:
|
|
70
|
+
'Variable rep-range (e.g. 15–20): range-max segments. Done reps are velocity-' +
|
|
71
|
+
'coloured (even past the floor), the committed-todo zone is grey, and the variable-' +
|
|
72
|
+
'todo zone (floor→max) is the cyan variable/opportunity pin.',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const Drop: Story = {
|
|
79
|
+
args: {
|
|
80
|
+
height: 8,
|
|
81
|
+
set: { status: 'drop', subloads: [decay(10, 0.85), decay(8, 0.7), decay(6, 0.6)] } as SetStripSet,
|
|
82
|
+
},
|
|
83
|
+
parameters: {
|
|
84
|
+
docs: {
|
|
85
|
+
description: {
|
|
86
|
+
story: 'One set, no rest: sub-loads butted internally and split by 2px notches.',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const Myo: Story = {
|
|
93
|
+
args: {
|
|
94
|
+
height: 8,
|
|
95
|
+
set: {
|
|
96
|
+
status: 'myo',
|
|
97
|
+
activation: decay(15, 0.75),
|
|
98
|
+
clusters: [decay(5, 0.6), decay(5, 0.55), decay(5, 0.5)],
|
|
99
|
+
} as SetStripSet,
|
|
100
|
+
},
|
|
101
|
+
parameters: {
|
|
102
|
+
docs: {
|
|
103
|
+
description: {
|
|
104
|
+
story: 'Myo-rep / rest-pause (done): activation chunk + mini-clusters split by 3px gaps.',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const MyoUpcoming: Story = {
|
|
111
|
+
args: {
|
|
112
|
+
height: 8,
|
|
113
|
+
set: { status: 'myo-upcoming', activationLen: 15 } as SetStripSet,
|
|
114
|
+
},
|
|
115
|
+
parameters: {
|
|
116
|
+
docs: {
|
|
117
|
+
description: {
|
|
118
|
+
story:
|
|
119
|
+
'Planned myo, length unknown: grey activation + a fading cyan "clusters-to-failure" ' +
|
|
120
|
+
'trail whose right edge is left open — "more coming, count unknown".',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
}
|