@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
|
@@ -3,19 +3,28 @@ import { useState } from 'react'
|
|
|
3
3
|
import { View, Text, Pressable } from 'react-native'
|
|
4
4
|
import { VelocityStrip, type VelocityZoneBandProp } from './VelocityStrip'
|
|
5
5
|
import { PlaceholderStrip } from './PlaceholderStrip'
|
|
6
|
-
import { WeightBadge } from './WeightBadge'
|
|
7
6
|
import { PrBadge } from './PrBadge'
|
|
8
|
-
import { TempoDisplay } from './TempoDisplay'
|
|
9
7
|
import { SetRow, type SetRowProps } from './SetRow'
|
|
8
|
+
import { type SetStripSet } from './SetStrip'
|
|
9
|
+
import { SetTableHeader } from './SetTableHeader'
|
|
10
|
+
import { ExerciseCardHeading } from './ExerciseCardHeading'
|
|
11
|
+
import { type ExerciseIndicatorKind } from './ExerciseIndicator'
|
|
10
12
|
import { roundWeight } from '../../../utils/workout-format'
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
export type ExerciseCardState = 'collapsed' | 'expanded' | 'upcoming'
|
|
13
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
14
14
|
|
|
15
15
|
export interface ExerciseCardProps {
|
|
16
16
|
name: string
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The dimmed, not-yet-reached representation (prescription + previous-best line).
|
|
19
|
+
* Overrides the expand state — an upcoming card never expands.
|
|
20
|
+
*/
|
|
21
|
+
upcoming?: boolean
|
|
22
|
+
/** Controlled expand state. Omit to run uncontrolled (see {@link defaultExpanded}). */
|
|
23
|
+
expanded?: boolean
|
|
24
|
+
/** Uncontrolled initial expand state. Default false. Ignored when `expanded` is set. */
|
|
25
|
+
defaultExpanded?: boolean
|
|
26
|
+
/** Notified when the user toggles the card between collapsed and expanded. */
|
|
27
|
+
onExpandedChange?: (expanded: boolean) => void
|
|
19
28
|
onNavigateDetail?: () => void
|
|
20
29
|
summary?: {
|
|
21
30
|
sets: number
|
|
@@ -23,27 +32,36 @@ export interface ExerciseCardProps {
|
|
|
23
32
|
weight: number
|
|
24
33
|
unit: 'lbs' | 'kg'
|
|
25
34
|
}
|
|
26
|
-
e1rm?: { value: number; unit: 'lbs' | 'kg' }
|
|
27
35
|
isPR?: boolean
|
|
36
|
+
/** Collapsed glance: per-set logged velocities (drives the mini strips). */
|
|
28
37
|
setVelocities?: number[][]
|
|
29
38
|
/** Optional velocity-zone bands shared across this exercise's sets (WA bands). */
|
|
30
39
|
velocityZones?: readonly VelocityZoneBandProp[]
|
|
40
|
+
/** Collapsed glance: total planned sets (placeholder strips fill the remainder). */
|
|
31
41
|
totalPlannedSets?: number
|
|
42
|
+
/** Expanded body: the per-set rows (done / live / todo). */
|
|
32
43
|
sets?: SetRowProps[]
|
|
33
44
|
tempo?: [number, number, number, number]
|
|
34
45
|
prescription?: string
|
|
35
46
|
previousBest?: string
|
|
36
47
|
supersetPosition?: 'first' | 'last' | 'middle' | null
|
|
37
48
|
supersetColor?: string
|
|
49
|
+
/** Expanded header per-set strip override; derived from `sets` when omitted. */
|
|
50
|
+
setStates?: SetStripSet[]
|
|
51
|
+
/** A small PR / issue / info chip in the title line (expanded header). */
|
|
52
|
+
indicator?: ExerciseIndicatorKind
|
|
53
|
+
/** Expanded header strip height in px. Default 8. */
|
|
54
|
+
stripHeight?: number
|
|
38
55
|
}
|
|
39
56
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
/** The sub-card render props: everything but the expand-control surface, plus a resolved toggle. */
|
|
58
|
+
type CardBodyProps = Omit<
|
|
59
|
+
ExerciseCardProps,
|
|
60
|
+
'upcoming' | 'expanded' | 'defaultExpanded' | 'onExpandedChange'
|
|
61
|
+
> & { onToggle: () => void }
|
|
44
62
|
|
|
45
63
|
function getSupersetBorderRadius(
|
|
46
|
-
position: ExerciseCardProps['supersetPosition']
|
|
64
|
+
position: ExerciseCardProps['supersetPosition']
|
|
47
65
|
): Record<string, number> {
|
|
48
66
|
switch (position) {
|
|
49
67
|
case 'first':
|
|
@@ -78,7 +96,7 @@ function getSupersetBorderRadius(
|
|
|
78
96
|
}
|
|
79
97
|
|
|
80
98
|
function getSupersetMargin(
|
|
81
|
-
position: ExerciseCardProps['supersetPosition']
|
|
99
|
+
position: ExerciseCardProps['supersetPosition']
|
|
82
100
|
): Record<string, number> {
|
|
83
101
|
if (position === 'first' || position === 'middle') {
|
|
84
102
|
return { marginBottom: 2 }
|
|
@@ -88,13 +106,13 @@ function getSupersetMargin(
|
|
|
88
106
|
|
|
89
107
|
function formatSummary(summary: ExerciseCardProps['summary']): string {
|
|
90
108
|
if (!summary) return ''
|
|
91
|
-
return `${summary.sets}
|
|
109
|
+
return `${summary.sets}×${summary.reps} @ ${roundWeight(summary.weight)} ${summary.unit}`
|
|
92
110
|
}
|
|
93
111
|
|
|
94
112
|
function formatAccessibilityLabel(
|
|
95
113
|
name: string,
|
|
96
114
|
summary: ExerciseCardProps['summary'],
|
|
97
|
-
prescription: ExerciseCardProps['prescription']
|
|
115
|
+
prescription: ExerciseCardProps['prescription']
|
|
98
116
|
): string {
|
|
99
117
|
if (summary) return `${name}, ${formatSummary(summary)}`
|
|
100
118
|
if (prescription) return `${name}, ${prescription}`
|
|
@@ -105,13 +123,12 @@ function CollapsedCard({
|
|
|
105
123
|
name,
|
|
106
124
|
onToggle,
|
|
107
125
|
summary,
|
|
108
|
-
e1rm,
|
|
109
126
|
isPR,
|
|
110
127
|
setVelocities,
|
|
111
128
|
velocityZones,
|
|
112
129
|
totalPlannedSets,
|
|
113
130
|
supersetPosition,
|
|
114
|
-
}:
|
|
131
|
+
}: CardBodyProps) {
|
|
115
132
|
const [pressed, setPressed] = useState(false)
|
|
116
133
|
const completedSets = setVelocities?.length ?? 0
|
|
117
134
|
const remaining = Math.max(0, (totalPlannedSets ?? 0) - completedSets)
|
|
@@ -137,10 +154,7 @@ function CollapsedCard({
|
|
|
137
154
|
...supersetMargin,
|
|
138
155
|
}}
|
|
139
156
|
>
|
|
140
|
-
<View
|
|
141
|
-
className="flex-row items-center"
|
|
142
|
-
testID="exercise-card-header"
|
|
143
|
-
>
|
|
157
|
+
<View className="flex-row items-center" testID="exercise-card-header">
|
|
144
158
|
<Text
|
|
145
159
|
className="text-text-primary"
|
|
146
160
|
style={{
|
|
@@ -166,15 +180,6 @@ function CollapsedCard({
|
|
|
166
180
|
{formatSummary(summary)}
|
|
167
181
|
</Text>
|
|
168
182
|
)}
|
|
169
|
-
{e1rm && (
|
|
170
|
-
<WeightBadge
|
|
171
|
-
value={roundWeight(e1rm.value)}
|
|
172
|
-
unit={e1rm.unit}
|
|
173
|
-
size="sm"
|
|
174
|
-
showIcon={false}
|
|
175
|
-
testID="exercise-card-e1rm"
|
|
176
|
-
/>
|
|
177
|
-
)}
|
|
178
183
|
{isPR && (
|
|
179
184
|
<View style={{ marginLeft: 4 }}>
|
|
180
185
|
<PrBadge type="e1rm" compact animate={false} />
|
|
@@ -183,11 +188,7 @@ function CollapsedCard({
|
|
|
183
188
|
</View>
|
|
184
189
|
|
|
185
190
|
{(completedSets > 0 || remaining > 0) && (
|
|
186
|
-
<View
|
|
187
|
-
className="flex-row"
|
|
188
|
-
style={{ marginTop: 6, gap: 4 }}
|
|
189
|
-
testID="exercise-card-strips"
|
|
190
|
-
>
|
|
191
|
+
<View className="flex-row" style={{ marginTop: 6, gap: 4 }} testID="exercise-card-strips">
|
|
191
192
|
{setVelocities?.map((velocities, i) => (
|
|
192
193
|
<VelocityStrip
|
|
193
194
|
key={i}
|
|
@@ -210,22 +211,52 @@ function CollapsedCard({
|
|
|
210
211
|
)
|
|
211
212
|
}
|
|
212
213
|
|
|
214
|
+
// --- Unified expanded card ---------------------------------------------------
|
|
215
|
+
// The expanded card is "one object" with its rail heading: the persistent header
|
|
216
|
+
// is the real ExerciseCardHeading, and the revealed body drops PREV and renders
|
|
217
|
+
// one real SetRow per set (muted done/todo, brightened live spotlight).
|
|
218
|
+
|
|
219
|
+
const DARK = getSemanticColors('dark')
|
|
220
|
+
/** The header↔body seam. */
|
|
221
|
+
const BODY_DIVIDER = DARK['border-subtle']
|
|
222
|
+
|
|
223
|
+
/** Project the set rows onto the heading strip's per-set state (done / active / todo). */
|
|
224
|
+
function deriveHeaderSetStates(sets: SetRowProps[]): SetStripSet[] {
|
|
225
|
+
return sets.map((set): SetStripSet => {
|
|
226
|
+
switch (set.state) {
|
|
227
|
+
case 'live':
|
|
228
|
+
return { status: 'active', velocities: set.velocities, planned: set.target.reps }
|
|
229
|
+
case 'done':
|
|
230
|
+
return { status: 'done', velocities: set.velocities }
|
|
231
|
+
case 'todo':
|
|
232
|
+
return { status: 'todo', planned: set.planned ?? set.target.reps }
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
213
237
|
function ExpandedCard({
|
|
214
238
|
name,
|
|
215
239
|
onToggle,
|
|
216
240
|
summary,
|
|
217
|
-
e1rm,
|
|
218
241
|
isPR,
|
|
219
242
|
sets,
|
|
220
243
|
tempo,
|
|
244
|
+
indicator,
|
|
245
|
+
setStates,
|
|
246
|
+
stripHeight = 8,
|
|
247
|
+
velocityZones,
|
|
221
248
|
supersetPosition,
|
|
222
|
-
}:
|
|
249
|
+
}: CardBodyProps) {
|
|
223
250
|
const borderRadius = getSupersetBorderRadius(supersetPosition)
|
|
224
251
|
const supersetMargin = getSupersetMargin(supersetPosition)
|
|
225
252
|
// Summary is the card-level authority for the weight column; fall back to the
|
|
226
253
|
// first set's unit, then lbs. (Mixed per-set units keep the card-level label.)
|
|
227
254
|
const unit = summary?.unit ?? sets?.[0]?.unit ?? 'lbs'
|
|
228
|
-
|
|
255
|
+
// The rail heading IS the header: its per-set strip comes from an explicit
|
|
256
|
+
// `setStates` when supplied, else it's derived from the set rows. e1RM is dropped
|
|
257
|
+
// entirely; a PR surfaces via the chip.
|
|
258
|
+
const headerStates = setStates ?? (sets ? deriveHeaderSetStates(sets) : [])
|
|
259
|
+
const headerIndicator = indicator ?? (isPR ? 'pr' : undefined)
|
|
229
260
|
|
|
230
261
|
return (
|
|
231
262
|
<View
|
|
@@ -237,26 +268,67 @@ function ExpandedCard({
|
|
|
237
268
|
}}
|
|
238
269
|
testID="exercise-card"
|
|
239
270
|
>
|
|
240
|
-
<
|
|
271
|
+
<ExerciseCardHeading
|
|
272
|
+
name={name}
|
|
273
|
+
sets={summary?.sets ?? sets?.length ?? 0}
|
|
274
|
+
reps={summary?.reps ?? 0}
|
|
275
|
+
load={summary?.weight ?? 0}
|
|
276
|
+
unit={unit}
|
|
277
|
+
tempo={tempo}
|
|
278
|
+
indicator={headerIndicator}
|
|
279
|
+
setStates={headerStates}
|
|
280
|
+
stripHeight={stripHeight}
|
|
241
281
|
onPress={onToggle}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
282
|
+
testID="exercise-card-heading"
|
|
283
|
+
/>
|
|
284
|
+
|
|
285
|
+
<View
|
|
286
|
+
style={{ borderTopWidth: 1, borderTopColor: BODY_DIVIDER, paddingBottom: 6 }}
|
|
287
|
+
testID="exercise-card-body"
|
|
288
|
+
>
|
|
289
|
+
<SetTableHeader unit={unit} showPrevious={false} testID="exercise-card-column-headers" />
|
|
290
|
+
|
|
291
|
+
{sets && (
|
|
292
|
+
<View testID="exercise-card-sets">
|
|
293
|
+
{sets.map((set, i) => (
|
|
294
|
+
<SetRow key={i} {...set} velocityZones={set.velocityZones ?? velocityZones} />
|
|
295
|
+
))}
|
|
296
|
+
</View>
|
|
297
|
+
)}
|
|
298
|
+
</View>
|
|
299
|
+
</View>
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function UpcomingCard({
|
|
304
|
+
name,
|
|
305
|
+
prescription,
|
|
306
|
+
previousBest,
|
|
307
|
+
supersetPosition,
|
|
308
|
+
onToggle,
|
|
309
|
+
}: CardBodyProps) {
|
|
310
|
+
const borderRadius = getSupersetBorderRadius(supersetPosition)
|
|
311
|
+
const supersetMargin = getSupersetMargin(supersetPosition)
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<Pressable onPress={onToggle} accessibilityRole="button">
|
|
315
|
+
<View
|
|
316
|
+
style={{
|
|
317
|
+
opacity: 0.6,
|
|
318
|
+
padding: 12,
|
|
319
|
+
paddingHorizontal: 14,
|
|
320
|
+
...borderRadius,
|
|
321
|
+
...supersetMargin,
|
|
322
|
+
}}
|
|
323
|
+
accessibilityLabel={formatAccessibilityLabel(name, undefined, prescription)}
|
|
324
|
+
testID="exercise-card"
|
|
246
325
|
>
|
|
247
|
-
<View
|
|
248
|
-
|
|
249
|
-
style={{
|
|
250
|
-
padding: 12,
|
|
251
|
-
paddingHorizontal: 14,
|
|
252
|
-
paddingBottom: 10,
|
|
253
|
-
borderBottomWidth: 1,
|
|
254
|
-
borderBottomColor: resolveColor('border-default'),
|
|
255
|
-
}}
|
|
256
|
-
>
|
|
326
|
+
<View className="flex-row items-center">
|
|
327
|
+
{/* Name never truncates (no numberOfLines); prescription + previousBest ellipsize first. */}
|
|
257
328
|
<Text
|
|
258
329
|
className="text-text-primary"
|
|
259
330
|
style={{
|
|
331
|
+
flexShrink: 0,
|
|
260
332
|
fontSize: 14,
|
|
261
333
|
fontFamily: '"Space Grotesk", sans-serif',
|
|
262
334
|
fontWeight: '700',
|
|
@@ -265,167 +337,73 @@ function ExpandedCard({
|
|
|
265
337
|
>
|
|
266
338
|
{name}
|
|
267
339
|
</Text>
|
|
268
|
-
|
|
269
|
-
{summary && (
|
|
340
|
+
{prescription && (
|
|
270
341
|
<Text
|
|
271
342
|
className="text-text-secondary"
|
|
343
|
+
numberOfLines={1}
|
|
272
344
|
style={{
|
|
345
|
+
flexShrink: 1,
|
|
273
346
|
fontSize: 12,
|
|
274
347
|
fontFamily: 'Inter, sans-serif',
|
|
275
|
-
|
|
348
|
+
marginLeft: 8,
|
|
276
349
|
}}
|
|
277
|
-
testID="exercise-card-
|
|
350
|
+
testID="exercise-card-prescription"
|
|
278
351
|
>
|
|
279
|
-
{
|
|
352
|
+
{prescription}
|
|
280
353
|
</Text>
|
|
281
354
|
)}
|
|
282
|
-
{
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
showIcon={false}
|
|
288
|
-
testID="exercise-card-e1rm"
|
|
289
|
-
/>
|
|
290
|
-
)}
|
|
291
|
-
{isPR && (
|
|
292
|
-
<View style={{ marginLeft: 4 }}>
|
|
293
|
-
<PrBadge type="e1rm" compact animate={false} />
|
|
294
|
-
</View>
|
|
295
|
-
)}
|
|
296
|
-
</View>
|
|
297
|
-
</Pressable>
|
|
298
|
-
|
|
299
|
-
{tempo && (
|
|
300
|
-
<View style={{ marginTop: 8, paddingHorizontal: 14 }} testID="exercise-card-tempo">
|
|
301
|
-
<TempoDisplay tempo={tempo} size="sm" />
|
|
302
|
-
</View>
|
|
303
|
-
)}
|
|
304
|
-
|
|
305
|
-
<View
|
|
306
|
-
className="flex-row"
|
|
307
|
-
style={{
|
|
308
|
-
paddingVertical: 8,
|
|
309
|
-
paddingHorizontal: 8,
|
|
310
|
-
paddingBottom: 4,
|
|
311
|
-
}}
|
|
312
|
-
testID="exercise-card-column-headers"
|
|
313
|
-
>
|
|
314
|
-
{columnHeaders.map((header, i) => {
|
|
315
|
-
const widths = [36, undefined, 44, 56, 36]
|
|
316
|
-
const width = widths[i]
|
|
317
|
-
const isFlex = width === undefined
|
|
318
|
-
|
|
319
|
-
return (
|
|
320
|
-
<View
|
|
321
|
-
key={header}
|
|
355
|
+
<View className="flex-1" style={{ minWidth: 8 }} />
|
|
356
|
+
{previousBest && (
|
|
357
|
+
<Text
|
|
358
|
+
className="text-text-tertiary"
|
|
359
|
+
numberOfLines={1}
|
|
322
360
|
style={{
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
gap: 8,
|
|
361
|
+
flexShrink: 1,
|
|
362
|
+
fontSize: 11,
|
|
363
|
+
fontFamily: 'Inter, sans-serif',
|
|
327
364
|
}}
|
|
328
|
-
|
|
365
|
+
testID="exercise-card-previous-best"
|
|
329
366
|
>
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
fontSize: 10,
|
|
334
|
-
fontWeight: '600',
|
|
335
|
-
fontFamily: 'Inter, sans-serif',
|
|
336
|
-
textTransform: 'uppercase',
|
|
337
|
-
letterSpacing: 0.8,
|
|
338
|
-
}}
|
|
339
|
-
>
|
|
340
|
-
{header}
|
|
341
|
-
</Text>
|
|
342
|
-
</View>
|
|
343
|
-
)
|
|
344
|
-
})}
|
|
345
|
-
</View>
|
|
346
|
-
|
|
347
|
-
{sets && (
|
|
348
|
-
<View testID="exercise-card-sets">
|
|
349
|
-
{sets.map((setProps, i) => (
|
|
350
|
-
<SetRow key={i} {...setProps} />
|
|
351
|
-
))}
|
|
367
|
+
{previousBest}
|
|
368
|
+
</Text>
|
|
369
|
+
)}
|
|
352
370
|
</View>
|
|
353
|
-
|
|
354
|
-
</
|
|
371
|
+
</View>
|
|
372
|
+
</Pressable>
|
|
355
373
|
)
|
|
356
374
|
}
|
|
357
375
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
376
|
+
/**
|
|
377
|
+
* The data-contract exercise card, in three representations:
|
|
378
|
+
* - `upcoming` — a dimmed, not-yet-reached row (prescription + previous best).
|
|
379
|
+
* - collapsed — a glance: name + summary + per-set velocity strips.
|
|
380
|
+
* - expanded — the unified card: the real {@link ExerciseCardHeading} header over
|
|
381
|
+
* the SET · REPS · LBS · RPE body (one {@link SetRow} per set).
|
|
382
|
+
*
|
|
383
|
+
* Expand is controlled (`expanded` + `onExpandedChange`) or uncontrolled
|
|
384
|
+
* (`defaultExpanded`, internal state). `upcoming` overrides expand.
|
|
385
|
+
*/
|
|
386
|
+
export function ExerciseCard({
|
|
387
|
+
upcoming,
|
|
388
|
+
expanded,
|
|
389
|
+
defaultExpanded,
|
|
390
|
+
onExpandedChange,
|
|
391
|
+
...rest
|
|
363
392
|
}: ExerciseCardProps) {
|
|
364
|
-
const
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
return (
|
|
368
|
-
<View
|
|
369
|
-
style={{
|
|
370
|
-
opacity: 0.6,
|
|
371
|
-
padding: 12,
|
|
372
|
-
paddingHorizontal: 14,
|
|
373
|
-
...borderRadius,
|
|
374
|
-
...supersetMargin,
|
|
375
|
-
}}
|
|
376
|
-
accessibilityLabel={formatAccessibilityLabel(name, undefined, prescription)}
|
|
377
|
-
testID="exercise-card"
|
|
378
|
-
>
|
|
379
|
-
<View className="flex-row items-center">
|
|
380
|
-
<Text
|
|
381
|
-
className="text-text-primary"
|
|
382
|
-
style={{
|
|
383
|
-
fontSize: 14,
|
|
384
|
-
fontFamily: '"Space Grotesk", sans-serif',
|
|
385
|
-
fontWeight: '700',
|
|
386
|
-
}}
|
|
387
|
-
testID="exercise-card-name"
|
|
388
|
-
>
|
|
389
|
-
{name}
|
|
390
|
-
</Text>
|
|
391
|
-
{prescription && (
|
|
392
|
-
<Text
|
|
393
|
-
className="text-text-secondary"
|
|
394
|
-
style={{
|
|
395
|
-
fontSize: 12,
|
|
396
|
-
fontFamily: 'Inter, sans-serif',
|
|
397
|
-
marginLeft: 8,
|
|
398
|
-
}}
|
|
399
|
-
testID="exercise-card-prescription"
|
|
400
|
-
>
|
|
401
|
-
{prescription}
|
|
402
|
-
</Text>
|
|
403
|
-
)}
|
|
404
|
-
<View className="flex-1" />
|
|
405
|
-
{previousBest && (
|
|
406
|
-
<Text
|
|
407
|
-
className="text-text-tertiary"
|
|
408
|
-
style={{
|
|
409
|
-
fontSize: 11,
|
|
410
|
-
fontFamily: 'Inter, sans-serif',
|
|
411
|
-
}}
|
|
412
|
-
testID="exercise-card-previous-best"
|
|
413
|
-
>
|
|
414
|
-
{previousBest}
|
|
415
|
-
</Text>
|
|
416
|
-
)}
|
|
417
|
-
</View>
|
|
418
|
-
</View>
|
|
419
|
-
)
|
|
420
|
-
}
|
|
393
|
+
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded ?? false)
|
|
394
|
+
const isControlled = expanded !== undefined
|
|
395
|
+
const isExpanded = isControlled ? expanded : internalExpanded
|
|
421
396
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
case 'expanded':
|
|
427
|
-
return <ExpandedCard {...props} />
|
|
428
|
-
case 'upcoming':
|
|
429
|
-
return <UpcomingCard {...props} />
|
|
397
|
+
const onToggle = () => {
|
|
398
|
+
const next = !isExpanded
|
|
399
|
+
onExpandedChange?.(next)
|
|
400
|
+
if (!isControlled) setInternalExpanded(next)
|
|
430
401
|
}
|
|
402
|
+
|
|
403
|
+
if (upcoming) return <UpcomingCard {...rest} onToggle={onToggle} />
|
|
404
|
+
return isExpanded ? (
|
|
405
|
+
<ExpandedCard {...rest} onToggle={onToggle} />
|
|
406
|
+
) : (
|
|
407
|
+
<CollapsedCard {...rest} onToggle={onToggle} />
|
|
408
|
+
)
|
|
431
409
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { ExerciseCardHeading } from './ExerciseCardHeading'
|
|
4
|
+
import type { SetStripSet } from './SetStrip'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `ExerciseCardHeading` — the complete, standalone session-rail heading: an
|
|
8
|
+
* `ExerciseHeading` info block over its per-set `SetStrip`. This is the unit the rail
|
|
9
|
+
* lists; it is independent of `ExerciseCard` (whose `state="rail"` delegates here).
|
|
10
|
+
* An empty `setStates` renders no strip.
|
|
11
|
+
*/
|
|
12
|
+
const meta: Meta<typeof ExerciseCardHeading> = {
|
|
13
|
+
title: 'Workout/ExerciseCardHeading',
|
|
14
|
+
component: ExerciseCardHeading,
|
|
15
|
+
tags: ['autodocs'],
|
|
16
|
+
parameters: {
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component:
|
|
20
|
+
'**Molecule.** The complete standalone session-rail heading. Composes ' +
|
|
21
|
+
'[ExerciseHeading](?path=/docs/workout-exerciseheading--docs) + ' +
|
|
22
|
+
'[SetStrip](?path=/docs/workout-setstrip--docs). ' +
|
|
23
|
+
'Used-by ↑ [SessionRail](?path=/docs/shell-sessionrail--docs); ' +
|
|
24
|
+
'[ExerciseCard](?path=/docs/workout-exercisecard--docs) `state="rail"` delegates here.',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
argTypes: {
|
|
29
|
+
unit: { control: 'select', options: ['lbs', 'kg'] },
|
|
30
|
+
indicator: {
|
|
31
|
+
control: 'select',
|
|
32
|
+
options: [undefined, 'imbalance', 'overshoot', 'velocity-loss', 'missed-reps', 'pr', 'info'],
|
|
33
|
+
},
|
|
34
|
+
stripHeight: { control: { type: 'range', min: 2, max: 16, step: 1 } },
|
|
35
|
+
dimmed: { control: 'boolean' },
|
|
36
|
+
onPress: { action: 'press' },
|
|
37
|
+
},
|
|
38
|
+
decorators: [
|
|
39
|
+
(Story) => (
|
|
40
|
+
<View style={{ width: 246, backgroundColor: '#131313' }}>
|
|
41
|
+
<Story />
|
|
42
|
+
</View>
|
|
43
|
+
),
|
|
44
|
+
],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default meta
|
|
48
|
+
type Story = StoryObj<typeof ExerciseCardHeading>
|
|
49
|
+
|
|
50
|
+
const decay = (n: number, start: number, span = 0.4): number[] =>
|
|
51
|
+
Array.from({ length: n }, (_, r) => +(start - (span * r) / Math.max(1, n - 1)).toFixed(3))
|
|
52
|
+
|
|
53
|
+
const inProgress: SetStripSet[] = [
|
|
54
|
+
{ status: 'done', velocities: decay(10, 0.72) },
|
|
55
|
+
{ status: 'active', velocities: decay(5, 0.62), planned: 10 },
|
|
56
|
+
{ status: 'todo', planned: 10 },
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
export const Default: Story = {
|
|
60
|
+
args: {
|
|
61
|
+
name: 'Cable Chest Press',
|
|
62
|
+
sets: 3,
|
|
63
|
+
reps: 10,
|
|
64
|
+
load: 90,
|
|
65
|
+
unit: 'lbs',
|
|
66
|
+
tempo: [2, 1, 2, 0],
|
|
67
|
+
indicator: 'info',
|
|
68
|
+
setStates: inProgress,
|
|
69
|
+
stripHeight: 8,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const Dimmed: Story = {
|
|
74
|
+
args: {
|
|
75
|
+
...Default.args,
|
|
76
|
+
dimmed: true,
|
|
77
|
+
setStates: [1, 2, 3].map((): SetStripSet => ({ status: 'todo', planned: 18 })),
|
|
78
|
+
},
|
|
79
|
+
parameters: {
|
|
80
|
+
docs: { description: { story: 'A not-yet-reached exercise — the whole heading dims.' } },
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const WithoutStrip: Story = {
|
|
85
|
+
args: {
|
|
86
|
+
name: 'Standing Calf Raise',
|
|
87
|
+
sets: 5,
|
|
88
|
+
reps: 20,
|
|
89
|
+
load: 25,
|
|
90
|
+
unit: 'lbs',
|
|
91
|
+
tempo: [2, 1, 2, 0],
|
|
92
|
+
setStates: [],
|
|
93
|
+
},
|
|
94
|
+
parameters: {
|
|
95
|
+
docs: {
|
|
96
|
+
description: { story: 'An empty `setStates` list renders the heading with no strip.' },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
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 { ExerciseCardHeading } from './ExerciseCardHeading'
|
|
5
|
+
import type { SetStripSet } from './SetStrip'
|
|
6
|
+
|
|
7
|
+
const setStates: SetStripSet[] = [
|
|
8
|
+
{ status: 'done', velocities: [1, 0.9] },
|
|
9
|
+
{ status: 'active', velocities: [0.6], planned: 8 },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
const baseProps = {
|
|
13
|
+
name: 'Cable Chest Press',
|
|
14
|
+
sets: 3,
|
|
15
|
+
reps: 10,
|
|
16
|
+
load: 90,
|
|
17
|
+
unit: 'lbs' as const,
|
|
18
|
+
tempo: [2, 1, 2, 0] as [number, number, number, number],
|
|
19
|
+
indicator: 'info' as const,
|
|
20
|
+
setStates,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('ExerciseCardHeading', () => {
|
|
24
|
+
it('composes the heading info block and the per-set strip', () => {
|
|
25
|
+
render(<ExerciseCardHeading {...baseProps} />)
|
|
26
|
+
expect(screen.getByTestId('exercise-card')).toBeInTheDocument()
|
|
27
|
+
expect(screen.getByTestId('exercise-heading')).toBeInTheDocument()
|
|
28
|
+
expect(screen.getByTestId('exercise-card-strip')).toBeInTheDocument()
|
|
29
|
+
expect(screen.getByTestId('set-strip')).toBeInTheDocument()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('renders the name and the sets/reps/load line', () => {
|
|
33
|
+
render(<ExerciseCardHeading {...baseProps} />)
|
|
34
|
+
expect(screen.getByTestId('exercise-card-name')).toHaveTextContent('Cable Chest Press')
|
|
35
|
+
const summary = screen.getByTestId('exercise-card-summary')
|
|
36
|
+
expect(summary).toHaveTextContent('3')
|
|
37
|
+
expect(summary).toHaveTextContent('10')
|
|
38
|
+
expect(summary).toHaveTextContent('90')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('renders no strip when setStates is empty', () => {
|
|
42
|
+
render(<ExerciseCardHeading {...baseProps} setStates={[]} />)
|
|
43
|
+
expect(screen.getByTestId('exercise-heading')).toBeInTheDocument()
|
|
44
|
+
expect(screen.queryByTestId('exercise-card-strip')).not.toBeInTheDocument()
|
|
45
|
+
expect(screen.queryByTestId('set-strip')).not.toBeInTheDocument()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('dims the whole heading when marked dimmed', () => {
|
|
49
|
+
render(<ExerciseCardHeading {...baseProps} dimmed />)
|
|
50
|
+
expect(screen.getByTestId('exercise-card')).toHaveStyle({ opacity: 0.55 })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('is full opacity by default', () => {
|
|
54
|
+
render(<ExerciseCardHeading {...baseProps} />)
|
|
55
|
+
expect(screen.getByTestId('exercise-card')).toHaveStyle({ opacity: 1 })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('fires onPress when the row is pressed', () => {
|
|
59
|
+
const onPress = vi.fn()
|
|
60
|
+
render(<ExerciseCardHeading {...baseProps} onPress={onPress} />)
|
|
61
|
+
fireEvent.click(screen.getByTestId('exercise-card-header'))
|
|
62
|
+
expect(onPress).toHaveBeenCalledOnce()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('accessibility', () => {
|
|
66
|
+
it('has no accessibility violations', async () => {
|
|
67
|
+
const { container } = render(<ExerciseCardHeading {...baseProps} onPress={vi.fn()} />)
|
|
68
|
+
const results = await axe(container)
|
|
69
|
+
expect(results).toHaveNoViolations()
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
})
|