@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.
Files changed (90) hide show
  1. package/dist/MesoStatusCard-CQk71hSi.d.mts +77 -0
  2. package/dist/MesoStatusCard-CQk71hSi.d.ts +77 -0
  3. package/dist/bodymap.d.mts +246 -3
  4. package/dist/bodymap.d.ts +246 -3
  5. package/dist/bodymap.js +441 -97
  6. package/dist/bodymap.js.map +1 -1
  7. package/dist/bodymap.mjs +1958 -66
  8. package/dist/bodymap.mjs.map +1 -1
  9. package/dist/index.d.mts +71 -738
  10. package/dist/index.d.ts +71 -738
  11. package/dist/index.js +888 -1583
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +4515 -2348
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/pages-D7Jlssvp.d.ts +791 -0
  16. package/dist/pages-MO0JCySL.d.mts +791 -0
  17. package/dist/pages.d.mts +4 -0
  18. package/dist/pages.d.ts +4 -0
  19. package/dist/pages.js +6646 -0
  20. package/dist/pages.js.map +1 -0
  21. package/dist/pages.mjs +6626 -0
  22. package/dist/pages.mjs.map +1 -0
  23. package/dist/theme/index.d.mts +28 -2
  24. package/dist/theme/index.d.ts +28 -2
  25. package/dist/theme/index.js +37 -0
  26. package/dist/theme/index.js.map +1 -1
  27. package/dist/theme/index.mjs +1880 -2
  28. package/dist/theme/index.mjs.map +1 -1
  29. package/dist/theme/tokens-css.mjs +573 -1
  30. package/dist/theme/tokens-css.mjs.map +1 -1
  31. package/package.json +9 -7
  32. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +10 -15
  33. package/src/components/custom/Workout/BaseBadge.stories.tsx +3 -3
  34. package/src/components/custom/Workout/BaseBadge.test.tsx +1 -1
  35. package/src/components/custom/Workout/BaseBadge.tsx +1 -1
  36. package/src/components/custom/Workout/BodyMap.tsx +7 -8
  37. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +25 -28
  38. package/src/components/custom/Workout/CapacityBandChart.tsx +2 -3
  39. package/src/components/custom/Workout/ExerciseCard.tsx +17 -28
  40. package/src/components/custom/Workout/ExerciseDetailPage.tsx +38 -31
  41. package/src/components/custom/Workout/InputBar.tsx +10 -11
  42. package/src/components/custom/Workout/IntensityBar.test.tsx +112 -60
  43. package/src/components/custom/Workout/IntensityBar.tsx +109 -58
  44. package/src/components/custom/Workout/MesoCard.tsx +2 -6
  45. package/src/components/custom/Workout/MesoStatusCard.tsx +17 -15
  46. package/src/components/custom/Workout/MuscleGroupChip.tsx +2 -4
  47. package/src/components/custom/Workout/PlaceholderStrip.tsx +5 -10
  48. package/src/components/custom/Workout/PrBadge.tsx +3 -3
  49. package/src/components/custom/Workout/PrHistoryModal.tsx +14 -16
  50. package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -12
  51. package/src/components/custom/Workout/README.md +50 -0
  52. package/src/components/custom/Workout/ReadinessCheck.tsx +13 -11
  53. package/src/components/custom/Workout/RestTimer.test.tsx +41 -10
  54. package/src/components/custom/Workout/RestTimer.tsx +55 -49
  55. package/src/components/custom/Workout/SetRow.tsx +20 -8
  56. package/src/components/custom/Workout/StrengthTrendChart.tsx +15 -17
  57. package/src/components/custom/Workout/TempoBar.stories.tsx +95 -0
  58. package/src/components/custom/Workout/TempoBar.test.tsx +103 -0
  59. package/src/components/custom/Workout/TempoBar.tsx +215 -0
  60. package/src/components/custom/Workout/TempoDisplay.stories.tsx +69 -2
  61. package/src/components/custom/Workout/TempoDisplay.test.tsx +35 -0
  62. package/src/components/custom/Workout/TempoDisplay.tsx +104 -1
  63. package/src/components/custom/Workout/TrainingStatusPage.tsx +8 -17
  64. package/src/components/custom/Workout/VelocityStrip.test.tsx +89 -2
  65. package/src/components/custom/Workout/VelocityStrip.tsx +210 -43
  66. package/src/components/custom/Workout/WeekRow.test.tsx +2 -2
  67. package/src/components/custom/Workout/WeekRow.tsx +2 -1
  68. package/src/components/custom/Workout/WeightBadge.test.tsx +2 -2
  69. package/src/components/custom/Workout/WeightBadge.tsx +2 -2
  70. package/src/components/custom/Workout/WorkoutCard.tsx +3 -8
  71. package/src/components/custom/Workout/icons.tsx +68 -0
  72. package/src/components/custom/Workout/index.ts +47 -47
  73. package/src/index.ts +3 -2
  74. package/src/pages.ts +61 -0
  75. package/src/test/nativewind-stub.ts +13 -0
  76. package/src/test/stories-smoke.test.tsx +50 -0
  77. package/src/theme/ThemeProvider.test.tsx +24 -0
  78. package/src/theme/ThemeProvider.tsx +50 -0
  79. package/src/theme/barrel.ts +53 -0
  80. package/src/theme/index.ts +8 -41
  81. package/src/theme/native-theming-guard.test.ts +117 -0
  82. package/src/theme/resolve-color.ts +19 -0
  83. package/dist/bodymap-BhG55xHM.d.mts +0 -318
  84. package/dist/bodymap-BhG55xHM.d.ts +0 -318
  85. package/dist/chunk-7XPB4NAO.mjs +0 -1013
  86. package/dist/chunk-7XPB4NAO.mjs.map +0 -1
  87. package/dist/chunk-P7TSQUN6.mjs +0 -846
  88. package/dist/chunk-P7TSQUN6.mjs.map +0 -1
  89. package/dist/chunk-TOD2WQBG.mjs +0 -1048
  90. 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={cn(className)}
