@titan-design/react-ui 0.3.0 → 0.5.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 +441 -97
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +1958 -66
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +71 -738
- package/dist/index.d.ts +71 -738
- package/dist/index.js +888 -1583
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4515 -2348
- 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 +6646 -0
- package/dist/pages.js.map +1 -0
- package/dist/pages.mjs +6626 -0
- package/dist/pages.mjs.map +1 -0
- package/dist/theme/index.d.mts +28 -2
- package/dist/theme/index.d.ts +28 -2
- package/dist/theme/index.js +37 -0
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +1880 -2
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.mjs +573 -1
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +9 -7
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +10 -15
- 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 +7 -8
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +25 -28
- package/src/components/custom/Workout/CapacityBandChart.tsx +2 -3
- package/src/components/custom/Workout/ExerciseCard.tsx +17 -28
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +38 -31
- package/src/components/custom/Workout/InputBar.tsx +10 -11
- package/src/components/custom/Workout/IntensityBar.test.tsx +112 -60
- package/src/components/custom/Workout/IntensityBar.tsx +109 -58
- package/src/components/custom/Workout/MesoCard.tsx +2 -6
- package/src/components/custom/Workout/MesoStatusCard.tsx +17 -15
- package/src/components/custom/Workout/MuscleGroupChip.tsx +2 -4
- package/src/components/custom/Workout/PlaceholderStrip.tsx +5 -10
- package/src/components/custom/Workout/PrBadge.tsx +3 -3
- package/src/components/custom/Workout/PrHistoryModal.tsx +14 -16
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -12
- package/src/components/custom/Workout/README.md +50 -0
- package/src/components/custom/Workout/ReadinessCheck.tsx +13 -11
- package/src/components/custom/Workout/RestTimer.test.tsx +41 -10
- package/src/components/custom/Workout/RestTimer.tsx +55 -49
- package/src/components/custom/Workout/SetRow.tsx +20 -8
- package/src/components/custom/Workout/StrengthTrendChart.tsx +15 -17
- package/src/components/custom/Workout/TempoBar.stories.tsx +95 -0
- package/src/components/custom/Workout/TempoBar.test.tsx +103 -0
- package/src/components/custom/Workout/TempoBar.tsx +215 -0
- 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 +104 -1
- package/src/components/custom/Workout/TrainingStatusPage.tsx +8 -17
- package/src/components/custom/Workout/VelocityStrip.test.tsx +89 -2
- package/src/components/custom/Workout/VelocityStrip.tsx +210 -43
- package/src/components/custom/Workout/WeekRow.test.tsx +2 -2
- package/src/components/custom/Workout/WeekRow.tsx +2 -1
- package/src/components/custom/Workout/WeightBadge.test.tsx +2 -2
- package/src/components/custom/Workout/WeightBadge.tsx +2 -2
- package/src/components/custom/Workout/WorkoutCard.tsx +3 -8
- package/src/components/custom/Workout/icons.tsx +68 -0
- package/src/components/custom/Workout/index.ts +47 -47
- package/src/index.ts +3 -2
- package/src/pages.ts +61 -0
- package/src/test/nativewind-stub.ts +13 -0
- package/src/test/stories-smoke.test.tsx +50 -0
- package/src/theme/ThemeProvider.test.tsx +24 -0
- package/src/theme/ThemeProvider.tsx +50 -0
- package/src/theme/barrel.ts +53 -0
- package/src/theme/index.ts +8 -41
- package/src/theme/native-theming-guard.test.ts +117 -0
- package/src/theme/resolve-color.ts +19 -0
- package/dist/bodymap-BhG55xHM.d.mts +0 -318
- package/dist/bodymap-BhG55xHM.d.ts +0 -318
- package/dist/chunk-7XPB4NAO.mjs +0 -1013
- package/dist/chunk-7XPB4NAO.mjs.map +0 -1
- package/dist/chunk-P7TSQUN6.mjs +0 -846
- package/dist/chunk-P7TSQUN6.mjs.map +0 -1
- package/dist/chunk-TOD2WQBG.mjs +0 -1048
- package/dist/chunk-TOD2WQBG.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 teal below the target ramp (< 75%)', () => {
|
|
48
|
+
render(<IntensityBar level={0.2} />)
|
|
49
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#14B8A6' })
|
|
50
|
+
})
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
it('stays teal just under the amber boundary', () => {
|
|
53
|
+
render(<IntensityBar level={0.74} />)
|
|
54
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#14B8A6' })
|
|
55
|
+
})
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
it('renders amber approaching the target (75-99%)', () => {
|
|
58
|
+
render(<IntensityBar level={0.75} />)
|
|
59
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#FFB020' })
|
|
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: '#A62626' })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('renders over-3 darkest red at 130%', () => {
|
|
78
|
+
render(<IntensityBar level={1.3} />)
|
|
79
|
+
expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#7A1C1C' })
|
|
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: '#A62626' })
|
|
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()
|
|
86
133
|
})
|
|
87
134
|
|
|
88
|
-
it('
|
|
89
|
-
render(<IntensityBar level={0.
|
|
90
|
-
expect(screen.
|
|
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()
|
|
91
138
|
})
|
|
92
139
|
|
|
93
|
-
it('
|
|
94
|
-
render(<IntensityBar level={0.
|
|
95
|
-
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()
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
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,20 @@
|
|
|
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'
|
|
4
11
|
|
|
5
12
|
export type IntensityBarOrientation = 'vertical' | 'horizontal'
|
|
6
13
|
|
|
7
14
|
export interface IntensityBarProps extends ViewProps {
|
|
8
|
-
/** Current intensity level
|
|
15
|
+
/** Current intensity level; 1 == 100% of target. Values >1 render as over-target. */
|
|
9
16
|
level: number
|
|
10
|
-
/**
|
|
17
|
+
/** Target position 0-1. When the level sits at this target the fill gains an at-target glow. */
|
|
11
18
|
threshold?: number
|
|
12
19
|
/** Orientation */
|
|
13
20
|
orientation?: IntensityBarOrientation
|
|
@@ -15,28 +22,57 @@ export interface IntensityBarProps extends ViewProps {
|
|
|
15
22
|
size?: number
|
|
16
23
|
/** Width for vertical (default 6px) or height for horizontal */
|
|
17
24
|
thickness?: number
|
|
18
|
-
/** Show
|
|
25
|
+
/** Show the "MRV" threshold label under the bar */
|
|
19
26
|
showThresholdLabel?: boolean
|
|
20
27
|
className?: string
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
const TRACK_BG = '
|
|
24
|
-
const THRESHOLD_COLOR = '#FFFFFF'
|
|
30
|
+
const TRACK_BG = '#333333'
|
|
25
31
|
const LABEL_COLOR = '#6B7280'
|
|
32
|
+
const TARGET_LINE_COLOR = 'rgba(33, 150, 243, 0.5)'
|
|
33
|
+
const AT_TARGET_GLOW = '0 0 5px 1px rgba(33, 150, 243, 0.35), 0 0 10px 3px rgba(33, 150, 243, 0.15)'
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
// Zone colors graded by TRUE percentage (level * 100).
|
|
36
|
+
const ZONE = {
|
|
37
|
+
building: '#14B8A6', // teal — below target ramp-up
|
|
38
|
+
approaching: '#FFB020', // amber — nearing target
|
|
39
|
+
target: '#FF7900', // brand — exactly at target
|
|
40
|
+
over1: '#D14343', // red — mild over-target
|
|
41
|
+
over2: '#A62626', // deep red — moderate over-target
|
|
42
|
+
over3: '#7A1C1C', // darkest red — severe over-target
|
|
43
|
+
} as const
|
|
44
|
+
|
|
45
|
+
type OverTier = 0 | 1 | 2 | 3
|
|
46
|
+
|
|
47
|
+
interface Zone {
|
|
48
|
+
color: string
|
|
49
|
+
over: OverTier
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const BULGE_SIZE: Record<Exclude<OverTier, 0>, number> = { 1: 8, 2: 10, 3: 11 }
|
|
53
|
+
|
|
54
|
+
/** Map a true percentage to its fill color and over-target tier. */
|
|
55
|
+
function zoneForPct(pct: number): Zone {
|
|
56
|
+
if (pct >= 125) return { color: ZONE.over3, over: 3 }
|
|
57
|
+
if (pct >= 115) return { color: ZONE.over2, over: 2 }
|
|
58
|
+
if (pct > 100) return { color: ZONE.over1, over: 1 }
|
|
59
|
+
if (pct === 100) return { color: ZONE.target, over: 0 }
|
|
60
|
+
if (pct >= 75) return { color: ZONE.approaching, over: 0 }
|
|
61
|
+
return { color: ZONE.building, over: 0 }
|
|
32
62
|
}
|
|
33
63
|
|
|
34
|
-
function
|
|
64
|
+
function clampFill(value: number): number {
|
|
35
65
|
if (value < 0) return 0
|
|
36
66
|
if (value > 1) return 1
|
|
37
67
|
return value
|
|
38
68
|
}
|
|
39
69
|
|
|
70
|
+
/** True when the level rests on the supplied target (within 5%). */
|
|
71
|
+
function isAtTarget(pct: number, threshold?: number): boolean {
|
|
72
|
+
if (threshold == null) return false
|
|
73
|
+
return Math.abs(pct - Math.round(threshold * 100)) <= 5
|
|
74
|
+
}
|
|
75
|
+
|
|
40
76
|
export function IntensityBar({
|
|
41
77
|
level,
|
|
42
78
|
threshold,
|
|
@@ -48,21 +84,22 @@ export function IntensityBar({
|
|
|
48
84
|
...props
|
|
49
85
|
}: IntensityBarProps) {
|
|
50
86
|
const isVertical = orientation === 'vertical'
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
87
|
+
const fillLevel = clampFill(level)
|
|
88
|
+
const pct = Math.round(Math.max(0, level) * 100)
|
|
89
|
+
const zone = zoneForPct(pct)
|
|
90
|
+
const atTarget = isAtTarget(pct, threshold)
|
|
54
91
|
|
|
55
|
-
const [fillAnim] = useState(() => new Animated.Value(
|
|
92
|
+
const [fillAnim] = useState(() => new Animated.Value(fillLevel))
|
|
56
93
|
|
|
57
94
|
useEffect(() => {
|
|
58
95
|
const animation = Animated.timing(fillAnim, {
|
|
59
|
-
toValue:
|
|
96
|
+
toValue: fillLevel,
|
|
60
97
|
duration: 250,
|
|
61
98
|
useNativeDriver: false,
|
|
62
99
|
})
|
|
63
100
|
animation.start()
|
|
64
101
|
return () => animation.stop()
|
|
65
|
-
}, [
|
|
102
|
+
}, [fillLevel, fillAnim])
|
|
66
103
|
|
|
67
104
|
const fillSizePercent = fillAnim.interpolate({
|
|
68
105
|
inputRange: [0, 1],
|
|
@@ -74,51 +111,50 @@ export function IntensityBar({
|
|
|
74
111
|
: { width: size, height: thickness }
|
|
75
112
|
|
|
76
113
|
const fillStyle = isVertical
|
|
114
|
+
? { position: 'absolute' as const, bottom: 0, left: 0, right: 0, height: fillSizePercent }
|
|
115
|
+
: { position: 'absolute' as const, top: 0, bottom: 0, left: 0, width: fillSizePercent }
|
|
116
|
+
|
|
117
|
+
const bulgeSize = zone.over === 0 ? 0 : BULGE_SIZE[zone.over]
|
|
118
|
+
const bulgeStyle: ViewStyle = {
|
|
119
|
+
position: 'absolute',
|
|
120
|
+
width: bulgeSize,
|
|
121
|
+
height: bulgeSize,
|
|
122
|
+
borderRadius: bulgeSize / 2,
|
|
123
|
+
backgroundColor: zone.color,
|
|
124
|
+
zIndex: 1,
|
|
125
|
+
...(isVertical
|
|
126
|
+
? { top: 0, left: '50%', transform: [{ translateX: -bulgeSize / 2 }] }
|
|
127
|
+
: { right: 0, top: '50%', transform: [{ translateY: -bulgeSize / 2 }] }),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const targetPct: DimensionValue = `${clampFill(threshold ?? 0) * 100}%`
|
|
131
|
+
const targetLineStyle: ViewStyle = isVertical
|
|
77
132
|
? {
|
|
78
|
-
position: 'absolute'
|
|
79
|
-
bottom:
|
|
80
|
-
left:
|
|
81
|
-
right:
|
|
82
|
-
height:
|
|
133
|
+
position: 'absolute',
|
|
134
|
+
bottom: targetPct,
|
|
135
|
+
left: -3,
|
|
136
|
+
right: -3,
|
|
137
|
+
height: 1.5,
|
|
138
|
+
backgroundColor: TARGET_LINE_COLOR,
|
|
139
|
+
zIndex: 4,
|
|
83
140
|
}
|
|
84
141
|
: {
|
|
85
|
-
position: 'absolute'
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
width:
|
|
142
|
+
position: 'absolute',
|
|
143
|
+
left: targetPct,
|
|
144
|
+
top: -3,
|
|
145
|
+
bottom: -3,
|
|
146
|
+
width: 1.5,
|
|
147
|
+
backgroundColor: TARGET_LINE_COLOR,
|
|
148
|
+
zIndex: 4,
|
|
90
149
|
}
|
|
91
150
|
|
|
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
151
|
return (
|
|
116
152
|
<View
|
|
117
153
|
className={className}
|
|
118
154
|
style={{ alignItems: 'center' }}
|
|
119
155
|
accessibilityRole="progressbar"
|
|
120
|
-
accessibilityValue={{ min: 0, max: 100, now:
|
|
121
|
-
accessibilityLabel={`Intensity level: ${
|
|
156
|
+
accessibilityValue={{ min: 0, max: 100, now: pct }}
|
|
157
|
+
accessibilityLabel={`Intensity level: ${pct}%`}
|
|
122
158
|
testID="intensity-bar"
|
|
123
159
|
{...props}
|
|
124
160
|
>
|
|
@@ -136,16 +172,31 @@ export function IntensityBar({
|
|
|
136
172
|
style={{
|
|
137
173
|
...fillStyle,
|
|
138
174
|
borderRadius: 3,
|
|
139
|
-
backgroundColor:
|
|
175
|
+
backgroundColor: zone.color,
|
|
140
176
|
zIndex: 2,
|
|
177
|
+
...(atTarget ? { boxShadow: AT_TARGET_GLOW } : null),
|
|
141
178
|
}}
|
|
142
179
|
testID="intensity-fill"
|
|
143
180
|
/>
|
|
144
181
|
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
|
|
182
|
+
{zone.over !== 0 && <View style={bulgeStyle} testID="intensity-bulge" />}
|
|
183
|
+
|
|
184
|
+
{threshold != null && <View style={targetLineStyle} testID="intensity-target" />}
|
|
148
185
|
</View>
|
|
186
|
+
|
|
187
|
+
<Text
|
|
188
|
+
style={{
|
|
189
|
+
fontSize: 8,
|
|
190
|
+
color: LABEL_COLOR,
|
|
191
|
+
fontFamily: '"Nunito Sans", sans-serif',
|
|
192
|
+
marginTop: 6,
|
|
193
|
+
}}
|
|
194
|
+
accessibilityElementsHidden
|
|
195
|
+
testID="intensity-label"
|
|
196
|
+
>
|
|
197
|
+
{pct}%
|
|
198
|
+
</Text>
|
|
199
|
+
|
|
149
200
|
{showThresholdLabel && threshold != null && (
|
|
150
201
|
<Text
|
|
151
202
|
style={{
|
|
@@ -153,7 +204,7 @@ export function IntensityBar({
|
|
|
153
204
|
fontWeight: '500',
|
|
154
205
|
color: LABEL_COLOR,
|
|
155
206
|
fontFamily: 'Inter, sans-serif',
|
|
156
|
-
marginTop:
|
|
207
|
+
marginTop: 2,
|
|
157
208
|
}}
|
|
158
209
|
accessibilityElementsHidden
|
|
159
210
|
testID="intensity-threshold-label"
|
|
@@ -9,10 +9,6 @@ const BRAND_PRIMARY = '#FF7900'
|
|
|
9
9
|
const BRAND_PRIMARY_DARK = '#C45E00'
|
|
10
10
|
const BRAND_PRIMARY_LIGHT = '#FFB066'
|
|
11
11
|
const BORDER_DEFAULT = '#1F1F1F'
|
|
12
|
-
// Theme-aware foregrounds: resolve to a readable color in both light and dark
|
|
13
|
-
// (react-native-web passes the CSS var through to the inline style).
|
|
14
|
-
const TEXT_PRIMARY = 'var(--color-text-primary)'
|
|
15
|
-
const TEXT_SECONDARY = 'var(--color-text-secondary)'
|
|
16
12
|
|
|
17
13
|
/** Gradient stops for the 3px top accent: dark -> primary -> light. */
|
|
18
14
|
const ACCENT_STOPS = [BRAND_PRIMARY_DARK, BRAND_PRIMARY, BRAND_PRIMARY_LIGHT]
|
|
@@ -119,11 +115,11 @@ export function MesoCard({
|
|
|
119
115
|
<View style={{ paddingHorizontal: 14, paddingTop: 12, paddingBottom: 10 }} testID="meso-card-body">
|
|
120
116
|
<View className="flex-row items-center" style={{ gap: 8 }} testID="meso-card-header">
|
|
121
117
|
<Text
|
|
118
|
+
className="text-text-primary"
|
|
122
119
|
style={{
|
|
123
120
|
fontSize: 15,
|
|
124
121
|
fontFamily: '"Space Grotesk", sans-serif',
|
|
125
122
|
fontWeight: '700',
|
|
126
|
-
color: TEXT_PRIMARY,
|
|
127
123
|
}}
|
|
128
124
|
testID="meso-card-name"
|
|
129
125
|
>
|
|
@@ -135,11 +131,11 @@ export function MesoCard({
|
|
|
135
131
|
</View>
|
|
136
132
|
|
|
137
133
|
<Text
|
|
134
|
+
className="text-text-secondary"
|
|
138
135
|
style={{
|
|
139
136
|
marginTop: 4,
|
|
140
137
|
fontSize: 12,
|
|
141
138
|
fontFamily: 'Inter, sans-serif',
|
|
142
|
-
color: TEXT_SECONDARY,
|
|
143
139
|
}}
|
|
144
140
|
testID="meso-card-subheader"
|
|
145
141
|
>
|
|
@@ -3,13 +3,11 @@ import { Fragment, type ReactNode } from 'react'
|
|
|
3
3
|
import { View, Text, type ViewProps, type ViewStyle } from 'react-native'
|
|
4
4
|
import { Card } from '../../ui/card'
|
|
5
5
|
import { StatusDot } from './StatusDot'
|
|
6
|
+
import { resolveColor } from '../../../theme/resolve-color'
|
|
6
7
|
|
|
7
8
|
const BRAND_PRIMARY = '#FF7900'
|
|
8
9
|
const BRAND_PRIMARY_DARK = '#C45E00'
|
|
9
10
|
const BRAND_PRIMARY_LIGHT = '#FFB066'
|
|
10
|
-
const TEXT_PRIMARY = 'var(--color-text-primary)'
|
|
11
|
-
const TEXT_SECONDARY = 'var(--color-text-secondary)'
|
|
12
|
-
const TEXT_TERTIARY = 'var(--color-text-tertiary)'
|
|
13
11
|
|
|
14
12
|
const SUCCESS = '#14B8A6'
|
|
15
13
|
const WARNING = '#FFB020'
|
|
@@ -18,9 +16,12 @@ const ERROR = '#D14343'
|
|
|
18
16
|
/** Gradient stops for the 3px top accent: dark -> primary -> light (matches MesoCard). */
|
|
19
17
|
const ACCENT_STOPS = [BRAND_PRIMARY_DARK, BRAND_PRIMARY, BRAND_PRIMARY_LIGHT]
|
|
20
18
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Card surface gradient: elevated -> raised at 135deg. Uses resolved dark-theme
|
|
21
|
+
* hexes (a gradient string can't carry a className token, and var() renders
|
|
22
|
+
* black on native).
|
|
23
|
+
*/
|
|
24
|
+
const CARD_GRADIENT = `linear-gradient(135deg, ${resolveColor('surface-elevated')} 0%, ${resolveColor('surface-raised')} 100%)`
|
|
24
25
|
|
|
25
26
|
/** Gauge track gradient (teal -> amber -> red) at 0.25 alpha. */
|
|
26
27
|
const GAUGE_GRADIENT =
|
|
@@ -118,7 +119,8 @@ function renderCoachingText(text: string, highlights?: string[]): ReactNode {
|
|
|
118
119
|
highlights.includes(part) ? (
|
|
119
120
|
<Text
|
|
120
121
|
key={`${part}-${index}`}
|
|
121
|
-
|
|
122
|
+
className="text-text-primary"
|
|
123
|
+
style={{ fontWeight: '700' }}
|
|
122
124
|
>
|
|
123
125
|
{part}
|
|
124
126
|
</Text>
|
|
@@ -166,24 +168,24 @@ function MetricCell({ metric }: { metric: MesoStatusMetric }) {
|
|
|
166
168
|
return (
|
|
167
169
|
<View style={{ width: '48%' }} testID="meso-status-card-metric">
|
|
168
170
|
<Text
|
|
171
|
+
className="text-text-tertiary"
|
|
169
172
|
style={{
|
|
170
173
|
fontSize: 10,
|
|
171
174
|
fontFamily: 'Inter, sans-serif',
|
|
172
175
|
fontWeight: '600',
|
|
173
176
|
letterSpacing: 0.5,
|
|
174
177
|
textTransform: 'uppercase',
|
|
175
|
-
color: TEXT_TERTIARY,
|
|
176
178
|
}}
|
|
177
179
|
>
|
|
178
180
|
{metric.label}
|
|
179
181
|
</Text>
|
|
180
182
|
<Text
|
|
183
|
+
className="text-text-primary"
|
|
181
184
|
style={{
|
|
182
185
|
marginTop: 2,
|
|
183
186
|
fontSize: 13,
|
|
184
187
|
fontFamily: 'Inter, sans-serif',
|
|
185
188
|
fontWeight: '600',
|
|
186
|
-
color: TEXT_PRIMARY,
|
|
187
189
|
}}
|
|
188
190
|
>
|
|
189
191
|
{metric.value}
|
|
@@ -212,24 +214,24 @@ function Gauge({ gauge }: { gauge: MesoStatusGauge }) {
|
|
|
212
214
|
accessibilityElementsHidden
|
|
213
215
|
>
|
|
214
216
|
<Text
|
|
217
|
+
className="text-text-secondary"
|
|
215
218
|
style={{
|
|
216
219
|
fontSize: 10,
|
|
217
220
|
fontFamily: 'Inter, sans-serif',
|
|
218
221
|
fontWeight: '500',
|
|
219
222
|
letterSpacing: 0.5,
|
|
220
223
|
textTransform: 'uppercase',
|
|
221
|
-
color: TEXT_SECONDARY,
|
|
222
224
|
}}
|
|
223
225
|
>
|
|
224
226
|
{gauge.label}
|
|
225
227
|
</Text>
|
|
226
228
|
{gauge.sublabel != null && (
|
|
227
229
|
<Text
|
|
230
|
+
className="text-text-tertiary"
|
|
228
231
|
style={{
|
|
229
232
|
fontSize: 10,
|
|
230
233
|
fontFamily: 'Inter, sans-serif',
|
|
231
234
|
fontWeight: '500',
|
|
232
|
-
color: TEXT_TERTIARY,
|
|
233
235
|
}}
|
|
234
236
|
>
|
|
235
237
|
{gauge.sublabel}
|
|
@@ -261,6 +263,7 @@ function Gauge({ gauge }: { gauge: MesoStatusGauge }) {
|
|
|
261
263
|
testID="meso-status-card-gauge-center"
|
|
262
264
|
/>
|
|
263
265
|
<View
|
|
266
|
+
className="border-text-primary"
|
|
264
267
|
style={{
|
|
265
268
|
position: 'absolute',
|
|
266
269
|
left: `${percentage}%`,
|
|
@@ -270,7 +273,6 @@ function Gauge({ gauge }: { gauge: MesoStatusGauge }) {
|
|
|
270
273
|
height: 14,
|
|
271
274
|
borderRadius: 9999,
|
|
272
275
|
borderWidth: 2,
|
|
273
|
-
borderColor: TEXT_PRIMARY,
|
|
274
276
|
backgroundColor: markerColor,
|
|
275
277
|
boxShadow: '0 0 4px rgba(0,0,0,0.5)',
|
|
276
278
|
}}
|
|
@@ -349,12 +351,12 @@ export function MesoStatusCard({
|
|
|
349
351
|
style={{ gap: 8 }}
|
|
350
352
|
>
|
|
351
353
|
<Text
|
|
354
|
+
className="text-text-primary"
|
|
352
355
|
style={{
|
|
353
356
|
flexShrink: 1,
|
|
354
357
|
fontSize: 15,
|
|
355
358
|
fontFamily: '"Space Grotesk", sans-serif',
|
|
356
359
|
fontWeight: '700',
|
|
357
|
-
color: TEXT_PRIMARY,
|
|
358
360
|
}}
|
|
359
361
|
testID="meso-status-card-name"
|
|
360
362
|
>
|
|
@@ -363,11 +365,11 @@ export function MesoStatusCard({
|
|
|
363
365
|
<StatusPill badge={statusBadge} />
|
|
364
366
|
</View>
|
|
365
367
|
<Text
|
|
368
|
+
className="text-text-secondary"
|
|
366
369
|
style={{
|
|
367
370
|
marginTop: 4,
|
|
368
371
|
fontSize: 12,
|
|
369
372
|
fontFamily: 'Inter, sans-serif',
|
|
370
|
-
color: TEXT_SECONDARY,
|
|
371
373
|
}}
|
|
372
374
|
testID="meso-status-card-subtitle"
|
|
373
375
|
>
|
|
@@ -408,11 +410,11 @@ export function MesoStatusCard({
|
|
|
408
410
|
testID="meso-status-card-coaching"
|
|
409
411
|
>
|
|
410
412
|
<Text
|
|
413
|
+
className="text-text-secondary"
|
|
411
414
|
style={{
|
|
412
415
|
fontSize: 12,
|
|
413
416
|
lineHeight: 17,
|
|
414
417
|
fontFamily: 'Inter, sans-serif',
|
|
415
|
-
color: TEXT_SECONDARY,
|
|
416
418
|
}}
|
|
417
419
|
>
|
|
418
420
|
{renderCoachingText(coaching.text, coaching.highlights)}
|