@titan-design/react-ui 0.2.6 → 0.3.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 (191) hide show
  1. package/dist/bodymap-BhG55xHM.d.mts +318 -0
  2. package/dist/bodymap-BhG55xHM.d.ts +318 -0
  3. package/dist/bodymap.d.mts +3 -0
  4. package/dist/bodymap.d.ts +3 -0
  5. package/dist/bodymap.js +2670 -0
  6. package/dist/bodymap.js.map +1 -0
  7. package/dist/bodymap.mjs +1101 -0
  8. package/dist/bodymap.mjs.map +1 -0
  9. package/dist/{chunk-UXVRPOME.mjs → chunk-7XPB4NAO.mjs} +125 -785
  10. package/dist/chunk-7XPB4NAO.mjs.map +1 -0
  11. package/dist/chunk-P7TSQUN6.mjs +846 -0
  12. package/dist/chunk-P7TSQUN6.mjs.map +1 -0
  13. package/dist/chunk-TOD2WQBG.mjs +1048 -0
  14. package/dist/chunk-TOD2WQBG.mjs.map +1 -0
  15. package/dist/index.d.mts +1131 -4
  16. package/dist/index.d.ts +1131 -4
  17. package/dist/index.js +6396 -225
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +5572 -641
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/theme/index.d.mts +317 -1
  22. package/dist/theme/index.d.ts +317 -1
  23. package/dist/theme/index.js +185 -2
  24. package/dist/theme/index.js.map +1 -1
  25. package/dist/theme/index.mjs +2 -1
  26. package/dist/theme/tokens-css.d.mts +20 -0
  27. package/dist/theme/tokens-css.d.ts +20 -0
  28. package/dist/theme/tokens-css.js +598 -0
  29. package/dist/theme/tokens-css.js.map +1 -0
  30. package/dist/theme/tokens-css.mjs +24 -0
  31. package/dist/theme/tokens-css.mjs.map +1 -0
  32. package/package.json +25 -5
  33. package/src/bodymap.ts +37 -0
  34. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  35. package/src/components/custom/Gauge/Gauge.stories.tsx +45 -0
  36. package/src/components/custom/Gauge/Gauge.test.tsx +71 -0
  37. package/src/components/custom/Gauge/Gauge.tsx +167 -0
  38. package/src/components/custom/Gauge/index.ts +2 -0
  39. package/src/components/custom/Scatter/Scatter.stories.tsx +85 -0
  40. package/src/components/custom/Scatter/Scatter.test.tsx +96 -0
  41. package/src/components/custom/Scatter/Scatter.tsx +260 -0
  42. package/src/components/custom/Scatter/index.ts +2 -0
  43. package/src/components/custom/Sidebar/Sidebar.tsx +1 -1
  44. package/src/components/custom/Table/Table.test.tsx +27 -0
  45. package/src/components/custom/Table/Table.tsx +33 -17
  46. package/src/components/custom/Treemap/Treemap.stories.tsx +60 -0
  47. package/src/components/custom/Treemap/Treemap.test.tsx +67 -0
  48. package/src/components/custom/Treemap/Treemap.tsx +192 -0
  49. package/src/components/custom/Treemap/index.ts +2 -0
  50. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +118 -0
  51. package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +171 -0
  52. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +447 -0
  53. package/src/components/custom/Workout/BaseBadge.stories.tsx +75 -0
  54. package/src/components/custom/Workout/BaseBadge.test.tsx +102 -0
  55. package/src/components/custom/Workout/BaseBadge.tsx +113 -0
  56. package/src/components/custom/Workout/BodyMap.stories.tsx +121 -0
  57. package/src/components/custom/Workout/BodyMap.test.tsx +124 -0
  58. package/src/components/custom/Workout/BodyMap.tsx +347 -0
  59. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +171 -0
  60. package/src/components/custom/Workout/BodyMapDetailPanel.test.tsx +225 -0
  61. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +477 -0
  62. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +125 -0
  63. package/src/components/custom/Workout/CapacityBandChart.test.tsx +193 -0
  64. package/src/components/custom/Workout/CapacityBandChart.tsx +493 -0
  65. package/src/components/custom/Workout/DeviationBar.stories.tsx +58 -0
  66. package/src/components/custom/Workout/DeviationBar.test.tsx +86 -0
  67. package/src/components/custom/Workout/DeviationBar.tsx +78 -0
  68. package/src/components/custom/Workout/ExerciseCard.stories.tsx +186 -0
  69. package/src/components/custom/Workout/ExerciseCard.test.tsx +343 -0
  70. package/src/components/custom/Workout/ExerciseCard.tsx +442 -0
  71. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +225 -0
  72. package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +128 -0
  73. package/src/components/custom/Workout/ExerciseDetailPage.tsx +630 -0
  74. package/src/components/custom/Workout/InputBar.stories.tsx +75 -0
  75. package/src/components/custom/Workout/InputBar.test.tsx +116 -0
  76. package/src/components/custom/Workout/InputBar.tsx +162 -0
  77. package/src/components/custom/Workout/IntensityBar.stories.tsx +60 -0
  78. package/src/components/custom/Workout/IntensityBar.test.tsx +113 -0
  79. package/src/components/custom/Workout/IntensityBar.tsx +166 -0
  80. package/src/components/custom/Workout/MesoCard.stories.tsx +152 -0
  81. package/src/components/custom/Workout/MesoCard.test.tsx +170 -0
  82. package/src/components/custom/Workout/MesoCard.tsx +237 -0
  83. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +85 -0
  84. package/src/components/custom/Workout/MesoProgressBar.test.tsx +153 -0
  85. package/src/components/custom/Workout/MesoProgressBar.tsx +165 -0
  86. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +126 -0
  87. package/src/components/custom/Workout/MesoStatusCard.test.tsx +209 -0
  88. package/src/components/custom/Workout/MesoStatusCard.tsx +456 -0
  89. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +118 -0
  90. package/src/components/custom/Workout/MuscleGroupChip.test.tsx +91 -0
  91. package/src/components/custom/Workout/MuscleGroupChip.tsx +92 -0
  92. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +94 -0
  93. package/src/components/custom/Workout/PlaceholderStrip.test.tsx +83 -0
  94. package/src/components/custom/Workout/PlaceholderStrip.tsx +90 -0
  95. package/src/components/custom/Workout/PrBadge.stories.tsx +105 -0
  96. package/src/components/custom/Workout/PrBadge.test.tsx +111 -0
  97. package/src/components/custom/Workout/PrBadge.tsx +103 -0
  98. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +116 -0
  99. package/src/components/custom/Workout/PrHistoryModal.test.tsx +161 -0
  100. package/src/components/custom/Workout/PrHistoryModal.tsx +244 -0
  101. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +257 -0
  102. package/src/components/custom/Workout/ProgramPlanningPage.test.tsx +142 -0
  103. package/src/components/custom/Workout/ProgramPlanningPage.tsx +422 -0
  104. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +126 -0
  105. package/src/components/custom/Workout/ReadinessCheck.test.tsx +181 -0
  106. package/src/components/custom/Workout/ReadinessCheck.tsx +265 -0
  107. package/src/components/custom/Workout/RestTimer.test.tsx +10 -0
  108. package/src/components/custom/Workout/RestTimer.tsx +15 -11
  109. package/src/components/custom/Workout/SetRow.stories.tsx +165 -0
  110. package/src/components/custom/Workout/SetRow.test.tsx +222 -0
  111. package/src/components/custom/Workout/SetRow.tsx +247 -0
  112. package/src/components/custom/Workout/Sparkline.stories.tsx +125 -0
  113. package/src/components/custom/Workout/Sparkline.test.tsx +113 -0
  114. package/src/components/custom/Workout/Sparkline.tsx +188 -0
  115. package/src/components/custom/Workout/StatusDot.stories.tsx +150 -0
  116. package/src/components/custom/Workout/StatusDot.test.tsx +165 -0
  117. package/src/components/custom/Workout/StatusDot.tsx +160 -0
  118. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +130 -0
  119. package/src/components/custom/Workout/StrengthTrendChart.test.tsx +217 -0
  120. package/src/components/custom/Workout/StrengthTrendChart.tsx +628 -0
  121. package/src/components/custom/Workout/SupersetWrapper.tsx +1 -2
  122. package/src/components/custom/Workout/TempoDisplay.stories.tsx +66 -0
  123. package/src/components/custom/Workout/TempoDisplay.test.tsx +90 -0
  124. package/src/components/custom/Workout/TempoDisplay.tsx +209 -0
  125. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +212 -0
  126. package/src/components/custom/Workout/TrainingStatusPage.test.tsx +131 -0
  127. package/src/components/custom/Workout/TrainingStatusPage.tsx +321 -0
  128. package/src/components/custom/Workout/VelocityStrip.stories.tsx +125 -0
  129. package/src/components/custom/Workout/VelocityStrip.test.tsx +244 -0
  130. package/src/components/custom/Workout/VelocityStrip.tsx +288 -0
  131. package/src/components/custom/Workout/WeekRow.stories.tsx +121 -0
  132. package/src/components/custom/Workout/WeekRow.test.tsx +117 -0
  133. package/src/components/custom/Workout/WeekRow.tsx +140 -0
  134. package/src/components/custom/Workout/WeightBadge.stories.tsx +87 -0
  135. package/src/components/custom/Workout/WeightBadge.test.tsx +164 -0
  136. package/src/components/custom/Workout/WeightBadge.tsx +113 -0
  137. package/src/components/custom/Workout/WorkoutCard.stories.tsx +135 -0
  138. package/src/components/custom/Workout/WorkoutCard.test.tsx +172 -0
  139. package/src/components/custom/Workout/WorkoutCard.tsx +228 -0
  140. package/src/components/custom/Workout/WorkoutPill.stories.tsx +63 -0
  141. package/src/components/custom/Workout/WorkoutPill.test.tsx +153 -0
  142. package/src/components/custom/Workout/WorkoutPill.tsx +177 -0
  143. package/src/components/custom/Workout/index.ts +152 -0
  144. package/src/components/custom/Workout/muscleTaxonomy.ts +252 -0
  145. package/src/components/custom/index.ts +3 -0
  146. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  147. package/src/components/custom/stepper/Stepper.tsx +2 -2
  148. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  149. package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
  150. package/src/components/ui/card/Card.tsx +1 -1
  151. package/src/components/ui/checkbox/Checkbox.tsx +1 -1
  152. package/src/components/ui/collapse/Collapse.tsx +1 -1
  153. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  154. package/src/components/ui/drawer/Drawer.tsx +3 -3
  155. package/src/components/ui/form-field/FormField.tsx +1 -1
  156. package/src/components/ui/help-tip/HelpTip.tsx +1 -1
  157. package/src/components/ui/menu/Menu.tsx +2 -2
  158. package/src/components/ui/pill/Pill.tsx +1 -1
  159. package/src/components/ui/popover/Popover.stories.tsx +2 -2
  160. package/src/components/ui/popover/Popover.tsx +1 -1
  161. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  162. package/src/components/ui/section/Section.stories.tsx +4 -4
  163. package/src/components/ui/select/Select.tsx +1 -1
  164. package/src/components/ui/stack/Stack.stories.tsx +4 -4
  165. package/src/components/ui/tabs/Tabs.tsx +2 -2
  166. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
  167. package/src/components/ui/toolbar-button/ToolbarButton.tsx +1 -1
  168. package/src/examples/react-hook-form/ControlledFields.tsx +171 -0
  169. package/src/examples/react-hook-form/ReactHookForm.stories.tsx +51 -0
  170. package/src/examples/react-hook-form/ReactHookForm.test.tsx +61 -0
  171. package/src/examples/react-hook-form/RhfContactForm.tsx +99 -0
  172. package/src/stories/PresetShowcase.stories.tsx +15 -15
  173. package/src/theme/config.completeness.test.ts +77 -0
  174. package/src/theme/config.ts +151 -0
  175. package/src/theme/index.ts +2 -1
  176. package/src/theme/manifest/css-property-manifest.example.json +79 -0
  177. package/src/theme/manifest/css-property-manifest.schema.json +93 -0
  178. package/src/theme/manifest/css-property-manifest.test.ts +81 -0
  179. package/src/theme/manifest/css-property-manifest.types.ts +49 -0
  180. package/src/theme/manifest/css-property-manifest.validate.ts +84 -0
  181. package/src/theme/manifest/design-freeze.test.ts +155 -0
  182. package/src/theme/manifest/extract-css-properties.fixture.html +29 -0
  183. package/src/theme/manifest/extract-css-properties.test.ts +112 -0
  184. package/src/theme/manifest/index.ts +11 -0
  185. package/src/theme/tokens-css.test.ts +28 -0
  186. package/src/theme/tokens-css.ts +36 -0
  187. package/src/theme/workout-tokens.ts +59 -0
  188. package/src/utils/index.ts +10 -0
  189. package/src/utils/workout-format.test.ts +81 -0
  190. package/src/utils/workout-format.ts +83 -0
  191. package/dist/chunk-UXVRPOME.mjs.map +0 -1
