@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,43 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { ExerciseHeading, type ExerciseHeadingProps } from './ExerciseHeading'
|
|
4
|
+
import { SetStrip, type SetStripSet } from './SetStrip'
|
|
5
|
+
|
|
6
|
+
export interface ExerciseCardHeadingProps extends ExerciseHeadingProps {
|
|
7
|
+
/** Per-set performance data driving the strip; an empty list renders no strip. */
|
|
8
|
+
setStates: SetStripSet[]
|
|
9
|
+
/** Strip height in px. Default 8. */
|
|
10
|
+
stripHeight?: number
|
|
11
|
+
/** Root testID. Default "exercise-card"; override when nested inside another card. */
|
|
12
|
+
testID?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The complete, standalone session-rail heading: an {@link ExerciseHeading} info
|
|
17
|
+
* block over its per-set {@link SetStrip}. The unit the rail lists — self-contained
|
|
18
|
+
* and independent of {@link ExerciseCard} (whose `rail` state delegates here).
|
|
19
|
+
* e1RM is intentionally dropped (a planning / live-panel concern). Dimming lives on
|
|
20
|
+
* the outer wrapper so the strip dims with the heading.
|
|
21
|
+
*/
|
|
22
|
+
export function ExerciseCardHeading({
|
|
23
|
+
setStates,
|
|
24
|
+
stripHeight = 8,
|
|
25
|
+
dimmed,
|
|
26
|
+
testID = 'exercise-card',
|
|
27
|
+
...heading
|
|
28
|
+
}: ExerciseCardHeadingProps) {
|
|
29
|
+
return (
|
|
30
|
+
<View
|
|
31
|
+
style={{ paddingVertical: 9, paddingHorizontal: 12, opacity: dimmed ? 0.55 : 1 }}
|
|
32
|
+
testID={testID}
|
|
33
|
+
>
|
|
34
|
+
<ExerciseHeading {...heading} />
|
|
35
|
+
|
|
36
|
+
{setStates.length > 0 && (
|
|
37
|
+
<View style={{ marginTop: 7 }} testID="exercise-card-strip">
|
|
38
|
+
<SetStrip sets={setStates} height={stripHeight} />
|
|
39
|
+
</View>
|
|
40
|
+
)}
|
|
41
|
+
</View>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHEET · Unified exercise card (page 2) — the LOCKED rail-expansion, hardened onto the
|
|
3
|
+
* REAL {@link ExerciseCard}. The collapsed rail item and the expanded card are ONE object: the
|
|
4
|
+
* persistent header is the real {@link ExerciseCardHeading}, and expanding reveals the body on the
|
|
5
|
+
* SAME surface — PREV dropped, done + upcoming sets share ONE muted treatment, the ACTIVE set
|
|
6
|
+
* stands out by brightness. Every per-row strip is the REAL {@link VelocityStrip} via its `set`
|
|
7
|
+
* descriptor: `mini` (flat 3px) for done/upcoming, and the `compact` velocity-height variant as
|
|
8
|
+
* the active-set SPOTLIGHT (option B, locked). No hand-built prototype strip remains.
|
|
9
|
+
*/
|
|
10
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
11
|
+
import { View, Text } from 'react-native'
|
|
12
|
+
import { ExerciseCard } from './ExerciseCard'
|
|
13
|
+
import { ExerciseCardHeading } from './ExerciseCardHeading'
|
|
14
|
+
import { VelocityStrip } from './VelocityStrip'
|
|
15
|
+
import { type SetRowProps } from './SetRow'
|
|
16
|
+
import { type SetStripSet } from './SetStrip'
|
|
17
|
+
import { INSET, BORDER_SUBTLE, T_PRIMARY, T_SECONDARY, Page, monoTag } from './setHeadingKit'
|
|
18
|
+
|
|
19
|
+
const INTER = 'Inter, sans-serif'
|
|
20
|
+
|
|
21
|
+
/** Per-rep mean velocity that DECAYS across a set (fast → slow). */
|
|
22
|
+
const decay = (n: number, start: number, span = 0.5) =>
|
|
23
|
+
Array.from({ length: n }, (_, r) => +(start - (span * r) / Math.max(1, n - 1)).toFixed(3))
|
|
24
|
+
|
|
25
|
+
// The exercise: a flat straight set (Cable Chest Press, 3 × 10 @ 90 lb).
|
|
26
|
+
const DONE = decay(10, 1.05)
|
|
27
|
+
const ACTIVE = decay(5, 0.95, 0.3)
|
|
28
|
+
|
|
29
|
+
// One data source drives both representations. Set 1 done · set 2 LIVE · set 3 upcoming.
|
|
30
|
+
const SETS: SetRowProps[] = [
|
|
31
|
+
{ state: 'done', setNumber: 1, unit: 'lbs', reps: 10, weight: 90, rpe: 7.5, velocities: DONE },
|
|
32
|
+
{
|
|
33
|
+
state: 'live',
|
|
34
|
+
setNumber: 2,
|
|
35
|
+
unit: 'lbs',
|
|
36
|
+
target: { reps: 10, weight: 90 },
|
|
37
|
+
reps: 5,
|
|
38
|
+
weight: 90,
|
|
39
|
+
velocities: ACTIVE,
|
|
40
|
+
},
|
|
41
|
+
{ state: 'todo', setNumber: 3, unit: 'lbs', target: { reps: 10, weight: 90 } },
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
// The header's per-set strip: the same lifecycle the card derives internally, built here
|
|
45
|
+
// so the collapsed rail item (ExerciseCardHeading) reads from the same source as the body.
|
|
46
|
+
const HEADER_STATES: SetStripSet[] = [
|
|
47
|
+
{ status: 'done', velocities: DONE },
|
|
48
|
+
{ status: 'active', velocities: ACTIVE, planned: 10 },
|
|
49
|
+
{ status: 'todo', planned: 10 },
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
const CARD_PROPS = {
|
|
53
|
+
name: 'Cable Chest Press',
|
|
54
|
+
summary: { sets: 3, reps: 10, weight: 90, unit: 'lbs' as const },
|
|
55
|
+
tempo: [2, 1, 2, 0] as [number, number, number, number],
|
|
56
|
+
indicator: 'info' as const,
|
|
57
|
+
sets: SETS,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Surface({ children, width = 260 }: { children: React.ReactNode; width?: number }) {
|
|
61
|
+
return (
|
|
62
|
+
<View style={{ width, backgroundColor: INSET, borderRadius: 8, overflow: 'hidden' }}>
|
|
63
|
+
{children}
|
|
64
|
+
</View>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const meta: Meta = {
|
|
69
|
+
title: 'Lab/Explorations/Unified Exercise Card',
|
|
70
|
+
parameters: { layout: 'fullscreen' },
|
|
71
|
+
}
|
|
72
|
+
export default meta
|
|
73
|
+
type Story = StoryObj
|
|
74
|
+
|
|
75
|
+
export const Unified: Story = {
|
|
76
|
+
name: 'One object · rail header → expanded body (real ExerciseCard)',
|
|
77
|
+
render: () => (
|
|
78
|
+
<Page
|
|
79
|
+
title="Unified exercise card — the rail heading IS the header"
|
|
80
|
+
note="The LOCKED rail-expansion, hardened onto the real ExerciseCard. Collapsed, it's the rail item (the real ExerciseCardHeading); expanded, the SAME surface reveals the table body — PREV dropped, done + upcoming sets share ONE muted treatment, the ACTIVE set stands out by brightness. Every per-row strip is the real VelocityStrip: `mini` for done/upcoming, the `compact` velocity-height variant as the active-set spotlight (B, locked). No hand-built prototype strip remains."
|
|
81
|
+
>
|
|
82
|
+
<View style={{ flexDirection: 'row', gap: 40, alignItems: 'flex-start', flexWrap: 'wrap' }}>
|
|
83
|
+
<View style={{ gap: 8 }}>
|
|
84
|
+
<Text style={{ ...monoTag, color: T_SECONDARY }}>
|
|
85
|
+
collapsed · rail item (ExerciseCardHeading)
|
|
86
|
+
</Text>
|
|
87
|
+
<Surface>
|
|
88
|
+
<ExerciseCardHeading
|
|
89
|
+
name={CARD_PROPS.name}
|
|
90
|
+
sets={3}
|
|
91
|
+
reps={10}
|
|
92
|
+
load={90}
|
|
93
|
+
unit="lbs"
|
|
94
|
+
tempo={CARD_PROPS.tempo}
|
|
95
|
+
indicator={CARD_PROPS.indicator}
|
|
96
|
+
setStates={HEADER_STATES}
|
|
97
|
+
/>
|
|
98
|
+
</Surface>
|
|
99
|
+
</View>
|
|
100
|
+
|
|
101
|
+
<View style={{ gap: 8 }}>
|
|
102
|
+
<Text style={{ ...monoTag, color: T_SECONDARY }}>
|
|
103
|
+
expanded · same header + revealed body
|
|
104
|
+
</Text>
|
|
105
|
+
<Surface>
|
|
106
|
+
<ExerciseCard {...CARD_PROPS} defaultExpanded />
|
|
107
|
+
</Surface>
|
|
108
|
+
</View>
|
|
109
|
+
</View>
|
|
110
|
+
|
|
111
|
+
<View style={{ height: 1, backgroundColor: BORDER_SUBTLE, marginVertical: 8 }} />
|
|
112
|
+
|
|
113
|
+
<View style={{ gap: 10, maxWidth: 320 }}>
|
|
114
|
+
<Text style={{ ...monoTag, color: T_SECONDARY }}>
|
|
115
|
+
the locked active-set spotlight · VelocityStrip variant="expanded" (bare)
|
|
116
|
+
</Text>
|
|
117
|
+
<View style={{ backgroundColor: INSET, borderRadius: 8, padding: 12 }}>
|
|
118
|
+
<VelocityStrip
|
|
119
|
+
variant="expanded"
|
|
120
|
+
showNumbers={false}
|
|
121
|
+
showInfo={false}
|
|
122
|
+
height={24}
|
|
123
|
+
scale="fixed"
|
|
124
|
+
set={{ type: 'straight', velocities: ACTIVE, planned: 10 }}
|
|
125
|
+
/>
|
|
126
|
+
</View>
|
|
127
|
+
<Text style={{ fontFamily: INTER, fontSize: 12.5, color: T_SECONDARY, lineHeight: 19 }}>
|
|
128
|
+
A small fixed-height (~24px) velocity-HEIGHT strip: bar height ∝ velocity against a fixed
|
|
129
|
+
1.15 m/s ceiling, planned reps as grey stubs, set-type aware — none of the full
|
|
130
|
+
variant's labels, info row, or mount animation. It reads as the same object as the
|
|
131
|
+
flat `mini` strips above it, just taller and brighter to mark the live set.
|
|
132
|
+
</Text>
|
|
133
|
+
</View>
|
|
134
|
+
|
|
135
|
+
<Text
|
|
136
|
+
style={{
|
|
137
|
+
fontFamily: INTER,
|
|
138
|
+
fontSize: 12.5,
|
|
139
|
+
color: T_PRIMARY,
|
|
140
|
+
lineHeight: 19,
|
|
141
|
+
maxWidth: 780,
|
|
142
|
+
marginTop: 6,
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
The header is the same molecule collapsed and expanded — the card reads as one object, the
|
|
146
|
+
body flowing out of the rail row rather than a separate panel. The expanded card is the real
|
|
147
|
+
ExerciseCard; its header per-set strip is derived from the same rows the body renders.
|
|
148
|
+
</Text>
|
|
149
|
+
</Page>
|
|
150
|
+
),
|
|
151
|
+
}
|
|
@@ -31,13 +31,15 @@ const meso: MesoStatusCardProps = {
|
|
|
31
31
|
|
|
32
32
|
function historySets(weight: number, topRpe: number): SetRowProps[] {
|
|
33
33
|
return [1, 2, 3, 4].map((setNumber) => ({
|
|
34
|
-
|
|
34
|
+
state: 'done' as const,
|
|
35
35
|
setNumber,
|
|
36
|
-
previous: { reps: 8, weight: weight - 5 },
|
|
37
36
|
reps: 8,
|
|
38
37
|
weight,
|
|
39
38
|
rpe: topRpe - (4 - setNumber) * 0.5,
|
|
40
39
|
unit: 'lbs' as const,
|
|
40
|
+
velocities: Array.from({ length: 8 }, (_, i) =>
|
|
41
|
+
Number((0.72 - (setNumber - 1) * 0.04 - i * 0.03).toFixed(2))
|
|
42
|
+
),
|
|
41
43
|
}))
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -188,7 +190,7 @@ const vbt: ExerciseVbt = {
|
|
|
188
190
|
}
|
|
189
191
|
|
|
190
192
|
const meta: Meta<typeof ExerciseDetailPage> = {
|
|
191
|
-
title: '
|
|
193
|
+
title: 'Pages/Exercise Detail',
|
|
192
194
|
component: ExerciseDetailPage,
|
|
193
195
|
parameters: { layout: 'fullscreen' },
|
|
194
196
|
tags: ['autodocs'],
|
|
@@ -16,8 +16,8 @@ const entry = (id: string, weight: number, e1rm: number, isPR = false): Exercise
|
|
|
16
16
|
e1rm: { value: e1rm, unit: 'lbs' },
|
|
17
17
|
isPR,
|
|
18
18
|
sets: [
|
|
19
|
-
{
|
|
20
|
-
{
|
|
19
|
+
{ state: 'done', setNumber: 1, unit: 'lbs', reps: 8, weight, rpe: 8, velocities: [] },
|
|
20
|
+
{ state: 'done', setNumber: 2, unit: 'lbs', reps: 8, weight, rpe: 8.5, velocities: [] },
|
|
21
21
|
],
|
|
22
22
|
})
|
|
23
23
|
|
|
@@ -67,11 +67,11 @@ describe('deriveExerciseStats', () => {
|
|
|
67
67
|
describe('toExerciseCardProps', () => {
|
|
68
68
|
it('collapses without sets and expands with them', () => {
|
|
69
69
|
const collapsed = toExerciseCardProps(history[0], false, () => {})
|
|
70
|
-
expect(collapsed.
|
|
70
|
+
expect(collapsed.expanded).toBe(false)
|
|
71
71
|
expect(collapsed.sets).toBeUndefined()
|
|
72
72
|
|
|
73
73
|
const expanded = toExerciseCardProps(history[0], true, () => {})
|
|
74
|
-
expect(expanded.
|
|
74
|
+
expect(expanded.expanded).toBe(true)
|
|
75
75
|
expect(expanded.sets).toHaveLength(2)
|
|
76
76
|
expect(expanded.name).toBe('Session a')
|
|
77
77
|
})
|
|
@@ -65,8 +65,8 @@ export interface ExerciseDetailEntry {
|
|
|
65
65
|
title: string
|
|
66
66
|
/** Collapsed summary (sets × reps @ weight). */
|
|
67
67
|
summary?: ExerciseCardProps['summary']
|
|
68
|
-
/** Estimated one-rep max
|
|
69
|
-
e1rm?:
|
|
68
|
+
/** Estimated one-rep max, feeding the page's best-e1RM stat. */
|
|
69
|
+
e1rm?: { value: number; unit: 'lbs' | 'kg' }
|
|
70
70
|
/** Flags a personal-record entry. */
|
|
71
71
|
isPR?: boolean
|
|
72
72
|
/** Prescribed tempo, revealed when expanded. */
|
|
@@ -162,10 +162,9 @@ export function toExerciseCardProps(
|
|
|
162
162
|
): ExerciseCardProps {
|
|
163
163
|
return {
|
|
164
164
|
name: entry.title,
|
|
165
|
-
|
|
166
|
-
onToggle,
|
|
165
|
+
expanded,
|
|
166
|
+
onExpandedChange: onToggle,
|
|
167
167
|
summary: entry.summary,
|
|
168
|
-
e1rm: entry.e1rm,
|
|
169
168
|
isPR: entry.isPR,
|
|
170
169
|
tempo: entry.tempo,
|
|
171
170
|
sets: expanded ? entry.sets : undefined,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { ExerciseHeading } from './ExerciseHeading'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `ExerciseHeading` — the exercise-heading info block (no strip): the name +
|
|
7
|
+
* `ExerciseIndicator` title row over a tight `SetsRepsLoad` prescription line beside
|
|
8
|
+
* the real `TempoDisplay` (`showLabel={false}`). The name row is the sole press
|
|
9
|
+
* target; TempoDisplay is a sibling (it is itself a Pressable).
|
|
10
|
+
*/
|
|
11
|
+
const meta: Meta<typeof ExerciseHeading> = {
|
|
12
|
+
title: 'Workout/ExerciseHeading',
|
|
13
|
+
component: ExerciseHeading,
|
|
14
|
+
tags: ['autodocs'],
|
|
15
|
+
parameters: {
|
|
16
|
+
docs: {
|
|
17
|
+
description: {
|
|
18
|
+
component:
|
|
19
|
+
'**Molecule.** The exercise-heading info block (no strip). Composes ' +
|
|
20
|
+
'[ExerciseIndicator](?path=/docs/workout-exerciseindicator--docs) + ' +
|
|
21
|
+
'[SetsRepsLoad](?path=/docs/workout-setsrepsload--docs) + ' +
|
|
22
|
+
'[TempoDisplay](?path=/docs/workout-tempodisplay--docs). ' +
|
|
23
|
+
'Used-by ↑ [ExerciseCardHeading](?path=/docs/workout-exercisecardheading--docs).',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
argTypes: {
|
|
28
|
+
unit: { control: 'select', options: ['lbs', 'kg'] },
|
|
29
|
+
indicator: {
|
|
30
|
+
control: 'select',
|
|
31
|
+
options: [undefined, 'imbalance', 'overshoot', 'velocity-loss', 'missed-reps', 'pr', 'info'],
|
|
32
|
+
},
|
|
33
|
+
dimmed: { control: 'boolean' },
|
|
34
|
+
onPress: { action: 'press' },
|
|
35
|
+
},
|
|
36
|
+
decorators: [
|
|
37
|
+
(Story) => (
|
|
38
|
+
<View style={{ width: 246, padding: 16, backgroundColor: '#131313' }}>
|
|
39
|
+
<Story />
|
|
40
|
+
</View>
|
|
41
|
+
),
|
|
42
|
+
],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default meta
|
|
46
|
+
type Story = StoryObj<typeof ExerciseHeading>
|
|
47
|
+
|
|
48
|
+
export const Default: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
name: 'Seated Cable Row',
|
|
51
|
+
sets: 5,
|
|
52
|
+
reps: 8,
|
|
53
|
+
load: 145,
|
|
54
|
+
unit: 'lbs',
|
|
55
|
+
tempo: [3, 1, 2, 0],
|
|
56
|
+
indicator: 'pr',
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const NoTempoNoIndicator: Story = {
|
|
61
|
+
args: { name: 'Incline DB Press', sets: 3, reps: 8, load: 70, unit: 'lbs' },
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const RepRange: Story = {
|
|
65
|
+
args: {
|
|
66
|
+
name: 'Face Pull',
|
|
67
|
+
sets: 3,
|
|
68
|
+
reps: '15-20',
|
|
69
|
+
load: 40,
|
|
70
|
+
unit: 'lbs',
|
|
71
|
+
tempo: [2, 1, 2, 0],
|
|
72
|
+
indicator: 'info',
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const Dimmed: Story = {
|
|
77
|
+
args: { ...Default.args, dimmed: true },
|
|
78
|
+
parameters: {
|
|
79
|
+
docs: { description: { story: 'Dimmed as a not-yet-reached exercise (standalone use).' } },
|
|
80
|
+
},
|
|
81
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { ExerciseHeading } from './ExerciseHeading'
|
|
5
|
+
|
|
6
|
+
const baseProps = {
|
|
7
|
+
name: 'Seated Cable Row',
|
|
8
|
+
sets: 5,
|
|
9
|
+
reps: 8,
|
|
10
|
+
load: 145,
|
|
11
|
+
unit: 'lbs' as const,
|
|
12
|
+
tempo: [3, 1, 2, 0] as [number, number, number, number],
|
|
13
|
+
indicator: 'pr' as const,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('ExerciseHeading', () => {
|
|
17
|
+
it('renders the exercise name', () => {
|
|
18
|
+
render(<ExerciseHeading {...baseProps} />)
|
|
19
|
+
expect(screen.getByTestId('exercise-card-name')).toHaveTextContent('Seated Cable Row')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('renders the sets/reps/load line with separators', () => {
|
|
23
|
+
render(<ExerciseHeading {...baseProps} />)
|
|
24
|
+
const summary = screen.getByTestId('exercise-card-summary')
|
|
25
|
+
expect(summary).toHaveTextContent('5')
|
|
26
|
+
expect(summary).toHaveTextContent('8')
|
|
27
|
+
expect(summary).toHaveTextContent('145')
|
|
28
|
+
expect(summary).toHaveTextContent('×')
|
|
29
|
+
expect(summary).toHaveTextContent('@')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('renders the indicator chip when provided', () => {
|
|
33
|
+
render(<ExerciseHeading {...baseProps} />)
|
|
34
|
+
expect(screen.getByLabelText('Personal record')).toBeInTheDocument()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('omits the indicator chip when absent', () => {
|
|
38
|
+
render(<ExerciseHeading {...baseProps} indicator={undefined} />)
|
|
39
|
+
expect(screen.queryByTestId('exercise-indicator')).not.toBeInTheDocument()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('renders the TempoDisplay without its TEMPO caption when tempo is given', () => {
|
|
43
|
+
render(<ExerciseHeading {...baseProps} />)
|
|
44
|
+
expect(screen.getByTestId('tempo-display')).toBeInTheDocument()
|
|
45
|
+
expect(screen.queryByText('TEMPO')).not.toBeInTheDocument()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('omits the TempoDisplay when no tempo', () => {
|
|
49
|
+
render(<ExerciseHeading {...baseProps} tempo={undefined} />)
|
|
50
|
+
expect(screen.queryByTestId('tempo-display')).not.toBeInTheDocument()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('fires onPress when the name row is pressed', () => {
|
|
54
|
+
const onPress = vi.fn()
|
|
55
|
+
render(<ExerciseHeading {...baseProps} onPress={onPress} />)
|
|
56
|
+
fireEvent.click(screen.getByTestId('exercise-card-header'))
|
|
57
|
+
expect(onPress).toHaveBeenCalledOnce()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('dims the block when marked dimmed', () => {
|
|
61
|
+
render(<ExerciseHeading {...baseProps} dimmed />)
|
|
62
|
+
expect(screen.getByTestId('exercise-heading')).toHaveStyle({ opacity: 0.55 })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('is full opacity by default', () => {
|
|
66
|
+
render(<ExerciseHeading {...baseProps} />)
|
|
67
|
+
expect(screen.getByTestId('exercise-heading')).toHaveStyle({ opacity: 1 })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('accessibility', () => {
|
|
71
|
+
it('has no accessibility violations', async () => {
|
|
72
|
+
const { container } = render(<ExerciseHeading {...baseProps} onPress={vi.fn()} />)
|
|
73
|
+
const results = await axe(container)
|
|
74
|
+
expect(results).toHaveNoViolations()
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import { View, Text, Pressable } from 'react-native'
|
|
3
|
+
import { roundWeight } from '../../../utils/workout-format'
|
|
4
|
+
import { SetsRepsLoad } from './SetsRepsLoad'
|
|
5
|
+
import { TempoDisplay } from './TempoDisplay'
|
|
6
|
+
import { ExerciseIndicator, type ExerciseIndicatorKind } from './ExerciseIndicator'
|
|
7
|
+
|
|
8
|
+
export interface ExerciseHeadingProps {
|
|
9
|
+
name: string
|
|
10
|
+
/** Prescription line values (sets × reps @ load). */
|
|
11
|
+
sets: number
|
|
12
|
+
reps: number | string
|
|
13
|
+
load: number
|
|
14
|
+
unit?: 'lbs' | 'kg'
|
|
15
|
+
/** Tempo tuple [eccentric, pauseBottom, concentric, pauseTop]; hidden when absent. */
|
|
16
|
+
tempo?: [number, number, number, number]
|
|
17
|
+
/** Small PR / issue / info chip in the title row. */
|
|
18
|
+
indicator?: ExerciseIndicatorKind
|
|
19
|
+
/** Dim the block as a not-yet-reached exercise (standalone use). */
|
|
20
|
+
dimmed?: boolean
|
|
21
|
+
/** Press target for the name row (expand / select). */
|
|
22
|
+
onPress?: () => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function headingLabel(
|
|
26
|
+
name: string,
|
|
27
|
+
sets: number,
|
|
28
|
+
reps: number | string,
|
|
29
|
+
load: number,
|
|
30
|
+
unit: string
|
|
31
|
+
) {
|
|
32
|
+
return `${name}, ${sets}×${reps} @ ${roundWeight(load)} ${unit}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The exercise-heading info block (no strip): the name + {@link ExerciseIndicator}
|
|
37
|
+
* title row over a tight {@link SetsRepsLoad} prescription line beside the real
|
|
38
|
+
* {@link TempoDisplay} (`showLabel={false}`). The name row is the sole press
|
|
39
|
+
* target — TempoDisplay is a sibling, never a descendant, since it is itself a
|
|
40
|
+
* Pressable (nesting would be an a11y nested-interactive violation).
|
|
41
|
+
*/
|
|
42
|
+
export function ExerciseHeading({
|
|
43
|
+
name,
|
|
44
|
+
sets,
|
|
45
|
+
reps,
|
|
46
|
+
load,
|
|
47
|
+
unit = 'lbs',
|
|
48
|
+
tempo,
|
|
49
|
+
indicator,
|
|
50
|
+
dimmed,
|
|
51
|
+
onPress,
|
|
52
|
+
}: ExerciseHeadingProps) {
|
|
53
|
+
return (
|
|
54
|
+
<View style={{ opacity: dimmed ? 0.55 : 1 }} testID="exercise-heading">
|
|
55
|
+
<Pressable
|
|
56
|
+
onPress={onPress}
|
|
57
|
+
accessibilityRole="button"
|
|
58
|
+
accessibilityLabel={headingLabel(name, sets, reps, load, unit)}
|
|
59
|
+
className="flex-row items-center"
|
|
60
|
+
style={{ gap: 8 }}
|
|
61
|
+
testID="exercise-card-header"
|
|
62
|
+
>
|
|
63
|
+
<Text
|
|
64
|
+
className="text-text-primary"
|
|
65
|
+
style={{ fontSize: 14, fontFamily: '"Space Grotesk", sans-serif', fontWeight: '700' }}
|
|
66
|
+
testID="exercise-card-name"
|
|
67
|
+
>
|
|
68
|
+
{name}
|
|
69
|
+
</Text>
|
|
70
|
+
<View className="flex-1" />
|
|
71
|
+
{indicator && <ExerciseIndicator kind={indicator} />}
|
|
72
|
+
</Pressable>
|
|
73
|
+
|
|
74
|
+
<View
|
|
75
|
+
className="flex-row items-center"
|
|
76
|
+
style={{ gap: 8, marginTop: 1 }}
|
|
77
|
+
testID="exercise-card-summary"
|
|
78
|
+
>
|
|
79
|
+
<SetsRepsLoad sets={sets} reps={reps} load={load} unit={unit} />
|
|
80
|
+
<View className="flex-1" />
|
|
81
|
+
{tempo && <TempoDisplay tempo={tempo} size="sm" showLabel={false} showInfo={false} />}
|
|
82
|
+
</View>
|
|
83
|
+
</View>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import {
|
|
4
|
+
ExerciseIndicator,
|
|
5
|
+
resolveIndicator,
|
|
6
|
+
INDICATOR_PRECEDENCE,
|
|
7
|
+
type ExerciseIndicatorKind,
|
|
8
|
+
} from './ExerciseIndicator'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `ExerciseIndicator` — a small circular OUTLINED chip in an exercise heading's
|
|
12
|
+
* title line. The LOCKED taxonomy is 6 precedence-ranked kinds across four tier
|
|
13
|
+
* colors (danger/warning/success/info): glyph = the specific signal, color = the
|
|
14
|
+
* severity tier. Only one chip is ever shown per exercise — {@link resolveIndicator}
|
|
15
|
+
* picks the highest-precedence active signal.
|
|
16
|
+
*/
|
|
17
|
+
const meta: Meta<typeof ExerciseIndicator> = {
|
|
18
|
+
title: 'Workout/ExerciseIndicator',
|
|
19
|
+
component: ExerciseIndicator,
|
|
20
|
+
tags: ['autodocs'],
|
|
21
|
+
parameters: {
|
|
22
|
+
docs: {
|
|
23
|
+
description: {
|
|
24
|
+
component:
|
|
25
|
+
'**Atom.** A small circular outlined chip in an exercise heading title line. Six ' +
|
|
26
|
+
'precedence-ranked kinds (imbalance › overshoot › velocity-loss › missed-reps › pr › ' +
|
|
27
|
+
'info) over four status-token tier colors; glyph = signal, color = severity tier. ' +
|
|
28
|
+
'`resolveIndicator()` selects the single chip to show. Used-by ↑ ' +
|
|
29
|
+
'[ExerciseHeading](?path=/docs/workout-exerciseheading--docs).',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
argTypes: {
|
|
34
|
+
kind: { control: 'select', options: [...INDICATOR_PRECEDENCE] },
|
|
35
|
+
onPress: { action: 'press' },
|
|
36
|
+
},
|
|
37
|
+
decorators: [
|
|
38
|
+
(Story) => (
|
|
39
|
+
<View style={{ padding: 24, backgroundColor: '#131313' }}>
|
|
40
|
+
<Story />
|
|
41
|
+
</View>
|
|
42
|
+
),
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default meta
|
|
47
|
+
type Story = StoryObj<typeof ExerciseIndicator>
|
|
48
|
+
|
|
49
|
+
export const Imbalance: Story = { args: { kind: 'imbalance' } }
|
|
50
|
+
export const Overshoot: Story = { args: { kind: 'overshoot' } }
|
|
51
|
+
export const VelocityLoss: Story = { args: { kind: 'velocity-loss' } }
|
|
52
|
+
export const MissedReps: Story = { args: { kind: 'missed-reps' } }
|
|
53
|
+
export const PersonalRecord: Story = { args: { kind: 'pr' } }
|
|
54
|
+
export const Info: Story = { args: { kind: 'info' } }
|
|
55
|
+
|
|
56
|
+
const KIND_LABELS: Record<ExerciseIndicatorKind, string> = {
|
|
57
|
+
imbalance: 'imbalance · danger',
|
|
58
|
+
overshoot: 'overshoot · danger',
|
|
59
|
+
'velocity-loss': 'velocity-loss · warning',
|
|
60
|
+
'missed-reps': 'missed-reps · warning',
|
|
61
|
+
pr: 'pr · success',
|
|
62
|
+
info: 'info · info',
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** All six kinds in precedence order (1=highest → 6=lowest). */
|
|
66
|
+
export const AllKinds: Story = {
|
|
67
|
+
render: () => (
|
|
68
|
+
<View style={{ gap: 12 }}>
|
|
69
|
+
{INDICATOR_PRECEDENCE.map((kind, i) => (
|
|
70
|
+
<View key={kind} style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
|
|
71
|
+
<Text style={{ color: '#8A8A8A', fontSize: 11, width: 14 }}>{i + 1}</Text>
|
|
72
|
+
<ExerciseIndicator kind={kind} />
|
|
73
|
+
<Text style={{ color: '#CFCFCF', fontSize: 12 }}>{KIND_LABELS[kind]}</Text>
|
|
74
|
+
</View>
|
|
75
|
+
))}
|
|
76
|
+
</View>
|
|
77
|
+
),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `resolveIndicator` demo — each row is a set of active signals; the resolver
|
|
82
|
+
* collapses them to the single highest-precedence chip actually shown.
|
|
83
|
+
*/
|
|
84
|
+
export const ResolvePrecedence: Story = {
|
|
85
|
+
render: () => {
|
|
86
|
+
const scenarios: ExerciseIndicatorKind[][] = [
|
|
87
|
+
['info', 'pr'],
|
|
88
|
+
['pr', 'missed-reps'],
|
|
89
|
+
['velocity-loss', 'overshoot', 'info'],
|
|
90
|
+
['missed-reps', 'imbalance', 'pr'],
|
|
91
|
+
]
|
|
92
|
+
return (
|
|
93
|
+
<View style={{ gap: 12 }}>
|
|
94
|
+
{scenarios.map((candidates, i) => {
|
|
95
|
+
const winner = resolveIndicator(candidates)
|
|
96
|
+
return (
|
|
97
|
+
<View key={i} style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
|
|
98
|
+
<Text style={{ color: '#8A8A8A', fontSize: 12, width: 200 }}>
|
|
99
|
+
{candidates.join(', ')}
|
|
100
|
+
</Text>
|
|
101
|
+
<Text style={{ color: '#8A8A8A', fontSize: 12 }}>→</Text>
|
|
102
|
+
{winner ? <ExerciseIndicator kind={winner} /> : null}
|
|
103
|
+
<Text style={{ color: '#CFCFCF', fontSize: 12 }}>{winner}</Text>
|
|
104
|
+
</View>
|
|
105
|
+
)
|
|
106
|
+
})}
|
|
107
|
+
</View>
|
|
108
|
+
)
|
|
109
|
+
},
|
|
110
|
+
}
|