@titan-design/react-ui 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/bodymap.js +232 -185
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +232 -185
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +164 -8
  6. package/dist/index.d.ts +164 -8
  7. package/dist/index.js +835 -342
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +816 -343
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/pages.js +570 -522
  12. package/dist/pages.js.map +1 -1
  13. package/dist/pages.mjs +570 -522
  14. package/dist/pages.mjs.map +1 -1
  15. package/dist/theme/index.d.mts +461 -199
  16. package/dist/theme/index.d.ts +461 -199
  17. package/dist/theme/index.js +311 -97
  18. package/dist/theme/index.js.map +1 -1
  19. package/dist/theme/index.mjs +303 -98
  20. package/dist/theme/index.mjs.map +1 -1
  21. package/dist/theme/tokens-css.js +148 -139
  22. package/dist/theme/tokens-css.js.map +1 -1
  23. package/dist/theme/tokens-css.mjs +148 -139
  24. package/dist/theme/tokens-css.mjs.map +1 -1
  25. package/package.json +3 -1
  26. package/src/components/custom/DateTime/DateTime.stories.tsx +82 -6
  27. package/src/components/custom/DateTime/DateTime.test.tsx +29 -3
  28. package/src/components/custom/DateTime/DateTime.tsx +55 -15
  29. package/src/components/custom/Gauge/Gauge.test.tsx +2 -2
  30. package/src/components/custom/Gauge/Gauge.tsx +6 -3
  31. package/src/components/custom/Scatter/Scatter.tsx +3 -5
  32. package/src/components/custom/Treemap/Treemap.tsx +3 -5
  33. package/src/components/custom/Typography/Typography.stories.tsx +45 -27
  34. package/src/components/custom/Typography/Typography.test.tsx +13 -3
  35. package/src/components/custom/Typography/Typography.tsx +5 -0
  36. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +2 -1
  37. package/src/components/custom/Workout/BodyMap.tsx +6 -3
  38. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +6 -3
  39. package/src/components/custom/Workout/CapacityBandChart.test.tsx +2 -2
  40. package/src/components/custom/Workout/CapacityBandChart.tsx +9 -6
  41. package/src/components/custom/Workout/DeviationBar.test.tsx +1 -1
  42. package/src/components/custom/Workout/DeviationBar.tsx +7 -4
  43. package/src/components/custom/Workout/ExerciseDetailPage.tsx +2 -1
  44. package/src/components/custom/Workout/InputBar.tsx +2 -1
  45. package/src/components/custom/Workout/IntensityBar.test.tsx +8 -8
  46. package/src/components/custom/Workout/IntensityBar.tsx +10 -6
  47. package/src/components/custom/Workout/MesoCard.tsx +8 -3
  48. package/src/components/custom/Workout/MesoProgressBar.test.tsx +1 -1
  49. package/src/components/custom/Workout/MesoProgressBar.tsx +3 -2
  50. package/src/components/custom/Workout/MesoStatusCard.tsx +20 -13
  51. package/src/components/custom/Workout/MuscleGroupChip.tsx +7 -4
  52. package/src/components/custom/Workout/PrBadge.tsx +5 -2
  53. package/src/components/custom/Workout/PrHistoryModal.tsx +4 -3
  54. package/src/components/custom/Workout/ProgramPlanningPage.tsx +2 -1
  55. package/src/components/custom/Workout/ReadinessCheck.tsx +7 -4
  56. package/src/components/custom/Workout/RestTimer.tsx +2 -1
  57. package/src/components/custom/Workout/SetRow.tsx +2 -1
  58. package/src/components/custom/Workout/Sparkline.tsx +2 -1
  59. package/src/components/custom/Workout/StatusDot.test.tsx +2 -2
  60. package/src/components/custom/Workout/StatusDot.tsx +12 -9
  61. package/src/components/custom/Workout/StrengthTrendChart.tsx +9 -6
  62. package/src/components/custom/Workout/SupersetWrapper.tsx +2 -1
  63. package/src/components/custom/Workout/TempoDisplay.tsx +6 -3
  64. package/src/components/custom/Workout/VelocityStrip.test.tsx +3 -3
  65. package/src/components/custom/Workout/WeekRow.tsx +3 -2
  66. package/src/components/custom/Workout/WeightBadge.tsx +6 -2
  67. package/src/components/custom/Workout/WorkoutCard.tsx +5 -2
  68. package/src/components/custom/Workout/WorkoutPill.tsx +12 -8
  69. package/src/components/custom/Workout/icons.tsx +6 -67
  70. package/src/components/icons/SvgIcon.tsx +50 -0
  71. package/src/components/icons/icons.stories.tsx +57 -0
  72. package/src/components/icons/icons.test.tsx +43 -0
  73. package/src/components/icons/icons.tsx +45 -0
  74. package/src/components/icons/index.ts +4 -0
  75. package/src/components/index.ts +6 -0
  76. package/src/components/shell/BrandLockup.stories.tsx +27 -0
  77. package/src/components/shell/BrandLockup.test.tsx +23 -0
  78. package/src/components/shell/BrandLockup.tsx +47 -0
  79. package/src/components/shell/DeviceIndicator.stories.tsx +28 -0
  80. package/src/components/shell/DeviceIndicator.test.tsx +26 -0
  81. package/src/components/shell/DeviceIndicator.tsx +52 -0
  82. package/src/components/shell/DeviceMenu.stories.tsx +47 -0
  83. package/src/components/shell/DeviceMenu.test.tsx +40 -0
  84. package/src/components/shell/DeviceMenu.tsx +65 -0
  85. package/src/components/shell/DeviceRow.stories.tsx +38 -0
  86. package/src/components/shell/DeviceRow.test.tsx +37 -0
  87. package/src/components/shell/DeviceRow.tsx +58 -0
  88. package/src/components/shell/README.md +86 -0
  89. package/src/components/shell/SessionStatePill.stories.tsx +27 -0
  90. package/src/components/shell/SessionStatePill.test.tsx +25 -0
  91. package/src/components/shell/SessionStatePill.tsx +46 -0
  92. package/src/components/shell/TopBar.stories.tsx +49 -0
  93. package/src/components/shell/TopBar.test.tsx +36 -0
  94. package/src/components/shell/TopBar.tsx +89 -0
  95. package/src/components/shell/index.ts +8 -0
  96. package/src/components/ui/indicator/Indicator.stories.tsx +37 -1
  97. package/src/components/ui/indicator/Indicator.test.tsx +19 -1
  98. package/src/components/ui/indicator/Indicator.tsx +45 -6
  99. package/src/components/ui/indicator/index.ts +1 -1
  100. package/src/components/ui/spinner/Spinner.tsx +3 -2
  101. package/src/components/ui/toolbar-button/ToolbarButton.tsx +2 -1
  102. package/src/theme/ColorSystem.stories.tsx +323 -0
  103. package/src/theme/Colors.stories.tsx +69 -1
  104. package/src/theme/barrel.ts +1 -0
  105. package/src/theme/config.ts +16 -3
  106. package/src/theme/extracted-colors-dataviz.ts +21 -0
  107. package/src/theme/extracted-colors-ui.ts +20 -0
  108. package/src/theme/global.css +64 -52
  109. package/src/theme/gradients.test.ts +25 -0
  110. package/src/theme/gradients.ts +36 -0
  111. package/src/theme/tokens/primitives.test.ts +232 -0
  112. package/src/theme/tokens/primitives.ts +247 -0
  113. package/src/theme/tokens/semantic.ts +93 -81
  114. package/src/theme/workout-tokens.ts +22 -21
  115. package/src/utils/avatar-color.ts +3 -3
  116. package/tailwind.config.js +10 -3
