@titan-design/react-ui 0.2.6 → 0.2.7

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 (157) hide show
  1. package/dist/chunk-3VLOBS6O.mjs +846 -0
  2. package/dist/chunk-3VLOBS6O.mjs.map +1 -0
  3. package/dist/{chunk-UXVRPOME.mjs → chunk-5SSKGS6E.mjs} +3 -783
  4. package/dist/chunk-5SSKGS6E.mjs.map +1 -0
  5. package/dist/index.d.mts +1005 -4
  6. package/dist/index.d.ts +1005 -4
  7. package/dist/index.js +6043 -213
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +5901 -177
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/theme/index.d.mts +57 -1
  12. package/dist/theme/index.d.ts +57 -1
  13. package/dist/theme/index.js +63 -0
  14. package/dist/theme/index.js.map +1 -1
  15. package/dist/theme/index.mjs +2 -1
  16. package/dist/theme/tokens-css.d.mts +18 -0
  17. package/dist/theme/tokens-css.d.ts +18 -0
  18. package/dist/theme/tokens-css.js +289 -0
  19. package/dist/theme/tokens-css.js.map +1 -0
  20. package/dist/theme/tokens-css.mjs +24 -0
  21. package/dist/theme/tokens-css.mjs.map +1 -0
  22. package/package.json +16 -5
  23. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  24. package/src/components/custom/Gauge/Gauge.stories.tsx +45 -0
  25. package/src/components/custom/Gauge/Gauge.test.tsx +71 -0
  26. package/src/components/custom/Gauge/Gauge.tsx +167 -0
  27. package/src/components/custom/Gauge/index.ts +2 -0
  28. package/src/components/custom/Scatter/Scatter.stories.tsx +85 -0
  29. package/src/components/custom/Scatter/Scatter.test.tsx +96 -0
  30. package/src/components/custom/Scatter/Scatter.tsx +260 -0
  31. package/src/components/custom/Scatter/index.ts +2 -0
  32. package/src/components/custom/Sidebar/Sidebar.tsx +1 -1
  33. package/src/components/custom/Treemap/Treemap.stories.tsx +60 -0
  34. package/src/components/custom/Treemap/Treemap.test.tsx +67 -0
  35. package/src/components/custom/Treemap/Treemap.tsx +192 -0
  36. package/src/components/custom/Treemap/index.ts +2 -0
  37. package/src/components/custom/Workout/BaseBadge.stories.tsx +75 -0
  38. package/src/components/custom/Workout/BaseBadge.test.tsx +102 -0
  39. package/src/components/custom/Workout/BaseBadge.tsx +113 -0
  40. package/src/components/custom/Workout/BodyMap.stories.tsx +121 -0
  41. package/src/components/custom/Workout/BodyMap.test.tsx +124 -0
  42. package/src/components/custom/Workout/BodyMap.tsx +348 -0
  43. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +171 -0
  44. package/src/components/custom/Workout/BodyMapDetailPanel.test.tsx +225 -0
  45. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +478 -0
  46. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +125 -0
  47. package/src/components/custom/Workout/CapacityBandChart.test.tsx +193 -0
  48. package/src/components/custom/Workout/CapacityBandChart.tsx +493 -0
  49. package/src/components/custom/Workout/DeviationBar.stories.tsx +58 -0
  50. package/src/components/custom/Workout/DeviationBar.test.tsx +86 -0
  51. package/src/components/custom/Workout/DeviationBar.tsx +78 -0
  52. package/src/components/custom/Workout/ExerciseCard.stories.tsx +186 -0
  53. package/src/components/custom/Workout/ExerciseCard.test.tsx +319 -0
  54. package/src/components/custom/Workout/ExerciseCard.tsx +429 -0
  55. package/src/components/custom/Workout/InputBar.stories.tsx +75 -0
  56. package/src/components/custom/Workout/InputBar.test.tsx +116 -0
  57. package/src/components/custom/Workout/InputBar.tsx +163 -0
  58. package/src/components/custom/Workout/IntensityBar.stories.tsx +60 -0
  59. package/src/components/custom/Workout/IntensityBar.test.tsx +113 -0
  60. package/src/components/custom/Workout/IntensityBar.tsx +166 -0
  61. package/src/components/custom/Workout/MesoCard.stories.tsx +152 -0
  62. package/src/components/custom/Workout/MesoCard.test.tsx +170 -0
  63. package/src/components/custom/Workout/MesoCard.tsx +235 -0
  64. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +85 -0
  65. package/src/components/custom/Workout/MesoProgressBar.test.tsx +141 -0
  66. package/src/components/custom/Workout/MesoProgressBar.tsx +161 -0
  67. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +126 -0
  68. package/src/components/custom/Workout/MesoStatusCard.test.tsx +209 -0
  69. package/src/components/custom/Workout/MesoStatusCard.tsx +456 -0
  70. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +118 -0
  71. package/src/components/custom/Workout/MuscleGroupChip.test.tsx +91 -0
  72. package/src/components/custom/Workout/MuscleGroupChip.tsx +92 -0
  73. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +94 -0
  74. package/src/components/custom/Workout/PlaceholderStrip.test.tsx +83 -0
  75. package/src/components/custom/Workout/PlaceholderStrip.tsx +91 -0
  76. package/src/components/custom/Workout/PrBadge.stories.tsx +105 -0
  77. package/src/components/custom/Workout/PrBadge.test.tsx +111 -0
  78. package/src/components/custom/Workout/PrBadge.tsx +103 -0
  79. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +116 -0
  80. package/src/components/custom/Workout/PrHistoryModal.test.tsx +161 -0
  81. package/src/components/custom/Workout/PrHistoryModal.tsx +244 -0
  82. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +126 -0
  83. package/src/components/custom/Workout/ReadinessCheck.test.tsx +181 -0
  84. package/src/components/custom/Workout/ReadinessCheck.tsx +266 -0
  85. package/src/components/custom/Workout/RestTimer.tsx +8 -6
  86. package/src/components/custom/Workout/SetRow.stories.tsx +165 -0
  87. package/src/components/custom/Workout/SetRow.test.tsx +222 -0
  88. package/src/components/custom/Workout/SetRow.tsx +253 -0
  89. package/src/components/custom/Workout/Sparkline.stories.tsx +125 -0
  90. package/src/components/custom/Workout/Sparkline.test.tsx +113 -0
  91. package/src/components/custom/Workout/Sparkline.tsx +188 -0
  92. package/src/components/custom/Workout/StatusDot.stories.tsx +150 -0
  93. package/src/components/custom/Workout/StatusDot.test.tsx +165 -0
  94. package/src/components/custom/Workout/StatusDot.tsx +159 -0
  95. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +130 -0
  96. package/src/components/custom/Workout/StrengthTrendChart.test.tsx +217 -0
  97. package/src/components/custom/Workout/StrengthTrendChart.tsx +628 -0
  98. package/src/components/custom/Workout/SupersetWrapper.tsx +1 -2
  99. package/src/components/custom/Workout/TempoDisplay.stories.tsx +66 -0
  100. package/src/components/custom/Workout/TempoDisplay.test.tsx +90 -0
  101. package/src/components/custom/Workout/TempoDisplay.tsx +207 -0
  102. package/src/components/custom/Workout/VelocityStrip.stories.tsx +125 -0
  103. package/src/components/custom/Workout/VelocityStrip.test.tsx +234 -0
  104. package/src/components/custom/Workout/VelocityStrip.tsx +287 -0
  105. package/src/components/custom/Workout/WeekRow.stories.tsx +121 -0
  106. package/src/components/custom/Workout/WeekRow.test.tsx +117 -0
  107. package/src/components/custom/Workout/WeekRow.tsx +140 -0
  108. package/src/components/custom/Workout/WeightBadge.stories.tsx +87 -0
  109. package/src/components/custom/Workout/WeightBadge.test.tsx +164 -0
  110. package/src/components/custom/Workout/WeightBadge.tsx +113 -0
  111. package/src/components/custom/Workout/WorkoutCard.stories.tsx +135 -0
  112. package/src/components/custom/Workout/WorkoutCard.test.tsx +172 -0
  113. package/src/components/custom/Workout/WorkoutCard.tsx +226 -0
  114. package/src/components/custom/Workout/WorkoutPill.stories.tsx +63 -0
  115. package/src/components/custom/Workout/WorkoutPill.test.tsx +153 -0
  116. package/src/components/custom/Workout/WorkoutPill.tsx +177 -0
  117. package/src/components/custom/Workout/index.ts +113 -0
  118. package/src/components/custom/Workout/muscleTaxonomy.ts +252 -0
  119. package/src/components/custom/index.ts +3 -0
  120. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  121. package/src/components/custom/stepper/Stepper.tsx +2 -2
  122. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  123. package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
  124. package/src/components/ui/card/Card.tsx +1 -1
  125. package/src/components/ui/checkbox/Checkbox.tsx +1 -1
  126. package/src/components/ui/collapse/Collapse.tsx +1 -1
  127. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  128. package/src/components/ui/drawer/Drawer.tsx +3 -3
  129. package/src/components/ui/form-field/FormField.tsx +1 -1
  130. package/src/components/ui/help-tip/HelpTip.tsx +1 -1
  131. package/src/components/ui/menu/Menu.tsx +2 -2
  132. package/src/components/ui/pill/Pill.tsx +1 -1
  133. package/src/components/ui/popover/Popover.stories.tsx +2 -2
  134. package/src/components/ui/popover/Popover.tsx +1 -1
  135. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  136. package/src/components/ui/section/Section.stories.tsx +4 -4
  137. package/src/components/ui/select/Select.tsx +1 -1
  138. package/src/components/ui/stack/Stack.stories.tsx +4 -4
  139. package/src/components/ui/tabs/Tabs.tsx +2 -2
  140. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
  141. package/src/components/ui/toolbar-button/ToolbarButton.tsx +1 -1
  142. package/src/examples/react-hook-form/ControlledFields.tsx +171 -0
  143. package/src/examples/react-hook-form/ReactHookForm.stories.tsx +51 -0
  144. package/src/examples/react-hook-form/ReactHookForm.test.tsx +61 -0
  145. package/src/examples/react-hook-form/RhfContactForm.tsx +99 -0
  146. package/src/stories/PresetShowcase.stories.tsx +15 -15
  147. package/src/theme/index.ts +2 -1
  148. package/src/theme/manifest/css-property-manifest.example.json +79 -0
  149. package/src/theme/manifest/css-property-manifest.schema.json +93 -0
  150. package/src/theme/manifest/css-property-manifest.test.ts +81 -0
  151. package/src/theme/manifest/css-property-manifest.types.ts +49 -0
  152. package/src/theme/manifest/css-property-manifest.validate.ts +84 -0
  153. package/src/theme/manifest/index.ts +11 -0
  154. package/src/theme/tokens-css.test.ts +28 -0
  155. package/src/theme/tokens-css.ts +34 -0
  156. package/src/theme/workout-tokens.ts +61 -0
  157. package/dist/chunk-UXVRPOME.mjs.map +0 -1
