@titan-design/react-ui 0.4.0 → 0.6.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/MesoStatusCard-CQk71hSi.d.mts +77 -0
- package/dist/MesoStatusCard-CQk71hSi.d.ts +77 -0
- package/dist/bodymap.d.mts +246 -3
- package/dist/bodymap.d.ts +246 -3
- package/dist/bodymap.js +232 -185
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +1963 -11
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +193 -744
- package/dist/index.d.ts +193 -744
- package/dist/index.js +1551 -1856
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10770 -8213
- package/dist/index.mjs.map +1 -1
- package/dist/pages-D7Jlssvp.d.ts +791 -0
- package/dist/pages-MO0JCySL.d.mts +791 -0
- package/dist/pages.d.mts +4 -0
- package/dist/pages.d.ts +4 -0
- package/dist/pages.js +6694 -0
- package/dist/pages.js.map +1 -0
- package/dist/pages.mjs +6674 -0
- package/dist/pages.mjs.map +1 -0
- package/dist/theme/index.d.mts +473 -211
- package/dist/theme/index.d.ts +473 -211
- package/dist/theme/index.js +341 -127
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +2085 -2
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +148 -139
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +582 -1
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +11 -7
- package/src/components/custom/DateTime/DateTime.stories.tsx +82 -6
- package/src/components/custom/DateTime/DateTime.test.tsx +29 -3
- package/src/components/custom/DateTime/DateTime.tsx +55 -15
- package/src/components/custom/Gauge/Gauge.test.tsx +2 -2
- package/src/components/custom/Gauge/Gauge.tsx +6 -3
- package/src/components/custom/Scatter/Scatter.tsx +3 -5
- package/src/components/custom/Treemap/Treemap.tsx +3 -5
- package/src/components/custom/Typography/Typography.stories.tsx +45 -27
- package/src/components/custom/Typography/Typography.test.tsx +13 -3
- package/src/components/custom/Typography/Typography.tsx +5 -0
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +2 -1
- package/src/components/custom/Workout/BaseBadge.stories.tsx +3 -3
- package/src/components/custom/Workout/BaseBadge.test.tsx +1 -1
- package/src/components/custom/Workout/BaseBadge.tsx +1 -1
- package/src/components/custom/Workout/BodyMap.tsx +6 -3
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +6 -3
- package/src/components/custom/Workout/CapacityBandChart.test.tsx +2 -2
- package/src/components/custom/Workout/CapacityBandChart.tsx +9 -6
- package/src/components/custom/Workout/DeviationBar.test.tsx +1 -1
- package/src/components/custom/Workout/DeviationBar.tsx +7 -4
- package/src/components/custom/Workout/ExerciseCard.tsx +5 -1
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +19 -5
- package/src/components/custom/Workout/InputBar.tsx +2 -1
- package/src/components/custom/Workout/IntensityBar.test.tsx +112 -60
- package/src/components/custom/Workout/IntensityBar.tsx +113 -58
- package/src/components/custom/Workout/MesoCard.tsx +8 -3
- package/src/components/custom/Workout/MesoProgressBar.test.tsx +1 -1
- package/src/components/custom/Workout/MesoProgressBar.tsx +3 -2
- package/src/components/custom/Workout/MesoStatusCard.tsx +20 -13
- package/src/components/custom/Workout/MuscleGroupChip.tsx +7 -4
- package/src/components/custom/Workout/PlaceholderStrip.tsx +5 -9
- package/src/components/custom/Workout/PrBadge.tsx +7 -4
- package/src/components/custom/Workout/PrHistoryModal.tsx +6 -5
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +2 -1
- package/src/components/custom/Workout/README.md +50 -0
- package/src/components/custom/Workout/ReadinessCheck.tsx +7 -4
- package/src/components/custom/Workout/RestTimer.test.tsx +41 -10
- package/src/components/custom/Workout/RestTimer.tsx +50 -44
- package/src/components/custom/Workout/SetRow.tsx +12 -2
- package/src/components/custom/Workout/Sparkline.tsx +2 -1
- package/src/components/custom/Workout/StatusDot.test.tsx +2 -2
- package/src/components/custom/Workout/StatusDot.tsx +12 -9
- package/src/components/custom/Workout/StrengthTrendChart.tsx +9 -6
- package/src/components/custom/Workout/SupersetWrapper.tsx +2 -1
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +69 -2
- package/src/components/custom/Workout/TempoDisplay.test.tsx +35 -0
- package/src/components/custom/Workout/TempoDisplay.tsx +109 -3
- package/src/components/custom/Workout/VelocityStrip.test.tsx +89 -2
- package/src/components/custom/Workout/VelocityStrip.tsx +203 -38
- package/src/components/custom/Workout/WeekRow.test.tsx +2 -2
- package/src/components/custom/Workout/WeekRow.tsx +3 -2
- package/src/components/custom/Workout/WeightBadge.test.tsx +2 -2
- package/src/components/custom/Workout/WeightBadge.tsx +8 -4
- package/src/components/custom/Workout/WorkoutCard.tsx +5 -2
- package/src/components/custom/Workout/WorkoutPill.tsx +12 -8
- package/src/components/custom/Workout/icons.tsx +7 -0
- package/src/components/custom/Workout/index.ts +38 -47
- package/src/components/icons/SvgIcon.tsx +50 -0
- package/src/components/icons/icons.stories.tsx +57 -0
- package/src/components/icons/icons.test.tsx +43 -0
- package/src/components/icons/icons.tsx +45 -0
- package/src/components/icons/index.ts +4 -0
- package/src/components/index.ts +6 -0
- package/src/components/shell/BrandLockup.stories.tsx +27 -0
- package/src/components/shell/BrandLockup.test.tsx +23 -0
- package/src/components/shell/BrandLockup.tsx +47 -0
- package/src/components/shell/DeviceIndicator.stories.tsx +28 -0
- package/src/components/shell/DeviceIndicator.test.tsx +26 -0
- package/src/components/shell/DeviceIndicator.tsx +52 -0
- package/src/components/shell/DeviceMenu.stories.tsx +47 -0
- package/src/components/shell/DeviceMenu.test.tsx +40 -0
- package/src/components/shell/DeviceMenu.tsx +65 -0
- package/src/components/shell/DeviceRow.stories.tsx +38 -0
- package/src/components/shell/DeviceRow.test.tsx +37 -0
- package/src/components/shell/DeviceRow.tsx +58 -0
- package/src/components/shell/README.md +86 -0
- package/src/components/shell/SessionStatePill.stories.tsx +27 -0
- package/src/components/shell/SessionStatePill.test.tsx +25 -0
- package/src/components/shell/SessionStatePill.tsx +46 -0
- package/src/components/shell/TopBar.stories.tsx +49 -0
- package/src/components/shell/TopBar.test.tsx +36 -0
- package/src/components/shell/TopBar.tsx +89 -0
- package/src/components/shell/index.ts +8 -0
- package/src/components/ui/indicator/Indicator.stories.tsx +37 -1
- package/src/components/ui/indicator/Indicator.test.tsx +19 -1
- package/src/components/ui/indicator/Indicator.tsx +45 -6
- package/src/components/ui/indicator/index.ts +1 -1
- package/src/components/ui/spinner/Spinner.tsx +3 -2
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +2 -1
- package/src/index.ts +3 -2
- package/src/pages.ts +61 -0
- package/src/test/stories-smoke.test.tsx +50 -0
- package/src/theme/ColorSystem.stories.tsx +323 -0
- package/src/theme/Colors.stories.tsx +69 -1
- package/src/theme/barrel.ts +54 -0
- package/src/theme/config.ts +16 -3
- package/src/theme/extracted-colors-dataviz.ts +21 -0
- package/src/theme/extracted-colors-ui.ts +20 -0
- package/src/theme/global.css +64 -52
- package/src/theme/gradients.test.ts +25 -0
- package/src/theme/gradients.ts +36 -0
- package/src/theme/index.ts +8 -43
- package/src/theme/native-theming-guard.test.ts +117 -0
- package/src/theme/tokens/primitives.test.ts +232 -0
- package/src/theme/tokens/primitives.ts +247 -0
- package/src/theme/tokens/semantic.ts +93 -81
- package/src/theme/workout-tokens.ts +22 -21
- package/src/utils/avatar-color.ts +3 -3
- package/tailwind.config.js +10 -3
- package/dist/bodymap-BhG55xHM.d.mts +0 -318
- package/dist/bodymap-BhG55xHM.d.ts +0 -318
- package/dist/chunk-2SISKTWM.mjs +0 -1027
- package/dist/chunk-2SISKTWM.mjs.map +0 -1
- package/dist/chunk-7XPB4NAO.mjs +0 -1013
- package/dist/chunk-7XPB4NAO.mjs.map +0 -1
- package/dist/chunk-SNVKL5WZ.mjs +0 -883
- package/dist/chunk-SNVKL5WZ.mjs.map +0 -1
|
@@ -3,6 +3,8 @@ import { render, screen } from '@testing-library/react'
|
|
|
3
3
|
import { axe } from 'jest-axe'
|
|
4
4
|
import { IntensityBar } from './IntensityBar'
|
|
5
5
|
|
|
6
|
+
const AT_TARGET_GLOW = '0 0 5px 1px rgba(33, 150, 243, 0.35), 0 0 10px 3px rgba(33, 150, 243, 0.15)'
|
|
7
|
+
|
|
6
8
|
describe('IntensityBar', () => {
|
|
7
9
|
it('renders the bar', () => {
|
|
8
10
|
render(<IntensityBar level={0.5} />)
|
|
@@ -14,98 +16,148 @@ describe('IntensityBar', () => {
|
|
|
14
16
|
expect(screen.getByTestId('intensity-fill')).toBeInTheDocument()
|
|
15
17
|
})
|
|
16
18
|
|
|
17
|
-
it('
|
|
18
|
-
render(<IntensityBar level={0.5} threshold={0.85} />)
|
|
19
|
-
expect(screen.getByTestId('intensity-threshold')).toBeInTheDocument()
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('does not render threshold line when threshold is omitted', () => {
|
|
19
|
+
it('has correct accessibility role', () => {
|
|
23
20
|
render(<IntensityBar level={0.5} />)
|
|
24
|
-
expect(screen.
|
|
21
|
+
expect(screen.getByRole('progressbar')).toBeInTheDocument()
|
|
25
22
|
})
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
describe('numeric percentage label', () => {
|
|
25
|
+
it('renders the level as a percentage under the bar', () => {
|
|
26
|
+
render(<IntensityBar level={0.2} />)
|
|
27
|
+
expect(screen.getByTestId('intensity-label')).toHaveTextContent('20%')
|
|
28
|
+
})
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
it('renders over-target percentages above 100', () => {
|
|
31
|
+
render(<IntensityBar level={1.3} />)
|
|
32
|
+
expect(screen.getByTestId('intensity-label')).toHaveTextContent('130%')
|
|
33
|
+
})
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
it('mirrors the label in the accessibility value', () => {
|
|
36
|
+
render(<IntensityBar level={1.1} />)
|
|
37
|
+
expect(screen.getByLabelText('Intensity level: 110%')).toBeInTheDocument()
|
|
38
|
+
})
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
it('floors a negative level to 0%', () => {
|
|
41
|
+
render(<IntensityBar level={-0.5} />)
|
|
42
|
+
expect(screen.getByTestId('intensity-label')).toHaveTextContent('0%')
|
|
43
|
+
})
|
|
45
44
|
})
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
describe('zone colors graded by true percentage', () => {
|
|
47
|
+
it('renders green below the target ramp (< 75%)', () => {
|
|
48
|
+
render(<IntensityBar level={0.2} />)
|
|
49
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#2ED573' })
|
|
50
|
+
})
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
it('stays green just under the amber boundary', () => {
|
|
53
|
+
render(<IntensityBar level={0.74} />)
|
|
54
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#2ED573' })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('renders amber approaching the target (75-99%)', () => {
|
|
58
|
+
render(<IntensityBar level={0.75} />)
|
|
59
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#F9B415' })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('renders the brand target color exactly at 100%', () => {
|
|
63
|
+
render(<IntensityBar level={1} />)
|
|
64
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#FF7900' })
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('renders over-1 red just past target (110%)', () => {
|
|
68
|
+
render(<IntensityBar level={1.1} />)
|
|
69
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#D14343' })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('renders over-2 deep red at 120%', () => {
|
|
73
|
+
render(<IntensityBar level={1.2} />)
|
|
74
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#A4221C' })
|
|
75
|
+
})
|
|
56
76
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
it('renders over-3 darkest red at 130%', () => {
|
|
78
|
+
render(<IntensityBar level={1.3} />)
|
|
79
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#7E1002' })
|
|
80
|
+
})
|
|
60
81
|
})
|
|
61
82
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
83
|
+
describe('over-target bulge', () => {
|
|
84
|
+
it('renders a bulge when the level exceeds target', () => {
|
|
85
|
+
render(<IntensityBar level={1.1} />)
|
|
86
|
+
expect(screen.getByTestId('intensity-bulge')).toBeInTheDocument()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('colors the bulge to match the over-target zone', () => {
|
|
90
|
+
render(<IntensityBar level={1.2} />)
|
|
91
|
+
expect(screen.getByTestId('intensity-bulge')).toHaveStyle({ backgroundColor: '#A4221C' })
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('does not render a bulge at or below target', () => {
|
|
95
|
+
render(<IntensityBar level={1} />)
|
|
96
|
+
expect(screen.queryByTestId('intensity-bulge')).not.toBeInTheDocument()
|
|
97
|
+
})
|
|
65
98
|
})
|
|
66
99
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
100
|
+
describe('target line', () => {
|
|
101
|
+
it('renders the target line when threshold is provided', () => {
|
|
102
|
+
render(<IntensityBar level={0.5} threshold={0.5} />)
|
|
103
|
+
expect(screen.getByTestId('intensity-target')).toBeInTheDocument()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('does not render the target line when threshold is omitted', () => {
|
|
107
|
+
render(<IntensityBar level={0.5} />)
|
|
108
|
+
expect(screen.queryByTestId('intensity-target')).not.toBeInTheDocument()
|
|
109
|
+
})
|
|
70
110
|
})
|
|
71
111
|
|
|
72
|
-
describe('
|
|
73
|
-
it('
|
|
74
|
-
render(<IntensityBar level={0.
|
|
75
|
-
expect(screen.getByTestId('intensity-fill')).toHaveStyle({
|
|
112
|
+
describe('at-target glow', () => {
|
|
113
|
+
it('applies the blue glow when the level rests on the target', () => {
|
|
114
|
+
render(<IntensityBar level={0.5} threshold={0.5} />)
|
|
115
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ boxShadow: AT_TARGET_GLOW })
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('does not glow when the level is away from the target', () => {
|
|
119
|
+
render(<IntensityBar level={0.2} threshold={0.85} />)
|
|
120
|
+
expect(screen.getByTestId('intensity-fill')).not.toHaveStyle({ boxShadow: AT_TARGET_GLOW })
|
|
76
121
|
})
|
|
77
122
|
|
|
78
|
-
it('
|
|
123
|
+
it('does not glow when no threshold is provided', () => {
|
|
79
124
|
render(<IntensityBar level={0.5} />)
|
|
80
|
-
expect(screen.getByTestId('intensity-fill')).toHaveStyle({
|
|
125
|
+
expect(screen.getByTestId('intensity-fill')).not.toHaveStyle({ boxShadow: AT_TARGET_GLOW })
|
|
81
126
|
})
|
|
127
|
+
})
|
|
82
128
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
129
|
+
describe('MRV threshold label', () => {
|
|
130
|
+
it('shows MRV when showThresholdLabel is true and threshold set', () => {
|
|
131
|
+
render(<IntensityBar level={0.6} threshold={0.85} showThresholdLabel />)
|
|
132
|
+
expect(screen.getByText('MRV')).toBeInTheDocument()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('hides MRV when showThresholdLabel is false', () => {
|
|
136
|
+
render(<IntensityBar level={0.6} threshold={0.85} />)
|
|
137
|
+
expect(screen.queryByTestId('intensity-threshold-label')).not.toBeInTheDocument()
|
|
86
138
|
})
|
|
87
139
|
|
|
88
|
-
it('
|
|
89
|
-
render(<IntensityBar level={0.
|
|
90
|
-
expect(screen.
|
|
140
|
+
it('hides MRV when threshold is omitted even if showThresholdLabel is true', () => {
|
|
141
|
+
render(<IntensityBar level={0.6} showThresholdLabel />)
|
|
142
|
+
expect(screen.queryByTestId('intensity-threshold-label')).not.toBeInTheDocument()
|
|
91
143
|
})
|
|
144
|
+
})
|
|
92
145
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
146
|
+
describe('geometry', () => {
|
|
147
|
+
it('renders horizontal orientation', () => {
|
|
148
|
+
render(<IntensityBar level={0.5} orientation="horizontal" />)
|
|
149
|
+
expect(screen.getByTestId('intensity-fill')).toBeInTheDocument()
|
|
96
150
|
})
|
|
97
151
|
|
|
98
|
-
it('
|
|
99
|
-
render(<IntensityBar level={0.
|
|
100
|
-
expect(screen.getByTestId('intensity-
|
|
152
|
+
it('accepts custom size and thickness', () => {
|
|
153
|
+
render(<IntensityBar level={0.5} size={36} thickness={8} />)
|
|
154
|
+
expect(screen.getByTestId('intensity-bar')).toBeInTheDocument()
|
|
101
155
|
})
|
|
102
156
|
})
|
|
103
157
|
|
|
104
158
|
describe('accessibility', () => {
|
|
105
159
|
it('has no accessibility violations', async () => {
|
|
106
|
-
const { container } = render(
|
|
107
|
-
<IntensityBar level={0.5} threshold={0.85} showThresholdLabel />,
|
|
108
|
-
)
|
|
160
|
+
const { container } = render(<IntensityBar level={0.5} threshold={0.85} showThresholdLabel />)
|
|
109
161
|
const results = await axe(container)
|
|
110
162
|
expect(results).toHaveNoViolations()
|
|
111
163
|
})
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
2
|
import { useEffect, useState } from 'react'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
View,
|
|
5
|
+
Text,
|
|
6
|
+
Animated,
|
|
7
|
+
type ViewProps,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
type DimensionValue,
|
|
10
|
+
} from 'react-native'
|
|
11
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
12
|
+
import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
|
|
13
|
+
|
|
14
|
+
const t = getSemanticColors('dark')
|
|
4
15
|
|
|
5
16
|
export type IntensityBarOrientation = 'vertical' | 'horizontal'
|
|
6
17
|
|
|
7
18
|
export interface IntensityBarProps extends ViewProps {
|
|
8
|
-
/** Current intensity level
|
|
19
|
+
/** Current intensity level; 1 == 100% of target. Values >1 render as over-target. */
|
|
9
20
|
level: number
|
|
10
|
-
/**
|
|
21
|
+
/** Target position 0-1. When the level sits at this target the fill gains an at-target glow. */
|
|
11
22
|
threshold?: number
|
|
12
23
|
/** Orientation */
|
|
13
24
|
orientation?: IntensityBarOrientation
|
|
@@ -15,28 +26,57 @@ export interface IntensityBarProps extends ViewProps {
|
|
|
15
26
|
size?: number
|
|
16
27
|
/** Width for vertical (default 6px) or height for horizontal */
|
|
17
28
|
thickness?: number
|
|
18
|
-
/** Show
|
|
29
|
+
/** Show the "MRV" threshold label under the bar */
|
|
19
30
|
showThresholdLabel?: boolean
|
|
20
31
|
className?: string
|
|
21
32
|
}
|
|
22
33
|
|
|
23
|
-
const TRACK_BG = '
|
|
24
|
-
const THRESHOLD_COLOR = '#FFFFFF'
|
|
34
|
+
const TRACK_BG = '#333333'
|
|
25
35
|
const LABEL_COLOR = '#6B7280'
|
|
36
|
+
const TARGET_LINE_COLOR = 'rgba(33, 150, 243, 0.5)'
|
|
37
|
+
const AT_TARGET_GLOW = '0 0 5px 1px rgba(33, 150, 243, 0.35), 0 0 10px 3px rgba(33, 150, 243, 0.15)'
|
|
38
|
+
|
|
39
|
+
// Zone colors graded by TRUE percentage (level * 100).
|
|
40
|
+
const ZONE = {
|
|
41
|
+
building: t['status-success'], // below target ramp-up
|
|
42
|
+
approaching: t['status-warning'], // nearing target
|
|
43
|
+
target: t['brand-primary'], // exactly at target
|
|
44
|
+
over1: t['status-error'], // mild over-target
|
|
45
|
+
over2: WORKOUT_TOKENS.intensity.over2, // moderate over-target
|
|
46
|
+
over3: WORKOUT_TOKENS.intensity.over3, // severe over-target
|
|
47
|
+
} as const
|
|
48
|
+
|
|
49
|
+
type OverTier = 0 | 1 | 2 | 3
|
|
26
50
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (level >= 0.4) return '#FFB020'
|
|
31
|
-
return '#14B8A6'
|
|
51
|
+
interface Zone {
|
|
52
|
+
color: string
|
|
53
|
+
over: OverTier
|
|
32
54
|
}
|
|
33
55
|
|
|
34
|
-
|
|
56
|
+
const BULGE_SIZE: Record<Exclude<OverTier, 0>, number> = { 1: 8, 2: 10, 3: 11 }
|
|
57
|
+
|
|
58
|
+
/** Map a true percentage to its fill color and over-target tier. */
|
|
59
|
+
function zoneForPct(pct: number): Zone {
|
|
60
|
+
if (pct >= 125) return { color: ZONE.over3, over: 3 }
|
|
61
|
+
if (pct >= 115) return { color: ZONE.over2, over: 2 }
|
|
62
|
+
if (pct > 100) return { color: ZONE.over1, over: 1 }
|
|
63
|
+
if (pct === 100) return { color: ZONE.target, over: 0 }
|
|
64
|
+
if (pct >= 75) return { color: ZONE.approaching, over: 0 }
|
|
65
|
+
return { color: ZONE.building, over: 0 }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function clampFill(value: number): number {
|
|
35
69
|
if (value < 0) return 0
|
|
36
70
|
if (value > 1) return 1
|
|
37
71
|
return value
|
|
38
72
|
}
|
|
39
73
|
|
|
74
|
+
/** True when the level rests on the supplied target (within 5%). */
|
|
75
|
+
function isAtTarget(pct: number, threshold?: number): boolean {
|
|
76
|
+
if (threshold == null) return false
|
|
77
|
+
return Math.abs(pct - Math.round(threshold * 100)) <= 5
|
|
78
|
+
}
|
|
79
|
+
|
|
40
80
|
export function IntensityBar({
|
|
41
81
|
level,
|
|
42
82
|
threshold,
|
|
@@ -48,21 +88,22 @@ export function IntensityBar({
|
|
|
48
88
|
...props
|
|
49
89
|
}: IntensityBarProps) {
|
|
50
90
|
const isVertical = orientation === 'vertical'
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
91
|
+
const fillLevel = clampFill(level)
|
|
92
|
+
const pct = Math.round(Math.max(0, level) * 100)
|
|
93
|
+
const zone = zoneForPct(pct)
|
|
94
|
+
const atTarget = isAtTarget(pct, threshold)
|
|
54
95
|
|
|
55
|
-
const [fillAnim] = useState(() => new Animated.Value(
|
|
96
|
+
const [fillAnim] = useState(() => new Animated.Value(fillLevel))
|
|
56
97
|
|
|
57
98
|
useEffect(() => {
|
|
58
99
|
const animation = Animated.timing(fillAnim, {
|
|
59
|
-
toValue:
|
|
100
|
+
toValue: fillLevel,
|
|
60
101
|
duration: 250,
|
|
61
102
|
useNativeDriver: false,
|
|
62
103
|
})
|
|
63
104
|
animation.start()
|
|
64
105
|
return () => animation.stop()
|
|
65
|
-
}, [
|
|
106
|
+
}, [fillLevel, fillAnim])
|
|
66
107
|
|
|
67
108
|
const fillSizePercent = fillAnim.interpolate({
|
|
68
109
|
inputRange: [0, 1],
|
|
@@ -74,51 +115,50 @@ export function IntensityBar({
|
|
|
74
115
|
: { width: size, height: thickness }
|
|
75
116
|
|
|
76
117
|
const fillStyle = isVertical
|
|
118
|
+
? { position: 'absolute' as const, bottom: 0, left: 0, right: 0, height: fillSizePercent }
|
|
119
|
+
: { position: 'absolute' as const, top: 0, bottom: 0, left: 0, width: fillSizePercent }
|
|
120
|
+
|
|
121
|
+
const bulgeSize = zone.over === 0 ? 0 : BULGE_SIZE[zone.over]
|
|
122
|
+
const bulgeStyle: ViewStyle = {
|
|
123
|
+
position: 'absolute',
|
|
124
|
+
width: bulgeSize,
|
|
125
|
+
height: bulgeSize,
|
|
126
|
+
borderRadius: bulgeSize / 2,
|
|
127
|
+
backgroundColor: zone.color,
|
|
128
|
+
zIndex: 1,
|
|
129
|
+
...(isVertical
|
|
130
|
+
? { top: 0, left: '50%', transform: [{ translateX: -bulgeSize / 2 }] }
|
|
131
|
+
: { right: 0, top: '50%', transform: [{ translateY: -bulgeSize / 2 }] }),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const targetPct: DimensionValue = `${clampFill(threshold ?? 0) * 100}%`
|
|
135
|
+
const targetLineStyle: ViewStyle = isVertical
|
|
77
136
|
? {
|
|
78
|
-
position: 'absolute'
|
|
79
|
-
bottom:
|
|
80
|
-
left:
|
|
81
|
-
right:
|
|
82
|
-
height:
|
|
137
|
+
position: 'absolute',
|
|
138
|
+
bottom: targetPct,
|
|
139
|
+
left: -3,
|
|
140
|
+
right: -3,
|
|
141
|
+
height: 1.5,
|
|
142
|
+
backgroundColor: TARGET_LINE_COLOR,
|
|
143
|
+
zIndex: 4,
|
|
83
144
|
}
|
|
84
145
|
: {
|
|
85
|
-
position: 'absolute'
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
width:
|
|
146
|
+
position: 'absolute',
|
|
147
|
+
left: targetPct,
|
|
148
|
+
top: -3,
|
|
149
|
+
bottom: -3,
|
|
150
|
+
width: 1.5,
|
|
151
|
+
backgroundColor: TARGET_LINE_COLOR,
|
|
152
|
+
zIndex: 4,
|
|
90
153
|
}
|
|
91
154
|
|
|
92
|
-
const thresholdLineStyle: ViewStyle | undefined =
|
|
93
|
-
threshold != null
|
|
94
|
-
? isVertical
|
|
95
|
-
? {
|
|
96
|
-
position: 'absolute' as const,
|
|
97
|
-
bottom: `${clamp01(threshold) * 100}%`,
|
|
98
|
-
left: -2,
|
|
99
|
-
right: -2,
|
|
100
|
-
height: 1,
|
|
101
|
-
backgroundColor: THRESHOLD_COLOR,
|
|
102
|
-
zIndex: 4,
|
|
103
|
-
}
|
|
104
|
-
: {
|
|
105
|
-
position: 'absolute' as const,
|
|
106
|
-
left: `${clamp01(threshold) * 100}%`,
|
|
107
|
-
top: -2,
|
|
108
|
-
bottom: -2,
|
|
109
|
-
width: 1,
|
|
110
|
-
backgroundColor: THRESHOLD_COLOR,
|
|
111
|
-
zIndex: 4,
|
|
112
|
-
}
|
|
113
|
-
: undefined
|
|
114
|
-
|
|
115
155
|
return (
|
|
116
156
|
<View
|
|
117
157
|
className={className}
|
|
118
158
|
style={{ alignItems: 'center' }}
|
|
119
159
|
accessibilityRole="progressbar"
|
|
120
|
-
accessibilityValue={{ min: 0, max: 100, now:
|
|
121
|
-
accessibilityLabel={`Intensity level: ${
|
|
160
|
+
accessibilityValue={{ min: 0, max: 100, now: pct }}
|
|
161
|
+
accessibilityLabel={`Intensity level: ${pct}%`}
|
|
122
162
|
testID="intensity-bar"
|
|
123
163
|
{...props}
|
|
124
164
|
>
|
|
@@ -136,16 +176,31 @@ export function IntensityBar({
|
|
|
136
176
|
style={{
|
|
137
177
|
...fillStyle,
|
|
138
178
|
borderRadius: 3,
|
|
139
|
-
backgroundColor:
|
|
179
|
+
backgroundColor: zone.color,
|
|
140
180
|
zIndex: 2,
|
|
181
|
+
...(atTarget ? { boxShadow: AT_TARGET_GLOW } : null),
|
|
141
182
|
}}
|
|
142
183
|
testID="intensity-fill"
|
|
143
184
|
/>
|
|
144
185
|
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
|
|
186
|
+
{zone.over !== 0 && <View style={bulgeStyle} testID="intensity-bulge" />}
|
|
187
|
+
|
|
188
|
+
{threshold != null && <View style={targetLineStyle} testID="intensity-target" />}
|
|
148
189
|
</View>
|
|
190
|
+
|
|
191
|
+
<Text
|
|
192
|
+
style={{
|
|
193
|
+
fontSize: 8,
|
|
194
|
+
color: LABEL_COLOR,
|
|
195
|
+
fontFamily: '"Nunito Sans", sans-serif',
|
|
196
|
+
marginTop: 6,
|
|
197
|
+
}}
|
|
198
|
+
accessibilityElementsHidden
|
|
199
|
+
testID="intensity-label"
|
|
200
|
+
>
|
|
201
|
+
{pct}%
|
|
202
|
+
</Text>
|
|
203
|
+
|
|
149
204
|
{showThresholdLabel && threshold != null && (
|
|
150
205
|
<Text
|
|
151
206
|
style={{
|
|
@@ -153,7 +208,7 @@ export function IntensityBar({
|
|
|
153
208
|
fontWeight: '500',
|
|
154
209
|
color: LABEL_COLOR,
|
|
155
210
|
fontFamily: 'Inter, sans-serif',
|
|
156
|
-
marginTop:
|
|
211
|
+
marginTop: 2,
|
|
157
212
|
}}
|
|
158
213
|
accessibilityElementsHidden
|
|
159
214
|
testID="intensity-threshold-label"
|
|
@@ -4,10 +4,15 @@ import { View, Text, Pressable, Animated, Easing, type ViewProps } from 'react-n
|
|
|
4
4
|
import { Card } from '../../ui/card'
|
|
5
5
|
import { Badge } from '../../ui/badge'
|
|
6
6
|
import { WeekRow, type WeekRowProps } from './WeekRow'
|
|
7
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
8
|
+
import {
|
|
9
|
+
MESO_ACCENT_GRADIENT_DARK,
|
|
10
|
+
MESO_ACCENT_GRADIENT_LIGHT,
|
|
11
|
+
} from '../../../theme/extracted-colors-dataviz'
|
|
7
12
|
|
|
8
|
-
const BRAND_PRIMARY = '
|
|
9
|
-
const BRAND_PRIMARY_DARK =
|
|
10
|
-
const BRAND_PRIMARY_LIGHT =
|
|
13
|
+
const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
|
|
14
|
+
const BRAND_PRIMARY_DARK = MESO_ACCENT_GRADIENT_DARK
|
|
15
|
+
const BRAND_PRIMARY_LIGHT = MESO_ACCENT_GRADIENT_LIGHT
|
|
11
16
|
const BORDER_DEFAULT = '#1F1F1F'
|
|
12
17
|
|
|
13
18
|
/** Gradient stops for the 3px top accent: dark -> primary -> light. */
|
|
@@ -40,7 +40,7 @@ describe('MesoProgressBar', () => {
|
|
|
40
40
|
it('applies completed color at 0.3 alpha', () => {
|
|
41
41
|
render(<MesoProgressBar mesos={mesos} activeMesoId={null} onMesoPress={vi.fn()} />)
|
|
42
42
|
expect(screen.getByTestId('meso-segment-inner-m1')).toHaveStyle({
|
|
43
|
-
backgroundColor: 'rgba(
|
|
43
|
+
backgroundColor: 'rgba(46,213,115,0.3)',
|
|
44
44
|
})
|
|
45
45
|
})
|
|
46
46
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
2
|
import { useState } from 'react'
|
|
3
3
|
import { View, Pressable, Animated, type ViewProps } from 'react-native'
|
|
4
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
4
5
|
|
|
5
|
-
const BRAND_PRIMARY = '
|
|
6
|
+
const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
|
|
6
7
|
|
|
7
8
|
export type MesoStatus = 'completed' | 'current' | 'upcoming'
|
|
8
9
|
|
|
@@ -26,7 +27,7 @@ export interface MesoProgressBarProps extends ViewProps {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
const segmentBgColors: Record<MesoStatus, string> = {
|
|
29
|
-
completed: 'rgba(
|
|
30
|
+
completed: 'rgba(46,213,115,0.3)',
|
|
30
31
|
current: 'rgba(255,121,0,0.5)',
|
|
31
32
|
upcoming: 'rgba(255,255,255,0.06)',
|
|
32
33
|
}
|
|
@@ -4,14 +4,21 @@ import { View, Text, type ViewProps, type ViewStyle } from 'react-native'
|
|
|
4
4
|
import { Card } from '../../ui/card'
|
|
5
5
|
import { StatusDot } from './StatusDot'
|
|
6
6
|
import { resolveColor } from '../../../theme/resolve-color'
|
|
7
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
8
|
+
import {
|
|
9
|
+
MESO_ACCENT_GRADIENT_DARK,
|
|
10
|
+
MESO_ACCENT_GRADIENT_LIGHT,
|
|
11
|
+
} from '../../../theme/extracted-colors-dataviz'
|
|
7
12
|
|
|
8
|
-
const
|
|
9
|
-
const BRAND_PRIMARY_DARK = '#C45E00'
|
|
10
|
-
const BRAND_PRIMARY_LIGHT = '#FFB066'
|
|
13
|
+
const t = getSemanticColors('dark')
|
|
11
14
|
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
+
const BRAND_PRIMARY = t['brand-primary']
|
|
16
|
+
const BRAND_PRIMARY_DARK = MESO_ACCENT_GRADIENT_DARK
|
|
17
|
+
const BRAND_PRIMARY_LIGHT = MESO_ACCENT_GRADIENT_LIGHT
|
|
18
|
+
|
|
19
|
+
const SUCCESS = t['status-success']
|
|
20
|
+
const WARNING = t['status-warning']
|
|
21
|
+
const ERROR = t['status-error']
|
|
15
22
|
|
|
16
23
|
/** Gradient stops for the 3px top accent: dark -> primary -> light (matches MesoCard). */
|
|
17
24
|
const ACCENT_STOPS = [BRAND_PRIMARY_DARK, BRAND_PRIMARY, BRAND_PRIMARY_LIGHT]
|
|
@@ -25,7 +32,7 @@ const CARD_GRADIENT = `linear-gradient(135deg, ${resolveColor('surface-elevated'
|
|
|
25
32
|
|
|
26
33
|
/** Gauge track gradient (teal -> amber -> red) at 0.25 alpha. */
|
|
27
34
|
const GAUGE_GRADIENT =
|
|
28
|
-
'linear-gradient(90deg, rgba(
|
|
35
|
+
'linear-gradient(90deg, rgba(46,213,115,0.25) 0%, rgba(249,180,21,0.25) 50%, rgba(209,67,67,0.25) 100%)'
|
|
29
36
|
|
|
30
37
|
export type MesoStatusBadgeVariant = 'success' | 'warning' | 'error'
|
|
31
38
|
|
|
@@ -33,8 +40,8 @@ const STATUS_VARIANTS: Record<
|
|
|
33
40
|
MesoStatusBadgeVariant,
|
|
34
41
|
{ bg: string; border: string; text: string }
|
|
35
42
|
> = {
|
|
36
|
-
success: { bg: 'rgba(
|
|
37
|
-
warning: { bg: 'rgba(
|
|
43
|
+
success: { bg: 'rgba(46,213,115,0.15)', border: 'rgba(46,213,115,0.3)', text: SUCCESS },
|
|
44
|
+
warning: { bg: 'rgba(249,180,21,0.15)', border: 'rgba(249,180,21,0.3)', text: WARNING },
|
|
38
45
|
error: { bg: 'rgba(209,67,67,0.15)', border: 'rgba(209,67,67,0.25)', text: ERROR },
|
|
39
46
|
}
|
|
40
47
|
|
|
@@ -400,9 +407,9 @@ export function MesoStatusCard({
|
|
|
400
407
|
{coaching != null && (
|
|
401
408
|
<View
|
|
402
409
|
style={{
|
|
403
|
-
backgroundColor: 'rgba(
|
|
410
|
+
backgroundColor: 'rgba(249,180,21,0.06)',
|
|
404
411
|
borderWidth: 1,
|
|
405
|
-
borderColor: 'rgba(
|
|
412
|
+
borderColor: 'rgba(249,180,21,0.15)',
|
|
406
413
|
borderRadius: 8,
|
|
407
414
|
paddingVertical: 10,
|
|
408
415
|
paddingHorizontal: 12,
|
|
@@ -427,9 +434,9 @@ export function MesoStatusCard({
|
|
|
427
434
|
className="flex-row items-center"
|
|
428
435
|
style={{
|
|
429
436
|
gap: 8,
|
|
430
|
-
backgroundColor: 'rgba(
|
|
437
|
+
backgroundColor: 'rgba(46,213,115,0.06)',
|
|
431
438
|
borderWidth: 1,
|
|
432
|
-
borderColor: 'rgba(
|
|
439
|
+
borderColor: 'rgba(46,213,115,0.2)',
|
|
433
440
|
borderRadius: 8,
|
|
434
441
|
paddingVertical: 10,
|
|
435
442
|
paddingHorizontal: 12,
|
|
@@ -1,5 +1,8 @@
|
|
|
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
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
4
|
+
|
|
5
|
+
const t = getSemanticColors('dark')
|
|
3
6
|
|
|
4
7
|
// Aliases for spec compatibility: under=behind, maintenance=ontrack, productive=target
|
|
5
8
|
export type VolumeStatus = 'untrained' | 'behind' | 'ontrack' | 'target' | 'over'
|
|
@@ -13,10 +16,10 @@ export interface MuscleGroupChipProps extends ViewProps {
|
|
|
13
16
|
|
|
14
17
|
const dotColorMap: Record<VolumeStatus, string> = {
|
|
15
18
|
untrained: '#2C2C2C',
|
|
16
|
-
behind: '
|
|
17
|
-
ontrack: '
|
|
18
|
-
target: '
|
|
19
|
-
over: '
|
|
19
|
+
behind: t['brand-secondary'],
|
|
20
|
+
ontrack: t['status-success'],
|
|
21
|
+
target: t['brand-primary'],
|
|
22
|
+
over: t['status-error'],
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export function MuscleGroupChip({
|
|
@@ -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, type ViewProps } from 'react-native'
|
|
3
3
|
import { cn } from '../../../utils/cn'
|
|
4
|
+
import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
|
|
4
5
|
|
|
5
6
|
export interface PlaceholderStripProps extends ViewProps {
|
|
6
7
|
width?: number | string
|
|
@@ -18,14 +19,12 @@ function SingleStrip({
|
|
|
18
19
|
}: Omit<PlaceholderStripProps, 'mode' | 'segments'>) {
|
|
19
20
|
return (
|
|
20
21
|
<View
|
|
21
|
-
className={
|
|
22
|
+
className={className}
|
|
22
23
|
style={[
|
|
23
24
|
{
|
|
24
25
|
height: 3,
|
|
25
|
-
backgroundColor:
|
|
26
|
-
|
|
27
|
-
borderStyle: 'dashed',
|
|
28
|
-
borderRadius: 1,
|
|
26
|
+
backgroundColor: WORKOUT_TOKENS.placeholder.fill,
|
|
27
|
+
borderRadius: 2,
|
|
29
28
|
opacity: 0.5,
|
|
30
29
|
},
|
|
31
30
|
width != null ? { width: width as number } : { flex: 1 },
|
|
@@ -59,13 +58,10 @@ function SegmentedStrip({
|
|
|
59
58
|
{Array.from({ length: segments }, (_, i) => (
|
|
60
59
|
<View
|
|
61
60
|
key={i}
|
|
62
|
-
className="border-border-strong"
|
|
63
61
|
style={{
|
|
64
62
|
flex: 1,
|
|
65
63
|
height: 3,
|
|
66
|
-
backgroundColor:
|
|
67
|
-
borderWidth: 1,
|
|
68
|
-
borderStyle: 'dashed',
|
|
64
|
+
backgroundColor: WORKOUT_TOKENS.placeholder.fill,
|
|
69
65
|
borderRadius: 1,
|
|
70
66
|
minWidth: 4,
|
|
71
67
|
}}
|