@@ -1,8 +1,9 @@
1
- import React from 'react'
1
+ import { useEffect, useState } from 'react'
2
2
  import { Text, type TextProps } from 'react-native'
3
3
  import { cn } from '../../../utils/cn'
4
+ import { Typography, type TypographyVariant } from '../Typography'
4
5
 
5
- export type DateTimeFormat =
6
+ export type DateTimeFormat =
6
7
  | 'date' // 2024-01-15
7
8
  | 'time' // 14:30
8
9
  | 'datetime' // 2024-01-15 14:30
@@ -13,14 +14,24 @@ export type DateTimeFormat =
13
14
  | 'full' // Monday, January 15, 2024
14
15
 
15
16
  export interface DateTimeProps extends TextProps {
16
- /** Date value (timestamp in ms, Date object, or ISO string) */
17
- value: number | Date | string | null | undefined
17
+ /** Date value (timestamp in ms, Date object, or ISO string). Optional when `live`. */
18
+ value?: number | Date | string | null | undefined
18
19
  /** Display format */
19
20
  format?: DateTimeFormat
20
21
  /** Custom format string (overrides format) */
21
22
  customFormat?: string
22
23
  /** Whether to show in UTC */
23
24
  isUTC?: boolean
25
+ /** Force 12h (true) or 24h (false) for time/datetime formats; locale default when omitted. */
26
+ hour12?: boolean
27
+ /** Include seconds in time/datetime formats. */
28
+ seconds?: boolean
29
+ /** Render through Typography with this variant (e.g. 'mono'); plain inheriting Text when omitted. */
30
+ variant?: TypographyVariant
31
+ /** Track the current time and re-render on an interval (ignores `value`). For clocks / relative time. */
32
+ live?: boolean
33
+ /** Refresh interval in ms when `live` (default 1000). */
34
+ refreshMs?: number
24
35
  /** Fallback text when value is null/undefined */
25
36
  fallback?: string
26
37
  /** Text color */
@@ -42,10 +53,12 @@ const colorStyles = {
42
53
  function formatDate(
43
54
  value: number | Date | string,
44
55
  format: DateTimeFormat,
45
- isUTC: boolean
56
+ isUTC: boolean,
57
+ hour12?: boolean,
58
+ seconds?: boolean
46
59
  ): string {
47
60
  const date = value instanceof Date ? value : new Date(value)
48
-
61
+
49
62
  if (isNaN(date.getTime())) {
50
63
  return 'Invalid Date'
51
64
  }
@@ -57,7 +70,7 @@ function formatDate(
57
70
 
58
71
  // Use Intl.DateTimeFormat for locale-aware formatting
59
72
  const options: Intl.DateTimeFormatOptions = {}
60
-
73
+
61
74
  switch (format) {
62
75
  case 'date':
63
76
  options.year = 'numeric'
@@ -101,6 +114,14 @@ function formatDate(
101
114
  options.timeZone = 'UTC'
102
115
  }
103
116
 
117
+ if (hour12 !== undefined && (options.hour !== undefined || options.minute !== undefined)) {
118
+ options.hour12 = hour12
119
+ }
120
+
121
+ if (seconds && options.hour !== undefined) {
122
+ options.second = '2-digit'
123
+ }
124
+
104
125
  return new Intl.DateTimeFormat(undefined, options).format(date)
105
126
  }
106
127
 
@@ -121,7 +142,7 @@ function getRelativeTime(date: Date): string {
121
142
  // Use Intl.RelativeTimeFormat if available
122
143
  if (typeof Intl !== 'undefined' && Intl.RelativeTimeFormat) {
123
144
  const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })
124
-
145
+
125
146
  if (Math.abs(diffSecs) < 60) {
126
147
  return rtf.format(diffSecs, 'second')
127
148
  }
@@ -197,24 +218,43 @@ export function DateTime({
197
218
  format = 'datetime',
198
219
  customFormat,
199
220
  isUTC = false,
221
+ hour12,
222
+ seconds,
223
+ variant,
224
+ live = false,
225
+ refreshMs = 1000,
200
226
  fallback = '-',
201
227
  color = 'inherit',
202
228
  className,
203
229
  ...props
204
230
  }: DateTimeProps) {
205
- if (value === null || value === undefined) {
231
+ // When live, track "now" and re-render on an interval (the value prop is ignored).
232
+ const [now, setNow] = useState(() => Date.now())
233
+ useEffect(() => {
234
+ if (!live) return
235
+ const id = setInterval(() => setNow(Date.now()), refreshMs)
236
+ return () => clearInterval(id)
237
+ }, [live, refreshMs])
238
+
239
+ const effectiveValue = live ? now : value
240
+ const text =
241
+ effectiveValue === null || effectiveValue === undefined
242
+ ? fallback
243
+ : formatDate(effectiveValue, format, isUTC, hour12, seconds)
244
+
245
+ // Route through Typography (shared text substrate) when a variant is given;
246
+ // otherwise render a plain inheriting Text (backward-compatible default).
247
+ if (variant) {
206
248
  return (
207
- <Text className={cn(colorStyles[color], className)} {...props}>
208
- {fallback}
209
- </Text>
249
+ <Typography variant={variant} color={color} className={className} {...props}>
250
+ {text}
251
+ </Typography>
210
252
  )
211
253
  }
212
254
 
213
- const formattedDate = formatDate(value, format, isUTC)
214
-
215
255
  return (
216
256
  <Text className={cn(colorStyles[color], className)} {...props}>
217
- {formattedDate}
257
+ {text}
218
258
  </Text>
219
259
  )
220
260
  }
@@ -20,9 +20,9 @@ describe('Gauge', () => {
20
20
  const { rerender } = render(<Gauge value={30} />)
21
21
  expect(screen.getByTestId('gauge-value')).toHaveStyle({ color: '#D14343' })
22
22
  rerender(<Gauge value={70} />)
23
- expect(screen.getByTestId('gauge-value')).toHaveStyle({ color: '#FFB020' })
23
+ expect(screen.getByTestId('gauge-value')).toHaveStyle({ color: '#F9B415' })
24
24
  rerender(<Gauge value={90} />)
25
- expect(screen.getByTestId('gauge-value')).toHaveStyle({ color: '#14B8A6' })
25
+ expect(screen.getByTestId('gauge-value')).toHaveStyle({ color: '#2ED573' })
26
26
  })
27
27
 
28
28
  it('honors a single-color override over thresholds', () => {
@@ -1,5 +1,8 @@
1
1
  import { View, Text, type ViewProps } from 'react-native'
2
2
  import { cn } from '../../../utils/cn'
3
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
4
+
5
+ const sem = getSemanticColors('dark')
3
6
 
4
7
  export interface GaugeThreshold {
5
8
  /** Band start, in the gauge's value units. */
@@ -30,9 +33,9 @@ export interface GaugeProps extends Omit<ViewProps, 'children'> {
30
33
  className?: string
31
34
  }
32
35
 
33
- const STATUS_SUCCESS = '#14B8A6'
34
- const STATUS_WARNING = '#FFB020'
35
- const STATUS_ERROR = '#D14343'
36
+ const STATUS_SUCCESS = sem['status-success']
37
+ const STATUS_WARNING = sem['status-warning']
38
+ const STATUS_ERROR = sem['status-error']
36
39
  const TRACK = 'rgba(255,255,255,0.08)'
37
40
 
38
41
  /** Titan status-token bands for a 0–100 score. */
@@ -1,5 +1,6 @@
1
1
  import { View, Text, Pressable, type ViewProps } from 'react-native'
2
2
  import { cn } from '../../../utils/cn'
3
+ import { DATAVIZ_CATEGORICAL_PALETTE } from '../../../theme/extracted-colors-dataviz'
3
4
 
4
5
  export interface ScatterDatum {
5
6
  /** Stable identity — returned by onPress and used as the React key. */
@@ -45,11 +46,8 @@ export interface ScatterProps extends Omit<ViewProps, 'children'> {
45
46
  className?: string
46
47
  }
47
48
 
48
- /** Titan categorical fallback palette (data-1..data-8). */
49
- const PALETTE = [
50
- '#5B9BD5', '#14B8A6', '#F4A736', '#F83030',
51
- '#823CA0', '#D4A520', '#406D87', '#43C6B7',
52
- ]
49
+ /** Titan categorical fallback palette (see extracted-colors-dataviz). */
50
+ const PALETTE = DATAVIZ_CATEGORICAL_PALETTE
53
51
 
54
52
  const GRID_LINE = 'rgba(255,255,255,0.07)'
55
53
  const AXIS_LINE = 'rgba(255,255,255,0.18)'
@@ -1,6 +1,7 @@
1
1
  import React from 'react'
2
2
  import { View, Text, Pressable, type ViewProps } from 'react-native'
3
3
  import { cn } from '../../../utils/cn'
4
+ import { DATAVIZ_CATEGORICAL_PALETTE } from '../../../theme/extracted-colors-dataviz'
4
5
 
5
6
  export interface TreemapDatum {
6
7
  /** Stable identity — returned by onPress and used as the React key. */
@@ -35,11 +36,8 @@ export interface TreemapProps extends Omit<ViewProps, 'children'> {
35
36
  className?: string
36
37
  }
37
38
 
38
- /** Titan categorical fallback palette (data-1..data-8). */
39
- const PALETTE = [
40
- '#5B9BD5', '#14B8A6', '#F4A736', '#F83030',
41
- '#823CA0', '#D4A520', '#406D87', '#43C6B7',
42
- ]
39
+ /** Titan categorical fallback palette (see extracted-colors-dataviz). */
40
+ const PALETTE = DATAVIZ_CATEGORICAL_PALETTE
43
41
 
44
42
  interface Rect {
45
43
  x: number
@@ -1,13 +1,6 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import { View } from 'react-native'
3
- import {
4
- Typography,
5
- Heading,
6
- Paragraph,
7
- Caption,
8
- Label,
9
- Overline,
10
- } from './Typography'
3
+ import { Typography, Heading, Paragraph, Caption, Label, Overline } from './Typography'
11
4
 
12
5
  const meta: Meta<typeof Typography> = {
13
6
  title: 'Custom/Typography',
@@ -17,16 +10,36 @@ const meta: Meta<typeof Typography> = {
17
10
  variant: {
18
11
  control: 'select',
19
12
  options: [
20
- 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
21
- 'body1', 'body2', 'subtitle1', 'subtitle2',
22
- 'caption', 'overline', 'button',
13
+ 'h1',
14
+ 'h2',
15
+ 'h3',
16
+ 'h4',
17
+ 'h5',
18
+ 'h6',
19
+ 'body1',
20
+ 'body2',
21
+ 'subtitle1',
22
+ 'subtitle2',
23
+ 'caption',
24
+ 'overline',
25
+ 'button',
26
+ 'mono',
27
+ 'monoLabel',
23
28
  ],
24
29
  },
25
30
  color: {
26
31
  control: 'select',
27
32
  options: [
28
- 'primary', 'secondary', 'tertiary', 'disabled',
29
- 'inverse', 'success', 'error', 'warning', 'info', 'inherit',
33
+ 'primary',
34
+ 'secondary',
35
+ 'tertiary',
36
+ 'disabled',
37
+ 'inverse',
38
+ 'success',
39
+ 'error',
40
+ 'warning',
41
+ 'info',
42
+ 'inherit',
30
43
  ],
31
44
  },
32
45
  align: {
@@ -65,19 +78,15 @@ export const AllBody: Story = {
65
78
  render: () => (
66
79
  <View style={{ gap: 16, maxWidth: 400 }}>
67
80
  <Typography variant="body1">
68
- Body 1 - This is the default body text style. It&apos;s used for main content
69
- and paragraphs throughout the application.
81
+ Body 1 - This is the default body text style. It&apos;s used for main content and paragraphs
82
+ throughout the application.
70
83
  </Typography>
71
84
  <Typography variant="body2">
72
- Body 2 - This is a smaller body text style. It&apos;s useful for secondary
73
- content or when you need more compact text.
74
- </Typography>
75
- <Typography variant="subtitle1">
76
- Subtitle 1 - Semi-bold text for emphasis
77
- </Typography>
78
- <Typography variant="subtitle2">
79
- Subtitle 2 - Smaller semi-bold text
85
+ Body 2 - This is a smaller body text style. It&apos;s useful for secondary content or when
86
+ you need more compact text.
80
87
  </Typography>
88
+ <Typography variant="subtitle1">Subtitle 1 - Semi-bold text for emphasis</Typography>
89
+ <Typography variant="subtitle2">Subtitle 2 - Smaller semi-bold text</Typography>
81
90
  </View>
82
91
  ),
83
92
  }
@@ -92,6 +101,16 @@ export const AllUtility: Story = {
92
101
  ),
93
102
  }
94
103
 
104
+ /** Monospace technical readouts (clocks, ids, metrics) and their all-caps label form. */
105
+ export const Mono: Story = {
106
+ render: () => (
107
+ <View style={{ gap: 16 }}>
108
+ <Typography variant="mono">mono — 16:12 · Voltra-A3F2 · 0.42 m/s</Typography>
109
+ <Typography variant="monoLabel">monoLabel — live status label</Typography>
110
+ </View>
111
+ ),
112
+ }
113
+
95
114
  export const AllColors: Story = {
96
115
  render: () => (
97
116
  <View style={{ gap: 8 }}>
@@ -121,12 +140,11 @@ export const ParagraphComponent: Story = {
121
140
  render: () => (
122
141
  <View style={{ gap: 16, maxWidth: 400 }}>
123
142
  <Paragraph>
124
- This is a regular paragraph using the default body1 variant.
125
- It&apos;s great for main content.
143
+ This is a regular paragraph using the default body1 variant. It&apos;s great for main
144
+ content.
126
145
  </Paragraph>
127
146
  <Paragraph small>
128
- This is a small paragraph using the body2 variant.
129
- It&apos;s useful for secondary content.
147
+ This is a small paragraph using the body2 variant. It&apos;s useful for secondary content.
130
148
  </Paragraph>
131
149
  </View>
132
150
  ),
@@ -77,6 +77,18 @@ describe('Typography', () => {
77
77
  })
78
78
  })
79
79
 
80
+ describe('mono variants', () => {
81
+ it('renders the mono variant', () => {
82
+ render(<Typography variant="mono">16:12</Typography>)
83
+ expect(screen.getByText('16:12')).toBeInTheDocument()
84
+ })
85
+
86
+ it('renders the monoLabel variant', () => {
87
+ render(<Typography variant="monoLabel">live</Typography>)
88
+ expect(screen.getByText('live')).toBeInTheDocument()
89
+ })
90
+ })
91
+
80
92
  describe('accessibility', () => {
81
93
  it('headings have no accessibility violations', async () => {
82
94
  const { container } = render(
@@ -91,9 +103,7 @@ describe('Typography', () => {
91
103
  })
92
104
 
93
105
  it('body text has no accessibility violations', async () => {
94
- const { container } = render(
95
- <Typography variant="body1">Body text content</Typography>
96
- )
106
+ const { container } = render(<Typography variant="body1">Body text content</Typography>)
97
107
 
98
108
  const results = await axe(container)
99
109
  expect(results).toHaveNoViolations()
@@ -16,6 +16,8 @@ export type TypographyVariant =
16
16
  | 'caption'
17
17
  | 'overline'
18
18
  | 'button'
19
+ | 'mono'
20
+ | 'monoLabel'
19
21
 
20
22
  export type TypographyColor =
21
23
  | 'primary'
@@ -70,6 +72,9 @@ const variantStyles: Record<TypographyVariant, string> = {
70
72
  caption: 'font-body text-xs font-normal leading-loose',
71
73
  overline: 'font-body text-xs font-semibold uppercase tracking-widest leading-relaxed',
72
74
  button: 'font-sans text-sm font-semibold leading-relaxed',
75
+ // Monospace technical readouts (clocks, ids, metrics) and their all-caps label form.
76
+ mono: 'font-mono text-xs font-normal leading-normal',
77
+ monoLabel: 'font-mono text-xs font-bold uppercase tracking-wide leading-normal',
73
78
  }
74
79
 
75
80
  const colorStyles: Record<TypographyColor, string> = {
@@ -7,8 +7,9 @@ import { RestTimer } from './RestTimer'
7
7
  import { SupersetWrapper } from './SupersetWrapper'
8
8
  import { type SetRowProps } from './SetRow'
9
9
  import { resolveColor } from '../../../theme/resolve-color'
10
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
10
11
 
11
- const BRAND_PRIMARY = '#FF7900'
12
+ const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
12
13
 
13
14
  /** Where an exercise sits in the during-workout flow. */
14
15
  export type ActiveExerciseStatus = 'completed' | 'active' | 'upcoming'
@@ -4,6 +4,7 @@ import { View, Text, Pressable, Animated, Easing, type ViewProps } from 'react-n
4
4
  import BodyHighlighter, { type ExtendedBodyPart, type Slug } from 'react-native-body-highlighter'
5
5
  import { cn } from '../../../utils/cn'
6
6
  import { resolveColor } from '../../../theme/resolve-color'
7
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
7
8
  import {
8
9
  MuscleGroup,
9
10
  SimpleMuscleGroup,
@@ -25,6 +26,8 @@ import {
25
26
  const Body = ((BodyHighlighter as unknown as { default?: typeof BodyHighlighter }).default ??
26
27
  BodyHighlighter) as typeof BodyHighlighter
27
28
 
29
+ const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
30
+
28
31
  const OUTLINE_FILL = 'rgba(255,255,255,0.08)'
29
32
  const OUTLINE_BORDER = 'rgba(255,255,255,0.12)'
30
33
  const BODY_SCALE = 0.8 // 200x400 intrinsic -> ~160x320 px
@@ -275,7 +278,7 @@ function ViewToggle({ view, onViewChange }: ViewToggleProps) {
275
278
  borderRadius: 9999,
276
279
  backgroundColor: active ? 'rgba(255,121,0,0.16)' : 'transparent',
277
280
  borderWidth: 1,
278
- borderColor: active ? '#FF7900' : resolveColor('border-strong'),
281
+ borderColor: active ? BRAND_PRIMARY : resolveColor('border-strong'),
279
282
  }}
280
283
  testID={`body-map-toggle-${side}`}
281
284
  >
@@ -284,7 +287,7 @@ function ViewToggle({ view, onViewChange }: ViewToggleProps) {
284
287
  fontSize: 12,
285
288
  fontFamily: 'Inter, sans-serif',
286
289
  fontWeight: active ? '700' : '500',
287
- color: active ? '#FF7900' : resolveColor('text-secondary'),
290
+ color: active ? BRAND_PRIMARY : resolveColor('text-secondary'),
288
291
  textTransform: 'capitalize',
289
292
  }}
290
293
  >
@@ -321,7 +324,7 @@ function MuscleButton({ entry, highlighted, onMusclePress }: MuscleButtonProps)
321
324
  paddingVertical: 3,
322
325
  borderRadius: 9999,
323
326
  borderWidth: 1,
324
- borderColor: highlighted ? '#FF7900' : resolveColor('border-default'),
327
+ borderColor: highlighted ? BRAND_PRIMARY : resolveColor('border-default'),
325
328
  }}
326
329
  testID={`body-map-muscle-${entry.key}`}
327
330
  >
@@ -18,11 +18,14 @@ import {
18
18
  type VolumeLandmarks,
19
19
  type VolumeStatus,
20
20
  } from './muscleTaxonomy'
21
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
21
22
 
22
- const BRAND_PRIMARY = '#FF7900'
23
+ const t = getSemanticColors('dark')
23
24
 
24
- /** Volume track gradient: status-info (blue) -> result-improve (teal) -> status-error (red). */
25
- const VOLUME_GRADIENT = 'linear-gradient(90deg, #2196F3 0%, #14B8A6 50%, #D14343 100%)'
25
+ const BRAND_PRIMARY = t['brand-primary']
26
+
27
+ /** Volume track gradient: status-info -> status-success -> status-error. */
28
+ const VOLUME_GRADIENT = `linear-gradient(90deg, ${t['status-info']} 0%, ${t['status-success']} 50%, ${t['status-error']} 100%)`
26
29
 
27
30
  /** Sheet slides up from this offset (px) and the backdrop fades to this opacity. */
28
31
  const SLIDE_OFFSET = 400
@@ -93,8 +93,8 @@ describe('CapacityBandChart', () => {
93
93
  it('colors dots by status relative to the band', () => {
94
94
  render(<CapacityBandChart {...baseProps} />)
95
95
  const dots = screen.getAllByTestId('capacity-band-chart-dot')
96
- expect(dots[0]).toHaveStyle({ backgroundColor: '#14B8A6' }) // within
97
- expect(dots[1]).toHaveStyle({ backgroundColor: '#FFB020' }) // above
96
+ expect(dots[0]).toHaveStyle({ backgroundColor: '#2ED573' }) // within
97
+ expect(dots[1]).toHaveStyle({ backgroundColor: '#F9B415' }) // above
98
98
  expect(dots[2]).toHaveStyle({ backgroundColor: '#2196F3' }) // below
99
99
  })
100
100
 
@@ -1,16 +1,19 @@
1
1
  // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
2
  import { useEffect, useState, useMemo } from 'react'
3
3
  import { View, Text, Pressable, Animated, Easing, type ViewProps } from 'react-native'
4
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
4
5
 
5
- const STATUS_SUCCESS = '#14B8A6'
6
- const STATUS_WARNING = '#FFB020'
7
- const STATUS_INFO = '#2196F3'
6
+ const t = getSemanticColors('dark')
7
+
8
+ const STATUS_SUCCESS = t['status-success']
9
+ const STATUS_WARNING = t['status-warning']
10
+ const STATUS_INFO = t['status-info']
8
11
  /** Dot outline: near-white ring so load dots read on the band fill and any
9
12
  * surface (matches the MesoStatusCard gauge-marker convention). */
10
13
  const DOT_BORDER = '#F3F4F6'
11
- const BAND_FILL = 'rgba(20,184,166,0.1)'
12
- const BAND_EDGE = 'rgba(20,184,166,0.45)'
13
- const PROJECTION_FILL = 'rgba(20,184,166,0.05)'
14
+ const BAND_FILL = 'rgba(46,213,115,0.1)'
15
+ const BAND_EDGE = 'rgba(46,213,115,0.45)'
16
+ const PROJECTION_FILL = 'rgba(46,213,115,0.05)'
14
17
 
15
18
  const PADDING_LEFT = 28
16
19
  const PADDING_RIGHT = 10
@@ -72,7 +72,7 @@ describe('DeviationBar', () => {
72
72
  const track = bar.firstElementChild as HTMLElement
73
73
  expect(track).toHaveStyle({
74
74
  backgroundImage:
75
- 'linear-gradient(90deg, rgba(20,184,166,0.25) 0%, rgba(107,114,128,0.15) 50%, rgba(255,176,32,0.25) 100%)',
75
+ 'linear-gradient(90deg, rgba(46,213,115,0.25) 0%, rgba(107,114,128,0.15) 50%, rgba(249,180,21,0.25) 100%)',
76
76
  })
77
77
  })
78
78
 
@@ -1,5 +1,8 @@
1
1
  // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
2
  import { View, type ViewProps, type ViewStyle } from 'react-native'
3
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
4
+
5
+ const t = getSemanticColors('dark')
3
6
 
4
7
  export interface DeviationBarProps extends ViewProps {
5
8
  deviation: number
@@ -9,10 +12,10 @@ export interface DeviationBarProps extends ViewProps {
9
12
 
10
13
  function getDotColor(deviation: number): string {
11
14
  const abs = Math.abs(deviation)
12
- if (deviation < -0.3) return '#14B8A6'
15
+ if (deviation < -0.3) return t['status-success']
13
16
  if (abs <= 0.3) return '#6B7280'
14
- if (abs <= 0.7) return '#FFB020'
15
- return '#D14343'
17
+ if (abs <= 0.7) return t['status-warning']
18
+ return t['status-error']
16
19
  }
17
20
 
18
21
  function getDeviationDescription(deviation: number): string {
@@ -55,7 +58,7 @@ export function DeviationBar({
55
58
  borderRadius: 3,
56
59
  // react-native-web renders backgroundImage at runtime; not in RN ViewStyle types
57
60
  backgroundImage:
58
- 'linear-gradient(90deg, rgba(20,184,166,0.25) 0%, rgba(107,114,128,0.15) 50%, rgba(255,176,32,0.25) 100%)',
61
+ 'linear-gradient(90deg, rgba(46,213,115,0.25) 0%, rgba(107,114,128,0.15) 50%, rgba(249,180,21,0.25) 100%)',
59
62
  } as ViewStyle}
60
63
  />
61
64
  <View
@@ -22,9 +22,10 @@ import {
22
22
  type VelocityZoneBandProp,
23
23
  } from './VelocityStrip'
24
24
  import { resolveColor } from '../../../theme/resolve-color'
25
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
25
26
  import { cn } from '../../../utils/cn'
26
27
 
27
- const BRAND_PRIMARY = '#FF7900'
28
+ const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
28
29
 
29
30
  /** Inner plot width: page width 390 − 16 page padding − 12 card padding on each side. */
30
31
  const CHART_WIDTH = 326
@@ -2,6 +2,7 @@
2
2
  import React from 'react'
3
3
  import { View, Text, Pressable, TextInput } from 'react-native'
4
4
  import { resolveColor } from '../../../theme/resolve-color'
5
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
5
6
 
6
7
  export interface InputBarProps {
7
8
  exerciseName: string
@@ -17,7 +18,7 @@ export interface InputBarProps {
17
18
  visible: boolean
18
19
  }
19
20
 
20
- const BRAND_PRIMARY = '#FF7900'
21
+ const BRAND_PRIMARY = getSemanticColors('dark')['brand-primary']
21
22
  const INPUT_CLASSNAME = 'bg-surface-raised border-border-strong text-text-primary'
22
23
 
23
24
  const inputStyle = {
@@ -44,19 +44,19 @@ describe('IntensityBar', () => {
44
44
  })
45
45
 
46
46
  describe('zone colors graded by true percentage', () => {
47
- it('renders teal below the target ramp (< 75%)', () => {
47
+ it('renders green below the target ramp (< 75%)', () => {
48
48
  render(<IntensityBar level={0.2} />)
49
- expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#14B8A6' })
49
+ expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#2ED573' })
50
50
  })
51
51
 
52
- it('stays teal just under the amber boundary', () => {
52
+ it('stays green just under the amber boundary', () => {
53
53
  render(<IntensityBar level={0.74} />)
54
- expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#14B8A6' })
54
+ expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#2ED573' })
55
55
  })
56
56
 
57
57
  it('renders amber approaching the target (75-99%)', () => {
58
58
  render(<IntensityBar level={0.75} />)
59
- expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#FFB020' })
59
+ expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#F9B415' })
60
60
  })
61
61
 
62
62
  it('renders the brand target color exactly at 100%', () => {
@@ -71,12 +71,12 @@ describe('IntensityBar', () => {
71
71
 
72
72
  it('renders over-2 deep red at 120%', () => {
73
73
  render(<IntensityBar level={1.2} />)
74
- expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#A62626' })
74
+ expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#A4221C' })
75
75
  })
76
76
 
77
77
  it('renders over-3 darkest red at 130%', () => {
78
78
  render(<IntensityBar level={1.3} />)
79
- expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#7A1C1C' })
79
+ expect(screen.getByTestId('intensity-fill')).toHaveStyle({ backgroundColor: '#7E1002' })
80
80
  })
81
81
  })
82
82
 
@@ -88,7 +88,7 @@ describe('IntensityBar', () => {
88
88
 
89
89
  it('colors the bulge to match the over-target zone', () => {
90
90
  render(<IntensityBar level={1.2} />)
91
- expect(screen.getByTestId('intensity-bulge')).toHaveStyle({ backgroundColor: '#A62626' })
91
+ expect(screen.getByTestId('intensity-bulge')).toHaveStyle({ backgroundColor: '#A4221C' })
92
92
  })
93
93
 
94
94
  it('does not render a bulge at or below target', () => {
@@ -8,6 +8,10 @@ import {
8
8
  type ViewStyle,
9
9
  type DimensionValue,
10
10
  } from 'react-native'
11
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
12
+ import { WORKOUT_TOKENS } from '../../../theme/workout-tokens'
13
+
14
+ const t = getSemanticColors('dark')
11
15
 
12
16
  export type IntensityBarOrientation = 'vertical' | 'horizontal'
13
17
 
@@ -34,12 +38,12 @@ const AT_TARGET_GLOW = '0 0 5px 1px rgba(33, 150, 243, 0.35), 0 0 10px 3px rgba(
34
38
 
35
39
  // Zone colors graded by TRUE percentage (level * 100).
36
40
  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
41
+ building: t['status-success'], // below target ramp-up
42
+ approaching: t['status-warning'], // nearing target
43
+ target: t['brand-primary'], // exactly at target
44
+ over1: t['status-error'], // mild over-target
45
+ over2: WORKOUT_TOKENS.intensity.over2, // moderate over-target
46
+ over3: WORKOUT_TOKENS.intensity.over3, // severe over-target
43
47
  } as const
44
48
 
45
49
  type OverTier = 0 | 1 | 2 | 3