@@ -0,0 +1,130 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+ import { View, Text } from 'react-native'
4
+ import { StrengthTrendChart } from './StrengthTrendChart'
5
+ import type { StrengthTrendDataPoint } from './StrengthTrendChart'
6
+
7
+ const actual: StrengthTrendDataPoint[] = [
8
+ { date: '2026-01-06', e1rm: 215, sessionLabel: 'Jan 6' },
9
+ { date: '2026-01-20', e1rm: 222, sessionLabel: 'Jan 20' },
10
+ { date: '2026-02-03', e1rm: 228, isPR: true, sessionLabel: 'Feb 3' },
11
+ { date: '2026-02-17', e1rm: 226, sessionLabel: 'Feb 17' },
12
+ { date: '2026-03-03', e1rm: 235, sessionLabel: 'Mar 3' },
13
+ { date: '2026-03-17', e1rm: 244, isPR: true, sessionLabel: 'Mar 17' },
14
+ ]
15
+
16
+ const projection: StrengthTrendDataPoint[] = [
17
+ { date: '2026-03-17', e1rm: 244 },
18
+ { date: '2026-03-31', e1rm: 252 },
19
+ { date: '2026-04-14', e1rm: 261 },
20
+ ]
21
+
22
+ const mesoBoundaries = [
23
+ { date: '2026-02-03', label: 'Intensify' },
24
+ { date: '2026-03-17', label: 'Peak' },
25
+ ]
26
+
27
+ const decline: StrengthTrendDataPoint[] = [
28
+ { date: '2026-03-03', e1rm: 248 },
29
+ { date: '2026-03-10', e1rm: 244 },
30
+ { date: '2026-03-17', e1rm: 239 },
31
+ { date: '2026-03-24', e1rm: 235 },
32
+ ]
33
+
34
+ const meta: Meta<typeof StrengthTrendChart> = {
35
+ title: 'Custom/Workout/StrengthTrendChart',
36
+ component: StrengthTrendChart,
37
+ tags: ['autodocs'],
38
+ argTypes: {
39
+ width: { control: { type: 'range', min: 160, max: 480, step: 10 }, description: 'Plot width in px' },
40
+ height: { control: { type: 'range', min: 100, max: 220, step: 10 }, description: 'Plot height in px (120-160 for compact)' },
41
+ unit: { control: 'inline-radio', options: ['lbs', 'kg'], description: 'Display unit' },
42
+ animateOnMount: { control: 'boolean', description: 'Left-to-right draw animation on mount' },
43
+ },
44
+ }
45
+
46
+ export default meta
47
+ type Story = StoryObj<typeof StrengthTrendChart>
48
+
49
+ export const Default: Story = {
50
+ args: {
51
+ data: actual,
52
+ projection,
53
+ mesoBoundaries,
54
+ width: 340,
55
+ height: 150,
56
+ unit: 'lbs',
57
+ animateOnMount: false,
58
+ },
59
+ }
60
+
61
+ export const Compact: Story = {
62
+ args: {
63
+ ...Default.args,
64
+ width: 240,
65
+ height: 120,
66
+ },
67
+ }
68
+
69
+ export const WithoutProjection: Story = {
70
+ args: {
71
+ ...Default.args,
72
+ projection: undefined,
73
+ mesoBoundaries: undefined,
74
+ },
75
+ }
76
+
77
+ export const Declining: Story = {
78
+ args: {
79
+ data: decline,
80
+ width: 340,
81
+ height: 150,
82
+ unit: 'lbs',
83
+ animateOnMount: false,
84
+ },
85
+ }
86
+
87
+ export const SinglePoint: Story = {
88
+ args: {
89
+ data: [{ date: '2026-03-17', e1rm: 244, isPR: true, sessionLabel: 'Mar 17' }],
90
+ width: 340,
91
+ height: 150,
92
+ unit: 'kg',
93
+ animateOnMount: false,
94
+ },
95
+ }
96
+
97
+ export const Empty: Story = {
98
+ args: {
99
+ data: [],
100
+ width: 340,
101
+ height: 150,
102
+ unit: 'lbs',
103
+ },
104
+ }
105
+
106
+ function InteractiveChart() {
107
+ const [selected, setSelected] = useState<StrengthTrendDataPoint | null>(null)
108
+ return (
109
+ <View style={{ maxWidth: 380, padding: 16 }}>
110
+ <StrengthTrendChart
111
+ data={actual}
112
+ projection={projection}
113
+ mesoBoundaries={mesoBoundaries}
114
+ width={340}
115
+ height={150}
116
+ unit="lbs"
117
+ onPointPress={setSelected}
118
+ />
119
+ <Text style={{ marginTop: 12, fontSize: 12, color: '#9CA3AF' }}>
120
+ {selected
121
+ ? `Tapped ${selected.sessionLabel ?? selected.date}: ${selected.e1rm} lbs`
122
+ : 'Tap a point on the chart.'}
123
+ </Text>
124
+ </View>
125
+ )
126
+ }
127
+
128
+ export const Interactive: Story = {
129
+ render: () => <InteractiveChart />,
130
+ }
@@ -0,0 +1,217 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { StrengthTrendChart } from './StrengthTrendChart'
5
+ import type { StrengthTrendDataPoint } from './StrengthTrendChart'
6
+
7
+ const data: StrengthTrendDataPoint[] = [
8
+ { date: '2026-01-06', e1rm: 215, sessionLabel: 'Jan 6' },
9
+ { date: '2026-02-03', e1rm: 228, isPR: true, sessionLabel: 'Feb 3' },
10
+ { date: '2026-03-17', e1rm: 244, isPR: true, sessionLabel: 'Mar 17' },
11
+ ]
12
+
13
+ const projection: StrengthTrendDataPoint[] = [
14
+ { date: '2026-03-17', e1rm: 244 },
15
+ { date: '2026-04-14', e1rm: 261 },
16
+ ]
17
+
18
+ const mesoBoundaries = [
19
+ { date: '2026-02-03', label: 'Intensify' },
20
+ { date: '2026-03-17', label: 'Peak' },
21
+ ]
22
+
23
+ const baseProps = {
24
+ data,
25
+ width: 340,
26
+ height: 150,
27
+ unit: 'lbs' as const,
28
+ animateOnMount: false,
29
+ }
30
+
31
+ describe('StrengthTrendChart', () => {
32
+ describe('rendering', () => {
33
+ it('renders the chart container and labelled canvas', () => {
34
+ render(<StrengthTrendChart {...baseProps} />)
35
+ expect(screen.getByTestId('strength-trend-chart')).toBeInTheDocument()
36
+ expect(screen.getByTestId('strength-trend-chart-canvas')).toBeInTheDocument()
37
+ })
38
+
39
+ it('renders a pressable point per actual data point', () => {
40
+ render(<StrengthTrendChart {...baseProps} />)
41
+ expect(screen.getAllByTestId('strength-trend-chart-point')).toHaveLength(3)
42
+ })
43
+
44
+ it('renders the actual line as connecting segments', () => {
45
+ render(<StrengthTrendChart {...baseProps} />)
46
+ // Two segments connect three points.
47
+ expect(
48
+ screen.getAllByTestId('strength-trend-chart-actual-segment'),
49
+ ).toHaveLength(2)
50
+ })
51
+
52
+ it('renders subtle horizontal gridlines', () => {
53
+ render(<StrengthTrendChart {...baseProps} />)
54
+ const grid = screen.getAllByTestId('strength-trend-chart-gridline')
55
+ expect(grid.length).toBeGreaterThanOrEqual(2)
56
+ expect(grid.length).toBeLessThanOrEqual(3)
57
+ })
58
+
59
+ it('renders a star marker for each PR point', () => {
60
+ render(<StrengthTrendChart {...baseProps} />)
61
+ expect(screen.getAllByTestId('strength-trend-chart-pr-star')).toHaveLength(2)
62
+ })
63
+
64
+ it('renders an Actual / Projected / PR legend', () => {
65
+ render(<StrengthTrendChart {...baseProps} />)
66
+ const legend = screen.getByTestId('strength-trend-chart-legend')
67
+ expect(legend).toHaveTextContent('Actual')
68
+ expect(legend).toHaveTextContent('Projected')
69
+ expect(legend).toHaveTextContent('PR')
70
+ })
71
+ })
72
+
73
+ describe('projection and meso boundaries', () => {
74
+ it('renders dashed projection segments when projection is provided', () => {
75
+ render(<StrengthTrendChart {...baseProps} projection={projection} />)
76
+ expect(
77
+ screen.getAllByTestId('strength-trend-chart-projection-segment'),
78
+ ).toHaveLength(1)
79
+ })
80
+
81
+ it('omits projection segments when none provided', () => {
82
+ render(<StrengthTrendChart {...baseProps} />)
83
+ expect(
84
+ screen.queryByTestId('strength-trend-chart-projection-segment'),
85
+ ).not.toBeInTheDocument()
86
+ })
87
+
88
+ it('renders a boundary marker and axis label per meso boundary', () => {
89
+ render(<StrengthTrendChart {...baseProps} mesoBoundaries={mesoBoundaries} />)
90
+ expect(
91
+ screen.getAllByTestId('strength-trend-chart-meso-boundary'),
92
+ ).toHaveLength(2)
93
+ expect(
94
+ screen.getAllByTestId('strength-trend-chart-axis-label'),
95
+ ).toHaveLength(2)
96
+ })
97
+ })
98
+
99
+ describe('trend pill', () => {
100
+ it('shows a positive percentage for an improving series', () => {
101
+ render(<StrengthTrendChart {...baseProps} />)
102
+ const pill = screen.getByTestId('strength-trend-chart-trend-pill')
103
+ expect(pill).toHaveTextContent('this meso')
104
+ expect(pill.textContent?.startsWith('+')).toBe(true)
105
+ })
106
+
107
+ it('shows a negative percentage for a declining series', () => {
108
+ render(
109
+ <StrengthTrendChart
110
+ {...baseProps}
111
+ data={[
112
+ { date: '2026-03-03', e1rm: 248 },
113
+ { date: '2026-03-17', e1rm: 235 },
114
+ ]}
115
+ />,
116
+ )
117
+ const pill = screen.getByTestId('strength-trend-chart-trend-pill')
118
+ expect(pill.textContent?.startsWith('-')).toBe(true)
119
+ })
120
+
121
+ it('stays neutral instead of using the whole series when the current meso has <2 points', () => {
122
+ render(
123
+ <StrengthTrendChart
124
+ {...baseProps}
125
+ data={[
126
+ { date: '2026-01-06', e1rm: 200 },
127
+ { date: '2026-02-01', e1rm: 210 },
128
+ { date: '2026-03-20', e1rm: 250 },
129
+ ]}
130
+ mesoBoundaries={[{ date: '2026-03-01', label: 'Peak' }]}
131
+ />,
132
+ )
133
+ // Only one point falls in the current meso, so the pill must not report
134
+ // the whole-series jump (+25%) under the "this meso" label.
135
+ const pill = screen.getByTestId('strength-trend-chart-trend-pill')
136
+ expect(pill).toHaveTextContent('+0.0% this meso')
137
+ })
138
+ })
139
+
140
+ describe('interaction', () => {
141
+ it('fires onPointPress with the tapped point', () => {
142
+ const onPointPress = vi.fn()
143
+ render(<StrengthTrendChart {...baseProps} onPointPress={onPointPress} />)
144
+ fireEvent.click(screen.getAllByTestId('strength-trend-chart-point')[1])
145
+ expect(onPointPress).toHaveBeenCalledWith(
146
+ expect.objectContaining({ e1rm: 228 }),
147
+ )
148
+ })
149
+
150
+ it('shows a tooltip with value and unit after a point is tapped', () => {
151
+ render(<StrengthTrendChart {...baseProps} />)
152
+ expect(
153
+ screen.queryByTestId('strength-trend-chart-tooltip'),
154
+ ).not.toBeInTheDocument()
155
+ fireEvent.click(screen.getAllByTestId('strength-trend-chart-point')[2])
156
+ const tooltip = screen.getByTestId('strength-trend-chart-tooltip')
157
+ expect(tooltip).toHaveTextContent('244 lbs')
158
+ })
159
+ })
160
+
161
+ describe('empty state', () => {
162
+ it('renders an empty placeholder when there is no data', () => {
163
+ render(<StrengthTrendChart {...baseProps} data={[]} />)
164
+ expect(
165
+ screen.getByTestId('strength-trend-chart-empty'),
166
+ ).toBeInTheDocument()
167
+ expect(
168
+ screen.queryByTestId('strength-trend-chart-canvas'),
169
+ ).not.toBeInTheDocument()
170
+ })
171
+
172
+ it('shows the placeholder when data is empty even if a projection exists', () => {
173
+ render(<StrengthTrendChart {...baseProps} data={[]} projection={projection} />)
174
+ expect(
175
+ screen.getByTestId('strength-trend-chart-empty'),
176
+ ).toBeInTheDocument()
177
+ expect(
178
+ screen.queryByTestId('strength-trend-chart-canvas'),
179
+ ).not.toBeInTheDocument()
180
+ })
181
+ })
182
+
183
+ describe('accessibility', () => {
184
+ it('labels the chart as an image with current value and trend', () => {
185
+ render(<StrengthTrendChart {...baseProps} />)
186
+ const canvas = screen.getByTestId('strength-trend-chart-canvas')
187
+ expect(canvas).toHaveAttribute('role', 'img')
188
+ expect(canvas.getAttribute('aria-label')).toContain('Current: 244 lbs')
189
+ expect(canvas.getAttribute('aria-label')).toContain('Trend:')
190
+ })
191
+
192
+ it('labels the empty state accessibly', () => {
193
+ render(<StrengthTrendChart {...baseProps} data={[]} />)
194
+ expect(
195
+ screen.getByLabelText('Strength trend chart, no data'),
196
+ ).toBeInTheDocument()
197
+ })
198
+
199
+ it('has no accessibility violations', async () => {
200
+ const { container } = render(<StrengthTrendChart {...baseProps} />)
201
+ expect(await axe(container)).toHaveNoViolations()
202
+ })
203
+
204
+ it('has no accessibility violations with all features enabled', async () => {
205
+ const { container } = render(
206
+ <StrengthTrendChart
207
+ {...baseProps}
208
+ projection={projection}
209
+ mesoBoundaries={mesoBoundaries}
210
+ onPointPress={vi.fn()}
211
+ unit="kg"
212
+ />,
213
+ )
214
+ expect(await axe(container)).toHaveNoViolations()
215
+ })
216
+ })
217
+ })