@titan-design/react-ui 0.2.5 → 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.
- package/dist/chunk-3VLOBS6O.mjs +846 -0
- package/dist/chunk-3VLOBS6O.mjs.map +1 -0
- package/dist/{chunk-UXVRPOME.mjs → chunk-5SSKGS6E.mjs} +3 -783
- package/dist/chunk-5SSKGS6E.mjs.map +1 -0
- package/dist/index.d.mts +1005 -4
- package/dist/index.d.ts +1005 -4
- package/dist/index.js +6043 -213
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5901 -177
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +57 -1
- package/dist/theme/index.d.ts +57 -1
- package/dist/theme/index.js +63 -0
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +2 -1
- package/dist/theme/tokens-css.d.mts +18 -0
- package/dist/theme/tokens-css.d.ts +18 -0
- package/dist/theme/tokens-css.js +289 -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/docs/WEB_SETUP.md +217 -0
- package/package.json +17 -5
- 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/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/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 +348 -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 +478 -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 +319 -0
- package/src/components/custom/Workout/ExerciseCard.tsx +429 -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 +163 -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 +235 -0
- package/src/components/custom/Workout/MesoProgressBar.stories.tsx +85 -0
- package/src/components/custom/Workout/MesoProgressBar.test.tsx +141 -0
- package/src/components/custom/Workout/MesoProgressBar.tsx +161 -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 +91 -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/ReadinessCheck.stories.tsx +126 -0
- package/src/components/custom/Workout/ReadinessCheck.test.tsx +181 -0
- package/src/components/custom/Workout/ReadinessCheck.tsx +266 -0
- package/src/components/custom/Workout/RestTimer.tsx +8 -6
- 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 +253 -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 +159 -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 +207 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +125 -0
- package/src/components/custom/Workout/VelocityStrip.test.tsx +234 -0
- package/src/components/custom/Workout/VelocityStrip.tsx +287 -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 +226 -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 +113 -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/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/index.ts +11 -0
- package/src/theme/tokens-css.test.ts +28 -0
- package/src/theme/tokens-css.ts +34 -0
- package/src/theme/workout-tokens.ts +61 -0
- package/dist/chunk-UXVRPOME.mjs.map +0 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { StatusDot } from './StatusDot'
|
|
5
|
+
|
|
6
|
+
describe('StatusDot', () => {
|
|
7
|
+
it('renders a dot', () => {
|
|
8
|
+
render(<StatusDot variant="success" />)
|
|
9
|
+
expect(screen.getByTestId('status-dot')).toBeInTheDocument()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('shows icon at md size', () => {
|
|
13
|
+
render(<StatusDot variant="success" size="md" icon="check" />)
|
|
14
|
+
expect(screen.getByText('\u2713')).toBeInTheDocument()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('does not show icon at sm size', () => {
|
|
18
|
+
render(<StatusDot variant="success" size="sm" icon="check" />)
|
|
19
|
+
expect(screen.queryByText('\u2713')).not.toBeInTheDocument()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('renders exclamation icon', () => {
|
|
23
|
+
render(<StatusDot variant="warning" size="md" icon="exclamation" />)
|
|
24
|
+
expect(screen.getByText('!')).toBeInTheDocument()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('renders dash icon', () => {
|
|
28
|
+
render(<StatusDot variant="error" size="md" icon="dash" />)
|
|
29
|
+
expect(screen.getByText('\u2014')).toBeInTheDocument()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('renders label text when provided', () => {
|
|
33
|
+
render(<StatusDot variant="success" label="On track" />)
|
|
34
|
+
expect(screen.getByText('On track')).toBeInTheDocument()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('does not render label when omitted', () => {
|
|
38
|
+
render(<StatusDot variant="success" />)
|
|
39
|
+
expect(screen.queryByText('On track')).not.toBeInTheDocument()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('defaults to sm size', () => {
|
|
43
|
+
render(<StatusDot variant="neutral" />)
|
|
44
|
+
expect(screen.getByTestId('status-dot')).toBeInTheDocument()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('sets accessibility label with variant', () => {
|
|
48
|
+
render(<StatusDot variant="warning" />)
|
|
49
|
+
expect(screen.getByLabelText('warning status')).toBeInTheDocument()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('solid variants', () => {
|
|
53
|
+
it('renders all solid variant types without error', () => {
|
|
54
|
+
const { rerender } = render(<StatusDot variant="success" />)
|
|
55
|
+
expect(screen.getByTestId('status-dot')).toBeInTheDocument()
|
|
56
|
+
|
|
57
|
+
rerender(<StatusDot variant="warning" />)
|
|
58
|
+
expect(screen.getByTestId('status-dot')).toBeInTheDocument()
|
|
59
|
+
|
|
60
|
+
rerender(<StatusDot variant="error" />)
|
|
61
|
+
expect(screen.getByTestId('status-dot')).toBeInTheDocument()
|
|
62
|
+
|
|
63
|
+
rerender(<StatusDot variant="neutral" />)
|
|
64
|
+
expect(screen.getByTestId('status-dot')).toBeInTheDocument()
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('ring variants', () => {
|
|
69
|
+
it('renders on-track variant', () => {
|
|
70
|
+
render(<StatusDot variant="on-track" />)
|
|
71
|
+
expect(screen.getByTestId('status-dot')).toBeInTheDocument()
|
|
72
|
+
expect(screen.getByLabelText('on-track status')).toBeInTheDocument()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('renders deviation variant', () => {
|
|
76
|
+
render(<StatusDot variant="deviation" />)
|
|
77
|
+
expect(screen.getByTestId('status-dot')).toBeInTheDocument()
|
|
78
|
+
expect(screen.getByLabelText('deviation status')).toBeInTheDocument()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('renders future variant', () => {
|
|
82
|
+
render(<StatusDot variant="future" />)
|
|
83
|
+
expect(screen.getByTestId('status-dot')).toBeInTheDocument()
|
|
84
|
+
expect(screen.getByLabelText('future status')).toBeInTheDocument()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('renders the future variant with a 1px dashed border', () => {
|
|
88
|
+
render(<StatusDot variant="future" />)
|
|
89
|
+
const dot = screen.getByTestId('status-dot')
|
|
90
|
+
expect(dot.style.borderTopStyle).toBe('dashed')
|
|
91
|
+
expect(dot.style.borderTopWidth).toBe('1px')
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('md size', () => {
|
|
96
|
+
it('renders at md size without error', () => {
|
|
97
|
+
render(<StatusDot variant="success" size="md" />)
|
|
98
|
+
expect(screen.getByTestId('status-dot')).toBeInTheDocument()
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('glow', () => {
|
|
103
|
+
it('applies a box-shadow when glow is set on a solid variant', () => {
|
|
104
|
+
render(<StatusDot variant="success" glow />)
|
|
105
|
+
const dot = screen.getByTestId('status-dot')
|
|
106
|
+
expect(dot.style.boxShadow).toContain('rgba(20,184,166,0.4)')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('applies a box-shadow when glow is set on a ring variant', () => {
|
|
110
|
+
render(<StatusDot variant="on-track" glow />)
|
|
111
|
+
const dot = screen.getByTestId('status-dot')
|
|
112
|
+
expect(dot.style.boxShadow).toContain('rgba(20,184,166,0.4)')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('applies a gray box-shadow when glow is set on the future variant', () => {
|
|
116
|
+
render(<StatusDot variant="future" glow />)
|
|
117
|
+
const dot = screen.getByTestId('status-dot')
|
|
118
|
+
expect(dot.style.boxShadow).toContain('rgba(107,114,128,0.4)')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('does not apply a box-shadow when glow is not set', () => {
|
|
122
|
+
render(<StatusDot variant="on-track" />)
|
|
123
|
+
const dot = screen.getByTestId('status-dot')
|
|
124
|
+
expect(dot.style.boxShadow).toBe('')
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('accessibility', () => {
|
|
129
|
+
it('has no accessibility violations', async () => {
|
|
130
|
+
const { container } = render(
|
|
131
|
+
<StatusDot variant="success" size="md" icon="check" label="On track" />,
|
|
132
|
+
)
|
|
133
|
+
const results = await axe(container)
|
|
134
|
+
expect(results).toHaveNoViolations()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('has no accessibility violations for ring variants', async () => {
|
|
138
|
+
const { container } = render(
|
|
139
|
+
<StatusDot variant="on-track" size="md" icon="check" label="On track" />,
|
|
140
|
+
)
|
|
141
|
+
const results = await axe(container)
|
|
142
|
+
expect(results).toHaveNoViolations()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('has no accessibility violations without label', async () => {
|
|
146
|
+
const { container } = render(<StatusDot variant="warning" size="md" />)
|
|
147
|
+
const results = await axe(container)
|
|
148
|
+
expect(results).toHaveNoViolations()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('has no accessibility violations for all solid variants', async () => {
|
|
152
|
+
for (const variant of ['success', 'warning', 'error', 'neutral'] as const) {
|
|
153
|
+
const { container, unmount } = render(<StatusDot variant={variant} />)
|
|
154
|
+
const results = await axe(container)
|
|
155
|
+
expect(results).toHaveNoViolations()
|
|
156
|
+
unmount()
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('sets combined accessibility label when label prop is provided', () => {
|
|
161
|
+
render(<StatusDot variant="success" label="On track" />)
|
|
162
|
+
expect(screen.getByLabelText('success status: On track')).toBeInTheDocument()
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
})
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import { View, Text, type ViewProps } from 'react-native'
|
|
3
|
+
|
|
4
|
+
export type StatusDotVariant =
|
|
5
|
+
| 'success'
|
|
6
|
+
| 'warning'
|
|
7
|
+
| 'error'
|
|
8
|
+
| 'neutral'
|
|
9
|
+
| 'on-track'
|
|
10
|
+
| 'deviation'
|
|
11
|
+
| 'future'
|
|
12
|
+
|
|
13
|
+
export interface StatusDotProps extends ViewProps {
|
|
14
|
+
variant: StatusDotVariant
|
|
15
|
+
icon?: 'check' | 'exclamation' | 'dash'
|
|
16
|
+
size?: 'sm' | 'md'
|
|
17
|
+
/** Adds a subtle glow effect in the variant color */
|
|
18
|
+
glow?: boolean
|
|
19
|
+
label?: string
|
|
20
|
+
className?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const solidVariantColors: Record<string, string> = {
|
|
24
|
+
success: '#14B8A6',
|
|
25
|
+
warning: '#FFB020',
|
|
26
|
+
error: '#D14343',
|
|
27
|
+
neutral: '#6B7280',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ringVariantStyles: Record<string, Record<string, unknown>> = {
|
|
31
|
+
'on-track': {
|
|
32
|
+
backgroundColor: 'rgba(20,184,166,0.15)',
|
|
33
|
+
borderWidth: 1,
|
|
34
|
+
borderColor: 'rgba(20,184,166,0.3)',
|
|
35
|
+
},
|
|
36
|
+
deviation: {
|
|
37
|
+
backgroundColor: 'rgba(245,158,11,0.15)',
|
|
38
|
+
borderWidth: 1,
|
|
39
|
+
borderColor: 'rgba(245,158,11,0.25)',
|
|
40
|
+
},
|
|
41
|
+
future: {
|
|
42
|
+
backgroundColor: 'rgba(107,114,128,0.1)',
|
|
43
|
+
borderWidth: 1,
|
|
44
|
+
borderStyle: 'dashed',
|
|
45
|
+
borderColor: 'rgba(107,114,128,0.2)',
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const glowStyles: Record<StatusDotVariant, Record<string, unknown>> = {
|
|
50
|
+
success: { boxShadow: '0 0 4px rgba(20,184,166,0.4)' },
|
|
51
|
+
warning: { boxShadow: '0 0 4px rgba(245,158,11,0.4)' },
|
|
52
|
+
error: { boxShadow: '0 0 4px rgba(239,68,68,0.4)' },
|
|
53
|
+
neutral: { boxShadow: '0 0 4px rgba(107,114,128,0.4)' },
|
|
54
|
+
'on-track': { boxShadow: '0 0 4px rgba(20,184,166,0.4)' },
|
|
55
|
+
deviation: { boxShadow: '0 0 4px rgba(245,158,11,0.4)' },
|
|
56
|
+
future: { boxShadow: '0 0 4px rgba(107,114,128,0.4)' },
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const iconChars: Record<string, string> = {
|
|
60
|
+
check: '\u2713',
|
|
61
|
+
exclamation: '!',
|
|
62
|
+
dash: '\u2014',
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Color-matched icon colors: dark on solid bg, light on transparent bg */
|
|
66
|
+
const iconColors: Record<StatusDotVariant, string> = {
|
|
67
|
+
success: '#0A5C52',
|
|
68
|
+
warning: '#6B4000',
|
|
69
|
+
error: '#5C1A1A',
|
|
70
|
+
neutral: '#D1D5DB',
|
|
71
|
+
'on-track': '#14B8A6',
|
|
72
|
+
deviation: '#FFB020',
|
|
73
|
+
future: '#6B7280',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sizeDimensions = {
|
|
77
|
+
sm: { size: 8, showIcon: false },
|
|
78
|
+
md: { size: 18, showIcon: true },
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isSolidVariant(variant: StatusDotVariant): boolean {
|
|
82
|
+
return variant in solidVariantColors
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function StatusDot({
|
|
86
|
+
variant,
|
|
87
|
+
icon,
|
|
88
|
+
size = 'sm',
|
|
89
|
+
glow,
|
|
90
|
+
label,
|
|
91
|
+
className,
|
|
92
|
+
...props
|
|
93
|
+
}: StatusDotProps) {
|
|
94
|
+
const config = sizeDimensions[size]
|
|
95
|
+
const solid = isSolidVariant(variant)
|
|
96
|
+
const ring = ringVariantStyles[variant]
|
|
97
|
+
|
|
98
|
+
const glowStyle = glow ? glowStyles[variant] : null
|
|
99
|
+
|
|
100
|
+
const dot = (
|
|
101
|
+
<View
|
|
102
|
+
style={[
|
|
103
|
+
{
|
|
104
|
+
width: config.size,
|
|
105
|
+
height: config.size,
|
|
106
|
+
borderRadius: 9999,
|
|
107
|
+
alignItems: 'center',
|
|
108
|
+
justifyContent: 'center',
|
|
109
|
+
flexShrink: 0,
|
|
110
|
+
},
|
|
111
|
+
solid ? { backgroundColor: solidVariantColors[variant] } : undefined,
|
|
112
|
+
ring ? (ring as Record<string, unknown>) : undefined,
|
|
113
|
+
glowStyle ? (glowStyle as Record<string, unknown>) : undefined,
|
|
114
|
+
]}
|
|
115
|
+
accessibilityRole="image"
|
|
116
|
+
accessibilityLabel={`${variant} status`}
|
|
117
|
+
testID="status-dot"
|
|
118
|
+
>
|
|
119
|
+
{config.showIcon && icon && (
|
|
120
|
+
<Text
|
|
121
|
+
style={{
|
|
122
|
+
fontSize: 11,
|
|
123
|
+
lineHeight: 11,
|
|
124
|
+
fontWeight: '900',
|
|
125
|
+
color: iconColors[variant],
|
|
126
|
+
}}
|
|
127
|
+
accessibilityElementsHidden
|
|
128
|
+
>
|
|
129
|
+
{iconChars[icon]}
|
|
130
|
+
</Text>
|
|
131
|
+
)}
|
|
132
|
+
</View>
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if (label) {
|
|
136
|
+
return (
|
|
137
|
+
<View
|
|
138
|
+
className={className}
|
|
139
|
+
style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}
|
|
140
|
+
accessibilityLabel={`${variant} status: ${label}`}
|
|
141
|
+
{...props}
|
|
142
|
+
>
|
|
143
|
+
<View accessibilityElementsHidden>{dot}</View>
|
|
144
|
+
<Text
|
|
145
|
+
style={{ fontSize: 12, lineHeight: 16, color: '#9CA3AF' }}
|
|
146
|
+
accessibilityElementsHidden
|
|
147
|
+
>
|
|
148
|
+
{label}
|
|
149
|
+
</Text>
|
|
150
|
+
</View>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<View className={className} {...props}>
|
|
156
|
+
{dot}
|
|
157
|
+
</View>
|
|
158
|
+
)
|
|
159
|
+
}
|