@titan-design/react-ui 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bodymap.js +173 -343
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +173 -343
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +416 -18
- package/dist/index.d.ts +416 -18
- package/dist/index.js +2512 -1307
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2480 -1299
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-DyEx-lBf.d.ts → pages-B1hqnGIC.d.ts} +149 -15
- package/dist/{pages-D7hOD4Vj.d.mts → pages-CvsFqNtx.d.mts} +149 -15
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +1644 -1575
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1645 -1576
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +409 -415
- package/dist/theme/index.d.ts +409 -415
- package/dist/theme/index.js +234 -408
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +227 -399
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +141 -140
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +141 -140
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +8 -3
- package/src/arch/Architecture.stories.tsx +9 -9
- package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
- package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
- package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
- package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
- package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
- package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
- package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
- package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
- package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
- package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
- package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
- package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
- package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
- package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
- package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
- package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
- package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
- package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
- package/src/components/custom/Fatigue/README.md +93 -0
- package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
- package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
- package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
- package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
- package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
- package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
- package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
- package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
- package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
- package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
- package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
- package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
- package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
- package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
- package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
- package/src/components/custom/Fatigue/index.ts +41 -0
- package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
- package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
- package/src/components/custom/Sidebar/Sidebar.tsx +4 -4
- package/src/components/custom/Table/Table.tsx +2 -2
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +3 -3
- package/src/components/custom/Workout/BaseBadge.test.tsx +6 -1
- package/src/components/custom/Workout/BaseBadge.tsx +1 -1
- package/src/components/custom/Workout/BodyMap.tsx +2 -2
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +2 -2
- package/src/components/custom/Workout/DeviationBar.test.tsx +2 -1
- package/src/components/custom/Workout/DeviationBar.tsx +3 -2
- package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
- package/src/components/custom/Workout/ExerciseCard.test.tsx +7 -7
- package/src/components/custom/Workout/ExerciseCard.tsx +5 -3
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +11 -5
- package/src/components/custom/Workout/InputBar.tsx +2 -2
- package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +1 -1
- package/src/components/custom/Workout/LiveAuraFrame.test.tsx +1 -1
- package/src/components/custom/Workout/LiveAuraFrame.tsx +3 -3
- package/src/components/custom/Workout/MesoCard.tsx +3 -1
- package/src/components/custom/Workout/MuscleGroupChip.tsx +4 -3
- package/src/components/custom/Workout/PrHistoryModal.tsx +2 -2
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +1 -1
- package/src/components/custom/Workout/README.md +62 -16
- package/src/components/custom/Workout/ReadinessCheck.tsx +2 -2
- package/src/components/custom/Workout/RestTimer.tsx +2 -2
- package/src/components/custom/Workout/S3SetTypes.stories.tsx +3 -3
- package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
- package/src/components/custom/Workout/SessionHeader.stories.tsx +2 -2
- package/src/components/custom/Workout/SessionHeader.tsx +1 -1
- package/src/components/custom/Workout/SessionRail.stories.tsx +2 -2
- package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
- package/src/components/custom/Workout/SessionRail.tsx +63 -18
- package/src/components/custom/Workout/SetBar.tsx +3 -3
- package/src/components/custom/Workout/SetRow.test.tsx +12 -9
- package/src/components/custom/Workout/SetRow.tsx +9 -2
- package/src/components/custom/Workout/StatusDot.tsx +5 -4
- package/src/components/custom/Workout/StrengthTrendChart.tsx +1 -1
- package/src/components/custom/Workout/TrainingStatusPage.tsx +2 -2
- package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
- package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
- package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
- package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
- package/src/components/custom/Workout/VelocityStrip.test.tsx +199 -79
- package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
- package/src/components/custom/Workout/VolumeLandmarkBar.tsx +3 -3
- package/src/components/custom/Workout/WeightBadge.test.tsx +6 -1
- package/src/components/custom/Workout/WeightBadge.tsx +2 -1
- package/src/components/custom/Workout/WorkoutCard.tsx +3 -1
- package/src/components/custom/Workout/WorkoutPill.tsx +5 -2
- package/src/components/custom/Workout/ZoneTrack.tsx +4 -4
- package/src/components/custom/Workout/index.ts +3 -0
- package/src/components/custom/Workout/setHeadingKit.tsx +14 -8
- package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
- package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
- package/src/components/custom/charts/SetBarChart.tsx +651 -0
- package/src/components/custom/charts/live-rep-growth.ts +91 -0
- package/src/components/custom/index.ts +1 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
- package/src/components/custom/stepper/Stepper.tsx +3 -3
- package/src/components/shell/DeviceMenu.tsx +2 -2
- package/src/components/shell/SideNav.tsx +1 -1
- package/src/components/shell/TopBar.tsx +1 -1
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
- package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
- package/src/components/ui/avatar/Avatar.tsx +2 -2
- package/src/components/ui/badge/Badge.tsx +2 -2
- package/src/components/ui/card/Card.tsx +3 -3
- package/src/components/ui/checkbox/Checkbox.tsx +1 -1
- package/src/components/ui/collapse/Collapse.tsx +1 -1
- package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
- package/src/components/ui/drawer/Drawer.tsx +3 -3
- package/src/components/ui/form-field/FormField.tsx +1 -1
- package/src/components/ui/help-tip/HelpTip.tsx +1 -1
- package/src/components/ui/menu/Menu.tsx +2 -2
- package/src/components/ui/pill/Pill.tsx +2 -2
- package/src/components/ui/popover/Popover.stories.tsx +2 -2
- package/src/components/ui/popover/Popover.tsx +1 -1
- package/src/components/ui/progress/Progress.tsx +3 -3
- package/src/components/ui/radio/Radio.stories.tsx +1 -1
- package/src/components/ui/radio/Radio.tsx +4 -4
- package/src/components/ui/select/Select.tsx +3 -3
- package/src/components/ui/surface/Surface.stories.tsx +3 -3
- package/src/components/ui/surface/Surface.test.tsx +32 -19
- package/src/components/ui/surface/Surface.tsx +3 -3
- package/src/components/ui/surface/SurfaceContext.ts +19 -29
- package/src/components/ui/surface/surface.contract.test.ts +57 -44
- package/src/components/ui/switch/Switch.tsx +1 -1
- package/src/components/ui/tabs/Tabs.tsx +2 -2
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +5 -5
- package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +1 -1
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +36 -22
- package/src/stories/PresetShowcase.stories.tsx +21 -21
- package/src/test/raw-color-patterns.test.ts +71 -0
- package/src/theme/ColorPalettes.stories.tsx +874 -0
- package/src/theme/ColorPrimitives.stories.tsx +348 -0
- package/src/theme/Depth.stories.tsx +312 -0
- package/src/theme/DepthCalibration.stories.tsx +655 -0
- package/src/theme/ThemeProvider.tsx +1 -1
- package/src/theme/barrel.ts +7 -14
- package/src/theme/color-stories.coverage.test.ts +125 -0
- package/src/theme/color-story-kit.tsx +170 -0
- package/src/theme/color-utils.ts +197 -0
- package/src/theme/config.ts +0 -8
- package/src/theme/depth-calibration.test.tsx +97 -0
- package/src/theme/elevation.test.ts +62 -1
- package/src/theme/elevation.ts +34 -8
- package/src/theme/global.css +47 -53
- package/src/theme/grey-ramp.test.ts +166 -0
- package/src/theme/materials.test.ts +106 -0
- package/src/theme/materials.ts +211 -0
- package/src/theme/presets/audiobook.ts +4 -4
- package/src/theme/presets/types.ts +3 -3
- package/src/theme/semantic-contrast.test.ts +83 -0
- package/src/theme/tokens/primitives.ts +62 -115
- package/src/theme/tokens/semantic.ts +130 -82
- package/src/utils/colors.ts +1 -1
- package/tailwind.config.js +2 -5
- package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
- package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
- package/src/theme/ColorSystem.stories.tsx +0 -508
- package/src/theme/Colors.stories.tsx +0 -323
- package/src/theme/shadows.stories.tsx +0 -523
- package/src/theme/shadows.ts +0 -439
|
@@ -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 { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
6
7
|
|
|
7
8
|
const baseCollapsedProps = {
|
|
8
9
|
name: 'Bench Press',
|
|
@@ -49,7 +50,7 @@ describe('ExerciseCard', () => {
|
|
|
49
50
|
expect(screen.getByTestId('pr-badge-star')).toBeInTheDocument()
|
|
50
51
|
})
|
|
51
52
|
|
|
52
|
-
it('renders
|
|
53
|
+
it('renders compact velocity strips for logged sets', () => {
|
|
53
54
|
render(
|
|
54
55
|
<ExerciseCard
|
|
55
56
|
{...baseCollapsedProps}
|
|
@@ -86,9 +87,8 @@ describe('ExerciseCard', () => {
|
|
|
86
87
|
sets: unifiedSets,
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
//
|
|
90
|
-
const T_ACTIVE = '
|
|
91
|
-
const T_MUTED = '#9CA3AF'
|
|
90
|
+
// Read from the tokens, not pinned (see SetRow.test.tsx).
|
|
91
|
+
const { 'text-primary': T_ACTIVE, 'text-secondary': T_MUTED } = getSemanticColors('dark')
|
|
92
92
|
|
|
93
93
|
it('renders exercise name via the real ExerciseCardHeading header', () => {
|
|
94
94
|
render(<ExerciseCard {...expandedProps} />)
|
|
@@ -158,10 +158,10 @@ describe('ExerciseCard', () => {
|
|
|
158
158
|
expect(within(rows[2]).getByText('3')).toHaveStyle({ color: T_MUTED })
|
|
159
159
|
})
|
|
160
160
|
|
|
161
|
-
it('uses the
|
|
161
|
+
it('uses the velocity-height spotlight for the live set and the flat compact strip elsewhere', () => {
|
|
162
162
|
render(<ExerciseCard {...expandedProps} />)
|
|
163
|
-
expect(screen.getAllByTestId('velocity-strip-
|
|
164
|
-
expect(screen.getAllByTestId('velocity-strip-
|
|
163
|
+
expect(screen.getAllByTestId('velocity-strip-spotlight')).toHaveLength(1)
|
|
164
|
+
expect(screen.getAllByTestId('velocity-strip-compact')).toHaveLength(2)
|
|
165
165
|
})
|
|
166
166
|
|
|
167
167
|
it('renders the TempoDisplay in the header when tempo provided', () => {
|
|
@@ -194,7 +194,9 @@ function CollapsedCard({
|
|
|
194
194
|
key={i}
|
|
195
195
|
velocities={velocities}
|
|
196
196
|
zones={velocityZones}
|
|
197
|
-
variant="
|
|
197
|
+
variant="compact"
|
|
198
|
+
height={8}
|
|
199
|
+
hideBaseline
|
|
198
200
|
testID={`exercise-card-velocity-strip-${i}`}
|
|
199
201
|
/>
|
|
200
202
|
))}
|
|
@@ -218,7 +220,7 @@ function CollapsedCard({
|
|
|
218
220
|
|
|
219
221
|
const DARK = getSemanticColors('dark')
|
|
220
222
|
/** The header↔body seam. */
|
|
221
|
-
const BODY_DIVIDER = DARK['
|
|
223
|
+
const BODY_DIVIDER = DARK['hairline-subtle']
|
|
222
224
|
|
|
223
225
|
/** Project the set rows onto the heading strip's per-set state (done / active / todo). */
|
|
224
226
|
function deriveHeaderSetStates(sets: SetRowProps[]): SetStripSet[] {
|
|
@@ -260,7 +262,7 @@ function ExpandedCard({
|
|
|
260
262
|
|
|
261
263
|
return (
|
|
262
264
|
<View
|
|
263
|
-
className="bg-surface-elevated border-
|
|
265
|
+
className="bg-surface-elevated border-hairline"
|
|
264
266
|
style={{
|
|
265
267
|
borderWidth: 1,
|
|
266
268
|
...borderRadius,
|
|
@@ -193,7 +193,7 @@ function TabBar({ active, onSelect }: TabBarProps) {
|
|
|
193
193
|
className="flex-row"
|
|
194
194
|
style={{
|
|
195
195
|
borderBottomWidth: 1,
|
|
196
|
-
borderBottomColor: resolveColor('
|
|
196
|
+
borderBottomColor: resolveColor('hairline-default'),
|
|
197
197
|
}}
|
|
198
198
|
accessibilityRole={'tablist' as ViewProps['accessibilityRole']}
|
|
199
199
|
testID="exercise-detail-page-tabs"
|
|
@@ -247,7 +247,7 @@ function StatStrip({ stats, unit }: { stats: ExerciseDetailStats; unit: 'lbs' |
|
|
|
247
247
|
return (
|
|
248
248
|
<View
|
|
249
249
|
key={key}
|
|
250
|
-
className="bg-surface-elevated border-
|
|
250
|
+
className="bg-surface-elevated border-hairline"
|
|
251
251
|
style={{
|
|
252
252
|
flex: 1,
|
|
253
253
|
padding: 12,
|
|
@@ -297,7 +297,7 @@ function SectionCard({
|
|
|
297
297
|
}) {
|
|
298
298
|
return (
|
|
299
299
|
<View
|
|
300
|
-
className="bg-surface-elevated border-
|
|
300
|
+
className="bg-surface-elevated border-hairline"
|
|
301
301
|
style={{
|
|
302
302
|
borderWidth: 1,
|
|
303
303
|
borderRadius: 12,
|
|
@@ -386,7 +386,13 @@ function VbtBreakdown({
|
|
|
386
386
|
{set.label}
|
|
387
387
|
</Text>
|
|
388
388
|
<View className="flex-1" style={{ height: 8, justifyContent: 'center' }}>
|
|
389
|
-
<VelocityStrip
|
|
389
|
+
<VelocityStrip
|
|
390
|
+
velocities={set.velocities}
|
|
391
|
+
zones={zones}
|
|
392
|
+
variant="compact"
|
|
393
|
+
height={8}
|
|
394
|
+
hideBaseline
|
|
395
|
+
/>
|
|
390
396
|
</View>
|
|
391
397
|
<Text
|
|
392
398
|
className="text-text-tertiary"
|
|
@@ -415,7 +421,7 @@ function VbtSummaryRow({ summary }: { summary: VbtSummary }) {
|
|
|
415
421
|
{cells.map((cell) => (
|
|
416
422
|
<View
|
|
417
423
|
key={cell.label}
|
|
418
|
-
className="bg-surface-elevated border-
|
|
424
|
+
className="bg-surface-elevated border-hairline"
|
|
419
425
|
style={{
|
|
420
426
|
flex: 1,
|
|
421
427
|
padding: 12,
|
|
@@ -19,7 +19,7 @@ export interface InputBarProps {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
|
|
22
|
-
const INPUT_CLASSNAME = 'bg-surface-raised border-
|
|
22
|
+
const INPUT_CLASSNAME = 'bg-surface-raised border-hairline-strong text-text-primary'
|
|
23
23
|
|
|
24
24
|
const inputStyle = {
|
|
25
25
|
fontSize: 14,
|
|
@@ -55,7 +55,7 @@ export function InputBar({
|
|
|
55
55
|
style={{
|
|
56
56
|
width: '100%',
|
|
57
57
|
borderTopWidth: 1,
|
|
58
|
-
borderTopColor: resolveColor('
|
|
58
|
+
borderTopColor: resolveColor('hairline-default'),
|
|
59
59
|
paddingTop: 10,
|
|
60
60
|
paddingHorizontal: 16,
|
|
61
61
|
paddingBottom: 12,
|
|
@@ -82,7 +82,7 @@ const meta: Meta<typeof LiveAuraFrame> = {
|
|
|
82
82
|
docs: {
|
|
83
83
|
description: {
|
|
84
84
|
component:
|
|
85
|
-
'Full-surface color-flood frame tied to a coaching category. Wraps content on a
|
|
85
|
+
'Full-surface color-flood frame tied to a coaching category. Wraps content on a grey ' +
|
|
86
86
|
'base and layers a radial wash — amber at threshold (VL20), red at stop (VL28+, pulsing) — ' +
|
|
87
87
|
'derived from the semantic warning / error tokens via `alpha()`. Children pass through. ' +
|
|
88
88
|
'Rendered here at live-screen size so the flood reads (it is meant to tint a whole panel, ' +
|
|
@@ -19,7 +19,7 @@ describe('LiveAuraFrame', () => {
|
|
|
19
19
|
expect(screen.getByText('Cable Row')).toBeInTheDocument()
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
-
it('sits on the
|
|
22
|
+
it('sits on the grey base surface', () => {
|
|
23
23
|
render(<LiveAuraFrame category="productive" />)
|
|
24
24
|
expect(screen.getByTestId('live-aura-frame')).toHaveStyle({
|
|
25
25
|
backgroundColor: t['background-base'],
|
|
@@ -45,7 +45,7 @@ export function liveAuraColor(category: LiveAuraCategory): string | null {
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Full-surface color-flood frame tied to a coaching category. Wraps `children`
|
|
48
|
-
* on a
|
|
48
|
+
* on a grey base and layers a radial wash (amber at threshold, red at stop)
|
|
49
49
|
* as a decorative, pointer-transparent overlay. The wash uses a solid
|
|
50
50
|
* `backgroundColor` fallback (native) plus a web-only `backgroundImage` radial
|
|
51
51
|
* gradient; both derive from the same semantic token via `alpha()`.
|
|
@@ -62,7 +62,7 @@ export function LiveAuraFrame({
|
|
|
62
62
|
const tint = color ? alpha(color, floodOpacity[category]) : 'transparent'
|
|
63
63
|
|
|
64
64
|
// Match R2's canonical aura: a top-down radial wash that FADES TO TRANSPARENT
|
|
65
|
-
// by ~62%, so the
|
|
65
|
+
// by ~62%, so the grey base shows through the lower panel — NOT a flat
|
|
66
66
|
// full-surface tint. On web (the target: MCP dashboard + Storybook) render the
|
|
67
67
|
// gradient only; native has no gradient support, so fall back to a solid tint.
|
|
68
68
|
const floodStyle = {
|
|
@@ -84,7 +84,7 @@ export function LiveAuraFrame({
|
|
|
84
84
|
borderRadius: 10,
|
|
85
85
|
backgroundColor: t['background-base'],
|
|
86
86
|
borderWidth: 1,
|
|
87
|
-
borderColor: t['
|
|
87
|
+
borderColor: t['hairline-default'],
|
|
88
88
|
},
|
|
89
89
|
style,
|
|
90
90
|
]}
|
|
@@ -10,10 +10,12 @@ import {
|
|
|
10
10
|
MESO_ACCENT_GRADIENT_LIGHT,
|
|
11
11
|
} from '../../../theme/extracted-colors-dataviz'
|
|
12
12
|
|
|
13
|
+
const HAIRLINE_DEFAULT = getSemanticColors('dark')['hairline-default']
|
|
14
|
+
|
|
13
15
|
const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
|
|
14
16
|
const BRAND_PRIMARY_DARK = MESO_ACCENT_GRADIENT_DARK
|
|
15
17
|
const BRAND_PRIMARY_LIGHT = MESO_ACCENT_GRADIENT_LIGHT
|
|
16
|
-
const BORDER_DEFAULT =
|
|
18
|
+
const BORDER_DEFAULT = HAIRLINE_DEFAULT
|
|
17
19
|
|
|
18
20
|
/** Gradient stops for the 3px top accent: dark -> primary -> light. */
|
|
19
21
|
const ACCENT_STOPS = [BRAND_PRIMARY_DARK, BRAND_PRIMARY, BRAND_PRIMARY_LIGHT]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
2
|
import { View, Text, Pressable, type ViewProps } from 'react-native'
|
|
3
3
|
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
4
|
+
import { greyRamp } from '../../../theme/tokens/primitives'
|
|
4
5
|
|
|
5
6
|
const t = getSemanticColors('dark')
|
|
6
7
|
|
|
@@ -15,7 +16,7 @@ export interface MuscleGroupChipProps extends ViewProps {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
const dotColorMap: Record<VolumeStatus, string> = {
|
|
18
|
-
untrained:
|
|
19
|
+
untrained: greyRamp[900],
|
|
19
20
|
behind: t['brand-secondary'],
|
|
20
21
|
ontrack: t['status-success'],
|
|
21
22
|
target: t['brand-primary'],
|
|
@@ -31,13 +32,13 @@ export function MuscleGroupChip({
|
|
|
31
32
|
}: MuscleGroupChipProps) {
|
|
32
33
|
const dotColor = volumeStatus
|
|
33
34
|
? dotColorMap[volumeStatus]
|
|
34
|
-
:
|
|
35
|
+
: greyRamp[900]
|
|
35
36
|
|
|
36
37
|
const statusLabel = volumeStatus ?? 'no status'
|
|
37
38
|
|
|
38
39
|
const content = (
|
|
39
40
|
<View
|
|
40
|
-
className={['bg-surface-raised border-
|
|
41
|
+
className={['bg-surface-raised border-hairline', className].filter(Boolean).join(' ')}
|
|
41
42
|
style={{
|
|
42
43
|
flexDirection: 'row',
|
|
43
44
|
alignItems: 'center',
|
|
@@ -10,7 +10,7 @@ const RECENT_BORDER = 'rgba(249,180,21,0.3)'
|
|
|
10
10
|
const RECENT_GLOW = '0 0 12px rgba(249,180,21,0.06)'
|
|
11
11
|
// Native-safe fallback for the conditional row border (recent uses a computed
|
|
12
12
|
// rgba, so className can't express both branches).
|
|
13
|
-
const BORDER_DEFAULT = resolveColor('
|
|
13
|
+
const BORDER_DEFAULT = resolveColor('hairline-default')
|
|
14
14
|
|
|
15
15
|
export type PrRecordType = 'e1rm' | 'weight' | 'reps' | 'volume' | 'velocity'
|
|
16
16
|
|
|
@@ -172,7 +172,7 @@ export function PrHistoryModal({
|
|
|
172
172
|
>
|
|
173
173
|
<View
|
|
174
174
|
accessibilityElementsHidden
|
|
175
|
-
className="bg-
|
|
175
|
+
className="bg-hairline"
|
|
176
176
|
style={{
|
|
177
177
|
alignSelf: 'center',
|
|
178
178
|
width: 40,
|
|
@@ -13,8 +13,8 @@ DeviationBar · IntensityBar · WorkoutPill · MuscleGroupChip · Sparkline ·
|
|
|
13
13
|
SupersetWrapper · InputBar · MetricCell · SetsRepsLoad · ExerciseIndicator · SetBar
|
|
14
14
|
|
|
15
15
|
**Molecules** — compose atoms, own a little local state:
|
|
16
|
-
VelocityStrip · SetRow · TempoDisplay · RestTimer · MesoProgressBar ·
|
|
17
|
-
WorkoutCard · SetStrip · ExerciseHeading · ExerciseCardHeading
|
|
16
|
+
VelocityStrip · DualVelocityStrip · SetRow · TempoDisplay · RestTimer · MesoProgressBar ·
|
|
17
|
+
WeekRow · WorkoutCard · SetStrip · ExerciseHeading · ExerciseCardHeading
|
|
18
18
|
|
|
19
19
|
**Organisms** — full features, often with their own data contract:
|
|
20
20
|
ExerciseCard · SessionRail · MesoCard · MesoStatusCard · PrHistoryModal ·
|
|
@@ -66,11 +66,12 @@ type props without pulling the dependency.
|
|
|
66
66
|
needs the same yellow/green/red "time-to-target" tone (a coach card, a rep-tempo
|
|
67
67
|
summary). It is pure logic, trivially portable.
|
|
68
68
|
3. **`LiveTempoRow` → a `TempoLiveRow` molecule** — only if a consumer wants the running
|
|
69
|
-
row
|
|
69
|
+
row _without_ the chip chrome (label/background/padding). Until then the chrome and the
|
|
70
70
|
row belong together as one component with a `live` prop, not two.
|
|
71
71
|
|
|
72
72
|
Not worth splitting today (speculative extraction the workflow warns against); this note
|
|
73
73
|
is the trigger list for when it stops being speculative.
|
|
74
|
+
|
|
74
75
|
- **S3 session-rail family** — `SessionRail` (organism) composes the standalone
|
|
75
76
|
`ExerciseCardHeading` molecule per exercise, which composes an `ExerciseHeading`
|
|
76
77
|
info block + a `SetStrip`:
|
|
@@ -96,7 +97,7 @@ type props without pulling the dependency.
|
|
|
96
97
|
vs. variable-todo cyan), `drop` (one set, sub-loads split by 2px notches), `myo`
|
|
97
98
|
(done rest-pause — activation + clusters split by 3px cluster gaps), and
|
|
98
99
|
`myo-upcoming` (planned myo, length unknown — grey activation + a fading, right-open
|
|
99
|
-
cyan "clusters-to-failure" trail). The chunk-size
|
|
100
|
+
cyan "clusters-to-failure" trail). The chunk-size _pattern_ carries set-type identity:
|
|
100
101
|
**butted reps (0) < notch (2px) < cluster gap (3px) < set gap (5px)**. `cyan-900`
|
|
101
102
|
(`SET_STRIP_VARIABLE_COLOR`) is the shared "variable / unknown / opportunity" pin.
|
|
102
103
|
`SegmentedBar` carries these via additive `leadingGap` (per-segment left margin) and
|
|
@@ -113,6 +114,14 @@ type props without pulling the dependency.
|
|
|
113
114
|
**Composes** link down the tree — matching the S1/S2 shell families. (The component
|
|
114
115
|
files stay flat on disk in `custom/Workout/`; only the story `title`s build the tree.)
|
|
115
116
|
|
|
117
|
+
- **Dual at the RAIL level — rejected, removed** — `DualSessionRail` (two
|
|
118
|
+
`SessionRail` columns side by side) was explored and rejected: bilateral
|
|
119
|
+
asymmetry is a property of how a SET was performed, not of the session's
|
|
120
|
+
structure, so the rail stays single and consistent and the dual distinction is
|
|
121
|
+
surfaced per-set/per-rep instead (`DualVelocityStrip`). Deleted 2026-07-26 —
|
|
122
|
+
see `packages/ui/REJECTED.md` for the full reasoning. Do not reintroduce a
|
|
123
|
+
rail-level split without reading it first.
|
|
124
|
+
|
|
116
125
|
- **VelocityStrip set-type modes (`set` prop)** — beyond the flat `velocities`
|
|
117
126
|
array (unchanged, still the source of truth for `SetRow` / `ExerciseCard`), the
|
|
118
127
|
strip accepts an optional structured `VelocitySet` descriptor and renders the
|
|
@@ -133,7 +142,7 @@ type props without pulling the dependency.
|
|
|
133
142
|
active-set spotlight (velocity-height done reps + short grey stubs) and renders
|
|
134
143
|
the advanced types as a short mini-style encoding. Every modality is documented
|
|
135
144
|
with a copy-paste `set` config in **`Workout/DataViz/VelocityStrip/Set
|
|
136
|
-
|
|
145
|
+
Modalities`** (each card carries a `Collapse` accordion; promoted from the
|
|
137
146
|
now-deleted `S3SetModalities` Lab specimen).
|
|
138
147
|
- **VelocityStrip `hero` variant** — the across-the-room, single-set **wall**
|
|
139
148
|
treatment (the north-star live page's velocity hero). Tall bars (default 220px
|
|
@@ -147,12 +156,49 @@ type props without pulling the dependency.
|
|
|
147
156
|
left-packed) with a caller-set fixed height; the eyebrow/section title is
|
|
148
157
|
organism chrome, not part of the primitive. **Live-update model:** the plan's
|
|
149
158
|
slots are pre-allocated (`max(done, target)` columns), so a landing rep converts
|
|
150
|
-
placeholder→bar in the
|
|
159
|
+
placeholder→bar in the _same_ slot with a pop — no reflow within the plan; pair
|
|
151
160
|
with `scale="fixed"` so heights never rescale either. The one reflow case is
|
|
152
161
|
set-expansion **beyond** `targetReps` (AMRAP overflow adds a column and flex-
|
|
153
162
|
narrows the rest — currently snaps; smooth overflow reflow is a deferred
|
|
154
163
|
follow-up). Documented by the `HeroPlayground` / `Hero*` stories on the wall
|
|
155
164
|
background.
|
|
165
|
+
- **DualVelocityStrip** (molecule) — the two-device (LEFT + RIGHT voltra) **diverging**
|
|
166
|
+
wall/rail chart. `composes ↓` the `VelocityStrip` machinery in the same file
|
|
167
|
+
(`buildSlots`/`VelocitySlot` slot model, the `makeBarColorFor` zone scale, the hero
|
|
168
|
+
geometry constants, the shared `useLiveRepPop` entrance, and the extracted
|
|
169
|
+
`DashedReferenceLine`) rather than restating it. `used-by ↑` the north-star dual-voltra
|
|
170
|
+
live page (organism chrome). ONE diverging chart shares a horizontal centre axis: LEFT
|
|
171
|
+
reps grow **up**, RIGHT reps grow **down**, one mirrored pair per rep index, so the L/R
|
|
172
|
+
asymmetry reads pre-attentively as the silhouette. **Side is POSITION only, never hue** —
|
|
173
|
+
both wings colour reps by velocity zone through the same resolver as the single strip.
|
|
174
|
+
Each side takes a `DualVelocityStream` (`velocities` OR a structured `set` — the same
|
|
175
|
+
shapes `VelocityStrip` accepts — plus an optional `label`). The **vertical edge label is
|
|
176
|
+
data**: each side renders its `DualVelocityStream.label` (a slot name, e.g. "Left Arm"),
|
|
177
|
+
NOT a hardcoded LEFT/RIGHT — a side with no `label` (or an empty one) renders no tag, and
|
|
178
|
+
when neither side carries one the gutter is omitted entirely. The label keeps the prior
|
|
179
|
+
vertical (rotated) orientation so it never overlaps the bars. Two scales: `hero` (tall
|
|
180
|
+
wings, per-rep m/s velocity labels, a dashed running-best reference line per side) and
|
|
181
|
+
`rail` (compact — no velocity labels / reference lines, slot names in a narrow gutter).
|
|
182
|
+
**Rep-index alignment is the invariant:** column *i* is rep *i* on
|
|
183
|
+
both sides, so the set-type slot *kinds* carry through (rep / todo / variable / continue,
|
|
184
|
+
coloured as in the single strip) but the wide-notch chunk **gaps** (drop / myo / cluster
|
|
185
|
+
boundaries) are intentionally NOT rendered — per-side horizontal gaps would break the
|
|
186
|
+
mirrored L↔R column alignment. Single-voltra sets keep using `VelocityStrip`
|
|
187
|
+
`variant="hero"`. Documented by the `Playground` / `Hero*` / `Rail` stories on the wall
|
|
188
|
+
background (`Workout/DataViz/DualVelocityStrip`).
|
|
189
|
+
|
|
190
|
+
**Reuse audit — `DashedReferenceLine` (in-file today, top-level follow-up).** The dashed
|
|
191
|
+
running-best line was hand-rolled three times inside `VelocityStrip.tsx` (the single
|
|
192
|
+
`hero`, plus the dual's L and R wings); it is now one in-file `DashedReferenceLine`
|
|
193
|
+
(`anchor: 'top' | 'bottom'`, pixel `offset`, `testID`) those three sites share. A FOURTH
|
|
194
|
+
consumer exists across files — `Sparkline`'s `referenceLines` prop renders the same dashed
|
|
195
|
+
overlay (with an added opacity, an optional label, and a data-domain→Y projection), and two
|
|
196
|
+
Lab specimens duplicate it again. Promoting `DashedReferenceLine` to a **top-level
|
|
197
|
+
`ReferenceLine` overlay primitive** and migrating `Sparkline` (keeping its label/opacity
|
|
198
|
+
options as props) is the ≥2-consumer extraction the DoD favours — deliberately deferred to
|
|
199
|
+
a follow-up so the hero PR stays focused (the cross-file migration touches `Sparkline`'s
|
|
200
|
+
test surface and the Lab specimens, which are out of this branch's scope). Proposed API:
|
|
201
|
+
`<ReferenceLine anchor offset color dashed opacity? label? testID />`.
|
|
156
202
|
- **RestTimer `ring` variant** — the across-the-room **wall** rest treatment (the
|
|
157
203
|
north-star rest page). Built as a **three-tier decomposition** (not a one-off):
|
|
158
204
|
`CircularProgress` (atom, gained a **`children`** center slot + a **`fill`**
|
|
@@ -174,7 +220,7 @@ type props without pulling the dependency.
|
|
|
174
220
|
dashboard scale, added as the idiomatic titan `size` union (a JS number-map per
|
|
175
221
|
component; **`default` values are byte-identical** to before, so existing consumers are
|
|
176
222
|
untouched). **ZoneTrack** (the shared gauge primitive) gained `size` that scales track,
|
|
177
|
-
needle, tick lines and tick labels together; its
|
|
223
|
+
needle, tick lines and tick labels together; its _other_ consumers (TrainingLoadGauge,
|
|
178
224
|
RpeCalibration) default to `default` and are unaffected. **FatigueMeter** passes `size`
|
|
179
225
|
through and lets `trackHeight` flow from it. `Wall*` / `WallDensity` stories on the wall
|
|
180
226
|
background. (**TempoBar** was retired here — the standalone active-tempo bar is superseded
|
|
@@ -187,17 +233,17 @@ type props without pulling the dependency.
|
|
|
187
233
|
per exercise, six distinct kinds over four tier colors (bound to titan status
|
|
188
234
|
tokens, read as literal hex via `getSemanticColors('dark')`, not `resolveColor`,
|
|
189
235
|
so tests can assert them). Glyph = the specific signal; color = the severity tier. Chips are
|
|
190
|
-
outlined (border + glyph, no fill) so they never collide with the
|
|
236
|
+
outlined (border + glyph, no fill) so they never collide with the _filled_
|
|
191
237
|
velocity strip, and static (no pulse — they are chrome). Precedence, high → low:
|
|
192
238
|
|
|
193
|
-
| #
|
|
194
|
-
|
|
195
|
-
| 1
|
|
196
|
-
| 2
|
|
197
|
-
| 3
|
|
198
|
-
| 4
|
|
199
|
-
| 5
|
|
200
|
-
| 6
|
|
239
|
+
| # | kind | tier | glyph (mirrors lucide) |
|
|
240
|
+
| --- | --------------- | -------------------------- | ---------------------- |
|
|
241
|
+
| 1 | `imbalance` | danger (`status-error`) | `Scale` |
|
|
242
|
+
| 2 | `overshoot` | danger (`status-error`) | `AlertTriangle` |
|
|
243
|
+
| 3 | `velocity-loss` | warning (`status-warning`) | `TrendingDown` |
|
|
244
|
+
| 4 | `missed-reps` | warning (`status-warning`) | `CircleSlash` |
|
|
245
|
+
| 5 | `pr` | success (`status-success`) | `Award` |
|
|
246
|
+
| 6 | `info` | info (`status-info`) | `Info` |
|
|
201
247
|
|
|
202
248
|
`resolveIndicator(candidates)` collapses the active signals to the single
|
|
203
249
|
highest-precedence kind to show (`undefined` when none). Alerts outrank `pr`. The
|
|
@@ -116,7 +116,7 @@ function EmojiSlider({ factor }: EmojiSliderProps) {
|
|
|
116
116
|
accessibilityLabel={`${factor.label} level ${level} of 5`}
|
|
117
117
|
aria-checked={selected}
|
|
118
118
|
testID="readiness-check-emoji"
|
|
119
|
-
className={selected ? undefined : 'border-
|
|
119
|
+
className={selected ? undefined : 'border-hairline bg-surface-raised'}
|
|
120
120
|
style={({ pressed }) => ({
|
|
121
121
|
flex: 1,
|
|
122
122
|
alignItems: 'center',
|
|
@@ -172,7 +172,7 @@ function WarmUpCard({ validation }: { validation: WarmUpValidation }) {
|
|
|
172
172
|
const badge = WARMUP_BADGE[validation.status]
|
|
173
173
|
return (
|
|
174
174
|
<View
|
|
175
|
-
className="border-
|
|
175
|
+
className="border-hairline bg-surface-raised"
|
|
176
176
|
style={{
|
|
177
177
|
marginTop: 16,
|
|
178
178
|
padding: 12,
|
|
@@ -188,7 +188,7 @@ export function RestTimer({
|
|
|
188
188
|
style={{
|
|
189
189
|
width: '100%',
|
|
190
190
|
borderTopWidth: 1,
|
|
191
|
-
borderTopColor: resolveColor('
|
|
191
|
+
borderTopColor: resolveColor('hairline-default'),
|
|
192
192
|
paddingVertical: 12,
|
|
193
193
|
paddingHorizontal: 16,
|
|
194
194
|
}}
|
|
@@ -253,7 +253,7 @@ export function RestTimer({
|
|
|
253
253
|
|
|
254
254
|
{/* Progress bar */}
|
|
255
255
|
<View
|
|
256
|
-
className="bg-
|
|
256
|
+
className="bg-hairline"
|
|
257
257
|
style={{
|
|
258
258
|
height: 3,
|
|
259
259
|
borderRadius: 2,
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
11
11
|
import { Fragment } from 'react'
|
|
12
|
-
import {
|
|
12
|
+
import { primitiveRamps as ramp, greyRamp } from '../../../theme/tokens/primitives'
|
|
13
13
|
import { GREY, RAIL_W, SET_GAP, T_PRIMARY, T_SECONDARY, T_TERTIARY, INSET, velColor, reps, Page, sectionTitle } from './setHeadingKit'
|
|
14
14
|
|
|
15
15
|
// ---- variable-zone (floor→max) upcoming color — real ramp candidates (no invented hex)
|
|
16
|
-
const VAR_GREY =
|
|
16
|
+
const VAR_GREY = greyRamp[800] // #3C3C3C — lighter grey (the paler-grey reference)
|
|
17
17
|
const VAR_BLUE_800 = ramp.blue[800] // #14407E — clearly blue, a touch brighter
|
|
18
18
|
const VAR_BLUE_900 = ramp.blue[900] // #112C5A — dark muted navy, recessive
|
|
19
19
|
const VAR_CYAN_900 = ramp.cyan[900] // #0B3149 — teal alt, sidesteps info-blue adjacency
|
|
@@ -115,7 +115,7 @@ function Sheet() {
|
|
|
115
115
|
{/* 1. variable rep-range — real ramp candidates for the variable zone */}
|
|
116
116
|
<Col label="1 · variable rep-range — LOCKED: cyan 900">
|
|
117
117
|
{([
|
|
118
|
-
['grey ·
|
|
118
|
+
['grey · grey 200', VAR_GREY],
|
|
119
119
|
['blue 800 · #14407E', VAR_BLUE_800],
|
|
120
120
|
['blue 900 · #112C5A', VAR_BLUE_900],
|
|
121
121
|
['cyan 900 · #0B3149', VAR_CYAN_900],
|
|
@@ -56,7 +56,7 @@ export const Weighted: Story = {
|
|
|
56
56
|
args: {
|
|
57
57
|
height: 12,
|
|
58
58
|
gap: 4,
|
|
59
|
-
radius:
|
|
59
|
+
radius: 2,
|
|
60
60
|
segments: [
|
|
61
61
|
{ color: '#2ED573', weight: 3 },
|
|
62
62
|
{ color: '#F9B415', weight: 2 },
|
|
@@ -72,7 +72,7 @@ export const PartialFills: Story = {
|
|
|
72
72
|
args: {
|
|
73
73
|
height: 12,
|
|
74
74
|
gap: 4,
|
|
75
|
-
radius:
|
|
75
|
+
radius: 2,
|
|
76
76
|
segments: [
|
|
77
77
|
{ color: '#2ED573', fill: 1 },
|
|
78
78
|
{ color: '#F9B415', fill: 0.6 },
|
|
@@ -89,7 +89,7 @@ export const PartialFills: Story = {
|
|
|
89
89
|
export const WithMarker: Story = {
|
|
90
90
|
args: {
|
|
91
91
|
height: 10,
|
|
92
|
-
radius:
|
|
92
|
+
radius: 2,
|
|
93
93
|
segments: [{ color: '#01B5D1', fill: 0.7 }],
|
|
94
94
|
marker: { position: 0.5, color: '#FFFFFF' },
|
|
95
95
|
},
|
|
@@ -9,7 +9,7 @@ import { SessionHeader, type SessionHeaderPlanEntry } from './SessionHeader'
|
|
|
9
9
|
* bar (fill ∝ sets done, coloured against the elapsed-vs-budget pace with a live marker),
|
|
10
10
|
* a mono `n/total sets` label and a ⏱ elapsed readout. Upcoming, the tiles become
|
|
11
11
|
* Date/Time/Until, the bar is the empty plan shape, and the readout shows the planned
|
|
12
|
-
* duration. Rendered on the raised
|
|
12
|
+
* duration. Rendered on the raised grey plane.
|
|
13
13
|
*/
|
|
14
14
|
const meta: Meta<typeof SessionHeader> = {
|
|
15
15
|
title: 'Shell/SessionRail/SessionHeader',
|
|
@@ -20,7 +20,7 @@ const meta: Meta<typeof SessionHeader> = {
|
|
|
20
20
|
description: {
|
|
21
21
|
component:
|
|
22
22
|
'**Molecule** (shell S3). The session-rail heading — title, stat tiles, chunked ' +
|
|
23
|
-
'pace bar, sets label + ⏱ readout, on the raised
|
|
23
|
+
'pace bar, sets label + ⏱ readout, on the raised grey plane. Composes ' +
|
|
24
24
|
'[MetricTiles](?path=/docs/workout-metrictiles--docs) / ' +
|
|
25
25
|
'[ScheduleTiles](?path=/docs/workout-scheduletiles--docs) + ' +
|
|
26
26
|
'[SegmentedProgressBar](?path=/docs/workout-segmentedprogressbar--docs) + ' +
|
|
@@ -51,7 +51,7 @@ export interface SessionHeaderProps extends ViewProps {
|
|
|
51
51
|
* done, coloured green/amber against the elapsed-vs-budget pace, with a live marker),
|
|
52
52
|
* a mono `n/total sets` label and a ⏱ elapsed readout. Upcoming (`next` set), the tiles
|
|
53
53
|
* become Date/Time/Until, the bar is the empty plan shape, and the readout shows the
|
|
54
|
-
* planned duration. Rendered on the raised
|
|
54
|
+
* planned duration. Rendered on the raised grey plane; presentational. Composes
|
|
55
55
|
* {@link MetricTiles} / {@link ScheduleTiles} + {@link SegmentedProgressBar} +
|
|
56
56
|
* {@link TimerReadout}.
|
|
57
57
|
*/
|
|
@@ -10,7 +10,7 @@ const t = getSemanticColors('dark')
|
|
|
10
10
|
* `SessionRail` (shell organism) — the live-workout exercise list: a flat raised
|
|
11
11
|
* `SessionHeader` glance (title, stat tiles, chunked pace bar) over a sunk, inset list
|
|
12
12
|
* of ExerciseCard rail headings. The header plan (chunk widths ∝ sets) is derived from
|
|
13
|
-
* `exercises`. Surfaces bind to the
|
|
13
|
+
* `exercises`. Surfaces bind to the grey ramp; the list depth is a subtle neumorphic
|
|
14
14
|
* inset. Driven entirely by props.
|
|
15
15
|
*/
|
|
16
16
|
const meta: Meta<typeof SessionRail> = {
|
|
@@ -37,7 +37,7 @@ const meta: Meta<typeof SessionRail> = {
|
|
|
37
37
|
description: {
|
|
38
38
|
component:
|
|
39
39
|
'**Organism** (shell S3). The live-workout exercise list: raised header glance over ' +
|
|
40
|
-
'a sunk inset list, subtle neumorphic depth (
|
|
40
|
+
'a sunk inset list, subtle neumorphic depth (grey ramp + `neumorphicShadows`). ' +
|
|
41
41
|
'Composes ' +
|
|
42
42
|
'[SessionHeader](?path=/docs/shell-sessionrail-sessionheader--docs) + ' +
|
|
43
43
|
'[ExerciseCardHeading](?path=/docs/workout-exercisecardheading--docs) × N. ' +
|
|
@@ -76,6 +76,49 @@ describe('SessionRail', () => {
|
|
|
76
76
|
expect(onExercisePress).toHaveBeenCalledWith(exercises[1], 1)
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
+
describe('expandedIndex', () => {
|
|
80
|
+
const withSets: SessionRailExercise[] = exercises.map((ex, i) =>
|
|
81
|
+
i === 1
|
|
82
|
+
? {
|
|
83
|
+
...ex,
|
|
84
|
+
sets: [
|
|
85
|
+
{
|
|
86
|
+
state: 'done' as const,
|
|
87
|
+
setNumber: 1,
|
|
88
|
+
unit: 'lbs' as const,
|
|
89
|
+
reps: 10,
|
|
90
|
+
weight: 90,
|
|
91
|
+
velocities: [0.72, 0.66],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
}
|
|
95
|
+
: ex
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
it('renders the named exercise expanded in place, leaving the others collapsed', () => {
|
|
99
|
+
render(<SessionRail {...baseProps} exercises={withSets} expandedIndex={1} />)
|
|
100
|
+
// The expanded row reveals the set table; the collapsed ones do not.
|
|
101
|
+
expect(screen.getByText('SET')).toBeInTheDocument()
|
|
102
|
+
expect(screen.getByText('RPE')).toBeInTheDocument()
|
|
103
|
+
// Every exercise still has a row.
|
|
104
|
+
expect(screen.getByText('Seated Cable Row')).toBeInTheDocument()
|
|
105
|
+
expect(screen.getByText('Cable Chest Press')).toBeInTheDocument()
|
|
106
|
+
expect(screen.getByText('Face Pull')).toBeInTheDocument()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('stays collapsed when the named exercise has no sets', () => {
|
|
110
|
+
// Guards the real case: an exercise whose sets have not loaded yet must not
|
|
111
|
+
// expand into an empty table.
|
|
112
|
+
render(<SessionRail {...baseProps} expandedIndex={1} />)
|
|
113
|
+
expect(screen.queryByText('SET')).not.toBeInTheDocument()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('leaves every row collapsed when expandedIndex is omitted', () => {
|
|
117
|
+
render(<SessionRail {...baseProps} exercises={withSets} />)
|
|
118
|
+
expect(screen.queryByText('SET')).not.toBeInTheDocument()
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
79
122
|
describe('accessibility', () => {
|
|
80
123
|
it('has no accessibility violations', async () => {
|
|
81
124
|
const { container } = render(<SessionRail {...baseProps} />)
|