@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.
- package/dist/bodymap-BhG55xHM.d.mts +318 -0
- package/dist/bodymap-BhG55xHM.d.ts +318 -0
- package/dist/bodymap.d.mts +3 -0
- package/dist/bodymap.d.ts +3 -0
- package/dist/bodymap.js +2670 -0
- package/dist/bodymap.js.map +1 -0
- package/dist/bodymap.mjs +1101 -0
- package/dist/bodymap.mjs.map +1 -0
- package/dist/{chunk-UXVRPOME.mjs → chunk-7XPB4NAO.mjs} +125 -785
- package/dist/chunk-7XPB4NAO.mjs.map +1 -0
- package/dist/chunk-P7TSQUN6.mjs +846 -0
- package/dist/chunk-P7TSQUN6.mjs.map +1 -0
- package/dist/chunk-TOD2WQBG.mjs +1048 -0
- package/dist/chunk-TOD2WQBG.mjs.map +1 -0
- package/dist/index.d.mts +1131 -4
- package/dist/index.d.ts +1131 -4
- package/dist/index.js +6396 -225
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5572 -641
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +317 -1
- package/dist/theme/index.d.ts +317 -1
- package/dist/theme/index.js +185 -2
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +2 -1
- package/dist/theme/tokens-css.d.mts +20 -0
- package/dist/theme/tokens-css.d.ts +20 -0
- package/dist/theme/tokens-css.js +598 -0
- package/dist/theme/tokens-css.js.map +1 -0
- package/dist/theme/tokens-css.mjs +24 -0
- package/dist/theme/tokens-css.mjs.map +1 -0
- package/package.json +25 -5
- package/src/bodymap.ts +37 -0
- package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.stories.tsx +45 -0
- package/src/components/custom/Gauge/Gauge.test.tsx +71 -0
- package/src/components/custom/Gauge/Gauge.tsx +167 -0
- package/src/components/custom/Gauge/index.ts +2 -0
- package/src/components/custom/Scatter/Scatter.stories.tsx +85 -0
- package/src/components/custom/Scatter/Scatter.test.tsx +96 -0
- package/src/components/custom/Scatter/Scatter.tsx +260 -0
- package/src/components/custom/Scatter/index.ts +2 -0
- package/src/components/custom/Sidebar/Sidebar.tsx +1 -1
- package/src/components/custom/Table/Table.test.tsx +27 -0
- package/src/components/custom/Table/Table.tsx +33 -17
- package/src/components/custom/Treemap/Treemap.stories.tsx +60 -0
- package/src/components/custom/Treemap/Treemap.test.tsx +67 -0
- package/src/components/custom/Treemap/Treemap.tsx +192 -0
- package/src/components/custom/Treemap/index.ts +2 -0
- package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +118 -0
- package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +171 -0
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +447 -0
- package/src/components/custom/Workout/BaseBadge.stories.tsx +75 -0
- package/src/components/custom/Workout/BaseBadge.test.tsx +102 -0
- package/src/components/custom/Workout/BaseBadge.tsx +113 -0
- package/src/components/custom/Workout/BodyMap.stories.tsx +121 -0
- package/src/components/custom/Workout/BodyMap.test.tsx +124 -0
- package/src/components/custom/Workout/BodyMap.tsx +347 -0
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +171 -0
- package/src/components/custom/Workout/BodyMapDetailPanel.test.tsx +225 -0
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +477 -0
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +125 -0
- package/src/components/custom/Workout/CapacityBandChart.test.tsx +193 -0
- package/src/components/custom/Workout/CapacityBandChart.tsx +493 -0
- package/src/components/custom/Workout/DeviationBar.stories.tsx +58 -0
- package/src/components/custom/Workout/DeviationBar.test.tsx +86 -0
- package/src/components/custom/Workout/DeviationBar.tsx +78 -0
- package/src/components/custom/Workout/ExerciseCard.stories.tsx +186 -0
- package/src/components/custom/Workout/ExerciseCard.test.tsx +343 -0
- package/src/components/custom/Workout/ExerciseCard.tsx +442 -0
- package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +225 -0
- package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +128 -0
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +630 -0
- package/src/components/custom/Workout/InputBar.stories.tsx +75 -0
- package/src/components/custom/Workout/InputBar.test.tsx +116 -0
- package/src/components/custom/Workout/InputBar.tsx +162 -0
- package/src/components/custom/Workout/IntensityBar.stories.tsx +60 -0
- package/src/components/custom/Workout/IntensityBar.test.tsx +113 -0
- package/src/components/custom/Workout/IntensityBar.tsx +166 -0
- package/src/components/custom/Workout/MesoCard.stories.tsx +152 -0
- package/src/components/custom/Workout/MesoCard.test.tsx +170 -0
- package/src/components/custom/Workout/MesoCard.tsx +237 -0
- package/src/components/custom/Workout/MesoProgressBar.stories.tsx +85 -0
- package/src/components/custom/Workout/MesoProgressBar.test.tsx +153 -0
- package/src/components/custom/Workout/MesoProgressBar.tsx +165 -0
- package/src/components/custom/Workout/MesoStatusCard.stories.tsx +126 -0
- package/src/components/custom/Workout/MesoStatusCard.test.tsx +209 -0
- package/src/components/custom/Workout/MesoStatusCard.tsx +456 -0
- package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +118 -0
- package/src/components/custom/Workout/MuscleGroupChip.test.tsx +91 -0
- package/src/components/custom/Workout/MuscleGroupChip.tsx +92 -0
- package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +94 -0
- package/src/components/custom/Workout/PlaceholderStrip.test.tsx +83 -0
- package/src/components/custom/Workout/PlaceholderStrip.tsx +90 -0
- package/src/components/custom/Workout/PrBadge.stories.tsx +105 -0
- package/src/components/custom/Workout/PrBadge.test.tsx +111 -0
- package/src/components/custom/Workout/PrBadge.tsx +103 -0
- package/src/components/custom/Workout/PrHistoryModal.stories.tsx +116 -0
- package/src/components/custom/Workout/PrHistoryModal.test.tsx +161 -0
- package/src/components/custom/Workout/PrHistoryModal.tsx +244 -0
- package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +257 -0
- package/src/components/custom/Workout/ProgramPlanningPage.test.tsx +142 -0
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +422 -0
- package/src/components/custom/Workout/ReadinessCheck.stories.tsx +126 -0
- package/src/components/custom/Workout/ReadinessCheck.test.tsx +181 -0
- package/src/components/custom/Workout/ReadinessCheck.tsx +265 -0
- package/src/components/custom/Workout/RestTimer.test.tsx +10 -0
- package/src/components/custom/Workout/RestTimer.tsx +15 -11
- package/src/components/custom/Workout/SetRow.stories.tsx +165 -0
- package/src/components/custom/Workout/SetRow.test.tsx +222 -0
- package/src/components/custom/Workout/SetRow.tsx +247 -0
- package/src/components/custom/Workout/Sparkline.stories.tsx +125 -0
- package/src/components/custom/Workout/Sparkline.test.tsx +113 -0
- package/src/components/custom/Workout/Sparkline.tsx +188 -0
- package/src/components/custom/Workout/StatusDot.stories.tsx +150 -0
- package/src/components/custom/Workout/StatusDot.test.tsx +165 -0
- package/src/components/custom/Workout/StatusDot.tsx +160 -0
- package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +130 -0
- package/src/components/custom/Workout/StrengthTrendChart.test.tsx +217 -0
- package/src/components/custom/Workout/StrengthTrendChart.tsx +628 -0
- package/src/components/custom/Workout/SupersetWrapper.tsx +1 -2
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +66 -0
- package/src/components/custom/Workout/TempoDisplay.test.tsx +90 -0
- package/src/components/custom/Workout/TempoDisplay.tsx +209 -0
- package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +212 -0
- package/src/components/custom/Workout/TrainingStatusPage.test.tsx +131 -0
- package/src/components/custom/Workout/TrainingStatusPage.tsx +321 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +125 -0
- package/src/components/custom/Workout/VelocityStrip.test.tsx +244 -0
- package/src/components/custom/Workout/VelocityStrip.tsx +288 -0
- package/src/components/custom/Workout/WeekRow.stories.tsx +121 -0
- package/src/components/custom/Workout/WeekRow.test.tsx +117 -0
- package/src/components/custom/Workout/WeekRow.tsx +140 -0
- package/src/components/custom/Workout/WeightBadge.stories.tsx +87 -0
- package/src/components/custom/Workout/WeightBadge.test.tsx +164 -0
- package/src/components/custom/Workout/WeightBadge.tsx +113 -0
- package/src/components/custom/Workout/WorkoutCard.stories.tsx +135 -0
- package/src/components/custom/Workout/WorkoutCard.test.tsx +172 -0
- package/src/components/custom/Workout/WorkoutCard.tsx +228 -0
- package/src/components/custom/Workout/WorkoutPill.stories.tsx +63 -0
- package/src/components/custom/Workout/WorkoutPill.test.tsx +153 -0
- package/src/components/custom/Workout/WorkoutPill.tsx +177 -0
- package/src/components/custom/Workout/index.ts +152 -0
- package/src/components/custom/Workout/muscleTaxonomy.ts +252 -0
- package/src/components/custom/index.ts +3 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
- package/src/components/custom/stepper/Stepper.tsx +2 -2
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
- package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
- package/src/components/ui/card/Card.tsx +1 -1
- package/src/components/ui/checkbox/Checkbox.tsx +1 -1
- package/src/components/ui/collapse/Collapse.tsx +1 -1
- package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
- package/src/components/ui/drawer/Drawer.tsx +3 -3
- package/src/components/ui/form-field/FormField.tsx +1 -1
- package/src/components/ui/help-tip/HelpTip.tsx +1 -1
- package/src/components/ui/menu/Menu.tsx +2 -2
- package/src/components/ui/pill/Pill.tsx +1 -1
- package/src/components/ui/popover/Popover.stories.tsx +2 -2
- package/src/components/ui/popover/Popover.tsx +1 -1
- package/src/components/ui/radio/Radio.stories.tsx +1 -1
- package/src/components/ui/section/Section.stories.tsx +4 -4
- package/src/components/ui/select/Select.tsx +1 -1
- package/src/components/ui/stack/Stack.stories.tsx +4 -4
- package/src/components/ui/tabs/Tabs.tsx +2 -2
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +1 -1
- package/src/examples/react-hook-form/ControlledFields.tsx +171 -0
- package/src/examples/react-hook-form/ReactHookForm.stories.tsx +51 -0
- package/src/examples/react-hook-form/ReactHookForm.test.tsx +61 -0
- package/src/examples/react-hook-form/RhfContactForm.tsx +99 -0
- package/src/stories/PresetShowcase.stories.tsx +15 -15
- package/src/theme/config.completeness.test.ts +77 -0
- package/src/theme/config.ts +151 -0
- package/src/theme/index.ts +2 -1
- package/src/theme/manifest/css-property-manifest.example.json +79 -0
- package/src/theme/manifest/css-property-manifest.schema.json +93 -0
- package/src/theme/manifest/css-property-manifest.test.ts +81 -0
- package/src/theme/manifest/css-property-manifest.types.ts +49 -0
- package/src/theme/manifest/css-property-manifest.validate.ts +84 -0
- package/src/theme/manifest/design-freeze.test.ts +155 -0
- package/src/theme/manifest/extract-css-properties.fixture.html +29 -0
- package/src/theme/manifest/extract-css-properties.test.ts +112 -0
- package/src/theme/manifest/index.ts +11 -0
- package/src/theme/tokens-css.test.ts +28 -0
- package/src/theme/tokens-css.ts +36 -0
- package/src/theme/workout-tokens.ts +59 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/workout-format.test.ts +81 -0
- package/src/utils/workout-format.ts +83 -0
- 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
|
+
}
|