@titan-design/react-ui 0.4.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.mjs +1911 -6
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +31 -738
- package/dist/index.d.ts +31 -738
- package/dist/index.js +894 -1692
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10287 -8203
- 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 +12 -12
- package/dist/theme/index.d.ts +12 -12
- package/dist/theme/index.js +30 -30
- 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/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/ExerciseCard.tsx +5 -1
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +17 -4
- package/src/components/custom/Workout/IntensityBar.test.tsx +112 -60
- package/src/components/custom/Workout/IntensityBar.tsx +109 -58
- package/src/components/custom/Workout/PlaceholderStrip.tsx +5 -9
- package/src/components/custom/Workout/PrBadge.tsx +3 -3
- package/src/components/custom/Workout/PrHistoryModal.tsx +2 -2
- package/src/components/custom/Workout/README.md +50 -0
- package/src/components/custom/Workout/RestTimer.test.tsx +41 -10
- package/src/components/custom/Workout/RestTimer.tsx +48 -43
- package/src/components/custom/Workout/SetRow.tsx +10 -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 +104 -1
- 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/WeightBadge.test.tsx +2 -2
- package/src/components/custom/Workout/WeightBadge.tsx +2 -2
- package/src/components/custom/Workout/icons.tsx +68 -0
- package/src/components/custom/Workout/index.ts +38 -47
- package/src/index.ts +3 -2
- package/src/pages.ts +61 -0
- package/src/test/stories-smoke.test.tsx +50 -0
- package/src/theme/barrel.ts +53 -0
- package/src/theme/index.ts +8 -43
- package/src/theme/native-theming-guard.test.ts +117 -0
- 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
|
@@ -1,11 +1,54 @@
|
|
|
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 { View, Text, Pressable, Animated, Easing, type ViewProps } from 'react-native'
|
|
3
|
+
import { View, Text, Pressable, Animated, Easing, type ViewProps, type ViewStyle } from 'react-native'
|
|
4
4
|
import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
|
|
5
5
|
import { formatVelocity } from '../../../utils/workout-format'
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Structural velocity-zone band accepted from an upstream analytics source
|
|
9
|
+
* (e.g. workout-analytics' `VelocityZones.bands`).
|
|
10
|
+
*
|
|
11
|
+
* Deliberately a plain structural shape — titan never imports the analytics
|
|
12
|
+
* package (same policy as TempoBar's presentational phase key). Any object with
|
|
13
|
+
* this shape can be passed, so `zones={waVelocityZones.bands}` works directly.
|
|
14
|
+
* Bands are ordered slow → fast, contiguous, and cover `[0, ∞)` with the top
|
|
15
|
+
* band's `max === null`. Bands carry NO color — color is a UI concern resolved
|
|
16
|
+
* here via {@link zoneIdToScaleToken}.
|
|
17
|
+
*/
|
|
18
|
+
export interface VelocityZoneBandProp {
|
|
19
|
+
/** Stable zone identity (e.g. WA's `VelocityZoneId`). Drives color mapping. */
|
|
20
|
+
id: string
|
|
21
|
+
/** Human-readable label shown in the summary row. */
|
|
22
|
+
label: string
|
|
23
|
+
/** Inclusive lower bound (m/s mean concentric velocity). */
|
|
24
|
+
min: number
|
|
25
|
+
/** Exclusive upper bound (m/s); `null` marks the open top band. */
|
|
26
|
+
max: number | null
|
|
27
|
+
}
|
|
28
|
+
|
|
7
29
|
export interface VelocityStripProps extends ViewProps {
|
|
30
|
+
/**
|
|
31
|
+
* Per-rep MEAN concentric velocity (m/s), one entry per rep (brain WA-D02).
|
|
32
|
+
* Drives both bar height and — in the default path — zone color. Callers must
|
|
33
|
+
* feed mean (not peak) concentric velocity so bar color, height, and the
|
|
34
|
+
* summary label all describe one metric.
|
|
35
|
+
*/
|
|
8
36
|
velocities: number[]
|
|
37
|
+
/**
|
|
38
|
+
* Optional velocity-zone bands (shape-compatible with WA's
|
|
39
|
+
* `VelocityZones.bands`). When provided, bar color and the summary zone label
|
|
40
|
+
* come from these bands via {@link zoneIdToScaleToken}. When omitted, the
|
|
41
|
+
* built-in default scale (≥1.0 / ≥0.75 / ≥0.5) is used — identical to prior
|
|
42
|
+
* releases. Colors NEVER come from the bands themselves.
|
|
43
|
+
*/
|
|
44
|
+
zones?: readonly VelocityZoneBandProp[]
|
|
45
|
+
/**
|
|
46
|
+
* Live mode: index of the most-recently-completed rep. That bar animates in
|
|
47
|
+
* with a "pop"; if it is also the current set peak (a new best), it "bounces"
|
|
48
|
+
* instead. Only the latest bar animates. Honors `prefers-reduced-motion`.
|
|
49
|
+
* Full variant only.
|
|
50
|
+
*/
|
|
51
|
+
liveRepIndex?: number
|
|
9
52
|
expanded?: boolean
|
|
10
53
|
onToggle?: () => void
|
|
11
54
|
onRepPress?: (index: number, velocity: number) => void
|
|
@@ -14,9 +57,36 @@ export interface VelocityStripProps extends ViewProps {
|
|
|
14
57
|
className?: string
|
|
15
58
|
}
|
|
16
59
|
|
|
17
|
-
// Canonical scale shared with SetRow's RPE color
|
|
60
|
+
// Canonical 4-color performance scale shared with SetRow's RPE color
|
|
61
|
+
// (see theme/workout-tokens.ts). green = fastest, red = slowest/grinding.
|
|
18
62
|
const VEL_COLORS = WORKOUT_TOKENS.scale
|
|
19
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Map a velocity-zone id (WA's 5-band taxonomy) onto the canonical 4-color
|
|
66
|
+
* scale. The scale has 4 hues but the taxonomy has 5 bands, so the two slowest
|
|
67
|
+
* bands — `maximalStrength` and `grinding` — intentionally share `red`: both
|
|
68
|
+
* read as "heavy / effortful" and there is no distinct 5th data-viz hue. The
|
|
69
|
+
* top three bands align 1:1 with the legacy default scale so an exercise moving
|
|
70
|
+
* from default to profile-derived zones never shifts its fast-end colors.
|
|
71
|
+
*
|
|
72
|
+
* Legacy default parity: speed=green, power=yellow, strengthSpeed=orange, and
|
|
73
|
+
* the old sub-0.5 "Strength" band (now split into maximalStrength + grinding)
|
|
74
|
+
* stays red.
|
|
75
|
+
*
|
|
76
|
+
* Unknown ids fall back to `green` (matching the historical default-path
|
|
77
|
+
* fallback), so a forward-compatible band id never renders an empty bar.
|
|
78
|
+
*/
|
|
79
|
+
const zoneIdToScaleToken: Record<string, keyof typeof VEL_COLORS> = {
|
|
80
|
+
speed: 'green',
|
|
81
|
+
power: 'yellow',
|
|
82
|
+
strengthSpeed: 'orange',
|
|
83
|
+
maximalStrength: 'red',
|
|
84
|
+
grinding: 'red',
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Default (no-zones) scale ------------------------------------------------
|
|
88
|
+
// Retained as named exports for back-compat; they define the default path.
|
|
89
|
+
|
|
20
90
|
export function getVelocityZoneColor(velocity: number): string {
|
|
21
91
|
if (velocity >= 1.0) return 'vel-green'
|
|
22
92
|
if (velocity >= 0.75) return 'vel-yellow'
|
|
@@ -31,35 +101,47 @@ export function getVelocityZoneName(velocity: number): string {
|
|
|
31
101
|
return 'Strength'
|
|
32
102
|
}
|
|
33
103
|
|
|
104
|
+
const zoneHexMap: Record<string, string> = {
|
|
105
|
+
'vel-green': VEL_COLORS.green,
|
|
106
|
+
'vel-yellow': VEL_COLORS.yellow,
|
|
107
|
+
'vel-orange': VEL_COLORS.orange,
|
|
108
|
+
'vel-red': VEL_COLORS.red,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Velocity loss for a set, as a whole percentage. Uses the running-best rep as
|
|
113
|
+
* the reference (matching WA-02.05 / brain WA-D01): `(vBest − vLast) / vBest`,
|
|
114
|
+
* clamped to ≥ 0 so a set that ends on its best rep reports 0 loss.
|
|
115
|
+
*/
|
|
34
116
|
export function calculateVelocityLoss(velocities: number[]): number {
|
|
35
117
|
if (velocities.length < 2) return 0
|
|
36
|
-
const
|
|
118
|
+
const best = Math.max(...velocities)
|
|
119
|
+
if (best <= 0) return 0
|
|
37
120
|
const last = velocities[velocities.length - 1]
|
|
38
|
-
|
|
39
|
-
return Math.round(((first - last) / first) * 100)
|
|
121
|
+
return Math.max(0, Math.round(((best - last) / best) * 100))
|
|
40
122
|
}
|
|
41
123
|
|
|
124
|
+
/** Arithmetic mean of the per-rep mean-concentric velocities. */
|
|
42
125
|
export function calculateMeanVelocity(velocities: number[]): number {
|
|
43
126
|
if (velocities.length === 0) return 0
|
|
44
127
|
const sum = velocities.reduce((acc, v) => acc + v, 0)
|
|
45
128
|
return sum / velocities.length
|
|
46
129
|
}
|
|
47
130
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
131
|
+
/** Classify a velocity into its band (slow → fast, min inclusive / max exclusive). */
|
|
132
|
+
function classifyBand(
|
|
133
|
+
velocity: number,
|
|
134
|
+
bands: readonly VelocityZoneBandProp[],
|
|
135
|
+
): VelocityZoneBandProp | undefined {
|
|
136
|
+
for (const band of bands) {
|
|
137
|
+
if (band.max === null || velocity < band.max) return band
|
|
55
138
|
}
|
|
139
|
+
return bands[bands.length - 1]
|
|
56
140
|
}
|
|
57
141
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
'vel-orange': VEL_COLORS.orange,
|
|
62
|
-
'vel-red': VEL_COLORS.red,
|
|
142
|
+
function bandColor(band: VelocityZoneBandProp): string {
|
|
143
|
+
const token = zoneIdToScaleToken[band.id]
|
|
144
|
+
return token ? VEL_COLORS[token] : VEL_COLORS.green
|
|
63
145
|
}
|
|
64
146
|
|
|
65
147
|
function getLossStyle(loss: number): Record<string, string> | null {
|
|
@@ -68,11 +150,33 @@ function getLossStyle(loss: number): Record<string, string> | null {
|
|
|
68
150
|
return null
|
|
69
151
|
}
|
|
70
152
|
|
|
153
|
+
function getReducedMotionPreference(): boolean {
|
|
154
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false
|
|
155
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Track the OS "reduce motion" preference; falls back to `false` (jsdom/SSR). */
|
|
159
|
+
function usePrefersReducedMotion(): boolean {
|
|
160
|
+
const [reduced, setReduced] = useState(getReducedMotionPreference)
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return
|
|
163
|
+
const mq = window.matchMedia('(prefers-reduced-motion: reduce)')
|
|
164
|
+
const handler = () => setReduced(mq.matches)
|
|
165
|
+
handler()
|
|
166
|
+
mq.addEventListener?.('change', handler)
|
|
167
|
+
return () => mq.removeEventListener?.('change', handler)
|
|
168
|
+
}, [])
|
|
169
|
+
return reduced
|
|
170
|
+
}
|
|
171
|
+
|
|
71
172
|
const ANIMATION_DURATION = 400
|
|
72
173
|
const ANIMATION_EASING = Easing.bezier(0.22, 1, 0.36, 1)
|
|
174
|
+
const POP_EASING = Easing.bezier(0.34, 1.56, 0.64, 1)
|
|
73
175
|
|
|
74
176
|
export function VelocityStrip({
|
|
75
177
|
velocities,
|
|
178
|
+
zones,
|
|
179
|
+
liveRepIndex,
|
|
76
180
|
expanded = false,
|
|
77
181
|
onToggle,
|
|
78
182
|
onRepPress,
|
|
@@ -84,11 +188,55 @@ export function VelocityStrip({
|
|
|
84
188
|
const maxVelocity = Math.max(...velocities, 0)
|
|
85
189
|
const meanVelocity = calculateMeanVelocity(velocities)
|
|
86
190
|
const loss = calculateVelocityLoss(velocities)
|
|
87
|
-
const meanZone = getVelocityZoneName(meanVelocity)
|
|
88
191
|
|
|
192
|
+
const hasZones = zones != null && zones.length > 0
|
|
193
|
+
const barColorFor = (v: number): string =>
|
|
194
|
+
hasZones ? bandColor(classifyBand(v, zones)!) : zoneHexMap[getVelocityZoneColor(v)]
|
|
195
|
+
const meanZone = hasZones
|
|
196
|
+
? (classifyBand(meanVelocity, zones)?.label ?? '')
|
|
197
|
+
: getVelocityZoneName(meanVelocity)
|
|
198
|
+
|
|
199
|
+
const prefersReducedMotion = usePrefersReducedMotion()
|
|
89
200
|
const [heightAnim] = useState(() => new Animated.Value(expanded ? 60 : 3))
|
|
90
201
|
const [labelOpacity] = useState(() => new Animated.Value(expanded ? 1 : 0))
|
|
91
202
|
const [infoOpacity] = useState(() => new Animated.Value(expanded ? 1 : 0))
|
|
203
|
+
const [liveScale] = useState(() => new Animated.Value(1))
|
|
204
|
+
|
|
205
|
+
// Newest-rep animation: pop for a normal rep, bounce when it sets a new peak.
|
|
206
|
+
const liveVelocity = liveRepIndex != null ? velocities[liveRepIndex] : undefined
|
|
207
|
+
const isNewPeak = liveVelocity != null && maxVelocity > 0 && liveVelocity === maxVelocity
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (variant === 'mini' || liveRepIndex == null || liveVelocity == null) return
|
|
210
|
+
if (prefersReducedMotion) {
|
|
211
|
+
liveScale.setValue(1)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
if (isNewPeak) {
|
|
215
|
+
liveScale.setValue(1)
|
|
216
|
+
Animated.sequence([
|
|
217
|
+
Animated.timing(liveScale, {
|
|
218
|
+
toValue: 1.25,
|
|
219
|
+
duration: 150,
|
|
220
|
+
easing: Easing.out(Easing.quad),
|
|
221
|
+
useNativeDriver: true,
|
|
222
|
+
}),
|
|
223
|
+
Animated.spring(liveScale, {
|
|
224
|
+
toValue: 1,
|
|
225
|
+
friction: 3,
|
|
226
|
+
tension: 140,
|
|
227
|
+
useNativeDriver: true,
|
|
228
|
+
}),
|
|
229
|
+
]).start()
|
|
230
|
+
} else {
|
|
231
|
+
liveScale.setValue(0.8)
|
|
232
|
+
Animated.timing(liveScale, {
|
|
233
|
+
toValue: 1,
|
|
234
|
+
duration: 300,
|
|
235
|
+
easing: POP_EASING,
|
|
236
|
+
useNativeDriver: true,
|
|
237
|
+
}).start()
|
|
238
|
+
}
|
|
239
|
+
}, [variant, liveRepIndex, liveVelocity, isNewPeak, prefersReducedMotion, liveScale])
|
|
92
240
|
|
|
93
241
|
useEffect(() => {
|
|
94
242
|
if (variant === 'mini') return
|
|
@@ -131,7 +279,7 @@ export function VelocityStrip({
|
|
|
131
279
|
key={i}
|
|
132
280
|
style={{
|
|
133
281
|
flex: 1,
|
|
134
|
-
backgroundColor:
|
|
282
|
+
backgroundColor: barColorFor(v),
|
|
135
283
|
borderRadius: 1,
|
|
136
284
|
minWidth: 4,
|
|
137
285
|
height: '100%' as unknown as number,
|
|
@@ -176,11 +324,44 @@ export function VelocityStrip({
|
|
|
176
324
|
style={{ flexDirection: 'row', flex: 1, gap: 2, alignItems: 'flex-end' }}
|
|
177
325
|
>
|
|
178
326
|
{velocities.map((v, i) => {
|
|
179
|
-
const
|
|
327
|
+
const barBackground = barColorFor(v)
|
|
180
328
|
// Guard all-zero velocities (idle / pre-rep): maxVelocity === 0 makes
|
|
181
329
|
// this 0 / 0 === NaN and emits height:'NaN%'. Flatten the bars instead.
|
|
182
330
|
const barHeightPct =
|
|
183
331
|
maxVelocity > 0 ? Math.round((v / (maxVelocity * 1.15)) * 100) : 0
|
|
332
|
+
const isLive = liveRepIndex === i
|
|
333
|
+
const liveLabelSuffix = isLive
|
|
334
|
+
? isNewPeak
|
|
335
|
+
? ', latest rep, new set peak'
|
|
336
|
+
: ', latest rep'
|
|
337
|
+
: ''
|
|
338
|
+
|
|
339
|
+
const barStyle: ViewStyle = expanded
|
|
340
|
+
? { height: `${barHeightPct}%`, minHeight: 2, borderTopLeftRadius: 2, borderTopRightRadius: 2, backgroundColor: barBackground }
|
|
341
|
+
: {
|
|
342
|
+
minHeight: '100%' as unknown as number,
|
|
343
|
+
borderTopLeftRadius: 2,
|
|
344
|
+
borderTopRightRadius: 2,
|
|
345
|
+
borderBottomLeftRadius: 0,
|
|
346
|
+
borderBottomRightRadius: 0,
|
|
347
|
+
backgroundColor: barBackground,
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const barInner = isLive ? (
|
|
351
|
+
<Animated.View
|
|
352
|
+
style={[barStyle, { transform: [{ scale: liveScale }] }]}
|
|
353
|
+
testID={`velocity-bar-${i}`}
|
|
354
|
+
accessibilityRole="image"
|
|
355
|
+
accessibilityLabel={`Rep ${i + 1}: ${formatVelocity(v)} meters per second${liveLabelSuffix}`}
|
|
356
|
+
/>
|
|
357
|
+
) : (
|
|
358
|
+
<View
|
|
359
|
+
style={barStyle}
|
|
360
|
+
testID={`velocity-bar-${i}`}
|
|
361
|
+
accessibilityRole="image"
|
|
362
|
+
accessibilityLabel={`Rep ${i + 1}: ${formatVelocity(v)} meters per second`}
|
|
363
|
+
/>
|
|
364
|
+
)
|
|
184
365
|
|
|
185
366
|
const bar = (
|
|
186
367
|
<View
|
|
@@ -198,23 +379,7 @@ export function VelocityStrip({
|
|
|
198
379
|
</Text>
|
|
199
380
|
</Animated.View>
|
|
200
381
|
)}
|
|
201
|
-
|
|
202
|
-
style={
|
|
203
|
-
expanded
|
|
204
|
-
? { height: `${barHeightPct}%`, minHeight: 2, borderTopLeftRadius: 2, borderTopRightRadius: 2, backgroundColor: zoneHexMap[zone] }
|
|
205
|
-
: {
|
|
206
|
-
minHeight: '100%' as unknown as number,
|
|
207
|
-
borderTopLeftRadius: 2,
|
|
208
|
-
borderTopRightRadius: 2,
|
|
209
|
-
borderBottomLeftRadius: 0,
|
|
210
|
-
borderBottomRightRadius: 0,
|
|
211
|
-
backgroundColor: zoneHexMap[zone],
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
testID={`velocity-bar-${i}`}
|
|
215
|
-
accessibilityRole="image"
|
|
216
|
-
accessibilityLabel={`Rep ${i + 1}: ${formatVelocity(v)} meters per second`}
|
|
217
|
-
/>
|
|
382
|
+
{barInner}
|
|
218
383
|
</View>
|
|
219
384
|
)
|
|
220
385
|
|
|
@@ -256,7 +421,7 @@ export function VelocityStrip({
|
|
|
256
421
|
fontFamily: 'Inter, sans-serif',
|
|
257
422
|
}}
|
|
258
423
|
>
|
|
259
|
-
{meanZone} {'
|
|
424
|
+
{meanZone} {'·'} {formatVelocity(meanVelocity)} m/s
|
|
260
425
|
</Text>
|
|
261
426
|
<Text
|
|
262
427
|
className="text-text-secondary"
|
|
@@ -37,12 +37,12 @@ describe('WeekRow', () => {
|
|
|
37
37
|
|
|
38
38
|
it('passes the threshold through to the intensity bar', () => {
|
|
39
39
|
render(<WeekRow {...baseProps} intensityThreshold={0.8} />)
|
|
40
|
-
expect(screen.getByTestId('intensity-
|
|
40
|
+
expect(screen.getByTestId('intensity-target')).toBeInTheDocument()
|
|
41
41
|
})
|
|
42
42
|
|
|
43
43
|
it('omits the threshold line when not provided', () => {
|
|
44
44
|
render(<WeekRow {...baseProps} />)
|
|
45
|
-
expect(screen.queryByTestId('intensity-
|
|
45
|
+
expect(screen.queryByTestId('intensity-target')).not.toBeInTheDocument()
|
|
46
46
|
})
|
|
47
47
|
})
|
|
48
48
|
|
|
@@ -75,10 +75,10 @@ describe('WeightBadge', () => {
|
|
|
75
75
|
})
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
-
it('uses brand-primary
|
|
78
|
+
it('uses a brand-primary border ring on PR state', () => {
|
|
79
79
|
render(<WeightBadge value={315} isPr />)
|
|
80
80
|
expect(screen.getByTestId('weight-badge')).toHaveStyle({
|
|
81
|
-
borderTopColor: 'rgba(255, 121, 0, 0.
|
|
81
|
+
borderTopColor: 'rgba(255, 121, 0, 0.3)',
|
|
82
82
|
})
|
|
83
83
|
})
|
|
84
84
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
2
|
import { Text } from 'react-native'
|
|
3
|
-
import {
|
|
3
|
+
import { DumbbellIcon } from './icons'
|
|
4
4
|
import {
|
|
5
5
|
BaseBadge,
|
|
6
6
|
baseBadgeSizeConfig,
|
|
@@ -61,7 +61,7 @@ export function WeightBadge({
|
|
|
61
61
|
className={className}
|
|
62
62
|
icon={
|
|
63
63
|
showIcon ? (
|
|
64
|
-
<
|
|
64
|
+
<DumbbellIcon size={config.iconSize} color={iconColor} strokeWidth={2} />
|
|
65
65
|
) : undefined
|
|
66
66
|
}
|
|
67
67
|
accessibilityLabel={fullLabel}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Inline SVG icons for the Workout badges — replaces the `lucide-react`
|
|
2
|
+
// dependency (dep hygiene, titan 0.5.0). The paths mirror lucide-react's
|
|
3
|
+
// `Dumbbell` and `Star` glyphs verbatim so rendering is pixel-identical to the
|
|
4
|
+
// previous `<Dumbbell>` / `<Star>` output (same viewBox, stroke, fill, and
|
|
5
|
+
// round line caps). Rendered as raw `<svg>` DOM elements — the same web-SVG
|
|
6
|
+
// mechanism `Progress.tsx` uses, and the same surface `lucide-react` produced
|
|
7
|
+
// (both are web-only SVG; native icon rendering was never wired here).
|
|
8
|
+
|
|
9
|
+
export interface WorkoutIconProps {
|
|
10
|
+
/** Rendered width/height in px (viewBox is fixed 24×24). */
|
|
11
|
+
size?: number
|
|
12
|
+
/** Stroke color. */
|
|
13
|
+
color?: string
|
|
14
|
+
/** Fill color; defaults to `none` (outline only). */
|
|
15
|
+
fill?: string
|
|
16
|
+
/** Stroke width. */
|
|
17
|
+
strokeWidth?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Dumbbell glyph (mirrors lucide-react `Dumbbell`). Outline-only by default. */
|
|
21
|
+
export function DumbbellIcon({
|
|
22
|
+
size = 24,
|
|
23
|
+
color = 'currentColor',
|
|
24
|
+
fill = 'none',
|
|
25
|
+
strokeWidth = 2,
|
|
26
|
+
}: WorkoutIconProps) {
|
|
27
|
+
return (
|
|
28
|
+
<svg
|
|
29
|
+
width={size}
|
|
30
|
+
height={size}
|
|
31
|
+
viewBox="0 0 24 24"
|
|
32
|
+
fill={fill}
|
|
33
|
+
stroke={color}
|
|
34
|
+
strokeWidth={strokeWidth}
|
|
35
|
+
strokeLinecap="round"
|
|
36
|
+
strokeLinejoin="round"
|
|
37
|
+
>
|
|
38
|
+
<path d="M14.4 14.4 9.6 9.6" />
|
|
39
|
+
<path d="M18.657 21.485a2 2 0 1 1-2.829-2.828l-1.767 1.768a2 2 0 1 1-2.829-2.829l6.364-6.364a2 2 0 1 1 2.829 2.829l-1.768 1.767a2 2 0 1 1 2.828 2.829z" />
|
|
40
|
+
<path d="m21.5 21.5-1.4-1.4" />
|
|
41
|
+
<path d="M3.9 3.9 2.5 2.5" />
|
|
42
|
+
<path d="M6.404 12.768a2 2 0 1 1-2.829-2.829l1.768-1.767a2 2 0 1 1-2.828-2.829l2.828-2.828a2 2 0 1 1 2.829 2.828l1.767-1.768a2 2 0 1 1 2.829 2.829z" />
|
|
43
|
+
</svg>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Star glyph (mirrors lucide-react `Star`). Pass `fill` for a solid star. */
|
|
48
|
+
export function StarIcon({
|
|
49
|
+
size = 24,
|
|
50
|
+
color = 'currentColor',
|
|
51
|
+
fill = 'none',
|
|
52
|
+
strokeWidth = 2,
|
|
53
|
+
}: WorkoutIconProps) {
|
|
54
|
+
return (
|
|
55
|
+
<svg
|
|
56
|
+
width={size}
|
|
57
|
+
height={size}
|
|
58
|
+
viewBox="0 0 24 24"
|
|
59
|
+
fill={fill}
|
|
60
|
+
stroke={color}
|
|
61
|
+
strokeWidth={strokeWidth}
|
|
62
|
+
strokeLinecap="round"
|
|
63
|
+
strokeLinejoin="round"
|
|
64
|
+
>
|
|
65
|
+
<path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" />
|
|
66
|
+
</svg>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -8,7 +8,12 @@ export { WeightBadge, type WeightBadgeProps, type WeightBadgeSize } from './Weig
|
|
|
8
8
|
export { PrBadge, type PrBadgeProps, type PRType } from './PrBadge'
|
|
9
9
|
export { StatusDot, type StatusDotVariant, type StatusDotProps } from './StatusDot'
|
|
10
10
|
export { PlaceholderStrip, type PlaceholderStripProps } from './PlaceholderStrip'
|
|
11
|
-
export {
|
|
11
|
+
export {
|
|
12
|
+
TempoDisplay,
|
|
13
|
+
type TempoDisplayProps,
|
|
14
|
+
type TempoLivePhase,
|
|
15
|
+
type TempoLiveState,
|
|
16
|
+
} from './TempoDisplay'
|
|
12
17
|
export {
|
|
13
18
|
TempoBar,
|
|
14
19
|
type TempoBarProps,
|
|
@@ -24,6 +29,7 @@ export { WorkoutPill, type WorkoutPillProps, type WorkoutPillStatus } from './Wo
|
|
|
24
29
|
export {
|
|
25
30
|
VelocityStrip,
|
|
26
31
|
type VelocityStripProps,
|
|
32
|
+
type VelocityZoneBandProp,
|
|
27
33
|
getVelocityZoneColor,
|
|
28
34
|
getVelocityZoneName,
|
|
29
35
|
calculateVelocityLoss,
|
|
@@ -103,37 +109,29 @@ export type {
|
|
|
103
109
|
TrainingStatusMuscle,
|
|
104
110
|
TrainingStatusSummary,
|
|
105
111
|
} from './TrainingStatusPage'
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
type ProgramNavLevel,
|
|
119
|
-
type ProgramSelection,
|
|
120
|
-
type ProgramBreadcrumb,
|
|
112
|
+
// ActiveWorkoutPage / ExerciseDetailPage / ProgramPlanningPage are page-level
|
|
113
|
+
// organisms demoted to the `@titan-design/react-ui/pages` subpath (titan 0.5.0).
|
|
114
|
+
// Their VALUE exports (components + derivation helpers) live there; only
|
|
115
|
+
// type-only re-exports stay on this barrel (erased at build, no runtime pull).
|
|
116
|
+
export type {
|
|
117
|
+
ProgramPlanningPageProps,
|
|
118
|
+
PlanMeso,
|
|
119
|
+
PlanWeek,
|
|
120
|
+
PlanWorkout,
|
|
121
|
+
ProgramNavLevel,
|
|
122
|
+
ProgramSelection,
|
|
123
|
+
ProgramBreadcrumb,
|
|
121
124
|
} from './ProgramPlanningPage'
|
|
122
|
-
export {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
type ExerciseTrend,
|
|
133
|
-
type ExerciseVbtSet,
|
|
134
|
-
type ExerciseVbt,
|
|
135
|
-
type ExerciseDetailStats,
|
|
136
|
-
type VbtSummary,
|
|
125
|
+
export type {
|
|
126
|
+
ExerciseDetailPageProps,
|
|
127
|
+
ExerciseDetailTab,
|
|
128
|
+
ExerciseDetailHeader,
|
|
129
|
+
ExerciseDetailEntry,
|
|
130
|
+
ExerciseTrend,
|
|
131
|
+
ExerciseVbtSet,
|
|
132
|
+
ExerciseVbt,
|
|
133
|
+
ExerciseDetailStats,
|
|
134
|
+
VbtSummary,
|
|
137
135
|
} from './ExerciseDetailPage'
|
|
138
136
|
// muscleTaxonomy imports `react-native-body-highlighter` at runtime. Its value
|
|
139
137
|
// exports (incl. the MuscleGroup / SimpleMuscleGroup enums) live behind the
|
|
@@ -144,20 +142,13 @@ export type {
|
|
|
144
142
|
MovementCategory,
|
|
145
143
|
VolumeLandmarks,
|
|
146
144
|
} from './muscleTaxonomy'
|
|
147
|
-
export {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
type ActiveWorkoutExercise,
|
|
157
|
-
type ActiveWorkoutSuperset,
|
|
158
|
-
type ActiveWorkoutInput,
|
|
159
|
-
type ActiveWorkoutRest,
|
|
160
|
-
type ActiveExerciseStatus,
|
|
161
|
-
type WorkoutProgress,
|
|
162
|
-
type WorkoutGroup,
|
|
145
|
+
export type {
|
|
146
|
+
ActiveWorkoutPageProps,
|
|
147
|
+
ActiveWorkoutExercise,
|
|
148
|
+
ActiveWorkoutSuperset,
|
|
149
|
+
ActiveWorkoutInput,
|
|
150
|
+
ActiveWorkoutRest,
|
|
151
|
+
ActiveExerciseStatus,
|
|
152
|
+
WorkoutProgress,
|
|
153
|
+
WorkoutGroup,
|
|
163
154
|
} from './ActiveWorkoutPage'
|
package/src/index.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// Components
|
|
4
4
|
export * from './components'
|
|
5
5
|
|
|
6
|
-
// Theme
|
|
7
|
-
|
|
6
|
+
// Theme (nativewind-free surface; ThemeProvider value lives on the /theme
|
|
7
|
+
// subpath so the root barrel does not pull the nativewind import chain)
|
|
8
|
+
export * from './theme/barrel'
|
|
8
9
|
|
|
9
10
|
// Utilities
|
|
10
11
|
export * from './utils'
|
package/src/pages.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Subpath entry: @titan-design/react-ui/pages
|
|
2
|
+
//
|
|
3
|
+
// Page-level organisms — phone-shaped reference screens (ActiveWorkoutPage,
|
|
4
|
+
// ExerciseDetailPage, ProgramPlanningPage). Demoted out of the root barrel for
|
|
5
|
+
// titan 0.5.0: they are full app screens, orphaned by both consumers, and their
|
|
6
|
+
// successors are the form-factor-responsive level views (plan B2). Their VALUE
|
|
7
|
+
// exports (components + derivation helpers) live HERE; type-only re-exports
|
|
8
|
+
// stay on the root barrel so consumers can still type props without importing
|
|
9
|
+
// the screens. Mirrors the `/bodymap` isolation pattern.
|
|
10
|
+
//
|
|
11
|
+
// TrainingStatusPage is NOT here — it depends on react-native-body-highlighter
|
|
12
|
+
// and lives under `@titan-design/react-ui/bodymap`.
|
|
13
|
+
export {
|
|
14
|
+
ActiveWorkoutPage,
|
|
15
|
+
countCompletedSets,
|
|
16
|
+
deriveWorkoutProgress,
|
|
17
|
+
findActiveExercise,
|
|
18
|
+
statusToCardState,
|
|
19
|
+
toActiveCardProps,
|
|
20
|
+
groupExercises,
|
|
21
|
+
type ActiveWorkoutPageProps,
|
|
22
|
+
type ActiveWorkoutExercise,
|
|
23
|
+
type ActiveWorkoutSuperset,
|
|
24
|
+
type ActiveWorkoutInput,
|
|
25
|
+
type ActiveWorkoutRest,
|
|
26
|
+
type ActiveExerciseStatus,
|
|
27
|
+
type WorkoutProgress,
|
|
28
|
+
type WorkoutGroup,
|
|
29
|
+
} from './components/custom/Workout/ActiveWorkoutPage'
|
|
30
|
+
export {
|
|
31
|
+
ExerciseDetailPage,
|
|
32
|
+
deriveExerciseStats,
|
|
33
|
+
toExerciseCardProps,
|
|
34
|
+
summarizeVbt,
|
|
35
|
+
EXERCISE_DETAIL_TABS,
|
|
36
|
+
type ExerciseDetailPageProps,
|
|
37
|
+
type ExerciseDetailTab,
|
|
38
|
+
type ExerciseDetailHeader,
|
|
39
|
+
type ExerciseDetailEntry,
|
|
40
|
+
type ExerciseTrend,
|
|
41
|
+
type ExerciseVbtSet,
|
|
42
|
+
type ExerciseVbt,
|
|
43
|
+
type ExerciseDetailStats,
|
|
44
|
+
type VbtSummary,
|
|
45
|
+
} from './components/custom/Workout/ExerciseDetailPage'
|
|
46
|
+
export {
|
|
47
|
+
ProgramPlanningPage,
|
|
48
|
+
deriveNavLevel,
|
|
49
|
+
toProgressBarMesos,
|
|
50
|
+
findMeso,
|
|
51
|
+
findWeek,
|
|
52
|
+
findWorkout,
|
|
53
|
+
buildBreadcrumbs,
|
|
54
|
+
type ProgramPlanningPageProps,
|
|
55
|
+
type PlanMeso,
|
|
56
|
+
type PlanWeek,
|
|
57
|
+
type PlanWorkout,
|
|
58
|
+
type ProgramNavLevel,
|
|
59
|
+
type ProgramSelection,
|
|
60
|
+
type ProgramBreadcrumb,
|
|
61
|
+
} from './components/custom/Workout/ProgramPlanningPage'
|