@@ -0,0 +1,125 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { Sparkline } from './Sparkline'
4
+
5
+ const meta: Meta<typeof Sparkline> = {
6
+ title: 'Custom/Workout/Sparkline',
7
+ component: Sparkline,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ width: {
11
+ control: 'number',
12
+ description: 'Chart width in pixels',
13
+ },
14
+ height: {
15
+ control: 'number',
16
+ description: 'Chart height in pixels',
17
+ },
18
+ color: {
19
+ control: 'color',
20
+ description: 'Line and dot color',
21
+ },
22
+ showDots: {
23
+ control: 'boolean',
24
+ description: 'Show dots at data points',
25
+ },
26
+ highlightLast: {
27
+ control: 'boolean',
28
+ description: 'Highlight the last data point with a larger dot',
29
+ },
30
+ },
31
+ }
32
+
33
+ export default meta
34
+ type Story = StoryObj<typeof Sparkline>
35
+
36
+ const ascending = [10, 15, 22, 28, 35, 42, 50]
37
+ const descending = [50, 45, 38, 30, 22, 15, 10]
38
+ const uCurve = [40, 25, 15, 10, 12, 20, 35]
39
+ const flat = [20, 21, 19, 20, 21, 20, 19]
40
+ const volatile = [10, 35, 15, 45, 20, 40, 25]
41
+
42
+ export const Default: Story = {
43
+ args: {
44
+ data: ascending,
45
+ },
46
+ }
47
+
48
+ export const Ascending: Story = {
49
+ args: {
50
+ data: ascending,
51
+ showDots: true,
52
+ },
53
+ }
54
+
55
+ export const Descending: Story = {
56
+ args: {
57
+ data: descending,
58
+ showDots: true,
59
+ color: '#ff4757',
60
+ },
61
+ }
62
+
63
+ export const UCurve: Story = {
64
+ args: {
65
+ data: uCurve,
66
+ showDots: true,
67
+ color: '#2ed573',
68
+ },
69
+ }
70
+
71
+ export const Flat: Story = {
72
+ args: {
73
+ data: flat,
74
+ showDots: true,
75
+ color: '#ffd43b',
76
+ },
77
+ }
78
+
79
+ export const WithReferenceLines: Story = {
80
+ args: {
81
+ data: ascending,
82
+ showDots: true,
83
+ referenceLines: [
84
+ { value: 30, color: '#ff4757', dashed: true },
85
+ { value: 45, color: '#2ed573', dashed: false },
86
+ ],
87
+ },
88
+ }
89
+
90
+ export const HighlightLast: Story = {
91
+ args: {
92
+ data: ascending,
93
+ highlightLast: true,
94
+ },
95
+ }
96
+
97
+ export const DotsAndHighlightLast: Story = {
98
+ args: {
99
+ data: volatile,
100
+ showDots: true,
101
+ highlightLast: true,
102
+ color: '#ffa502',
103
+ },
104
+ }
105
+
106
+ export const CustomSize: Story = {
107
+ args: {
108
+ data: ascending,
109
+ width: 120,
110
+ height: 50,
111
+ showDots: true,
112
+ },
113
+ }
114
+
115
+ export const AllShapes: Story = {
116
+ render: () => (
117
+ <View style={{ gap: 16, padding: 16 }}>
118
+ <Sparkline data={ascending} showDots highlightLast />
119
+ <Sparkline data={descending} showDots color="#ff4757" />
120
+ <Sparkline data={uCurve} showDots color="#2ed573" />
121
+ <Sparkline data={flat} showDots color="#ffd43b" />
122
+ <Sparkline data={volatile} showDots color="#ffa502" highlightLast />
123
+ </View>
124
+ ),
125
+ }
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { Sparkline } from './Sparkline'
5
+
6
+ const sampleData = [10, 25, 15, 30, 20]
7
+
8
+ describe('Sparkline', () => {
9
+ it('renders with data points', () => {
10
+ render(<Sparkline data={sampleData} />)
11
+ expect(screen.getByTestId('sparkline')).toBeInTheDocument()
12
+ })
13
+
14
+ it('renders empty state for no data', () => {
15
+ render(<Sparkline data={[]} />)
16
+ expect(screen.getByTestId('sparkline-empty')).toBeInTheDocument()
17
+ })
18
+
19
+ it('renders line segments between points', () => {
20
+ render(<Sparkline data={sampleData} />)
21
+ for (let i = 1; i < sampleData.length; i++) {
22
+ expect(
23
+ screen.getByTestId(`sparkline-segment-${i}`),
24
+ ).toBeInTheDocument()
25
+ }
26
+ })
27
+
28
+ it('renders dots when showDots is true', () => {
29
+ render(<Sparkline data={sampleData} showDots />)
30
+ for (let i = 0; i < sampleData.length; i++) {
31
+ expect(screen.getByTestId(`sparkline-dot-${i}`)).toBeInTheDocument()
32
+ }
33
+ })
34
+
35
+ it('does not render dots when showDots is false', () => {
36
+ render(<Sparkline data={sampleData} />)
37
+ expect(screen.queryByTestId('sparkline-dot-0')).not.toBeInTheDocument()
38
+ })
39
+
40
+ it('highlights last dot when highlightLast is true', () => {
41
+ render(<Sparkline data={sampleData} highlightLast />)
42
+ const lastIndex = sampleData.length - 1
43
+ expect(
44
+ screen.getByTestId(`sparkline-dot-${lastIndex}`),
45
+ ).toBeInTheDocument()
46
+ // Other dots should not be present (showDots is false)
47
+ expect(screen.queryByTestId('sparkline-dot-0')).not.toBeInTheDocument()
48
+ })
49
+
50
+ it('renders both regular dots and highlighted last', () => {
51
+ render(<Sparkline data={sampleData} showDots highlightLast />)
52
+ for (let i = 0; i < sampleData.length; i++) {
53
+ expect(screen.getByTestId(`sparkline-dot-${i}`)).toBeInTheDocument()
54
+ }
55
+ })
56
+
57
+ it('renders reference lines', () => {
58
+ render(
59
+ <Sparkline
60
+ data={sampleData}
61
+ referenceLines={[
62
+ { value: 20, color: '#ff0000' },
63
+ { value: 25, color: '#00ff00', dashed: true },
64
+ ]}
65
+ />,
66
+ )
67
+ expect(screen.getByTestId('sparkline-reference-0')).toBeInTheDocument()
68
+ expect(screen.getByTestId('sparkline-reference-1')).toBeInTheDocument()
69
+ })
70
+
71
+ it('does not render reference lines when not provided', () => {
72
+ render(<Sparkline data={sampleData} />)
73
+ expect(
74
+ screen.queryByTestId('sparkline-reference-0'),
75
+ ).not.toBeInTheDocument()
76
+ })
77
+
78
+ it('uses default dimensions', () => {
79
+ render(<Sparkline data={sampleData} />)
80
+ const sparkline = screen.getByTestId('sparkline')
81
+ expect(sparkline).toHaveStyle({ width: '80px', height: '30px' })
82
+ })
83
+
84
+ it('accepts custom dimensions', () => {
85
+ render(<Sparkline data={sampleData} width={120} height={50} />)
86
+ const sparkline = screen.getByTestId('sparkline')
87
+ expect(sparkline).toHaveStyle({ width: '120px', height: '50px' })
88
+ })
89
+
90
+ it('sets accessibility label', () => {
91
+ render(<Sparkline data={sampleData} />)
92
+ expect(
93
+ screen.getByLabelText('Sparkline chart with 5 data points'),
94
+ ).toBeInTheDocument()
95
+ })
96
+
97
+ it('sets empty accessibility label for no data', () => {
98
+ render(<Sparkline data={[]} />)
99
+ expect(
100
+ screen.getByLabelText('Sparkline chart, no data'),
101
+ ).toBeInTheDocument()
102
+ })
103
+
104
+ describe('accessibility', () => {
105
+ it('has no accessibility violations', async () => {
106
+ const { container } = render(
107
+ <Sparkline data={sampleData} showDots highlightLast />,
108
+ )
109
+ const results = await axe(container)
110
+ expect(results).toHaveNoViolations()
111
+ })
112
+ })
113
+ })
@@ -0,0 +1,188 @@
1
+ // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
+ import React from 'react'
3
+ import { View, Text, type ViewProps } from 'react-native'
4
+ import { cn } from '../../../utils/cn'
5
+
6
+ const BRAND_PRIMARY = '#FF7900'
7
+
8
+ export interface SparklineProps extends ViewProps {
9
+ data: number[]
10
+ width?: number
11
+ height?: number
12
+ color?: string
13
+ showDots?: boolean
14
+ referenceLines?: Array<{
15
+ value: number
16
+ color: string
17
+ dashed?: boolean
18
+ label?: string
19
+ }>
20
+ highlightLast?: boolean
21
+ className?: string
22
+ }
23
+
24
+ function normalizeData(
25
+ data: number[],
26
+ height: number,
27
+ ): number[] {
28
+ if (data.length === 0) return []
29
+ const min = Math.min(...data)
30
+ const max = Math.max(...data)
31
+ const range = max - min || 1
32
+ return data.map((v) => height - ((v - min) / range) * height)
33
+ }
34
+
35
+ function normalizeValue(
36
+ value: number,
37
+ data: number[],
38
+ height: number,
39
+ ): number {
40
+ const min = Math.min(...data)
41
+ const max = Math.max(...data)
42
+ const range = max - min || 1
43
+ return height - ((value - min) / range) * height
44
+ }
45
+
46
+ export function Sparkline({
47
+ data,
48
+ width = 80,
49
+ height = 30,
50
+ color,
51
+ showDots = false,
52
+ referenceLines,
53
+ highlightLast = false,
54
+ className,
55
+ ...props
56
+ }: SparklineProps) {
57
+ if (data.length === 0) {
58
+ return (
59
+ <View
60
+ style={{ width, height }}
61
+ className={className}
62
+ testID="sparkline-empty"
63
+ accessibilityLabel="Sparkline chart, no data"
64
+ {...props}
65
+ />
66
+ )
67
+ }
68
+
69
+ const resolvedColor = color ?? BRAND_PRIMARY
70
+ const normalized = normalizeData(data, height)
71
+ const stepX = data.length > 1 ? width / (data.length - 1) : 0
72
+
73
+ const dotSize = 3
74
+ const highlightSize = 6
75
+
76
+ return (
77
+ <View
78
+ style={{ width, height, position: 'relative' }}
79
+ className={cn(className)}
80
+ accessibilityRole="image"
81
+ accessibilityLabel={`Sparkline chart with ${data.length} data points`}
82
+ testID="sparkline"
83
+ {...props}
84
+ >
85
+ {referenceLines?.map((line, i) => {
86
+ const y = normalizeValue(line.value, data, height)
87
+ return (
88
+ <View
89
+ key={`ref-${i}`}
90
+ style={[
91
+ {
92
+ position: 'absolute',
93
+ top: y,
94
+ left: 0,
95
+ right: 0,
96
+ opacity: 0.6,
97
+ },
98
+ line.dashed
99
+ ? {
100
+ height: 0,
101
+ borderStyle: 'dashed',
102
+ borderTopWidth: 1,
103
+ borderTopColor: line.color,
104
+ }
105
+ : {
106
+ height: 1,
107
+ backgroundColor: line.color,
108
+ },
109
+ ]}
110
+ accessibilityElementsHidden
111
+ testID={`sparkline-reference-${i}`}
112
+ >
113
+ {line.label && (
114
+ <Text
115
+ style={{
116
+ position: 'absolute',
117
+ right: 0,
118
+ top: -10,
119
+ fontSize: 7,
120
+ color: line.color,
121
+ opacity: 1,
122
+ }}
123
+ testID={`sparkline-reference-label-${i}`}
124
+ >
125
+ {line.label}
126
+ </Text>
127
+ )}
128
+ </View>
129
+ )
130
+ })}
131
+
132
+ {/* Line segments connecting data points */}
133
+ {normalized.map((y, i) => {
134
+ if (i === 0) return null
135
+ const prevY = normalized[i - 1]
136
+ const x1 = (i - 1) * stepX
137
+ const x2 = i * stepX
138
+ const dx = x2 - x1
139
+ const dy = y - prevY
140
+ const length = Math.sqrt(dx * dx + dy * dy)
141
+ const angle = Math.atan2(dy, dx) * (180 / Math.PI)
142
+ return (
143
+ <View
144
+ key={`line-${i}`}
145
+ style={{
146
+ position: 'absolute',
147
+ left: x1,
148
+ top: prevY,
149
+ width: length,
150
+ height: 1.5,
151
+ backgroundColor: resolvedColor,
152
+ transformOrigin: '0 0',
153
+ transform: [{ rotate: `${angle}deg` }],
154
+ }}
155
+ accessibilityElementsHidden
156
+ testID={`sparkline-segment-${i}`}
157
+ />
158
+ )
159
+ })}
160
+
161
+ {/* Data point dots */}
162
+ {(showDots || highlightLast) &&
163
+ normalized.map((y, i) => {
164
+ const isLast = i === data.length - 1
165
+ const shouldShow = showDots || (highlightLast && isLast)
166
+ if (!shouldShow) return null
167
+
168
+ const size = highlightLast && isLast ? highlightSize : dotSize
169
+ return (
170
+ <View
171
+ key={`dot-${i}`}
172
+ style={{
173
+ position: 'absolute',
174
+ left: i * stepX - size / 2,
175
+ top: y - size / 2,
176
+ width: size,
177
+ height: size,
178
+ borderRadius: size / 2,
179
+ backgroundColor: resolvedColor,
180
+ }}
181
+ accessibilityElementsHidden
182
+ testID={`sparkline-dot-${i}`}
183
+ />
184
+ )
185
+ })}
186
+ </View>
187
+ )
188
+ }
@@ -0,0 +1,150 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { StatusDot } from './StatusDot'
4
+
5
+ const meta: Meta<typeof StatusDot> = {
6
+ title: 'Custom/Workout/StatusDot',
7
+ component: StatusDot,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ variant: {
11
+ control: 'select',
12
+ options: ['success', 'warning', 'error', 'neutral', 'on-track', 'deviation', 'future'],
13
+ description: 'Status variant',
14
+ },
15
+ icon: {
16
+ control: 'select',
17
+ options: [undefined, 'check', 'exclamation', 'dash'],
18
+ description: 'Icon character inside the dot',
19
+ },
20
+ size: {
21
+ control: 'select',
22
+ options: ['sm', 'md'],
23
+ description: 'Dot size',
24
+ },
25
+ glow: {
26
+ control: 'boolean',
27
+ description: 'Subtle glow effect in variant color',
28
+ },
29
+ label: {
30
+ control: 'text',
31
+ description: 'Optional label text next to dot',
32
+ },
33
+ },
34
+ }
35
+
36
+ export default meta
37
+ type Story = StoryObj<typeof StatusDot>
38
+
39
+ export const Success: Story = {
40
+ args: {
41
+ variant: 'success',
42
+ size: 'md',
43
+ icon: 'check',
44
+ },
45
+ }
46
+
47
+ export const Warning: Story = {
48
+ args: {
49
+ variant: 'warning',
50
+ size: 'md',
51
+ icon: 'exclamation',
52
+ },
53
+ }
54
+
55
+ export const Error: Story = {
56
+ args: {
57
+ variant: 'error',
58
+ size: 'md',
59
+ icon: 'dash',
60
+ },
61
+ }
62
+
63
+ export const Neutral: Story = {
64
+ args: {
65
+ variant: 'neutral',
66
+ size: 'sm',
67
+ },
68
+ }
69
+
70
+ export const OnTrack: Story = {
71
+ args: {
72
+ variant: 'on-track',
73
+ size: 'md',
74
+ icon: 'check',
75
+ },
76
+ }
77
+
78
+ export const Deviation: Story = {
79
+ args: {
80
+ variant: 'deviation',
81
+ size: 'md',
82
+ icon: 'exclamation',
83
+ },
84
+ }
85
+
86
+ export const Future: Story = {
87
+ args: {
88
+ variant: 'future',
89
+ size: 'md',
90
+ },
91
+ }
92
+
93
+ export const WithGlow: Story = {
94
+ args: {
95
+ variant: 'success',
96
+ size: 'md',
97
+ icon: 'check',
98
+ glow: true,
99
+ },
100
+ }
101
+
102
+ export const WithLabel: Story = {
103
+ args: {
104
+ variant: 'success',
105
+ size: 'md',
106
+ icon: 'check',
107
+ label: 'On track',
108
+ },
109
+ }
110
+
111
+ export const SmallWithLabel: Story = {
112
+ args: {
113
+ variant: 'warning',
114
+ size: 'sm',
115
+ label: 'Needs attention',
116
+ },
117
+ }
118
+
119
+ export const AllVariants: Story = {
120
+ render: () => (
121
+ <View style={{ gap: 12 }}>
122
+ <View style={{ flexDirection: 'row', gap: 16, alignItems: 'center' }}>
123
+ <StatusDot variant="success" size="sm" />
124
+ <StatusDot variant="warning" size="sm" />
125
+ <StatusDot variant="error" size="sm" />
126
+ <StatusDot variant="neutral" size="sm" />
127
+ <StatusDot variant="on-track" size="sm" />
128
+ <StatusDot variant="deviation" size="sm" />
129
+ <StatusDot variant="future" size="sm" />
130
+ </View>
131
+ <View style={{ flexDirection: 'row', gap: 16, alignItems: 'center' }}>
132
+ <StatusDot variant="success" size="md" icon="check" />
133
+ <StatusDot variant="warning" size="md" icon="exclamation" />
134
+ <StatusDot variant="error" size="md" icon="dash" />
135
+ <StatusDot variant="neutral" size="md" />
136
+ <StatusDot variant="on-track" size="md" icon="check" />
137
+ <StatusDot variant="deviation" size="md" icon="exclamation" />
138
+ <StatusDot variant="future" size="md" />
139
+ </View>
140
+ <View style={{ gap: 8 }}>
141
+ <StatusDot variant="success" size="md" icon="check" label="On track" />
142
+ <StatusDot variant="on-track" size="md" icon="check" label="On track (ring)" glow />
143
+ <StatusDot variant="deviation" size="md" icon="exclamation" label="Deviation" glow />
144
+ <StatusDot variant="future" size="sm" label="Planned" />
145
+ <StatusDot variant="error" size="md" icon="dash" label="Failed" />
146
+ <StatusDot variant="neutral" size="sm" label="Planned" />
147
+ </View>
148
+ </View>
149
+ ),
150
+ }