@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
|
@@ -34,16 +34,14 @@ export function MuscleGroupChip({
|
|
|
34
34
|
|
|
35
35
|
const content = (
|
|
36
36
|
<View
|
|
37
|
-
className={className}
|
|
37
|
+
className={['bg-surface-raised border-border', className].filter(Boolean).join(' ')}
|
|
38
38
|
style={{
|
|
39
39
|
flexDirection: 'row',
|
|
40
40
|
alignItems: 'center',
|
|
41
41
|
borderRadius: 9999,
|
|
42
42
|
paddingHorizontal: 9,
|
|
43
43
|
paddingVertical: 3,
|
|
44
|
-
backgroundColor: 'var(--color-surface-raised)',
|
|
45
44
|
borderWidth: 1,
|
|
46
|
-
borderColor: 'var(--color-border-default)',
|
|
47
45
|
}}
|
|
48
46
|
accessibilityLabel={onPress ? undefined : `${name}, volume status: ${statusLabel}`}
|
|
49
47
|
testID="muscle-group-chip"
|
|
@@ -62,11 +60,11 @@ export function MuscleGroupChip({
|
|
|
62
60
|
testID="muscle-group-chip-dot"
|
|
63
61
|
/>
|
|
64
62
|
<Text
|
|
63
|
+
className="text-text-secondary"
|
|
65
64
|
style={{
|
|
66
65
|
fontFamily: 'Inter, sans-serif',
|
|
67
66
|
fontWeight: '500',
|
|
68
67
|
fontSize: 11,
|
|
69
|
-
color: 'var(--color-text-secondary)',
|
|
70
68
|
}}
|
|
71
69
|
accessibilityElementsHidden
|
|
72
70
|
>
|
|
@@ -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,15 +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
|
-
borderColor: 'var(--color-border-strong)',
|
|
29
|
-
borderRadius: 1,
|
|
26
|
+
backgroundColor: WORKOUT_TOKENS.placeholder.fill,
|
|
27
|
+
borderRadius: 2,
|
|
30
28
|
opacity: 0.5,
|
|
31
29
|
},
|
|
32
30
|
width != null ? { width: width as number } : { flex: 1 },
|
|
@@ -63,10 +61,7 @@ function SegmentedStrip({
|
|
|
63
61
|
style={{
|
|
64
62
|
flex: 1,
|
|
65
63
|
height: 3,
|
|
66
|
-
backgroundColor:
|
|
67
|
-
borderWidth: 1,
|
|
68
|
-
borderStyle: 'dashed',
|
|
69
|
-
borderColor: 'var(--color-border-strong)',
|
|
64
|
+
backgroundColor: WORKOUT_TOKENS.placeholder.fill,
|
|
70
65
|
borderRadius: 1,
|
|
71
66
|
minWidth: 4,
|
|
72
67
|
}}
|
|
@@ -1,7 +1,7 @@
|
|
|
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
3
|
import { View, Text, Animated, Easing, type ViewProps } from 'react-native'
|
|
4
|
-
import {
|
|
4
|
+
import { StarIcon } from './icons'
|
|
5
5
|
import { BaseBadge } from './BaseBadge'
|
|
6
6
|
|
|
7
7
|
export type PRType = 'e1rm' | 'weight' | 'reps' | 'volume' | 'velocity'
|
|
@@ -66,7 +66,7 @@ export function PrBadge({
|
|
|
66
66
|
testID="pr-badge-star"
|
|
67
67
|
{...props}
|
|
68
68
|
>
|
|
69
|
-
<
|
|
69
|
+
<StarIcon size={14} color="#FF7900" fill="#FF7900" strokeWidth={2} />
|
|
70
70
|
</View>
|
|
71
71
|
) : (
|
|
72
72
|
<BaseBadge
|
|
@@ -77,7 +77,7 @@ export function PrBadge({
|
|
|
77
77
|
>
|
|
78
78
|
<Text
|
|
79
79
|
style={{
|
|
80
|
-
fontSize:
|
|
80
|
+
fontSize: 12,
|
|
81
81
|
color: '#FF7900',
|
|
82
82
|
fontWeight: '700',
|
|
83
83
|
fontFamily: 'Inter, sans-serif',
|
|
@@ -1,16 +1,15 @@
|
|
|
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 {
|
|
3
|
+
import { StarIcon } from './icons'
|
|
4
4
|
import { Drawer, DrawerBody } from '../../ui/drawer'
|
|
5
|
+
import { resolveColor } from '../../../theme/resolve-color'
|
|
5
6
|
|
|
6
7
|
const BRAND_PRIMARY = '#FF7900'
|
|
7
8
|
const RECENT_BORDER = 'rgba(255,176,32,0.3)'
|
|
8
9
|
const RECENT_GLOW = '0 0 12px rgba(255,176,32,0.06)'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const TEXT_SECONDARY = 'var(--color-text-secondary)'
|
|
13
|
-
const TEXT_TERTIARY = 'var(--color-text-tertiary)'
|
|
10
|
+
// Native-safe fallback for the conditional row border (recent uses a computed
|
|
11
|
+
// rgba, so className can't express both branches).
|
|
12
|
+
const BORDER_DEFAULT = resolveColor('border-default')
|
|
14
13
|
|
|
15
14
|
export type PrRecordType = 'e1rm' | 'weight' | 'reps' | 'volume' | 'velocity'
|
|
16
15
|
|
|
@@ -72,51 +71,50 @@ function PrRecordRow({ record, index }: { record: PrRecord; index: number }) {
|
|
|
72
71
|
<View
|
|
73
72
|
accessibilityRole="text"
|
|
74
73
|
accessibilityLabel={a11yLabel}
|
|
75
|
-
className="flex-row items-center"
|
|
74
|
+
className="flex-row items-center bg-surface-raised"
|
|
76
75
|
style={{
|
|
77
76
|
gap: 10,
|
|
78
77
|
paddingVertical: 10,
|
|
79
78
|
paddingHorizontal: 12,
|
|
80
79
|
marginBottom: 8,
|
|
81
80
|
borderRadius: 8,
|
|
82
|
-
backgroundColor: SURFACE_RAISED,
|
|
83
81
|
borderWidth: 1,
|
|
84
82
|
borderColor: record.isRecent ? RECENT_BORDER : BORDER_DEFAULT,
|
|
85
83
|
...(record.isRecent ? { boxShadow: RECENT_GLOW } : {}),
|
|
86
84
|
}}
|
|
87
85
|
testID={`pr-history-modal-record-${index}`}
|
|
88
86
|
>
|
|
89
|
-
<
|
|
87
|
+
<StarIcon size={16} color={BRAND_PRIMARY} fill={BRAND_PRIMARY} strokeWidth={2} />
|
|
90
88
|
<Text
|
|
89
|
+
className="text-text-secondary"
|
|
91
90
|
style={{
|
|
92
91
|
flex: 1,
|
|
93
92
|
fontSize: 13,
|
|
94
93
|
fontFamily: 'Inter, sans-serif',
|
|
95
94
|
fontWeight: '600',
|
|
96
|
-
color: TEXT_SECONDARY,
|
|
97
95
|
}}
|
|
98
96
|
testID={`pr-history-modal-record-${index}-type`}
|
|
99
97
|
>
|
|
100
98
|
{label}
|
|
101
99
|
</Text>
|
|
102
100
|
<Text
|
|
101
|
+
className="text-text-primary"
|
|
103
102
|
style={{
|
|
104
103
|
fontSize: 14,
|
|
105
104
|
fontFamily: '"Space Grotesk", sans-serif',
|
|
106
105
|
fontWeight: '700',
|
|
107
|
-
color: TEXT_PRIMARY,
|
|
108
106
|
}}
|
|
109
107
|
testID={`pr-history-modal-record-${index}-value`}
|
|
110
108
|
>
|
|
111
109
|
{valueText}
|
|
112
110
|
</Text>
|
|
113
111
|
<Text
|
|
112
|
+
className="text-text-tertiary"
|
|
114
113
|
style={{
|
|
115
114
|
minWidth: 64,
|
|
116
115
|
textAlign: 'right',
|
|
117
116
|
fontSize: 11,
|
|
118
117
|
fontFamily: 'Inter, sans-serif',
|
|
119
|
-
color: TEXT_TERTIARY,
|
|
120
118
|
}}
|
|
121
119
|
testID={`pr-history-modal-record-${index}-date`}
|
|
122
120
|
>
|
|
@@ -173,13 +171,13 @@ export function PrHistoryModal({
|
|
|
173
171
|
>
|
|
174
172
|
<View
|
|
175
173
|
accessibilityElementsHidden
|
|
174
|
+
className="bg-border"
|
|
176
175
|
style={{
|
|
177
176
|
alignSelf: 'center',
|
|
178
177
|
width: 40,
|
|
179
178
|
height: 4,
|
|
180
179
|
borderRadius: 2,
|
|
181
180
|
marginTop: 8,
|
|
182
|
-
backgroundColor: BORDER_DEFAULT,
|
|
183
181
|
}}
|
|
184
182
|
testID="pr-history-modal-handle"
|
|
185
183
|
/>
|
|
@@ -191,11 +189,11 @@ export function PrHistoryModal({
|
|
|
191
189
|
>
|
|
192
190
|
<Text
|
|
193
191
|
accessibilityRole="header"
|
|
192
|
+
className="text-text-primary"
|
|
194
193
|
style={{
|
|
195
194
|
fontSize: 16,
|
|
196
195
|
fontFamily: '"Space Grotesk", sans-serif',
|
|
197
196
|
fontWeight: '700',
|
|
198
|
-
color: TEXT_PRIMARY,
|
|
199
197
|
}}
|
|
200
198
|
testID="pr-history-modal-title"
|
|
201
199
|
>
|
|
@@ -209,7 +207,7 @@ export function PrHistoryModal({
|
|
|
209
207
|
style={{ padding: 4, margin: -4 }}
|
|
210
208
|
testID="pr-history-modal-close"
|
|
211
209
|
>
|
|
212
|
-
<Text style={{ fontSize: 22, lineHeight: 22
|
|
210
|
+
<Text className="text-text-secondary" style={{ fontSize: 22, lineHeight: 22 }}>
|
|
213
211
|
{'×'}
|
|
214
212
|
</Text>
|
|
215
213
|
</Pressable>
|
|
@@ -219,11 +217,11 @@ export function PrHistoryModal({
|
|
|
219
217
|
{records.length === 0 ? (
|
|
220
218
|
<View style={{ paddingVertical: 24 }} testID="pr-history-modal-empty">
|
|
221
219
|
<Text
|
|
220
|
+
className="text-text-tertiary"
|
|
222
221
|
style={{
|
|
223
222
|
textAlign: 'center',
|
|
224
223
|
fontSize: 13,
|
|
225
224
|
fontFamily: 'Inter, sans-serif',
|
|
226
|
-
color: TEXT_TERTIARY,
|
|
227
225
|
}}
|
|
228
226
|
>
|
|
229
227
|
No personal records yet
|
|
@@ -7,13 +7,9 @@ import { type WeekRowProps, type WeekRowWorkout } from './WeekRow'
|
|
|
7
7
|
import { WorkoutCard, type WorkoutStatus, type WorkoutMuscleGroup } from './WorkoutCard'
|
|
8
8
|
import { type WorkoutPillStatus } from './WorkoutPill'
|
|
9
9
|
import { type ExerciseCardProps } from './ExerciseCard'
|
|
10
|
+
import { cn } from '../../../utils/cn'
|
|
10
11
|
|
|
11
|
-
const SURFACE_ELEVATED = 'var(--color-surface-elevated)'
|
|
12
|
-
const BORDER_DEFAULT = 'var(--color-border-default)'
|
|
13
12
|
const BRAND_PRIMARY = '#FF7900'
|
|
14
|
-
const TEXT_PRIMARY = 'var(--color-text-primary)'
|
|
15
|
-
const TEXT_TERTIARY = 'var(--color-text-tertiary)'
|
|
16
|
-
const PAGE_BG = 'var(--color-background-base)'
|
|
17
13
|
|
|
18
14
|
/** A single workout within a planned week, plus its exercise breakdown. */
|
|
19
15
|
export interface PlanWorkout {
|
|
@@ -157,7 +153,7 @@ function Breadcrumbs({ crumbs, onNavigate }: BreadcrumbsProps) {
|
|
|
157
153
|
const isLast = index === crumbs.length - 1
|
|
158
154
|
return (
|
|
159
155
|
<View key={crumb.key} className="flex-row items-center" style={{ gap: 4 }}>
|
|
160
|
-
{index > 0 && <Text style={{ fontSize: 12
|
|
156
|
+
{index > 0 && <Text className="text-text-tertiary" style={{ fontSize: 12 }}>{'›'}</Text>}
|
|
161
157
|
<Pressable
|
|
162
158
|
onPress={() => onNavigate(crumb.level)}
|
|
163
159
|
disabled={isLast}
|
|
@@ -165,11 +161,12 @@ function Breadcrumbs({ crumbs, onNavigate }: BreadcrumbsProps) {
|
|
|
165
161
|
testID={`program-planning-page-crumb-${crumb.key}`}
|
|
166
162
|
>
|
|
167
163
|
<Text
|
|
164
|
+
className={isLast ? 'text-text-primary' : undefined}
|
|
168
165
|
style={{
|
|
169
166
|
fontSize: 12,
|
|
170
167
|
fontFamily: 'Inter, sans-serif',
|
|
171
168
|
fontWeight: isLast ? '700' : '500',
|
|
172
|
-
color: isLast ?
|
|
169
|
+
color: isLast ? undefined : BRAND_PRIMARY,
|
|
173
170
|
}}
|
|
174
171
|
>
|
|
175
172
|
{crumb.label}
|
|
@@ -351,8 +348,8 @@ export function ProgramPlanningPage({
|
|
|
351
348
|
|
|
352
349
|
return (
|
|
353
350
|
<View
|
|
354
|
-
className={className}
|
|
355
|
-
style={{ width: 390
|
|
351
|
+
className={cn(className, 'bg-background-base')}
|
|
352
|
+
style={{ width: 390 }}
|
|
356
353
|
accessibilityRole={'main' as ViewProps['accessibilityRole']}
|
|
357
354
|
aria-label={title}
|
|
358
355
|
testID="program-planning-page"
|
|
@@ -361,11 +358,11 @@ export function ProgramPlanningPage({
|
|
|
361
358
|
<View style={{ padding: 16, gap: 14 }} testID="program-planning-page-content">
|
|
362
359
|
<Text
|
|
363
360
|
accessibilityRole="header"
|
|
361
|
+
className="text-text-primary"
|
|
364
362
|
style={{
|
|
365
363
|
fontSize: 20,
|
|
366
364
|
fontFamily: '"Space Grotesk", sans-serif',
|
|
367
365
|
fontWeight: '700',
|
|
368
|
-
color: TEXT_PRIMARY,
|
|
369
366
|
}}
|
|
370
367
|
testID="program-planning-page-title"
|
|
371
368
|
>
|
|
@@ -388,10 +385,9 @@ export function ProgramPlanningPage({
|
|
|
388
385
|
)}
|
|
389
386
|
|
|
390
387
|
<View
|
|
388
|
+
className="bg-surface-elevated border-border"
|
|
391
389
|
style={{
|
|
392
|
-
backgroundColor: SURFACE_ELEVATED,
|
|
393
390
|
borderWidth: 1,
|
|
394
|
-
borderColor: BORDER_DEFAULT,
|
|
395
391
|
borderRadius: 12,
|
|
396
392
|
padding: 12,
|
|
397
393
|
}}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Workout component family
|
|
2
|
+
|
|
3
|
+
Training-specific components (badges, bars, timers, cards, charts, and page-level
|
|
4
|
+
screens) built on titan's atoms. They live flat on disk — the tiering below is a
|
|
5
|
+
documentation contract, not a directory layout, so promoting or composing a
|
|
6
|
+
component is pure import churn with no file moves.
|
|
7
|
+
|
|
8
|
+
## Tier map
|
|
9
|
+
|
|
10
|
+
**Atoms** — single-purpose, no cross-component state:
|
|
11
|
+
BaseBadge · WeightBadge · PrBadge · StatusDot · PlaceholderStrip · TempoBar ·
|
|
12
|
+
DeviationBar · IntensityBar · WorkoutPill · MuscleGroupChip · Sparkline ·
|
|
13
|
+
SupersetWrapper · InputBar
|
|
14
|
+
|
|
15
|
+
**Molecules** — compose atoms, own a little local state:
|
|
16
|
+
VelocityStrip · SetRow · TempoDisplay · RestTimer · MesoProgressBar · WeekRow ·
|
|
17
|
+
WorkoutCard
|
|
18
|
+
|
|
19
|
+
**Organisms** — full features, often with their own data contract:
|
|
20
|
+
ExerciseCard · MesoCard · MesoStatusCard · PrHistoryModal · ReadinessCheck ·
|
|
21
|
+
StrengthTrendChart · CapacityBandChart · BodyMap · BodyMapDetailPanel
|
|
22
|
+
|
|
23
|
+
**Pages** — phone-shaped reference screens (whole-screen compositions):
|
|
24
|
+
ActiveWorkoutPage · ExerciseDetailPage · ProgramPlanningPage · TrainingStatusPage
|
|
25
|
+
|
|
26
|
+
## Subpaths
|
|
27
|
+
|
|
28
|
+
Two families are kept off the root barrel so `@titan-design/react-ui` stays free
|
|
29
|
+
of their heavy runtime dependencies. In both cases the value exports move to the
|
|
30
|
+
subpath while type-only re-exports remain on the root, so consumers can still
|
|
31
|
+
type props without pulling the dependency.
|
|
32
|
+
|
|
33
|
+
- **`@titan-design/react-ui/bodymap`** — isolates
|
|
34
|
+
`react-native-body-highlighter` (a native SVG dep). BodyMap, BodyMapDetailPanel,
|
|
35
|
+
TrainingStatusPage, and the muscle taxonomy live here.
|
|
36
|
+
- **`@titan-design/react-ui/pages`** — isolates the page-level organisms
|
|
37
|
+
(ActiveWorkoutPage, ExerciseDetailPage, ProgramPlanningPage). They are full app
|
|
38
|
+
screens orphaned by both consumers, kept as reference implementations while their
|
|
39
|
+
successors (the responsive level views) are built. TrainingStatusPage is the
|
|
40
|
+
exception — it stays under `/bodymap` because of the body-highlighter dep.
|
|
41
|
+
|
|
42
|
+
## Notes
|
|
43
|
+
|
|
44
|
+
- **BaseBadge is an internal composition primitive** — the shared shell that
|
|
45
|
+
WeightBadge and PrBadge build on. It is exempt from orphan accounting; it is not
|
|
46
|
+
meant to be consumed directly even though it is exported for composition.
|
|
47
|
+
- Badge icons (WeightBadge's dumbbell, PrBadge / PrHistoryModal's star) are inline
|
|
48
|
+
SVGs (`./icons.tsx`), not `lucide-react` — that dependency was dropped in 0.5.0
|
|
49
|
+
to keep the root barrel light. The SVG paths mirror lucide's glyphs so rendering
|
|
50
|
+
is unchanged.
|
|
@@ -8,8 +8,6 @@ const BRAND_PRIMARY_SUBTLE = 'rgba(255,121,0,0.12)'
|
|
|
8
8
|
const STATUS_SUCCESS = '#14B8A6'
|
|
9
9
|
const STATUS_WARNING = '#FFB020'
|
|
10
10
|
const STATUS_ERROR = '#D14343'
|
|
11
|
-
const TEXT_PRIMARY = 'var(--color-text-primary)'
|
|
12
|
-
const TEXT_SECONDARY = 'var(--color-text-secondary)'
|
|
13
11
|
|
|
14
12
|
/** Emoji option sets per known factor, keyed by lowercase id/label. */
|
|
15
13
|
const EMOJI_SETS: Record<string, readonly string[]> = {
|
|
@@ -91,7 +89,8 @@ function EmojiSlider({ factor }: EmojiSliderProps) {
|
|
|
91
89
|
return (
|
|
92
90
|
<View style={{ marginTop: 14 }} testID={`readiness-check-factor-${factor.id}`}>
|
|
93
91
|
<Text
|
|
94
|
-
|
|
92
|
+
className="text-text-secondary"
|
|
93
|
+
style={{ fontSize: 12, fontWeight: '500', fontFamily: 'Inter, sans-serif' }}
|
|
95
94
|
testID="readiness-check-factor-label"
|
|
96
95
|
>
|
|
97
96
|
{factor.label}
|
|
@@ -114,14 +113,15 @@ function EmojiSlider({ factor }: EmojiSliderProps) {
|
|
|
114
113
|
accessibilityLabel={`${factor.label} level ${level} of 5`}
|
|
115
114
|
aria-checked={selected}
|
|
116
115
|
testID="readiness-check-emoji"
|
|
116
|
+
className={selected ? undefined : 'border-border bg-surface-raised'}
|
|
117
117
|
style={({ pressed }) => ({
|
|
118
118
|
flex: 1,
|
|
119
119
|
alignItems: 'center',
|
|
120
120
|
paddingVertical: 8,
|
|
121
121
|
borderRadius: 8,
|
|
122
122
|
borderWidth: 1,
|
|
123
|
-
borderColor: selected ? BRAND_PRIMARY :
|
|
124
|
-
backgroundColor: selected ? BRAND_PRIMARY_SUBTLE :
|
|
123
|
+
borderColor: selected ? BRAND_PRIMARY : undefined,
|
|
124
|
+
backgroundColor: selected ? BRAND_PRIMARY_SUBTLE : undefined,
|
|
125
125
|
opacity: selected ? 1 : pressed ? 0.8 : 0.55,
|
|
126
126
|
transform: [{ scale: selected ? 1.06 : 1 }],
|
|
127
127
|
})}
|
|
@@ -143,13 +143,13 @@ function ScoreGauge({ score }: { score: number }) {
|
|
|
143
143
|
accessibilityRole="image"
|
|
144
144
|
accessibilityLabel={`Readiness score: ${score} out of 100`}
|
|
145
145
|
testID="readiness-check-score"
|
|
146
|
+
className="bg-surface-raised"
|
|
146
147
|
style={{
|
|
147
148
|
width: 60,
|
|
148
149
|
height: 60,
|
|
149
150
|
borderRadius: 30,
|
|
150
151
|
borderWidth: 4,
|
|
151
152
|
borderColor: color,
|
|
152
|
-
backgroundColor: 'var(--color-surface-raised)',
|
|
153
153
|
alignItems: 'center',
|
|
154
154
|
justifyContent: 'center',
|
|
155
155
|
}}
|
|
@@ -169,19 +169,19 @@ function WarmUpCard({ validation }: { validation: WarmUpValidation }) {
|
|
|
169
169
|
const badge = WARMUP_BADGE[validation.status]
|
|
170
170
|
return (
|
|
171
171
|
<View
|
|
172
|
+
className="border-border bg-surface-raised"
|
|
172
173
|
style={{
|
|
173
174
|
marginTop: 16,
|
|
174
175
|
padding: 12,
|
|
175
176
|
borderRadius: 8,
|
|
176
177
|
borderWidth: 1,
|
|
177
|
-
borderColor: 'var(--color-border-default)',
|
|
178
|
-
backgroundColor: 'var(--color-surface-raised)',
|
|
179
178
|
}}
|
|
180
179
|
testID="readiness-check-warmup"
|
|
181
180
|
>
|
|
182
181
|
<View className="flex-row items-center justify-between" style={{ gap: 8 }}>
|
|
183
182
|
<Text
|
|
184
|
-
|
|
183
|
+
className="text-text-secondary"
|
|
184
|
+
style={{ fontSize: 12, fontWeight: '600', fontFamily: 'Inter, sans-serif' }}
|
|
185
185
|
testID="readiness-check-warmup-deficit"
|
|
186
186
|
>
|
|
187
187
|
{velocityDeficitText(validation.velocityDeficit)}
|
|
@@ -191,7 +191,8 @@ function WarmUpCard({ validation }: { validation: WarmUpValidation }) {
|
|
|
191
191
|
</Badge>
|
|
192
192
|
</View>
|
|
193
193
|
<Text
|
|
194
|
-
|
|
194
|
+
className="text-text-primary"
|
|
195
|
+
style={{ marginTop: 6, fontSize: 13, fontFamily: 'Inter, sans-serif' }}
|
|
195
196
|
testID="readiness-check-warmup-recommendation"
|
|
196
197
|
>
|
|
197
198
|
{validation.recommendation}
|
|
@@ -226,7 +227,8 @@ export function ReadinessCheck({
|
|
|
226
227
|
<View style={{ padding: 16 }}>
|
|
227
228
|
<View className="flex-row items-center justify-between" style={{ gap: 12 }}>
|
|
228
229
|
<Text
|
|
229
|
-
|
|
230
|
+
className="text-text-primary"
|
|
231
|
+
style={{ fontSize: 18, fontWeight: '700', fontFamily: '"Space Grotesk", sans-serif' }}
|
|
230
232
|
testID="readiness-check-title"
|
|
231
233
|
>
|
|
232
234
|
How are you feeling?
|
|
@@ -75,16 +75,51 @@ describe('RestTimer', () => {
|
|
|
75
75
|
})
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
-
describe('
|
|
79
|
-
it('
|
|
78
|
+
describe('display-only mode', () => {
|
|
79
|
+
it('renders the Skip and +30s actions by default', () => {
|
|
80
|
+
render(<RestTimer {...defaultProps} />)
|
|
81
|
+
expect(screen.getByTestId('rest-timer-skip')).toBeInTheDocument()
|
|
82
|
+
expect(screen.getByTestId('rest-timer-add-time')).toBeInTheDocument()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('hides the Skip and +30s actions when displayOnly', () => {
|
|
86
|
+
render(<RestTimer {...defaultProps} displayOnly />)
|
|
87
|
+
expect(screen.queryByTestId('rest-timer-skip')).not.toBeInTheDocument()
|
|
88
|
+
expect(screen.queryByTestId('rest-timer-add-time')).not.toBeInTheDocument()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('keeps the countdown, progress bar, and next-set line when displayOnly', () => {
|
|
80
92
|
render(
|
|
81
93
|
<RestTimer
|
|
82
94
|
{...defaultProps}
|
|
95
|
+
elapsedMs={30000}
|
|
96
|
+
displayOnly
|
|
83
97
|
nextSetInfo="Bench Press — Set 3 of 4"
|
|
84
|
-
|
|
98
|
+
/>
|
|
99
|
+
)
|
|
100
|
+
expect(screen.getByTestId('rest-timer-time')).toHaveTextContent('2:00')
|
|
101
|
+
expect(screen.getByTestId('rest-timer-progress-fill')).toBeInTheDocument()
|
|
102
|
+
expect(screen.getByTestId('rest-timer-next-set')).toHaveTextContent(
|
|
103
|
+
'Bench Press — Set 3 of 4'
|
|
85
104
|
)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('does not fire callbacks in display-only mode (controls absent)', () => {
|
|
108
|
+
const onSkip = vi.fn()
|
|
109
|
+
const onAddTime = vi.fn()
|
|
110
|
+
render(<RestTimer {...defaultProps} onSkip={onSkip} onAddTime={onAddTime} displayOnly />)
|
|
111
|
+
expect(screen.queryByLabelText('Skip rest')).not.toBeInTheDocument()
|
|
112
|
+
expect(screen.queryByLabelText('Add 30 seconds')).not.toBeInTheDocument()
|
|
113
|
+
expect(onSkip).not.toHaveBeenCalled()
|
|
114
|
+
expect(onAddTime).not.toHaveBeenCalled()
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('next set info', () => {
|
|
119
|
+
it('displays nextSetInfo when provided', () => {
|
|
120
|
+
render(<RestTimer {...defaultProps} nextSetInfo="Bench Press — Set 3 of 4" />)
|
|
86
121
|
expect(screen.getByTestId('rest-timer-next-set')).toHaveTextContent(
|
|
87
|
-
'Bench Press — Set 3 of 4'
|
|
122
|
+
'Bench Press — Set 3 of 4'
|
|
88
123
|
)
|
|
89
124
|
})
|
|
90
125
|
|
|
@@ -127,9 +162,7 @@ describe('RestTimer', () => {
|
|
|
127
162
|
|
|
128
163
|
it('has timer role with remaining seconds in label', () => {
|
|
129
164
|
render(<RestTimer {...defaultProps} totalSeconds={90} elapsedMs={30000} />)
|
|
130
|
-
expect(
|
|
131
|
-
screen.getByLabelText('Rest timer, 60 seconds remaining'),
|
|
132
|
-
).toBeInTheDocument()
|
|
165
|
+
expect(screen.getByLabelText('Rest timer, 60 seconds remaining')).toBeInTheDocument()
|
|
133
166
|
})
|
|
134
167
|
|
|
135
168
|
it('skip button has accessible label', () => {
|
|
@@ -146,9 +179,7 @@ describe('RestTimer', () => {
|
|
|
146
179
|
|
|
147
180
|
describe('RestTimer zero-duration timer (NaN guard)', () => {
|
|
148
181
|
it('emits a valid 100% width (not NaN) when totalSeconds is 0', () => {
|
|
149
|
-
const { container } = render(
|
|
150
|
-
<RestTimer {...defaultProps} totalSeconds={0} elapsedMs={0} />,
|
|
151
|
-
)
|
|
182
|
+
const { container } = render(<RestTimer {...defaultProps} totalSeconds={0} elapsedMs={0} />)
|
|
152
183
|
expect(screen.getByTestId('rest-timer-progress-fill')).toHaveStyle({ width: '100%' })
|
|
153
184
|
expect(container.innerHTML).not.toContain('NaN')
|
|
154
185
|
})
|