@titan-design/react-ui 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bodymap.js +12 -7
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +12 -7
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +855 -124
- package/dist/index.d.ts +855 -124
- package/dist/index.js +2276 -801
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2230 -802
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-MO0JCySL.d.mts → pages-8u_4WbL-.d.mts} +280 -57
- package/dist/{pages-D7Jlssvp.d.ts → pages-DaWiBOGa.d.ts} +280 -57
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +1255 -798
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1255 -798
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +16 -82
- package/dist/theme/index.d.ts +16 -82
- package/dist/theme/index.js +13 -115
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +13 -115
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +14 -6
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +14 -6
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +1 -1
- package/src/arch/Architecture.stories.tsx +870 -0
- package/src/arch/annotations.json +48 -0
- package/src/arch/arch-graph.json +4301 -0
- package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
- package/src/components/custom/Metric/Metric.stories.tsx +1 -1
- package/src/components/custom/Scatter/Scatter.stories.tsx +1 -1
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
- package/src/components/custom/Table/Table.stories.tsx +1 -1
- package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +62 -0
- package/src/components/custom/TimerReadout/TimerReadout.test.tsx +72 -0
- package/src/components/custom/TimerReadout/TimerReadout.tsx +75 -0
- package/src/components/custom/TimerReadout/index.ts +1 -0
- package/src/components/custom/Treemap/Treemap.stories.tsx +1 -1
- package/src/components/custom/Typography/Typography.stories.tsx +1 -1
- package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +21 -13
- package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +16 -13
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +9 -15
- package/src/components/custom/Workout/BaseBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/BodyMap.stories.tsx +1 -1
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
- package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
- package/src/components/custom/Workout/ExerciseCard.stories.tsx +51 -66
- package/src/components/custom/Workout/ExerciseCard.test.tsx +146 -143
- package/src/components/custom/Workout/ExerciseCard.tsx +176 -198
- package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +99 -0
- package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +72 -0
- package/src/components/custom/Workout/ExerciseCardHeading.tsx +43 -0
- package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +151 -0
- package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +5 -3
- package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +4 -4
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -5
- package/src/components/custom/Workout/ExerciseHeading.stories.tsx +81 -0
- package/src/components/custom/Workout/ExerciseHeading.test.tsx +77 -0
- package/src/components/custom/Workout/ExerciseHeading.tsx +85 -0
- package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +110 -0
- package/src/components/custom/Workout/ExerciseIndicator.test.tsx +93 -0
- package/src/components/custom/Workout/ExerciseIndicator.tsx +120 -0
- package/src/components/custom/Workout/FatigueMeter.stories.tsx +51 -0
- package/src/components/custom/Workout/FatigueMeter.test.tsx +73 -0
- package/src/components/custom/Workout/FatigueMeter.tsx +82 -0
- package/src/components/custom/Workout/InputBar.stories.tsx +1 -1
- package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
- package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +153 -0
- package/src/components/custom/Workout/LiveAuraFrame.test.tsx +74 -0
- package/src/components/custom/Workout/LiveAuraFrame.tsx +105 -0
- package/src/components/custom/Workout/MesoCard.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
- package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
- package/src/components/custom/Workout/MetricTiles.stories.tsx +60 -0
- package/src/components/custom/Workout/MetricTiles.test.tsx +58 -0
- package/src/components/custom/Workout/MetricTiles.tsx +48 -0
- package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +1 -1
- package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
- package/src/components/custom/Workout/PrBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
- package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -6
- package/src/components/custom/Workout/README.md +146 -4
- package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
- package/src/components/custom/Workout/RestTimer.stories.tsx +1 -1
- package/src/components/custom/Workout/RestTimer.tsx +15 -10
- package/src/components/custom/Workout/RowInventory.stories.tsx +405 -0
- package/src/components/custom/Workout/S3FullRail.stories.tsx +34 -0
- package/src/components/custom/Workout/S3SessionPace.stories.tsx +95 -0
- package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +61 -0
- package/src/components/custom/Workout/S3SetTypes.stories.tsx +176 -0
- package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +240 -0
- package/src/components/custom/Workout/ScheduleTiles.stories.tsx +40 -0
- package/src/components/custom/Workout/ScheduleTiles.test.tsx +34 -0
- package/src/components/custom/Workout/ScheduleTiles.tsx +70 -0
- package/src/components/custom/Workout/SegmentedBar.stories.tsx +99 -0
- package/src/components/custom/Workout/SegmentedBar.test.tsx +77 -0
- package/src/components/custom/Workout/SegmentedBar.tsx +172 -0
- package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +50 -0
- package/src/components/custom/Workout/SegmentedProgressBar.test.tsx +80 -0
- package/src/components/custom/Workout/SegmentedProgressBar.tsx +67 -0
- package/src/components/custom/Workout/SessionHeader.stories.tsx +121 -0
- package/src/components/custom/Workout/SessionHeader.test.tsx +122 -0
- package/src/components/custom/Workout/SessionHeader.tsx +152 -0
- package/src/components/custom/Workout/SessionRail.stories.tsx +153 -0
- package/src/components/custom/Workout/SessionRail.test.tsx +86 -0
- package/src/components/custom/Workout/SessionRail.tsx +126 -0
- package/src/components/custom/Workout/SetBar.stories.tsx +124 -0
- package/src/components/custom/Workout/SetBar.test.tsx +123 -0
- package/src/components/custom/Workout/SetBar.tsx +171 -0
- package/src/components/custom/Workout/SetRow.stories.tsx +50 -100
- package/src/components/custom/Workout/SetRow.test.tsx +94 -166
- package/src/components/custom/Workout/SetRow.tsx +169 -232
- package/src/components/custom/Workout/SetStrip.stories.tsx +154 -0
- package/src/components/custom/Workout/SetStrip.test.tsx +109 -0
- package/src/components/custom/Workout/SetStrip.tsx +71 -0
- package/src/components/custom/Workout/SetTableHeader.stories.tsx +66 -0
- package/src/components/custom/Workout/SetTableHeader.test.tsx +47 -0
- package/src/components/custom/Workout/SetTableHeader.tsx +88 -0
- package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +47 -0
- package/src/components/custom/Workout/SetsRepsLoad.test.tsx +44 -0
- package/src/components/custom/Workout/SetsRepsLoad.tsx +52 -0
- package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
- package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
- package/src/components/custom/Workout/StatusPill.stories.tsx +59 -0
- package/src/components/custom/Workout/StatusPill.test.tsx +63 -0
- package/src/components/custom/Workout/StatusPill.tsx +88 -0
- package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +1 -1
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
- package/src/components/custom/Workout/TempoBar.stories.tsx +1 -1
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +12 -1
- package/src/components/custom/Workout/TempoDisplay.tsx +23 -33
- package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +1 -1
- package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +254 -0
- package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +145 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +129 -66
- package/src/components/custom/Workout/VelocityStrip.test.tsx +376 -141
- package/src/components/custom/Workout/VelocityStrip.tsx +508 -120
- package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +73 -0
- package/src/components/custom/Workout/VolumeLandmarkBar.test.tsx +119 -0
- package/src/components/custom/Workout/VolumeLandmarkBar.tsx +154 -0
- package/src/components/custom/Workout/WeekRow.stories.tsx +1 -1
- package/src/components/custom/Workout/WeightBadge.stories.tsx +1 -1
- package/src/components/custom/Workout/WorkoutCard.stories.tsx +2 -6
- package/src/components/custom/Workout/WorkoutCard.test.tsx +13 -46
- package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
- package/src/components/custom/Workout/ZoneTrack.stories.tsx +140 -0
- package/src/components/custom/Workout/ZoneTrack.test.tsx +200 -0
- package/src/components/custom/Workout/ZoneTrack.tsx +315 -0
- package/src/components/custom/Workout/index.ts +55 -2
- package/src/components/custom/Workout/metricText.stories.tsx +69 -0
- package/src/components/custom/Workout/metricText.tsx +34 -0
- package/src/components/custom/Workout/paceTone.test.ts +29 -0
- package/src/components/custom/Workout/paceTone.ts +27 -0
- package/src/components/custom/Workout/setHeadingKit.tsx +177 -0
- package/src/components/custom/index.ts +1 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
- package/src/components/icons/icons.stories.tsx +67 -2
- package/src/components/icons/icons.test.tsx +32 -2
- package/src/components/icons/icons.tsx +108 -0
- package/src/components/shell/BrandLockup.stories.tsx +3 -3
- package/src/components/shell/DashboardShell.stories.tsx +65 -0
- package/src/components/shell/DashboardShell.test.tsx +42 -0
- package/src/components/shell/DashboardShell.tsx +74 -0
- package/src/components/shell/DeviceIndicator.stories.tsx +2 -2
- package/src/components/shell/DeviceMenu.stories.tsx +5 -5
- package/src/components/shell/DeviceRow.stories.tsx +3 -3
- package/src/components/shell/NavItem.stories.tsx +52 -0
- package/src/components/shell/NavItem.test.tsx +35 -0
- package/src/components/shell/NavItem.tsx +75 -0
- package/src/components/shell/README.md +30 -0
- package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
- package/src/components/shell/SessionStatePill.stories.tsx +3 -3
- package/src/components/shell/SessionStatePill.tsx +1 -1
- package/src/components/shell/SideNav.stories.tsx +81 -0
- package/src/components/shell/SideNav.test.tsx +51 -0
- package/src/components/shell/SideNav.tsx +74 -0
- package/src/components/shell/TopBar.stories.tsx +5 -5
- package/src/components/shell/index.ts +5 -0
- package/src/components/ui/alert/Alert.stories.tsx +1 -1
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
- package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
- package/src/components/ui/badge/Badge.stories.tsx +1 -1
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
- package/src/components/ui/button/Button.stories.tsx +1 -1
- package/src/components/ui/card/Card.stories.tsx +1 -1
- package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
- package/src/components/ui/chip/Chip.stories.tsx +1 -1
- package/src/components/ui/collapse/Collapse.stories.tsx +1 -1
- package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
- package/src/components/ui/divider/Divider.stories.tsx +1 -1
- package/src/components/ui/drawer/Drawer.stories.tsx +1 -1
- package/src/components/ui/form-field/FormField.stories.tsx +1 -1
- package/src/components/ui/help-tip/HelpTip.stories.tsx +1 -1
- package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/indicator/Indicator.stories.tsx +2 -2
- package/src/components/ui/indicator/Indicator.test.tsx +1 -0
- package/src/components/ui/indicator/Indicator.tsx +6 -1
- package/src/components/ui/input/Input.stories.tsx +1 -1
- package/src/components/ui/link/Link.stories.tsx +1 -1
- package/src/components/ui/list-item/ListItem.stories.tsx +1 -1
- package/src/components/ui/menu/Menu.stories.tsx +1 -1
- package/src/components/ui/modal/Modal.stories.tsx +1 -1
- package/src/components/ui/pill/Pill.stories.tsx +1 -1
- package/src/components/ui/popover/Popover.stories.tsx +1 -1
- package/src/components/ui/progress/Progress.stories.tsx +1 -1
- package/src/components/ui/radio/Radio.stories.tsx +1 -1
- package/src/components/ui/section/Section.stories.tsx +1 -1
- package/src/components/ui/select/Select.stories.tsx +1 -1
- package/src/components/ui/skeleton/Skeleton.stories.tsx +1 -1
- package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
- package/src/components/ui/stack/Stack.stories.tsx +1 -1
- package/src/components/ui/surface/Surface.stories.tsx +1 -1
- package/src/components/ui/switch/Switch.stories.tsx +1 -1
- package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
- package/src/components/ui/tile/Tile.stories.tsx +79 -0
- package/src/components/ui/tile/Tile.test.tsx +48 -0
- package/src/components/ui/tile/Tile.tsx +64 -0
- package/src/components/ui/tile/index.ts +2 -0
- package/src/components/ui/toast/Toast.stories.tsx +1 -1
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
- package/src/components/ui/tooltip/Tooltip.stories.tsx +1 -1
- package/src/examples/react-hook-form/ReactHookForm.stories.tsx +1 -1
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useTimer.test.ts +150 -0
- package/src/hooks/useTimer.ts +77 -0
- package/src/index.ts +3 -0
- package/src/pages.ts +1 -1
- package/src/stories/PresetShowcase.stories.tsx +1 -1
- package/src/stories/ThemePresets.stories.tsx +1 -1
- package/src/theme/ColorSystem.stories.tsx +1 -1
- package/src/theme/Colors.stories.tsx +5 -8
- package/src/theme/config.ts +4 -0
- package/src/theme/elevation.stories.tsx +1 -1
- package/src/theme/global.css +5 -1
- package/src/theme/shadows.stories.tsx +1 -1
- package/src/theme/tokens/primitives.ts +11 -107
- package/src/theme/tokens/semantic.ts +8 -0
- package/src/theme/workout-tokens.ts +11 -7
- package/tailwind.config.js +5 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import { View, StyleSheet, Platform, type ViewProps, type ViewStyle } from 'react-native'
|
|
3
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
4
|
+
import { alpha } from '../../../utils/colors'
|
|
5
|
+
|
|
6
|
+
const t = getSemanticColors('dark')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Coaching-category flood level for the live surface. Mirrors {@link StatusPill}:
|
|
10
|
+
* - `productive` — quiet, no flood
|
|
11
|
+
* - `threshold` — amber wash at VL20
|
|
12
|
+
* - `stop` — red wash at VL28+ (optionally pulsing)
|
|
13
|
+
*/
|
|
14
|
+
export type LiveAuraCategory = 'productive' | 'threshold' | 'stop'
|
|
15
|
+
|
|
16
|
+
export interface LiveAuraFrameProps extends ViewProps {
|
|
17
|
+
category: LiveAuraCategory
|
|
18
|
+
/**
|
|
19
|
+
* Pulse the flood on `stop`. Uses the nativewind `animate-pulse` utility so
|
|
20
|
+
* it stays static-friendly under test (className→style, no live clock).
|
|
21
|
+
* Defaults to true.
|
|
22
|
+
*/
|
|
23
|
+
pulse?: boolean
|
|
24
|
+
className?: string
|
|
25
|
+
children?: React.ReactNode
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const categoryToken: Record<LiveAuraCategory, 'status-warning' | 'status-error' | null> = {
|
|
29
|
+
productive: null,
|
|
30
|
+
threshold: 'status-warning',
|
|
31
|
+
stop: 'status-error',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const floodOpacity: Record<LiveAuraCategory, number> = {
|
|
35
|
+
productive: 0,
|
|
36
|
+
threshold: 0.12,
|
|
37
|
+
stop: 0.18,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Resolved flood color for a category, or `null` when there is no flood. */
|
|
41
|
+
export function liveAuraColor(category: LiveAuraCategory): string | null {
|
|
42
|
+
const token = categoryToken[category]
|
|
43
|
+
return token ? t[token] : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Full-surface color-flood frame tied to a coaching category. Wraps `children`
|
|
48
|
+
* on a charcoal base and layers a radial wash (amber at threshold, red at stop)
|
|
49
|
+
* as a decorative, pointer-transparent overlay. The wash uses a solid
|
|
50
|
+
* `backgroundColor` fallback (native) plus a web-only `backgroundImage` radial
|
|
51
|
+
* gradient; both derive from the same semantic token via `alpha()`.
|
|
52
|
+
*/
|
|
53
|
+
export function LiveAuraFrame({
|
|
54
|
+
category,
|
|
55
|
+
pulse = true,
|
|
56
|
+
className,
|
|
57
|
+
children,
|
|
58
|
+
style,
|
|
59
|
+
...props
|
|
60
|
+
}: LiveAuraFrameProps) {
|
|
61
|
+
const color = liveAuraColor(category)
|
|
62
|
+
const tint = color ? alpha(color, floodOpacity[category]) : 'transparent'
|
|
63
|
+
|
|
64
|
+
// Match R2's canonical aura: a top-down radial wash that FADES TO TRANSPARENT
|
|
65
|
+
// by ~62%, so the charcoal base shows through the lower panel — NOT a flat
|
|
66
|
+
// full-surface tint. On web (the target: MCP dashboard + Storybook) render the
|
|
67
|
+
// gradient only; native has no gradient support, so fall back to a solid tint.
|
|
68
|
+
const floodStyle = {
|
|
69
|
+
...StyleSheet.absoluteFillObject,
|
|
70
|
+
...(Platform.OS === 'web'
|
|
71
|
+
? // backgroundImage is a runtime RNW style, not in RN ViewStyle types.
|
|
72
|
+
{ backgroundImage: `radial-gradient(130% 95% at 50% 0%, ${tint}, transparent 62%)` }
|
|
73
|
+
: { backgroundColor: tint }),
|
|
74
|
+
} as ViewStyle
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<View
|
|
78
|
+
testID="live-aura-frame"
|
|
79
|
+
className={className}
|
|
80
|
+
style={[
|
|
81
|
+
{
|
|
82
|
+
position: 'relative',
|
|
83
|
+
overflow: 'hidden',
|
|
84
|
+
borderRadius: 10,
|
|
85
|
+
backgroundColor: t['background-base'],
|
|
86
|
+
borderWidth: 1,
|
|
87
|
+
borderColor: t['border-default'],
|
|
88
|
+
},
|
|
89
|
+
style,
|
|
90
|
+
]}
|
|
91
|
+
{...props}
|
|
92
|
+
>
|
|
93
|
+
{color && (
|
|
94
|
+
<View
|
|
95
|
+
testID="live-aura-flood"
|
|
96
|
+
accessibilityElementsHidden
|
|
97
|
+
pointerEvents="none"
|
|
98
|
+
className={category === 'stop' && pulse ? 'animate-pulse' : undefined}
|
|
99
|
+
style={floodStyle}
|
|
100
|
+
/>
|
|
101
|
+
)}
|
|
102
|
+
{children}
|
|
103
|
+
</View>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
@@ -4,7 +4,7 @@ import { View } from 'react-native'
|
|
|
4
4
|
import { MesoProgressBar, type Meso } from './MesoProgressBar'
|
|
5
5
|
|
|
6
6
|
const meta: Meta<typeof MesoProgressBar> = {
|
|
7
|
-
title: '
|
|
7
|
+
title: 'Workout/MesoProgressBar',
|
|
8
8
|
component: MesoProgressBar,
|
|
9
9
|
tags: ['autodocs'],
|
|
10
10
|
argTypes: {
|
|
@@ -3,7 +3,7 @@ import { View } from 'react-native'
|
|
|
3
3
|
import { MesoStatusCard } from './MesoStatusCard'
|
|
4
4
|
|
|
5
5
|
const meta: Meta<typeof MesoStatusCard> = {
|
|
6
|
-
title: '
|
|
6
|
+
title: 'Workout/MesoStatusCard',
|
|
7
7
|
component: MesoStatusCard,
|
|
8
8
|
tags: ['autodocs'],
|
|
9
9
|
argTypes: {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { MetricTiles } from './MetricTiles'
|
|
4
|
+
import { getSemanticColors } from '../../../theme'
|
|
5
|
+
|
|
6
|
+
const AMBER = getSemanticColors('dark')['status-warning']
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof MetricTiles> = {
|
|
9
|
+
title: 'Workout/MetricTiles',
|
|
10
|
+
component: MetricTiles,
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
parameters: {
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
'**Molecule.** An HStack of [Tile](?path=/docs/components-atoms-tile--docs). Used-by ↑ SessionHeader.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
decorators: [
|
|
21
|
+
(Story) => (
|
|
22
|
+
<View style={{ width: 246 }}>
|
|
23
|
+
<Story />
|
|
24
|
+
</View>
|
|
25
|
+
),
|
|
26
|
+
],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default meta
|
|
30
|
+
type Story = StoryObj<typeof MetricTiles>
|
|
31
|
+
|
|
32
|
+
export const SessionStats: Story = {
|
|
33
|
+
args: {
|
|
34
|
+
metrics: [
|
|
35
|
+
{ label: 'Volume', value: '76%' },
|
|
36
|
+
{ label: 'Load', value: '7.3k' },
|
|
37
|
+
{ label: 'Fatigue', value: 'MOD', valueColor: AMBER },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const TwoTiles: Story = {
|
|
43
|
+
args: {
|
|
44
|
+
metrics: [
|
|
45
|
+
{ label: 'Time', value: '6:30 PM' },
|
|
46
|
+
{ label: 'Until', value: '5h', valueColor: AMBER },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const FourTiles: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
metrics: [
|
|
54
|
+
{ label: 'Sets', value: '7/12' },
|
|
55
|
+
{ label: 'Volume', value: '76%' },
|
|
56
|
+
{ label: 'Load', value: '7.3k' },
|
|
57
|
+
{ label: 'Fatigue', value: 'MOD', valueColor: AMBER },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { MetricTiles } from './MetricTiles'
|
|
5
|
+
|
|
6
|
+
const STATS = [
|
|
7
|
+
{ label: 'Volume', value: '76%' },
|
|
8
|
+
{ label: 'Load', value: '7.3k' },
|
|
9
|
+
{ label: 'Fatigue', value: 'MOD', valueColor: '#F5A623' },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
describe('MetricTiles', () => {
|
|
13
|
+
it('renders a tile per metric', () => {
|
|
14
|
+
render(<MetricTiles metrics={STATS} />)
|
|
15
|
+
expect(screen.getByText('Volume')).toBeInTheDocument()
|
|
16
|
+
expect(screen.getByText('76%')).toBeInTheDocument()
|
|
17
|
+
expect(screen.getByText('Load')).toBeInTheDocument()
|
|
18
|
+
expect(screen.getByText('7.3k')).toBeInTheDocument()
|
|
19
|
+
expect(screen.getByText('Fatigue')).toBeInTheDocument()
|
|
20
|
+
expect(screen.getByText('MOD')).toBeInTheDocument()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('passes valueColor through to the tile value', () => {
|
|
24
|
+
render(<MetricTiles metrics={STATS} />)
|
|
25
|
+
expect(screen.getByText('MOD')).toHaveStyle({ color: '#F5A623' })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('does not tint values without a valueColor', () => {
|
|
29
|
+
render(<MetricTiles metrics={STATS} />)
|
|
30
|
+
expect(screen.getByText('76%').style.color).toBe('')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('reflows an arbitrary count of metrics', () => {
|
|
34
|
+
render(
|
|
35
|
+
<MetricTiles
|
|
36
|
+
metrics={[
|
|
37
|
+
{ label: 'Time', value: '6:30 PM' },
|
|
38
|
+
{ label: 'Until', value: '5h' },
|
|
39
|
+
]}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
expect(screen.getByText('6:30 PM')).toBeInTheDocument()
|
|
43
|
+
expect(screen.getByText('5h')).toBeInTheDocument()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('passes additional props through', () => {
|
|
47
|
+
render(<MetricTiles metrics={STATS} testID="metric-tiles" />)
|
|
48
|
+
expect(screen.getByText('Volume')).toBeInTheDocument()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('accessibility', () => {
|
|
52
|
+
it('has no accessibility violations', async () => {
|
|
53
|
+
const { container } = render(<MetricTiles metrics={STATS} />)
|
|
54
|
+
const results = await axe(container)
|
|
55
|
+
expect(results).toHaveNoViolations()
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type ViewProps } from 'react-native'
|
|
2
|
+
import { HStack } from '../../ui/stack'
|
|
3
|
+
import { Tile } from '../../ui/tile'
|
|
4
|
+
|
|
5
|
+
export interface MetricTileData {
|
|
6
|
+
/** Uppercase micro-label shown above the value */
|
|
7
|
+
label: string
|
|
8
|
+
/** Primary value */
|
|
9
|
+
value: string
|
|
10
|
+
/** Tints the value (status/accent hex); defaults to primary text */
|
|
11
|
+
valueColor?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface MetricTilesProps extends ViewProps {
|
|
15
|
+
/** Stat cells rendered left-to-right; typically three */
|
|
16
|
+
metrics: MetricTileData[]
|
|
17
|
+
/** Space between tiles (Stack gap scale, default 1 ≈ 4px) */
|
|
18
|
+
gap?: 0 | 1 | 2 | 3 | 4
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* MetricTiles — a row of equal-width stat tiles.
|
|
23
|
+
*
|
|
24
|
+
* A thin, presentational HStack of {@link Tile}. Each tile flexes to fill its
|
|
25
|
+
* share so the row reflows gracefully in a responsive rail. Used by SessionHeader
|
|
26
|
+
* for Volume / Load / Fatigue (and Date / Time / Until when upcoming).
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* <MetricTiles metrics={[
|
|
30
|
+
* { label: 'Volume', value: '76%' },
|
|
31
|
+
* { label: 'Load', value: '7.3k' },
|
|
32
|
+
* { label: 'Fatigue', value: 'MOD', valueColor: amber },
|
|
33
|
+
* ]} />
|
|
34
|
+
*/
|
|
35
|
+
export function MetricTiles({ metrics, gap = 1, ...props }: MetricTilesProps) {
|
|
36
|
+
return (
|
|
37
|
+
<HStack gap={gap} align="stretch" {...props}>
|
|
38
|
+
{metrics.map((metric) => (
|
|
39
|
+
<Tile
|
|
40
|
+
key={metric.label}
|
|
41
|
+
label={metric.label}
|
|
42
|
+
value={metric.value}
|
|
43
|
+
valueColor={metric.valueColor}
|
|
44
|
+
/>
|
|
45
|
+
))}
|
|
46
|
+
</HStack>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -3,7 +3,7 @@ import { View } from 'react-native'
|
|
|
3
3
|
import { MuscleGroupChip, type VolumeStatus } from './MuscleGroupChip'
|
|
4
4
|
|
|
5
5
|
const meta: Meta<typeof MuscleGroupChip> = {
|
|
6
|
-
title: '
|
|
6
|
+
title: 'Components/Atoms/MuscleGroupChip',
|
|
7
7
|
component: MuscleGroupChip,
|
|
8
8
|
tags: ['autodocs'],
|
|
9
9
|
argTypes: {
|
|
@@ -3,7 +3,7 @@ import { View } from 'react-native'
|
|
|
3
3
|
import { PlaceholderStrip } from './PlaceholderStrip'
|
|
4
4
|
|
|
5
5
|
const meta: Meta<typeof PlaceholderStrip> = {
|
|
6
|
-
title: '
|
|
6
|
+
title: 'Workout/PlaceholderStrip',
|
|
7
7
|
component: PlaceholderStrip,
|
|
8
8
|
tags: ['autodocs'],
|
|
9
9
|
argTypes: {
|
|
@@ -18,10 +18,7 @@ const lowerMuscles: WorkoutMuscleGroup[] = [
|
|
|
18
18
|
const upperExercises: ExerciseCardProps[] = [
|
|
19
19
|
{
|
|
20
20
|
name: 'Barbell Bench Press',
|
|
21
|
-
state: 'collapsed',
|
|
22
|
-
onToggle: () => {},
|
|
23
21
|
summary: { sets: 4, reps: 8, weight: 185, unit: 'lbs' },
|
|
24
|
-
e1rm: { value: 235, unit: 'lbs' },
|
|
25
22
|
isPR: true,
|
|
26
23
|
setVelocities: [
|
|
27
24
|
[1.05, 0.94, 0.82, 0.74],
|
|
@@ -31,16 +28,12 @@ const upperExercises: ExerciseCardProps[] = [
|
|
|
31
28
|
},
|
|
32
29
|
{
|
|
33
30
|
name: 'Incline Dumbbell Press',
|
|
34
|
-
state: 'collapsed',
|
|
35
|
-
onToggle: () => {},
|
|
36
31
|
summary: { sets: 3, reps: 10, weight: 70, unit: 'lbs' },
|
|
37
|
-
e1rm: { value: 92, unit: 'lbs' },
|
|
38
32
|
totalPlannedSets: 3,
|
|
39
33
|
},
|
|
40
34
|
{
|
|
41
35
|
name: 'Cable Triceps Pushdown',
|
|
42
|
-
|
|
43
|
-
onToggle: () => {},
|
|
36
|
+
upcoming: true,
|
|
44
37
|
prescription: '3×12-15 @ RPE 8',
|
|
45
38
|
previousBest: '50 lbs × 14',
|
|
46
39
|
},
|
|
@@ -49,23 +42,18 @@ const upperExercises: ExerciseCardProps[] = [
|
|
|
49
42
|
const lowerExercises: ExerciseCardProps[] = [
|
|
50
43
|
{
|
|
51
44
|
name: 'Back Squat',
|
|
52
|
-
state: 'collapsed',
|
|
53
|
-
onToggle: () => {},
|
|
54
45
|
summary: { sets: 5, reps: 5, weight: 275, unit: 'lbs' },
|
|
55
|
-
e1rm: { value: 335, unit: 'lbs' },
|
|
56
46
|
totalPlannedSets: 5,
|
|
57
47
|
},
|
|
58
48
|
{
|
|
59
49
|
name: 'Romanian Deadlift',
|
|
60
|
-
|
|
61
|
-
onToggle: () => {},
|
|
50
|
+
upcoming: true,
|
|
62
51
|
prescription: '3×8-10 @ RPE 8',
|
|
63
52
|
previousBest: '225 lbs × 9',
|
|
64
53
|
},
|
|
65
54
|
{
|
|
66
55
|
name: 'Leg Press',
|
|
67
|
-
|
|
68
|
-
onToggle: () => {},
|
|
56
|
+
upcoming: true,
|
|
69
57
|
prescription: '3×12 @ RPE 9',
|
|
70
58
|
previousBest: '450 lbs × 12',
|
|
71
59
|
},
|
|
@@ -240,7 +228,7 @@ const accumulationDone: PlanMeso = {
|
|
|
240
228
|
const mesos: PlanMeso[] = [accumulationDone, accumulation, intensification, peak]
|
|
241
229
|
|
|
242
230
|
const meta: Meta<typeof ProgramPlanningPage> = {
|
|
243
|
-
title: '
|
|
231
|
+
title: 'Pages/Program Planning',
|
|
244
232
|
component: ProgramPlanningPage,
|
|
245
233
|
parameters: { layout: 'fullscreen' },
|
|
246
234
|
tags: ['autodocs'],
|
|
@@ -154,7 +154,11 @@ function Breadcrumbs({ crumbs, onNavigate }: BreadcrumbsProps) {
|
|
|
154
154
|
const isLast = index === crumbs.length - 1
|
|
155
155
|
return (
|
|
156
156
|
<View key={crumb.key} className="flex-row items-center" style={{ gap: 4 }}>
|
|
157
|
-
{index > 0 &&
|
|
157
|
+
{index > 0 && (
|
|
158
|
+
<Text className="text-text-tertiary" style={{ fontSize: 12 }}>
|
|
159
|
+
{'›'}
|
|
160
|
+
</Text>
|
|
161
|
+
)}
|
|
158
162
|
<Pressable
|
|
159
163
|
onPress={() => onNavigate(crumb.level)}
|
|
160
164
|
disabled={isLast}
|
|
@@ -270,13 +274,11 @@ interface WorkoutLevelProps {
|
|
|
270
274
|
}
|
|
271
275
|
|
|
272
276
|
function WorkoutLevel({ workout, expandedExercise, onToggleExercise }: WorkoutLevelProps) {
|
|
277
|
+
// `upcoming` cards ignore `expanded`, so it's safe to wire the toggle uniformly.
|
|
273
278
|
const exercises = workout.exercises.map((exercise, index) => ({
|
|
274
279
|
...exercise,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
? exercise.state
|
|
278
|
-
: ((index === expandedExercise ? 'expanded' : 'collapsed') as ExerciseCardProps['state']),
|
|
279
|
-
onToggle: () => onToggleExercise(index),
|
|
280
|
+
expanded: index === expandedExercise,
|
|
281
|
+
onExpandedChange: () => onToggleExercise(index),
|
|
280
282
|
}))
|
|
281
283
|
return (
|
|
282
284
|
<View testID="program-planning-page-workout-level">
|
|
@@ -10,15 +10,15 @@ component is pure import churn with no file moves.
|
|
|
10
10
|
**Atoms** — single-purpose, no cross-component state:
|
|
11
11
|
BaseBadge · WeightBadge · PrBadge · StatusDot · PlaceholderStrip · TempoBar ·
|
|
12
12
|
DeviationBar · IntensityBar · WorkoutPill · MuscleGroupChip · Sparkline ·
|
|
13
|
-
SupersetWrapper · InputBar
|
|
13
|
+
SupersetWrapper · InputBar · MetricCell · SetsRepsLoad · ExerciseIndicator · SetBar
|
|
14
14
|
|
|
15
15
|
**Molecules** — compose atoms, own a little local state:
|
|
16
16
|
VelocityStrip · SetRow · TempoDisplay · RestTimer · MesoProgressBar · WeekRow ·
|
|
17
|
-
WorkoutCard
|
|
17
|
+
WorkoutCard · SetStrip · ExerciseHeading · ExerciseCardHeading
|
|
18
18
|
|
|
19
19
|
**Organisms** — full features, often with their own data contract:
|
|
20
|
-
ExerciseCard · MesoCard · MesoStatusCard · PrHistoryModal ·
|
|
21
|
-
StrengthTrendChart · CapacityBandChart · BodyMap · BodyMapDetailPanel
|
|
20
|
+
ExerciseCard · SessionRail · MesoCard · MesoStatusCard · PrHistoryModal ·
|
|
21
|
+
ReadinessCheck · StrengthTrendChart · CapacityBandChart · BodyMap · BodyMapDetailPanel
|
|
22
22
|
|
|
23
23
|
**Pages** — phone-shaped reference screens (whole-screen compositions):
|
|
24
24
|
ActiveWorkoutPage · ExerciseDetailPage · ProgramPlanningPage · TrainingStatusPage
|
|
@@ -44,7 +44,149 @@ type props without pulling the dependency.
|
|
|
44
44
|
- **BaseBadge is an internal composition primitive** — the shared shell that
|
|
45
45
|
WeightBadge and PrBadge build on. It is exempt from orphan accounting; it is not
|
|
46
46
|
meant to be consumed directly even though it is exported for composition.
|
|
47
|
+
- **MetricCell is an internal composition primitive** — the shared Inter · 600 ·
|
|
48
|
+
letter-spacing-1 value/separator cell. `TempoDisplay` (tempo digits) and
|
|
49
|
+
`SetsRepsLoad` (sets × reps @ load) both compose it so the two read as one
|
|
50
|
+
visual language. Exported for composition, not for direct app use.
|
|
51
|
+
- **S3 session-rail family** — `SessionRail` (organism) composes the standalone
|
|
52
|
+
`ExerciseCardHeading` molecule per exercise, which composes an `ExerciseHeading`
|
|
53
|
+
info block + a `SetStrip`:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
SessionRail
|
|
57
|
+
└─ ExerciseCardHeading (complete standalone heading)
|
|
58
|
+
├─ ExerciseHeading (name/indicator + metrics, no strip)
|
|
59
|
+
│ ├─ ExerciseIndicator
|
|
60
|
+
│ ├─ SetsRepsLoad → MetricCell
|
|
61
|
+
│ └─ TempoDisplay (showLabel={false}) → MetricCell
|
|
62
|
+
└─ SetStrip
|
|
63
|
+
└─ SetBar × N (one set's per-rep colour bar)
|
|
64
|
+
|
|
65
|
+
ExerciseCard state="rail" ──delegates to──▶ ExerciseCardHeading
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`ExerciseCard`'s `rail` state is now a thin adapter that delegates to
|
|
69
|
+
`ExerciseCardHeading` (so the heading is reusable without the card). `SetBar`
|
|
70
|
+
owns the per-set colour/pulse logic; `SetStrip` lays several side by side.
|
|
71
|
+
Beyond the flat `done`/`active`/`todo` sets, `SetStripSet` models set-type
|
|
72
|
+
prescriptions: `range` (variable rep-range — range-max segments, committed-todo grey
|
|
73
|
+
vs. variable-todo cyan), `drop` (one set, sub-loads split by 2px notches), `myo`
|
|
74
|
+
(done rest-pause — activation + clusters split by 3px cluster gaps), and
|
|
75
|
+
`myo-upcoming` (planned myo, length unknown — grey activation + a fading, right-open
|
|
76
|
+
cyan "clusters-to-failure" trail). The chunk-size *pattern* carries set-type identity:
|
|
77
|
+
**butted reps (0) < notch (2px) < cluster gap (3px) < set gap (5px)**. `cyan-900`
|
|
78
|
+
(`SET_STRIP_VARIABLE_COLOR`) is the shared "variable / unknown / opportunity" pin.
|
|
79
|
+
`SegmentedBar` carries these via additive `leadingGap` (per-segment left margin) and
|
|
80
|
+
static `opacity` props — the flat sets stay byte-identical.
|
|
81
|
+
`SetStrip`/`SetBar` colors are the real titan ramp pins (`primitiveRamps` red-600 /
|
|
82
|
+
orange-400 / amber-300 / green-300); the rail surfaces bind to the charcoal ramp +
|
|
83
|
+
a subtle neumorphic inset (`neumorphicShadows.charcoal.pressed.subtle`). The heading
|
|
84
|
+
design is locked in `coordination/.../S3-sessionrail/DECISIONS-ExerciseRow.md`; the
|
|
85
|
+
exploration specimens live under `Custom/Workout/Explorations/*` (do not repoint yet).
|
|
86
|
+
In Storybook the family nests by composition under **`Shell/SessionRail/…`**
|
|
87
|
+
(organism → `ExerciseCardHeading` → `ExerciseHeading` → its atoms/molecules →
|
|
88
|
+
`MetricCell`, and `SetStrip` → `SetBar`; `ExerciseCard` sits as a leaf whose
|
|
89
|
+
Composes link points at `ExerciseCardHeading`), each node's autodocs carrying a
|
|
90
|
+
**Composes** link down the tree — matching the S1/S2 shell families. (The component
|
|
91
|
+
files stay flat on disk in `custom/Workout/`; only the story `title`s build the tree.)
|
|
92
|
+
|
|
93
|
+
- **VelocityStrip set-type modes (`set` prop)** — beyond the flat `velocities`
|
|
94
|
+
array (unchanged, still the source of truth for `SetRow` / `ExerciseCard`), the
|
|
95
|
+
strip accepts an optional structured `VelocitySet` descriptor and renders the
|
|
96
|
+
strength set-type vocabulary as a typed slot list. Slots are
|
|
97
|
+
`rep` (velocity-coloured) · `todo` (charcoal-300 grey) · `variable` /
|
|
98
|
+
`continue` (`SET_STRIP_VARIABLE_COLOR` cyan-900; `continue` adds a cyan-800
|
|
99
|
+
outline to read as "keep going"). Types: `straight` (done + grey todo to
|
|
100
|
+
`planned`), `range` (committed grey + a cyan variable window `floor..max`),
|
|
101
|
+
`amrap` (reps + a trailing cyan continue), `drop` (sub-loads split by a wide
|
|
102
|
+
notch), `myo` (activation + clusters split by wide gaps, `open` adds a continue),
|
|
103
|
+
`cluster` (fixed count grouped by wide intra-rest gaps). The gap hierarchy is
|
|
104
|
+
**butted reps (`REP_GAP` 2px) < chunk boundary (`WIDE_GAP` 7px)**, applied as
|
|
105
|
+
per-slot `marginLeft` (the mini container drops its uniform `gap` so wide gaps
|
|
106
|
+
render; a no-`set` strip is `[0, 2, 2, …]`, i.e. byte-identical to before). The
|
|
107
|
+
done-velocity array (`velocities`, or subloads/activation+clusters flattened) is
|
|
108
|
+
derived from the `set` and drives the mean · loss · zone summary either way. The
|
|
109
|
+
`mini` variant is the primary set-type surface; `expanded` gives `straight` the
|
|
110
|
+
active-set spotlight (velocity-height done reps + short grey stubs) and renders
|
|
111
|
+
the advanced types as a short mini-style encoding. Every modality is documented
|
|
112
|
+
with a copy-paste `set` config in **`Workout/DataViz/VelocityStrip/Set
|
|
113
|
+
Modalities`** (each card carries a `Collapse` accordion; promoted from the
|
|
114
|
+
now-deleted `S3SetModalities` Lab specimen).
|
|
47
115
|
- Badge icons (WeightBadge's dumbbell, PrBadge / PrHistoryModal's star) are inline
|
|
48
116
|
SVGs (`./icons.tsx`), not `lucide-react` — that dependency was dropped in 0.5.0
|
|
49
117
|
to keep the root barrel light. The SVG paths mirror lucide's glyphs so rendering
|
|
50
118
|
is unchanged.
|
|
119
|
+
- **`ExerciseIndicator` taxonomy (LOCKED, TD-03.51)** — one precedence-ranked slot
|
|
120
|
+
per exercise, six distinct kinds over four tier colors (bound to titan status
|
|
121
|
+
tokens, read as literal hex via `getSemanticColors('dark')` — the TempoBar
|
|
122
|
+
pattern). Glyph = the specific signal; color = the severity tier. Chips are
|
|
123
|
+
outlined (border + glyph, no fill) so they never collide with the *filled*
|
|
124
|
+
velocity strip, and static (no pulse — they are chrome). Precedence, high → low:
|
|
125
|
+
|
|
126
|
+
| # | kind | tier | glyph (mirrors lucide) |
|
|
127
|
+
|---|------|------|------------------------|
|
|
128
|
+
| 1 | `imbalance` | danger (`status-error`) | `Scale` |
|
|
129
|
+
| 2 | `overshoot` | danger (`status-error`) | `AlertTriangle` |
|
|
130
|
+
| 3 | `velocity-loss` | warning (`status-warning`) | `TrendingDown` |
|
|
131
|
+
| 4 | `missed-reps` | warning (`status-warning`) | `CircleSlash` |
|
|
132
|
+
| 5 | `pr` | success (`status-success`) | `Award` |
|
|
133
|
+
| 6 | `info` | info (`status-info`) | `Info` |
|
|
134
|
+
|
|
135
|
+
`resolveIndicator(candidates)` collapses the active signals to the single
|
|
136
|
+
highest-precedence kind to show (`undefined` when none). Alerts outrank `pr`. The
|
|
137
|
+
glyphs are new inline SVGs in the shared `components/icons` set (not the local
|
|
138
|
+
`./icons.tsx` badge glyphs). The old generic `issue` kind is superseded by the
|
|
139
|
+
specific danger/warning kinds and was removed.
|
|
140
|
+
|
|
141
|
+
## Shell/SessionRail feature area
|
|
142
|
+
|
|
143
|
+
The live-workout rail is a **feature assembly**: the genuinely rail-specific parts
|
|
144
|
+
(header / pace / expand drawer / set-table header) nest under `Shell/SessionRail/*`,
|
|
145
|
+
while reused leaves stay in their `Workout/*` tier and are **referenced via Composes
|
|
146
|
+
links, not re-homed** (`SetRow`, `ExerciseCardHeading`). This is the one deliberate
|
|
147
|
+
exception to the flat `Shell` taxonomy — the NEW components below are the only
|
|
148
|
+
ones that nest.
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
Shell/SessionRail (organism — SessionRail.tsx)
|
|
152
|
+
├─ SessionHeader (NEW · real — title, stat tiles, chunked pace bar, ⏱ readout)
|
|
153
|
+
└─ ExerciseCardHeading × N (Workout/ · linked, NOT re-homed)
|
|
154
|
+
|
|
155
|
+
Shell/SessionRail/ExpandedDrawer (NEW · 🚧 WIP — opens when an exercise expands)
|
|
156
|
+
├─ TableHeader (NEW · 🚧 WIP — exported as `SetTableHeader`; the generic
|
|
157
|
+
│ `TableHeader` name is owned by the DataViz `Table`)
|
|
158
|
+
└─ SetRow × N (Workout/ · linked, NOT re-homed)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
- `SessionHeader` is the rail's session glance: the title over a stat row (Volume/Load/
|
|
162
|
+
Fatigue live, Date/Time/Until upcoming) and a per-exercise chunked pace bar with a
|
|
163
|
+
mono sets label + ⏱ readout. It composes the merged primitives (`MetricTiles` /
|
|
164
|
+
`ScheduleTiles` + `SegmentedProgressBar` + `TimerReadout`); the old footer pace tile is
|
|
165
|
+
gone — the header carries the glance. It is the REAL (non-WIP) new component.
|
|
166
|
+
- `SetTableHeader` (story leaf `.../ExpandedDrawer/TableHeader`) is the expanded-set
|
|
167
|
+
column-header row extracted from `ExerciseCard`'s `expanded` state; `ExerciseCard`
|
|
168
|
+
now consumes it too (backward-compatible — it passes its own
|
|
169
|
+
`testID="exercise-card-column-headers"`).
|
|
170
|
+
- Each new node's autodocs carry a `**Tier.** Composes […] / Used-by ↑ […]` line; the
|
|
171
|
+
WIP two (`ExpandedDrawer`, `TableHeader`) are prefixed `**🚧 WIP / placeholder.**`.
|
|
172
|
+
|
|
173
|
+
## Dead candidates (do NOT remove yet — pending TD-03.56 responsive unification)
|
|
174
|
+
|
|
175
|
+
Superseded by the target Shell/SessionRail scaffold; flagged now, removed only when the
|
|
176
|
+
responsive level views land (removal is out of scope for this scaffold ticket):
|
|
177
|
+
|
|
178
|
+
- **`setHeadingKit.tsx`** — the throwaway CSS R&D kit (raw HTML `<div>`s at the rail
|
|
179
|
+
width). Its own header says it moves into real components "when these decisions
|
|
180
|
+
harden" — which has now happened (`SessionRail` / `SessionHeader` /
|
|
181
|
+
`ExerciseCardHeading` / `ExerciseHeading` / `SetStrip` / `SetBar` /
|
|
182
|
+
`SetTableHeader`).
|
|
183
|
+
- **The five `Lab/Explorations` specimens that import it** — `S3FullRail`
|
|
184
|
+
(Full Rail), `S3SessionRailHeading` (Session Rail Heading), `S3WorkoutExpansion`
|
|
185
|
+
(Workout Expansion), `S3SetTypes` (Set Types), `S3SessionPace` (Session Pace).
|
|
186
|
+
Each is superseded by a real Shell/SessionRail component + its Storybook stories;
|
|
187
|
+
`S3SessionPace` in particular is superseded by the `SessionHeader` pace glance.
|
|
188
|
+
- **`ExerciseCard`'s `collapsed` / `upcoming` state representations** — the rail now
|
|
189
|
+
owns the live-list heading (`rail` state → `ExerciseCardHeading`) and the expanded
|
|
190
|
+
view is moving to `ExpandedDrawer`. Once the responsive unification (TD-03.56)
|
|
191
|
+
lands a single responsive card, the `collapsed`/`upcoming` branches of
|
|
192
|
+
`ExerciseCard` are the superseded representations to retire.
|
|
@@ -16,7 +16,7 @@ const staticFactors: ReadinessFactor[] = FACTOR_SEED.map((f) => ({
|
|
|
16
16
|
}))
|
|
17
17
|
|
|
18
18
|
const meta: Meta<typeof ReadinessCheck> = {
|
|
19
|
-
title: '
|
|
19
|
+
title: 'Workout/ReadinessCheck',
|
|
20
20
|
component: ReadinessCheck,
|
|
21
21
|
tags: ['autodocs'],
|
|
22
22
|
argTypes: {
|
|
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
|
3
3
|
import { RestTimer } from './RestTimer'
|
|
4
4
|
|
|
5
5
|
const meta: Meta<typeof RestTimer> = {
|
|
6
|
-
title: '
|
|
6
|
+
title: 'Workout/RestTimer',
|
|
7
7
|
component: RestTimer,
|
|
8
8
|
tags: ['autodocs'],
|
|
9
9
|
argTypes: {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { View, Text, Pressable } from 'react-native'
|
|
3
3
|
import { resolveColor } from '../../../theme/resolve-color'
|
|
4
4
|
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
5
|
+
import { useTimer } from '../../../hooks/useTimer'
|
|
5
6
|
|
|
6
7
|
const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
|
|
7
8
|
|
|
@@ -25,17 +26,21 @@ export function RestTimer({
|
|
|
25
26
|
visible,
|
|
26
27
|
displayOnly = false,
|
|
27
28
|
}: RestTimerProps) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
29
|
+
// useTimer owns the countdown math (remaining/progress/mm:ss); a zero-duration
|
|
30
|
+
// timer is complete, which keeps the width out of the 0/0 === NaN case.
|
|
31
|
+
const {
|
|
32
|
+
remainingMs,
|
|
33
|
+
progress,
|
|
34
|
+
label: timeDisplay,
|
|
35
|
+
} = useTimer({
|
|
36
|
+
mode: 'down',
|
|
37
|
+
durationMs: totalSeconds * 1000,
|
|
38
|
+
elapsedMs,
|
|
39
|
+
})
|
|
31
40
|
const remainingSec = Math.ceil(remainingMs / 1000)
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// A zero-duration timer is already complete; guard the 0 / 0 === NaN case
|
|
36
|
-
// that would otherwise emit width:'NaN%'.
|
|
37
|
-
const totalMs = totalSeconds * 1000
|
|
38
|
-
const progressPct = totalMs > 0 ? Math.min(100, (elapsedMs / totalMs) * 100) : 100
|
|
41
|
+
const progressPct = progress * 100
|
|
42
|
+
|
|
43
|
+
if (!visible) return null
|
|
39
44
|
|
|
40
45
|
return (
|
|
41
46
|
<View
|