22
+ className={className}
22
23
  style={[
23
24
  {
24
25
  height: 3,
25
- backgroundColor: 'transparent',
26
- borderWidth: 1,
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: 'transparent',
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 { Star } from 'lucide-react'
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
- <Star size={14} color="#FF7900" fill="#FF7900" strokeWidth={2} />
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: 10,
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 { Star } from 'lucide-react'
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
- const SURFACE_RAISED = 'var(--color-surface-raised)'
10
- const BORDER_DEFAULT = 'var(--color-border-default)'
11
- const TEXT_PRIMARY = 'var(--color-text-primary)'
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
- <Star size={16} color={BRAND_PRIMARY} fill={BRAND_PRIMARY} strokeWidth={2} />
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, color: TEXT_SECONDARY }}>
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, color: TEXT_TERTIARY }}>{'›'}</Text>}
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 ? TEXT_PRIMARY : BRAND_PRIMARY,
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, backgroundColor: PAGE_BG }}
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
- style={{ fontSize: 12, fontWeight: '500', fontFamily: 'Inter, sans-serif', color: TEXT_SECONDARY }}
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 : 'var(--color-border-default)',
124
- backgroundColor: selected ? BRAND_PRIMARY_SUBTLE : 'var(--color-surface-raised)',
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
- style={{ fontSize: 12, fontWeight: '600', fontFamily: 'Inter, sans-serif', color: TEXT_SECONDARY }}
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
- style={{ marginTop: 6, fontSize: 13, fontFamily: 'Inter, sans-serif', color: TEXT_PRIMARY }}
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
- style={{ fontSize: 18, fontWeight: '700', fontFamily: '"Space Grotesk", sans-serif', color: TEXT_PRIMARY }}
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('next set info', () => {
79
- it('displays nextSetInfo when provided', () => {
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
  })