@titan-design/react-ui 0.13.0 → 0.14.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/README.md +14 -0
- package/dist/bodymap.js +129 -76
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +129 -76
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +117 -26
- package/dist/index.d.ts +117 -26
- package/dist/index.js +677 -450
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +673 -451
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-_3Ajpiqu.d.ts → pages-CsjsUn0G.d.ts} +30 -13
- package/dist/{pages-CpW3OB3g.d.mts → pages-ZbT_pD_U.d.mts} +30 -13
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +676 -701
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +676 -701
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +31 -7
- package/dist/theme/index.d.ts +31 -7
- package/dist/theme/index.js +84 -20
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +83 -21
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +84 -20
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +84 -20
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/dist/theme/tokens.d.mts +56 -7
- package/dist/theme/tokens.d.ts +56 -7
- package/dist/theme/tokens.js +72 -20
- package/dist/theme/tokens.js.map +1 -1
- package/dist/theme/tokens.mjs +71 -21
- package/dist/theme/tokens.mjs.map +1 -1
- package/docs/render-testing-pattern.md +130 -0
- package/package.json +1 -1
- package/src/arch/Architecture.stories.tsx +3 -1
- package/src/arch/arch-graph.json +74 -72
- package/src/components/custom/ActiveWork/InitiativeBrief.tsx +2 -1
- package/src/components/custom/CircularTimer/CircularTimer.stories.tsx +8 -3
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.stories.tsx +4 -1
- package/src/components/custom/Gauge/Gauge.tsx +3 -1
- package/src/components/custom/Scatter/Scatter.stories.tsx +10 -7
- package/src/components/custom/Scatter/Scatter.tsx +6 -4
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
- package/src/components/custom/Table/Table.stories.tsx +1 -1
- package/src/components/custom/Table/Table.tsx +2 -2
- package/src/components/custom/Treemap/Treemap.stories.tsx +13 -10
- package/src/components/custom/Treemap/Treemap.tsx +4 -1
- package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +2 -1
- package/src/components/custom/Workout/BaseBadge.stories.tsx +11 -7
- package/src/components/custom/Workout/BaseBadge.tsx +3 -2
- package/src/components/custom/Workout/BodyMap.tsx +5 -3
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +13 -3
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +5 -3
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +2 -1
- package/src/components/custom/Workout/CapacityBandChart.tsx +5 -3
- package/src/components/custom/Workout/DeviationBar.test.tsx +5 -2
- package/src/components/custom/Workout/DeviationBar.tsx +5 -2
- package/src/components/custom/Workout/DualVelocityStrip.stories.tsx +7 -6
- package/src/components/custom/Workout/DualVelocityStrip.test.tsx +29 -16
- package/src/components/custom/Workout/ExerciseCard.stories.tsx +8 -8
- package/src/components/custom/Workout/ExerciseCard.test.tsx +39 -19
- package/src/components/custom/Workout/ExerciseCard.tsx +92 -167
- package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +89 -6
- package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +189 -1
- package/src/components/custom/Workout/ExerciseCardHeading.tsx +167 -18
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +3 -2
- package/src/components/custom/Workout/ExerciseHeading.tsx +192 -47
- package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +6 -5
- package/src/components/custom/Workout/InputBar.tsx +4 -2
- package/src/components/custom/Workout/IntensityBar.tsx +4 -1
- package/src/components/custom/Workout/MesoCard.tsx +2 -1
- package/src/components/custom/Workout/MesoStatusCard.tsx +11 -10
- package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +8 -3
- package/src/components/custom/Workout/PrHistoryModal.stories.tsx +12 -2
- package/src/components/custom/Workout/README.md +58 -12
- package/src/components/custom/Workout/ReadinessCheck.tsx +4 -2
- package/src/components/custom/Workout/RestTimer.stories.tsx +13 -2
- package/src/components/custom/Workout/RestTimer.tsx +4 -2
- package/src/components/custom/Workout/SegmentedBar.stories.tsx +16 -12
- package/src/components/custom/Workout/SegmentedBar.tsx +4 -3
- package/src/components/custom/Workout/SetStrip.stories.tsx +3 -2
- package/src/components/custom/Workout/SetsRepsLoad.tsx +23 -9
- package/src/components/custom/Workout/Sparkline.stories.tsx +13 -10
- package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +2 -1
- package/src/components/custom/Workout/StrengthTrendChart.tsx +8 -6
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +4 -3
- package/src/components/custom/Workout/TempoDisplay.tsx +2 -0
- package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +3 -2
- package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +1 -1
- package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +40 -0
- package/src/components/custom/Workout/VelocityStrip.test.tsx +48 -6
- package/src/components/custom/Workout/VelocityStrip.tsx +23 -14
- package/src/components/custom/Workout/ZoneTrack.stories.tsx +3 -1
- package/src/components/custom/Workout/exerciseRowState.ts +29 -0
- package/src/components/custom/Workout/index.ts +12 -2
- package/src/components/custom/Workout/metricText.stories.tsx +9 -8
- package/src/components/custom/Workout/metricText.tsx +4 -2
- package/src/components/custom/Workout/setHeadingKit.tsx +12 -10
- package/src/components/custom/Workout/velocity-story-kit.tsx +2 -2
- package/src/components/custom/charts/README.md +5 -0
- package/src/components/custom/charts/SetBarChart.test.tsx +16 -0
- package/src/components/custom/charts/SetBarChart.tsx +14 -5
- package/src/components/custom/charts/flatBarGeometry.test.ts +41 -0
- package/src/components/custom/charts/flatBarGeometry.ts +32 -0
- package/src/components/custom/stepper/Stepper.tsx +8 -6
- package/src/components/ui/alert/Alert.stories.tsx +14 -4
- package/src/components/ui/alert/Alert.tsx +24 -3
- package/src/components/ui/button/Button.tsx +12 -12
- package/src/components/ui/checkbox/Checkbox.tsx +2 -2
- package/src/components/ui/data-row/DataRow.stories.tsx +6 -2
- package/src/components/ui/drawer/Drawer.tsx +4 -1
- package/src/components/ui/icon-box/IconBox.tsx +8 -6
- package/src/components/ui/indicator/Indicator.stories.tsx +4 -3
- package/src/components/ui/list-item/ListItem.stories.tsx +7 -4
- package/src/components/ui/modal/Modal.tsx +2 -0
- package/src/components/ui/pill/Pill.stories.tsx +53 -0
- package/src/components/ui/pill/Pill.tsx +10 -6
- package/src/components/ui/popover/Popover.stories.tsx +1 -1
- package/src/components/ui/progress/Progress.tsx +9 -6
- package/src/components/ui/radio/Radio.tsx +1 -1
- package/src/components/ui/section/Section.stories.tsx +10 -10
- package/src/components/ui/select/Select.tsx +2 -1
- package/src/components/ui/spinner/Spinner.tsx +3 -1
- package/src/components/ui/stack/Stack.stories.tsx +7 -7
- package/src/components/ui/surface/Surface.stories.tsx +75 -28
- package/src/components/ui/switch/Switch.tsx +1 -1
- package/src/components/ui/tabs/Tabs.tsx +4 -2
- package/src/components/ui/toast/Toast.tsx +6 -4
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +13 -6
- package/src/stories/PresetShowcase.stories.tsx +1 -1
- package/src/test/no-raw-color.test.ts +39 -0
- package/src/theme/Alpha.stories.tsx +103 -0
- package/src/theme/ProposedTokensVW82.stories.tsx +414 -0
- package/src/theme/config.ts +12 -0
- package/src/theme/global.css +15 -3
- package/src/theme/subtle-tone-weight.test.ts +163 -0
- package/src/theme/tailwind-var-opacity.test.ts +163 -0
- package/src/theme/tokens/primitives.ts +33 -0
- package/src/theme/tokens/semantic-hex-guard.test.ts +322 -0
- package/src/theme/tokens/semantic.ts +56 -20
- package/src/utils/colors.alpha-adoption.test.ts +408 -0
- package/tailwind.config.js +50 -9
|
@@ -3,6 +3,7 @@ import { render, screen, fireEvent, within } from '@testing-library/react'
|
|
|
3
3
|
import { axe } from 'jest-axe'
|
|
4
4
|
import { ExerciseCard } from './ExerciseCard'
|
|
5
5
|
import type { SetRowProps } from './SetRow'
|
|
6
|
+
import { exerciseRowStateColor } from './exerciseRowState'
|
|
6
7
|
import { resolveColor } from '../../../theme/resolve-color'
|
|
7
8
|
|
|
8
9
|
const baseCollapsedProps = {
|
|
@@ -40,17 +41,21 @@ describe('ExerciseCard', () => {
|
|
|
40
41
|
expect(screen.getByTestId('exercise-card-name')).toHaveTextContent('Bench Press')
|
|
41
42
|
})
|
|
42
43
|
|
|
43
|
-
it('renders
|
|
44
|
+
it('renders the prescription through the shared SetsRepsLoad line', () => {
|
|
44
45
|
render(<ExerciseCard {...baseCollapsedProps} />)
|
|
45
|
-
|
|
46
|
+
const summary = screen.getByTestId('exercise-card-summary')
|
|
47
|
+
expect(summary).toHaveTextContent('3')
|
|
48
|
+
expect(summary).toHaveTextContent('6')
|
|
49
|
+
expect(summary).toHaveTextContent('175')
|
|
50
|
+
expect(summary).toHaveTextContent('lbs')
|
|
46
51
|
})
|
|
47
52
|
|
|
48
|
-
it('
|
|
53
|
+
it('surfaces a PR through the indicator chip, as the expanded card already did', () => {
|
|
49
54
|
render(<ExerciseCard {...baseCollapsedProps} isPR />)
|
|
50
|
-
expect(screen.
|
|
55
|
+
expect(screen.getByLabelText('Personal record')).toBeInTheDocument()
|
|
51
56
|
})
|
|
52
57
|
|
|
53
|
-
it('renders
|
|
58
|
+
it('renders logged and remaining sets as one SetStrip', () => {
|
|
54
59
|
render(
|
|
55
60
|
<ExerciseCard
|
|
56
61
|
{...baseCollapsedProps}
|
|
@@ -61,21 +66,22 @@ describe('ExerciseCard', () => {
|
|
|
61
66
|
totalPlannedSets={4}
|
|
62
67
|
/>
|
|
63
68
|
)
|
|
64
|
-
expect(screen.getByTestId('exercise-card-
|
|
65
|
-
expect(screen.getByTestId('
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
it('renders placeholder strips for remaining planned sets', () => {
|
|
69
|
-
render(
|
|
70
|
-
<ExerciseCard {...baseCollapsedProps} setVelocities={[[1.1, 0.95]]} totalPlannedSets={3} />
|
|
69
|
+
expect(screen.getByTestId('exercise-card-strip')).toBeInTheDocument()
|
|
70
|
+
expect(screen.getByTestId('set-strip')).toHaveAccessibleName(
|
|
71
|
+
'Set progress: 2 done, 0 in progress, 2 upcoming'
|
|
71
72
|
)
|
|
72
|
-
expect(screen.getByTestId('exercise-card-placeholder-0')).toBeInTheDocument()
|
|
73
|
-
expect(screen.getByTestId('exercise-card-placeholder-1')).toBeInTheDocument()
|
|
74
73
|
})
|
|
75
74
|
|
|
76
|
-
it('does not render
|
|
75
|
+
it('does not render the strip when there are no velocities and no planned sets', () => {
|
|
77
76
|
render(<ExerciseCard {...baseCollapsedProps} />)
|
|
78
|
-
expect(screen.queryByTestId('exercise-card-
|
|
77
|
+
expect(screen.queryByTestId('exercise-card-strip')).not.toBeInTheDocument()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('washes the row when selected', () => {
|
|
81
|
+
render(<ExerciseCard {...baseCollapsedProps} isSelected />)
|
|
82
|
+
expect(screen.getByTestId('exercise-card')).toHaveStyle({
|
|
83
|
+
backgroundColor: exerciseRowStateColor('selected'),
|
|
84
|
+
})
|
|
79
85
|
})
|
|
80
86
|
})
|
|
81
87
|
|
|
@@ -183,6 +189,18 @@ describe('ExerciseCard', () => {
|
|
|
183
189
|
render(<ExerciseCard {...expandedProps} />)
|
|
184
190
|
expect(screen.queryByTestId('tempo-display')).not.toBeInTheDocument()
|
|
185
191
|
})
|
|
192
|
+
|
|
193
|
+
// VW-97 item 3: ExerciseCardHeading takes `indicator`, but ExerciseCard didn't forward one —
|
|
194
|
+
// so the expanded rail row lost its indicator badge. Already fixed on main; this pins it.
|
|
195
|
+
it('forwards `indicator` to the header, so the expanded rail row keeps its badge', () => {
|
|
196
|
+
render(<ExerciseCard {...expandedProps} indicator="info" />)
|
|
197
|
+
expect(screen.getByTestId('exercise-indicator')).toBeInTheDocument()
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('falls back to the `pr` indicator from `isPR` when no explicit indicator is given', () => {
|
|
201
|
+
render(<ExerciseCard {...expandedProps} isPR />)
|
|
202
|
+
expect(screen.getByTestId('exercise-indicator')).toBeInTheDocument()
|
|
203
|
+
})
|
|
186
204
|
})
|
|
187
205
|
|
|
188
206
|
describe('upcoming', () => {
|
|
@@ -267,10 +285,12 @@ describe('ExerciseCard', () => {
|
|
|
267
285
|
})
|
|
268
286
|
})
|
|
269
287
|
|
|
288
|
+
// The row IS the card now, so the superset chrome lands on the row root rather
|
|
289
|
+
// than on a Pressable's inner View.
|
|
270
290
|
describe('superset position border radius', () => {
|
|
271
291
|
it('applies first position border radius', () => {
|
|
272
292
|
render(<ExerciseCard {...baseCollapsedProps} supersetPosition="first" />)
|
|
273
|
-
const container = screen.getByTestId('exercise-card')
|
|
293
|
+
const container = screen.getByTestId('exercise-card')
|
|
274
294
|
expect(container).toHaveStyle({
|
|
275
295
|
borderTopLeftRadius: '12px',
|
|
276
296
|
borderTopRightRadius: '12px',
|
|
@@ -281,7 +301,7 @@ describe('ExerciseCard', () => {
|
|
|
281
301
|
|
|
282
302
|
it('applies last position border radius', () => {
|
|
283
303
|
render(<ExerciseCard {...baseCollapsedProps} supersetPosition="last" />)
|
|
284
|
-
const container = screen.getByTestId('exercise-card')
|
|
304
|
+
const container = screen.getByTestId('exercise-card')
|
|
285
305
|
expect(container).toHaveStyle({
|
|
286
306
|
borderTopLeftRadius: '8px',
|
|
287
307
|
borderTopRightRadius: '8px',
|
|
@@ -292,7 +312,7 @@ describe('ExerciseCard', () => {
|
|
|
292
312
|
|
|
293
313
|
it('applies middle position border radius', () => {
|
|
294
314
|
render(<ExerciseCard {...baseCollapsedProps} supersetPosition="middle" />)
|
|
295
|
-
const container = screen.getByTestId('exercise-card')
|
|
315
|
+
const container = screen.getByTestId('exercise-card')
|
|
296
316
|
expect(container).toHaveStyle({
|
|
297
317
|
borderTopLeftRadius: '8px',
|
|
298
318
|
borderTopRightRadius: '8px',
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
2
|
import { useState } from 'react'
|
|
3
|
-
import { View
|
|
4
|
-
import {
|
|
5
|
-
import { PlaceholderStrip } from './PlaceholderStrip'
|
|
6
|
-
import { PrBadge } from './PrBadge'
|
|
3
|
+
import { View } from 'react-native'
|
|
4
|
+
import { type VelocityZoneBandProp } from './VelocityStrip'
|
|
7
5
|
import { SetRow, type SetRowProps } from './SetRow'
|
|
8
6
|
import { type SetStripSet } from './SetStrip'
|
|
9
7
|
import { SetTableHeader } from './SetTableHeader'
|
|
10
8
|
import { ExerciseCardHeading } from './ExerciseCardHeading'
|
|
11
9
|
import { type ExerciseIndicatorKind } from './ExerciseIndicator'
|
|
12
|
-
import { Typography } from '../Typography'
|
|
13
|
-
import { roundWeight } from '../../../utils/workout-format'
|
|
14
10
|
import { resolveColor } from '../../../theme/resolve-color'
|
|
15
11
|
|
|
16
12
|
export interface ExerciseCardProps {
|
|
@@ -34,12 +30,19 @@ export interface ExerciseCardProps {
|
|
|
34
30
|
unit: 'lbs' | 'kg'
|
|
35
31
|
}
|
|
36
32
|
isPR?: boolean
|
|
37
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Collapsed glance: per-set logged velocities. Projected onto `setStates` for the
|
|
35
|
+
* row's `SetStrip` — pass `setStates` directly for anything richer than done/todo.
|
|
36
|
+
*/
|
|
38
37
|
setVelocities?: number[][]
|
|
39
|
-
/**
|
|
38
|
+
/** Velocity-zone bands shared across this exercise's sets (WA bands). Expanded body only. */
|
|
40
39
|
velocityZones?: readonly VelocityZoneBandProp[]
|
|
41
|
-
/** Collapsed glance: total planned sets (
|
|
40
|
+
/** Collapsed glance: total planned sets (todo bars fill the remainder). */
|
|
42
41
|
totalPlannedSets?: number
|
|
42
|
+
/** The row the user has chosen — a persistent wash on the collapsed / upcoming row. */
|
|
43
|
+
isSelected?: boolean
|
|
44
|
+
/** This exercise is being performed right now — the name takes the live tone. */
|
|
45
|
+
isLive?: boolean
|
|
43
46
|
/** Expanded body: the per-set rows (done / live / todo). */
|
|
44
47
|
sets?: SetRowProps[]
|
|
45
48
|
tempo?: [number, number, number, number]
|
|
@@ -47,11 +50,14 @@ export interface ExerciseCardProps {
|
|
|
47
50
|
previousBest?: string
|
|
48
51
|
supersetPosition?: 'first' | 'last' | 'middle' | null
|
|
49
52
|
supersetColor?: string
|
|
50
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Per-set strip override for the collapsed and expanded rows; derived from
|
|
55
|
+
* `setVelocities` (collapsed) or `sets` (expanded) when omitted.
|
|
56
|
+
*/
|
|
51
57
|
setStates?: SetStripSet[]
|
|
52
|
-
/** A small PR / issue / info chip in the title line
|
|
58
|
+
/** A small PR / issue / info chip in the title line. */
|
|
53
59
|
indicator?: ExerciseIndicatorKind
|
|
54
|
-
/**
|
|
60
|
+
/** Row strip height in px. Default 8. */
|
|
55
61
|
stripHeight?: number
|
|
56
62
|
}
|
|
57
63
|
|
|
@@ -105,19 +111,28 @@ function getSupersetMargin(
|
|
|
105
111
|
return {}
|
|
106
112
|
}
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return
|
|
114
|
+
/** The chrome a card wraps its row in: superset corner radii + the inter-card margin. */
|
|
115
|
+
function supersetChrome(position: ExerciseCardProps['supersetPosition']): Record<string, number> {
|
|
116
|
+
return { ...getSupersetBorderRadius(position), ...getSupersetMargin(position) }
|
|
111
117
|
}
|
|
112
118
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
/** Project the collapsed glance's velocities + planned total onto the strip's per-set states. */
|
|
120
|
+
function deriveCollapsedSetStates(
|
|
121
|
+
setVelocities: number[][] | undefined,
|
|
122
|
+
totalPlannedSets: number | undefined,
|
|
123
|
+
plannedReps: number
|
|
124
|
+
): SetStripSet[] {
|
|
125
|
+
const done = (setVelocities ?? []).map(
|
|
126
|
+
(velocities): SetStripSet => ({ status: 'done', velocities })
|
|
127
|
+
)
|
|
128
|
+
const remaining = Math.max(0, (totalPlannedSets ?? 0) - done.length)
|
|
129
|
+
return [
|
|
130
|
+
...done,
|
|
131
|
+
...Array.from(
|
|
132
|
+
{ length: remaining },
|
|
133
|
+
(): SetStripSet => ({ status: 'todo', planned: plannedReps })
|
|
134
|
+
),
|
|
135
|
+
]
|
|
121
136
|
}
|
|
122
137
|
|
|
123
138
|
function CollapsedCard({
|
|
@@ -125,90 +140,40 @@ function CollapsedCard({
|
|
|
125
140
|
onToggle,
|
|
126
141
|
summary,
|
|
127
142
|
isPR,
|
|
143
|
+
indicator,
|
|
128
144
|
setVelocities,
|
|
129
|
-
|
|
145
|
+
setStates,
|
|
130
146
|
totalPlannedSets,
|
|
147
|
+
stripHeight,
|
|
131
148
|
supersetPosition,
|
|
149
|
+
isSelected,
|
|
150
|
+
isLive,
|
|
132
151
|
}: CardBodyProps) {
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
accessibilityLabel={formatAccessibilityLabel(name, summary, undefined)}
|
|
146
|
-
testID="exercise-card"
|
|
147
|
-
>
|
|
148
|
-
<View
|
|
149
|
-
className={pressed ? 'bg-surface-raised' : undefined}
|
|
150
|
-
style={{
|
|
151
|
-
padding: 12,
|
|
152
|
-
paddingHorizontal: 14,
|
|
153
|
-
cursor: 'pointer',
|
|
154
|
-
...borderRadius,
|
|
155
|
-
...supersetMargin,
|
|
156
|
-
}}
|
|
157
|
-
>
|
|
158
|
-
<View className="flex-row items-center" testID="exercise-card-header">
|
|
159
|
-
{/* 14px/700 Space Grotesk is `h6` at the `sm` step; the face and the heading
|
|
160
|
-
role come from the variant, the weight and size from the scale. */}
|
|
161
|
-
<Typography
|
|
162
|
-
variant="h6"
|
|
163
|
-
color="primary"
|
|
164
|
-
className="text-sm font-bold leading-[normal]"
|
|
165
|
-
testID="exercise-card-name"
|
|
166
|
-
>
|
|
167
|
-
{name}
|
|
168
|
-
</Typography>
|
|
169
|
-
<View className="flex-1" />
|
|
170
|
-
{summary && (
|
|
171
|
-
/* The demo's "body" face is Inter, which is the library's `font-sans`
|
|
172
|
-
(the mapping B1 established for PrBadge), not Typography's `font-body`. */
|
|
173
|
-
<Typography
|
|
174
|
-
variant="caption"
|
|
175
|
-
color="secondary"
|
|
176
|
-
className="font-sans mr-2 leading-[normal]"
|
|
177
|
-
testID="exercise-card-summary"
|
|
178
|
-
>
|
|
179
|
-
{formatSummary(summary)}
|
|
180
|
-
</Typography>
|
|
181
|
-
)}
|
|
182
|
-
{isPR && (
|
|
183
|
-
<View style={{ marginLeft: 4 }}>
|
|
184
|
-
<PrBadge type="e1rm" compact animate={false} />
|
|
185
|
-
</View>
|
|
186
|
-
)}
|
|
187
|
-
</View>
|
|
152
|
+
const plannedReps = typeof summary?.reps === 'number' ? summary.reps : 0
|
|
153
|
+
const row = {
|
|
154
|
+
density: 'compact' as const,
|
|
155
|
+
name,
|
|
156
|
+
indicator: indicator ?? (isPR ? ('pr' as const) : undefined),
|
|
157
|
+
setStates: setStates ?? deriveCollapsedSetStates(setVelocities, totalPlannedSets, plannedReps),
|
|
158
|
+
stripHeight,
|
|
159
|
+
isSelected,
|
|
160
|
+
isLive,
|
|
161
|
+
onPress: onToggle,
|
|
162
|
+
style: supersetChrome(supersetPosition),
|
|
163
|
+
}
|
|
188
164
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
))}
|
|
202
|
-
{Array.from({ length: remaining }, (_, i) => (
|
|
203
|
-
<PlaceholderStrip
|
|
204
|
-
key={`placeholder-${i}`}
|
|
205
|
-
testID={`exercise-card-placeholder-${i}`}
|
|
206
|
-
/>
|
|
207
|
-
))}
|
|
208
|
-
</View>
|
|
209
|
-
)}
|
|
210
|
-
</View>
|
|
211
|
-
</Pressable>
|
|
165
|
+
// Two call shapes, not a conditional spread: the prescription union only narrows
|
|
166
|
+
// when every field of a member is present at the call site.
|
|
167
|
+
return summary ? (
|
|
168
|
+
<ExerciseCardHeading
|
|
169
|
+
{...row}
|
|
170
|
+
sets={summary.sets}
|
|
171
|
+
reps={summary.reps}
|
|
172
|
+
load={summary.weight}
|
|
173
|
+
unit={summary.unit}
|
|
174
|
+
/>
|
|
175
|
+
) : (
|
|
176
|
+
<ExerciseCardHeading {...row} />
|
|
212
177
|
)
|
|
213
178
|
}
|
|
214
179
|
|
|
@@ -243,9 +208,8 @@ function ExpandedCard({
|
|
|
243
208
|
stripHeight = 8,
|
|
244
209
|
velocityZones,
|
|
245
210
|
supersetPosition,
|
|
211
|
+
isLive,
|
|
246
212
|
}: CardBodyProps) {
|
|
247
|
-
const borderRadius = getSupersetBorderRadius(supersetPosition)
|
|
248
|
-
const supersetMargin = getSupersetMargin(supersetPosition)
|
|
249
213
|
// Summary is the card-level authority for the weight column; fall back to the
|
|
250
214
|
// first set's unit, then lbs. (Mixed per-set units keep the card-level label.)
|
|
251
215
|
const unit = summary?.unit ?? sets?.[0]?.unit ?? 'lbs'
|
|
@@ -258,11 +222,7 @@ function ExpandedCard({
|
|
|
258
222
|
return (
|
|
259
223
|
<View
|
|
260
224
|
className="bg-surface-elevated border-hairline"
|
|
261
|
-
style={{
|
|
262
|
-
borderWidth: 1,
|
|
263
|
-
...borderRadius,
|
|
264
|
-
...supersetMargin,
|
|
265
|
-
}}
|
|
225
|
+
style={{ borderWidth: 1, ...supersetChrome(supersetPosition) }}
|
|
266
226
|
testID="exercise-card"
|
|
267
227
|
>
|
|
268
228
|
<ExerciseCardHeading
|
|
@@ -275,6 +235,7 @@ function ExpandedCard({
|
|
|
275
235
|
indicator={headerIndicator}
|
|
276
236
|
setStates={headerStates}
|
|
277
237
|
stripHeight={stripHeight}
|
|
238
|
+
isLive={isLive}
|
|
278
239
|
onPress={onToggle}
|
|
279
240
|
testID="exercise-card-heading"
|
|
280
241
|
/>
|
|
@@ -306,72 +267,36 @@ function UpcomingCard({
|
|
|
306
267
|
name,
|
|
307
268
|
prescription,
|
|
308
269
|
previousBest,
|
|
270
|
+
indicator,
|
|
309
271
|
supersetPosition,
|
|
272
|
+
isSelected,
|
|
310
273
|
onToggle,
|
|
311
274
|
}: CardBodyProps) {
|
|
312
|
-
const
|
|
313
|
-
|
|
275
|
+
const row = {
|
|
276
|
+
density: 'upcoming' as const,
|
|
277
|
+
name,
|
|
278
|
+
previousBest,
|
|
279
|
+
indicator,
|
|
280
|
+
isSelected,
|
|
281
|
+
onPress: onToggle,
|
|
282
|
+
style: supersetChrome(supersetPosition),
|
|
283
|
+
}
|
|
314
284
|
|
|
315
|
-
return (
|
|
316
|
-
<
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
opacity: 0.6,
|
|
320
|
-
padding: 12,
|
|
321
|
-
paddingHorizontal: 14,
|
|
322
|
-
...borderRadius,
|
|
323
|
-
...supersetMargin,
|
|
324
|
-
}}
|
|
325
|
-
accessibilityLabel={formatAccessibilityLabel(name, undefined, prescription)}
|
|
326
|
-
testID="exercise-card"
|
|
327
|
-
>
|
|
328
|
-
<View className="flex-row items-center">
|
|
329
|
-
{/* Name never truncates (no numberOfLines); prescription + previousBest ellipsize first. */}
|
|
330
|
-
<Typography
|
|
331
|
-
variant="h6"
|
|
332
|
-
color="primary"
|
|
333
|
-
className="shrink-0 text-sm font-bold leading-[normal]"
|
|
334
|
-
testID="exercise-card-name"
|
|
335
|
-
>
|
|
336
|
-
{name}
|
|
337
|
-
</Typography>
|
|
338
|
-
{prescription && (
|
|
339
|
-
<Typography
|
|
340
|
-
variant="caption"
|
|
341
|
-
color="secondary"
|
|
342
|
-
maxLines={1}
|
|
343
|
-
className="font-sans shrink ml-2 leading-[normal]"
|
|
344
|
-
testID="exercise-card-prescription"
|
|
345
|
-
>
|
|
346
|
-
{prescription}
|
|
347
|
-
</Typography>
|
|
348
|
-
)}
|
|
349
|
-
<View className="flex-1" style={{ minWidth: 8 }} />
|
|
350
|
-
{previousBest && (
|
|
351
|
-
/* 11px is off the type scale; `2xs` (10px) is the step below, as B2's
|
|
352
|
-
WorkoutCard date took for the same size. */
|
|
353
|
-
<Typography
|
|
354
|
-
variant="caption"
|
|
355
|
-
color="tertiary"
|
|
356
|
-
maxLines={1}
|
|
357
|
-
className="font-sans shrink text-2xs leading-[normal]"
|
|
358
|
-
testID="exercise-card-previous-best"
|
|
359
|
-
>
|
|
360
|
-
{previousBest}
|
|
361
|
-
</Typography>
|
|
362
|
-
)}
|
|
363
|
-
</View>
|
|
364
|
-
</View>
|
|
365
|
-
</Pressable>
|
|
285
|
+
return prescription !== undefined ? (
|
|
286
|
+
<ExerciseCardHeading {...row} prescription={prescription} />
|
|
287
|
+
) : (
|
|
288
|
+
<ExerciseCardHeading {...row} />
|
|
366
289
|
)
|
|
367
290
|
}
|
|
368
291
|
|
|
369
292
|
/**
|
|
370
|
-
* The data-contract exercise card, in three representations
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
* -
|
|
374
|
-
*
|
|
293
|
+
* The data-contract exercise card, in three representations — all three now ONE
|
|
294
|
+
* {@link ExerciseCardHeading}, selected by its `density`, rather than three hand-rolled
|
|
295
|
+
* heads:
|
|
296
|
+
* - `upcoming` — a dimmed, not-yet-reached row (`density="upcoming"`).
|
|
297
|
+
* - collapsed — a glance: name + prescription + the per-set strip (`density="compact"`).
|
|
298
|
+
* - expanded — the `rail` heading over the SET · REPS · LBS · RPE body (one
|
|
299
|
+
* {@link SetRow} per set).
|
|
375
300
|
*
|
|
376
301
|
* Expand is controlled (`expanded` + `onExpandedChange`) or uncontrolled
|
|
377
302
|
* (`defaultExpanded`, internal state). `upcoming` overrides expand.
|
|
@@ -4,10 +4,10 @@ import type { SetStripSet } from './SetStrip'
|
|
|
4
4
|
import { Surface } from '../../ui/surface'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* `ExerciseCardHeading` —
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* `ExerciseCardHeading` — THE exercise row. One component for the three densities the
|
|
8
|
+
* workout surfaces list an exercise in: the session-rail heading (`rail`), the collapsed
|
|
9
|
+
* card row (`compact`) and the not-yet-reached row (`upcoming`). An `ExerciseHeading` info
|
|
10
|
+
* block over its per-set `SetStrip`; an empty `setStates` renders no strip.
|
|
11
11
|
*/
|
|
12
12
|
const meta: Meta<typeof ExerciseCardHeading> = {
|
|
13
13
|
title: 'Custom/Workout/ExerciseCardHeading',
|
|
@@ -17,15 +17,22 @@ const meta: Meta<typeof ExerciseCardHeading> = {
|
|
|
17
17
|
docs: {
|
|
18
18
|
description: {
|
|
19
19
|
component:
|
|
20
|
-
'**Molecule.** The
|
|
20
|
+
'**Molecule.** The exercise row, in three densities driven by props rather than by ' +
|
|
21
|
+
'three call sites (TD-03.56). Composes ' +
|
|
21
22
|
'[ExerciseHeading](?path=/docs/custom-workout-exerciseheading--docs) + ' +
|
|
22
23
|
'[SetStrip](?path=/docs/custom-workout-setstrip--docs). ' +
|
|
24
|
+
'Interaction states (TD-03.55) are `interactive-*` token washes — press > selection > ' +
|
|
25
|
+
'hover — plus an `isLive` name tone. All static: this row renders on the wall during a ' +
|
|
26
|
+
'set, so nothing here animates. Hover is web-only; on a touch surface the row simply ' +
|
|
27
|
+
'has no hover state and selection carries the same meaning. ' +
|
|
23
28
|
'Used-by ↑ [SessionRail](?path=/docs/shell-sessionrail--docs); ' +
|
|
24
|
-
'[ExerciseCard](?path=/docs/custom-workout-exercisecard--docs)
|
|
29
|
+
'[ExerciseCard](?path=/docs/custom-workout-exercisecard--docs) delegates all three of its ' +
|
|
30
|
+
'representations here.',
|
|
25
31
|
},
|
|
26
32
|
},
|
|
27
33
|
},
|
|
28
34
|
argTypes: {
|
|
35
|
+
density: { control: 'inline-radio', options: ['rail', 'compact', 'upcoming'] },
|
|
29
36
|
unit: { control: 'select', options: ['lbs', 'kg'] },
|
|
30
37
|
indicator: {
|
|
31
38
|
control: 'select',
|
|
@@ -33,6 +40,8 @@ const meta: Meta<typeof ExerciseCardHeading> = {
|
|
|
33
40
|
},
|
|
34
41
|
stripHeight: { control: { type: 'range', min: 2, max: 16, step: 1 } },
|
|
35
42
|
dimmed: { control: 'boolean' },
|
|
43
|
+
isSelected: { control: 'boolean' },
|
|
44
|
+
isLive: { control: 'boolean' },
|
|
36
45
|
onPress: { action: 'press' },
|
|
37
46
|
},
|
|
38
47
|
decorators: [
|
|
@@ -68,6 +77,80 @@ export const Default: Story = {
|
|
|
68
77
|
setStates: inProgress,
|
|
69
78
|
stripHeight: 8,
|
|
70
79
|
},
|
|
80
|
+
parameters: {
|
|
81
|
+
docs: {
|
|
82
|
+
description: {
|
|
83
|
+
story: 'The `rail` density: two lines (name, then prescription beside the tempo).',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const Compact: Story = {
|
|
90
|
+
args: {
|
|
91
|
+
...Default.args,
|
|
92
|
+
density: 'compact',
|
|
93
|
+
},
|
|
94
|
+
parameters: {
|
|
95
|
+
docs: {
|
|
96
|
+
description: {
|
|
97
|
+
story:
|
|
98
|
+
'The collapsed card row: name and prescription on ONE line, strip below. The tempo is ' +
|
|
99
|
+
'dropped — a single line has no room for a second metric lockup.',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const Upcoming: Story = {
|
|
106
|
+
args: {
|
|
107
|
+
name: 'Overhead Press',
|
|
108
|
+
density: 'upcoming',
|
|
109
|
+
prescription: '3×8-12 @ RPE 8',
|
|
110
|
+
previousBest: '135 lbs × 10',
|
|
111
|
+
},
|
|
112
|
+
parameters: {
|
|
113
|
+
docs: {
|
|
114
|
+
description: {
|
|
115
|
+
story:
|
|
116
|
+
'A not-yet-reached exercise: `compact`, dimmed by its own density, with the previous ' +
|
|
117
|
+
'best pinned right. `prescription` is the free-text alternative to the structured ' +
|
|
118
|
+
'`sets` / `reps` / `load` triple, for an exercise whose numbers are not loaded yet.',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const Live: Story = {
|
|
125
|
+
args: {
|
|
126
|
+
...Default.args,
|
|
127
|
+
isLive: true,
|
|
128
|
+
},
|
|
129
|
+
parameters: {
|
|
130
|
+
docs: {
|
|
131
|
+
description: {
|
|
132
|
+
story:
|
|
133
|
+
'The exercise being performed right now — the name takes `status-live`. A tone change, ' +
|
|
134
|
+
'never motion: this row is on a wall display during a set.',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const Selected: Story = {
|
|
141
|
+
args: {
|
|
142
|
+
...Default.args,
|
|
143
|
+
isSelected: true,
|
|
144
|
+
},
|
|
145
|
+
parameters: {
|
|
146
|
+
docs: {
|
|
147
|
+
description: {
|
|
148
|
+
story:
|
|
149
|
+
'The row the user has chosen: a persistent `interactive-selected` wash. Hover the row ' +
|
|
150
|
+
'to see the lighter `interactive-hover` wash it outranks.',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
71
154
|
}
|
|
72
155
|
|
|
73
156
|
export const Dimmed: Story = {
|