@titan-design/react-ui 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/MesoStatusCard-CQk71hSi.d.mts +77 -0
- package/dist/MesoStatusCard-CQk71hSi.d.ts +77 -0
- package/dist/bodymap.d.mts +246 -3
- package/dist/bodymap.d.ts +246 -3
- package/dist/bodymap.js +232 -185
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +1963 -11
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +193 -744
- package/dist/index.d.ts +193 -744
- package/dist/index.js +1551 -1856
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10770 -8213
- package/dist/index.mjs.map +1 -1
- package/dist/pages-D7Jlssvp.d.ts +791 -0
- package/dist/pages-MO0JCySL.d.mts +791 -0
- package/dist/pages.d.mts +4 -0
- package/dist/pages.d.ts +4 -0
- package/dist/pages.js +6694 -0
- package/dist/pages.js.map +1 -0
- package/dist/pages.mjs +6674 -0
- package/dist/pages.mjs.map +1 -0
- package/dist/theme/index.d.mts +473 -211
- package/dist/theme/index.d.ts +473 -211
- package/dist/theme/index.js +341 -127
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +2085 -2
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +148 -139
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +582 -1
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +11 -7
- package/src/components/custom/DateTime/DateTime.stories.tsx +82 -6
- package/src/components/custom/DateTime/DateTime.test.tsx +29 -3
- package/src/components/custom/DateTime/DateTime.tsx +55 -15
- package/src/components/custom/Gauge/Gauge.test.tsx +2 -2
- package/src/components/custom/Gauge/Gauge.tsx +6 -3
- package/src/components/custom/Scatter/Scatter.tsx +3 -5
- package/src/components/custom/Treemap/Treemap.tsx +3 -5
- package/src/components/custom/Typography/Typography.stories.tsx +45 -27
- package/src/components/custom/Typography/Typography.test.tsx +13 -3
- package/src/components/custom/Typography/Typography.tsx +5 -0
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +2 -1
- package/src/components/custom/Workout/BaseBadge.stories.tsx +3 -3
- package/src/components/custom/Workout/BaseBadge.test.tsx +1 -1
- package/src/components/custom/Workout/BaseBadge.tsx +1 -1
- package/src/components/custom/Workout/BodyMap.tsx +6 -3
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +6 -3
- package/src/components/custom/Workout/CapacityBandChart.test.tsx +2 -2
- package/src/components/custom/Workout/CapacityBandChart.tsx +9 -6
- package/src/components/custom/Workout/DeviationBar.test.tsx +1 -1
- package/src/components/custom/Workout/DeviationBar.tsx +7 -4
- package/src/components/custom/Workout/ExerciseCard.tsx +5 -1
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +19 -5
- package/src/components/custom/Workout/InputBar.tsx +2 -1
- package/src/components/custom/Workout/IntensityBar.test.tsx +112 -60
- package/src/components/custom/Workout/IntensityBar.tsx +113 -58
- package/src/components/custom/Workout/MesoCard.tsx +8 -3
- package/src/components/custom/Workout/MesoProgressBar.test.tsx +1 -1
- package/src/components/custom/Workout/MesoProgressBar.tsx +3 -2
- package/src/components/custom/Workout/MesoStatusCard.tsx +20 -13
- package/src/components/custom/Workout/MuscleGroupChip.tsx +7 -4
- package/src/components/custom/Workout/PlaceholderStrip.tsx +5 -9
- package/src/components/custom/Workout/PrBadge.tsx +7 -4
- package/src/components/custom/Workout/PrHistoryModal.tsx +6 -5
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +2 -1
- package/src/components/custom/Workout/README.md +50 -0
- package/src/components/custom/Workout/ReadinessCheck.tsx +7 -4
- package/src/components/custom/Workout/RestTimer.test.tsx +41 -10
- package/src/components/custom/Workout/RestTimer.tsx +50 -44
- package/src/components/custom/Workout/SetRow.tsx +12 -2
- package/src/components/custom/Workout/Sparkline.tsx +2 -1
- package/src/components/custom/Workout/StatusDot.test.tsx +2 -2
- package/src/components/custom/Workout/StatusDot.tsx +12 -9
- package/src/components/custom/Workout/StrengthTrendChart.tsx +9 -6
- package/src/components/custom/Workout/SupersetWrapper.tsx +2 -1
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +69 -2
- package/src/components/custom/Workout/TempoDisplay.test.tsx +35 -0
- package/src/components/custom/Workout/TempoDisplay.tsx +109 -3
- package/src/components/custom/Workout/VelocityStrip.test.tsx +89 -2
- package/src/components/custom/Workout/VelocityStrip.tsx +203 -38
- package/src/components/custom/Workout/WeekRow.test.tsx +2 -2
- package/src/components/custom/Workout/WeekRow.tsx +3 -2
- package/src/components/custom/Workout/WeightBadge.test.tsx +2 -2
- package/src/components/custom/Workout/WeightBadge.tsx +8 -4
- package/src/components/custom/Workout/WorkoutCard.tsx +5 -2
- package/src/components/custom/Workout/WorkoutPill.tsx +12 -8
- package/src/components/custom/Workout/icons.tsx +7 -0
- package/src/components/custom/Workout/index.ts +38 -47
- package/src/components/icons/SvgIcon.tsx +50 -0
- package/src/components/icons/icons.stories.tsx +57 -0
- package/src/components/icons/icons.test.tsx +43 -0
- package/src/components/icons/icons.tsx +45 -0
- package/src/components/icons/index.ts +4 -0
- package/src/components/index.ts +6 -0
- package/src/components/shell/BrandLockup.stories.tsx +27 -0
- package/src/components/shell/BrandLockup.test.tsx +23 -0
- package/src/components/shell/BrandLockup.tsx +47 -0
- package/src/components/shell/DeviceIndicator.stories.tsx +28 -0
- package/src/components/shell/DeviceIndicator.test.tsx +26 -0
- package/src/components/shell/DeviceIndicator.tsx +52 -0
- package/src/components/shell/DeviceMenu.stories.tsx +47 -0
- package/src/components/shell/DeviceMenu.test.tsx +40 -0
- package/src/components/shell/DeviceMenu.tsx +65 -0
- package/src/components/shell/DeviceRow.stories.tsx +38 -0
- package/src/components/shell/DeviceRow.test.tsx +37 -0
- package/src/components/shell/DeviceRow.tsx +58 -0
- package/src/components/shell/README.md +86 -0
- package/src/components/shell/SessionStatePill.stories.tsx +27 -0
- package/src/components/shell/SessionStatePill.test.tsx +25 -0
- package/src/components/shell/SessionStatePill.tsx +46 -0
- package/src/components/shell/TopBar.stories.tsx +49 -0
- package/src/components/shell/TopBar.test.tsx +36 -0
- package/src/components/shell/TopBar.tsx +89 -0
- package/src/components/shell/index.ts +8 -0
- package/src/components/ui/indicator/Indicator.stories.tsx +37 -1
- package/src/components/ui/indicator/Indicator.test.tsx +19 -1
- package/src/components/ui/indicator/Indicator.tsx +45 -6
- package/src/components/ui/indicator/index.ts +1 -1
- package/src/components/ui/spinner/Spinner.tsx +3 -2
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +2 -1
- package/src/index.ts +3 -2
- package/src/pages.ts +61 -0
- package/src/test/stories-smoke.test.tsx +50 -0
- package/src/theme/ColorSystem.stories.tsx +323 -0
- package/src/theme/Colors.stories.tsx +69 -1
- package/src/theme/barrel.ts +54 -0
- package/src/theme/config.ts +16 -3
- package/src/theme/extracted-colors-dataviz.ts +21 -0
- package/src/theme/extracted-colors-ui.ts +20 -0
- package/src/theme/global.css +64 -52
- package/src/theme/gradients.test.ts +25 -0
- package/src/theme/gradients.ts +36 -0
- package/src/theme/index.ts +8 -43
- package/src/theme/native-theming-guard.test.ts +117 -0
- package/src/theme/tokens/primitives.test.ts +232 -0
- package/src/theme/tokens/primitives.ts +247 -0
- package/src/theme/tokens/semantic.ts +93 -81
- package/src/theme/workout-tokens.ts +22 -21
- package/src/utils/avatar-color.ts +3 -3
- package/tailwind.config.js +10 -3
- package/dist/bodymap-BhG55xHM.d.mts +0 -318
- package/dist/bodymap-BhG55xHM.d.ts +0 -318
- package/dist/chunk-2SISKTWM.mjs +0 -1027
- package/dist/chunk-2SISKTWM.mjs.map +0 -1
- package/dist/chunk-7XPB4NAO.mjs +0 -1013
- package/dist/chunk-7XPB4NAO.mjs.map +0 -1
- package/dist/chunk-SNVKL5WZ.mjs +0 -883
- package/dist/chunk-SNVKL5WZ.mjs.map +0 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
2
|
import { Text } from 'react-native'
|
|
3
|
-
import {
|
|
3
|
+
import { DumbbellIcon } from './icons'
|
|
4
4
|
import {
|
|
5
5
|
BaseBadge,
|
|
6
6
|
baseBadgeSizeConfig,
|
|
7
7
|
type BaseBadgeSize,
|
|
8
8
|
type BaseBadgeProps,
|
|
9
9
|
} from './BaseBadge'
|
|
10
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
10
11
|
|
|
11
12
|
export type WeightBadgeSize = BaseBadgeSize
|
|
12
13
|
|
|
@@ -42,7 +43,7 @@ export function WeightBadge({
|
|
|
42
43
|
...props
|
|
43
44
|
}: WeightBadgeProps) {
|
|
44
45
|
const config = baseBadgeSizeConfig[size]
|
|
45
|
-
const textColor = isPr ? '
|
|
46
|
+
const textColor = isPr ? getSemanticColors('dark')['brand-primary'] : '#9CA3AF'
|
|
46
47
|
// Dumbbell reads clearly as a weight/strength metric and is unit-agnostic
|
|
47
48
|
// across estimated-1RM and N-rep-max contexts (vs. TrendingUp which implies
|
|
48
49
|
// change/trend rather than a load value).
|
|
@@ -61,7 +62,7 @@ export function WeightBadge({
|
|
|
61
62
|
className={className}
|
|
62
63
|
icon={
|
|
63
64
|
showIcon ? (
|
|
64
|
-
<
|
|
65
|
+
<DumbbellIcon size={config.iconSize} color={iconColor} strokeWidth={2} />
|
|
65
66
|
) : undefined
|
|
66
67
|
}
|
|
67
68
|
accessibilityLabel={fullLabel}
|
|
@@ -100,7 +101,10 @@ export function WeightBadge({
|
|
|
100
101
|
fontFamily: '"Space Grotesk", sans-serif',
|
|
101
102
|
fontSize: config.fontSize,
|
|
102
103
|
marginLeft: 4,
|
|
103
|
-
color:
|
|
104
|
+
color:
|
|
105
|
+
delta >= 0
|
|
106
|
+
? getSemanticColors('dark')['result-improve']
|
|
107
|
+
: getSemanticColors('dark')['result-degrade'],
|
|
104
108
|
}}
|
|
105
109
|
testID="weight-badge-delta"
|
|
106
110
|
>
|
|
@@ -6,6 +6,9 @@ import {
|
|
|
6
6
|
MuscleGroupChip,
|
|
7
7
|
type VolumeStatus,
|
|
8
8
|
} from './MuscleGroupChip'
|
|
9
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
10
|
+
|
|
11
|
+
const t = getSemanticColors('dark')
|
|
9
12
|
|
|
10
13
|
export type WorkoutStatus = 'completed' | 'today' | 'upcoming'
|
|
11
14
|
|
|
@@ -43,8 +46,8 @@ export interface WorkoutCardProps extends ViewProps {
|
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
const COLORS = {
|
|
46
|
-
statusSuccess: '
|
|
47
|
-
brandPrimary: '
|
|
49
|
+
statusSuccess: t['status-success'],
|
|
50
|
+
brandPrimary: t['brand-primary'],
|
|
48
51
|
borderDefault: '#1F1F1F',
|
|
49
52
|
brandPrimarySubtle: 'rgba(255,121,0,0.06)',
|
|
50
53
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
2
|
import { useEffect, useState } from 'react'
|
|
3
3
|
import { View, Text, Pressable, Animated, Easing, type ViewProps } from 'react-native'
|
|
4
|
+
import { getSemanticColors } from '../../../theme/tokens/semantic'
|
|
5
|
+
import { WORKOUT_PILL_DELOAD } from '../../../theme/extracted-colors-dataviz'
|
|
6
|
+
|
|
7
|
+
const t = getSemanticColors('dark')
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Spec statuses: completed | current | upcoming | deload.
|
|
@@ -19,30 +23,30 @@ export interface WorkoutPillProps extends ViewProps {
|
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
const statusBgColors: Record<WorkoutPillStatus, string> = {
|
|
22
|
-
completed: 'rgba(
|
|
26
|
+
completed: 'rgba(46,213,115,0.15)',
|
|
23
27
|
current: 'rgba(255,121,0,0.15)',
|
|
24
28
|
next: 'transparent',
|
|
25
29
|
upcoming: '#1C1C1C',
|
|
26
30
|
missed: 'rgba(209,67,67,0.1)',
|
|
27
|
-
deload: 'rgba(
|
|
31
|
+
deload: 'rgba(186,41,150,0.15)',
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
const statusBorderStyles: Record<WorkoutPillStatus, Record<string, unknown>> = {
|
|
31
|
-
completed: { borderWidth: 1, borderColor: 'rgba(
|
|
35
|
+
completed: { borderWidth: 1, borderColor: 'rgba(46,213,115,0.3)' },
|
|
32
36
|
current: { borderWidth: 1, borderColor: 'rgba(255,121,0,0.3)' },
|
|
33
37
|
next: { borderWidth: 1, borderColor: 'rgba(255,121,0,0.4)' },
|
|
34
38
|
upcoming: { borderWidth: 1, borderColor: '#1F1F1F' },
|
|
35
39
|
missed: { borderWidth: 1, borderColor: 'rgba(209,67,67,0.25)' },
|
|
36
|
-
deload: { borderWidth: 1, borderColor: 'rgba(
|
|
40
|
+
deload: { borderWidth: 1, borderColor: 'rgba(186,41,150,0.3)' },
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
const textColors: Record<WorkoutPillStatus, string> = {
|
|
40
|
-
completed: '
|
|
41
|
-
current: '
|
|
42
|
-
next: '
|
|
44
|
+
completed: t['status-success'],
|
|
45
|
+
current: t['brand-primary'],
|
|
46
|
+
next: t['brand-primary'],
|
|
43
47
|
upcoming: '#6B7280',
|
|
44
48
|
missed: 'rgba(209,67,67,0.7)',
|
|
45
|
-
deload:
|
|
49
|
+
deload: WORKOUT_PILL_DELOAD,
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
function usePulse(enabled: boolean) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Workout icons were promoted to the shared icon primitive
|
|
2
|
+
// (`components/icons`). This shim re-exports them so existing Workout consumers
|
|
3
|
+
// keep their local `./icons` import unchanged.
|
|
4
|
+
export { DumbbellIcon, StarIcon } from '../../icons'
|
|
5
|
+
|
|
6
|
+
/** @deprecated Use `IconProps` from `components/icons`. */
|
|
7
|
+
export type { IconProps as WorkoutIconProps } from '../../icons'
|
|
@@ -8,7 +8,12 @@ export { WeightBadge, type WeightBadgeProps, type WeightBadgeSize } from './Weig
|
|
|
8
8
|
export { PrBadge, type PrBadgeProps, type PRType } from './PrBadge'
|
|
9
9
|
export { StatusDot, type StatusDotVariant, type StatusDotProps } from './StatusDot'
|
|
10
10
|
export { PlaceholderStrip, type PlaceholderStripProps } from './PlaceholderStrip'
|
|
11
|
-
export {
|
|
11
|
+
export {
|
|
12
|
+
TempoDisplay,
|
|
13
|
+
type TempoDisplayProps,
|
|
14
|
+
type TempoLivePhase,
|
|
15
|
+
type TempoLiveState,
|
|
16
|
+
} from './TempoDisplay'
|
|
12
17
|
export {
|
|
13
18
|
TempoBar,
|
|
14
19
|
type TempoBarProps,
|
|
@@ -24,6 +29,7 @@ export { WorkoutPill, type WorkoutPillProps, type WorkoutPillStatus } from './Wo
|
|
|
24
29
|
export {
|
|
25
30
|
VelocityStrip,
|
|
26
31
|
type VelocityStripProps,
|
|
32
|
+
type VelocityZoneBandProp,
|
|
27
33
|
getVelocityZoneColor,
|
|
28
34
|
getVelocityZoneName,
|
|
29
35
|
calculateVelocityLoss,
|
|
@@ -103,37 +109,29 @@ export type {
|
|
|
103
109
|
TrainingStatusMuscle,
|
|
104
110
|
TrainingStatusSummary,
|
|
105
111
|
} from './TrainingStatusPage'
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
type ProgramNavLevel,
|
|
119
|
-
type ProgramSelection,
|
|
120
|
-
type ProgramBreadcrumb,
|
|
112
|
+
// ActiveWorkoutPage / ExerciseDetailPage / ProgramPlanningPage are page-level
|
|
113
|
+
// organisms demoted to the `@titan-design/react-ui/pages` subpath (titan 0.5.0).
|
|
114
|
+
// Their VALUE exports (components + derivation helpers) live there; only
|
|
115
|
+
// type-only re-exports stay on this barrel (erased at build, no runtime pull).
|
|
116
|
+
export type {
|
|
117
|
+
ProgramPlanningPageProps,
|
|
118
|
+
PlanMeso,
|
|
119
|
+
PlanWeek,
|
|
120
|
+
PlanWorkout,
|
|
121
|
+
ProgramNavLevel,
|
|
122
|
+
ProgramSelection,
|
|
123
|
+
ProgramBreadcrumb,
|
|
121
124
|
} from './ProgramPlanningPage'
|
|
122
|
-
export {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
type ExerciseTrend,
|
|
133
|
-
type ExerciseVbtSet,
|
|
134
|
-
type ExerciseVbt,
|
|
135
|
-
type ExerciseDetailStats,
|
|
136
|
-
type VbtSummary,
|
|
125
|
+
export type {
|
|
126
|
+
ExerciseDetailPageProps,
|
|
127
|
+
ExerciseDetailTab,
|
|
128
|
+
ExerciseDetailHeader,
|
|
129
|
+
ExerciseDetailEntry,
|
|
130
|
+
ExerciseTrend,
|
|
131
|
+
ExerciseVbtSet,
|
|
132
|
+
ExerciseVbt,
|
|
133
|
+
ExerciseDetailStats,
|
|
134
|
+
VbtSummary,
|
|
137
135
|
} from './ExerciseDetailPage'
|
|
138
136
|
// muscleTaxonomy imports `react-native-body-highlighter` at runtime. Its value
|
|
139
137
|
// exports (incl. the MuscleGroup / SimpleMuscleGroup enums) live behind the
|
|
@@ -144,20 +142,13 @@ export type {
|
|
|
144
142
|
MovementCategory,
|
|
145
143
|
VolumeLandmarks,
|
|
146
144
|
} from './muscleTaxonomy'
|
|
147
|
-
export {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
type ActiveWorkoutExercise,
|
|
157
|
-
type ActiveWorkoutSuperset,
|
|
158
|
-
type ActiveWorkoutInput,
|
|
159
|
-
type ActiveWorkoutRest,
|
|
160
|
-
type ActiveExerciseStatus,
|
|
161
|
-
type WorkoutProgress,
|
|
162
|
-
type WorkoutGroup,
|
|
145
|
+
export type {
|
|
146
|
+
ActiveWorkoutPageProps,
|
|
147
|
+
ActiveWorkoutExercise,
|
|
148
|
+
ActiveWorkoutSuperset,
|
|
149
|
+
ActiveWorkoutInput,
|
|
150
|
+
ActiveWorkoutRest,
|
|
151
|
+
ActiveExerciseStatus,
|
|
152
|
+
WorkoutProgress,
|
|
153
|
+
WorkoutGroup,
|
|
163
154
|
} from './ActiveWorkoutPage'
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface IconProps {
|
|
4
|
+
/** Rendered width/height in px (viewBox is fixed 24×24). */
|
|
5
|
+
size?: number
|
|
6
|
+
/** Stroke color (or fill, for solid icons). Defaults to `currentColor` so a wrapper's text token drives it. */
|
|
7
|
+
color?: string
|
|
8
|
+
/** Stroke width for outline icons. */
|
|
9
|
+
strokeWidth?: number
|
|
10
|
+
/** Fill color; defaults to `none` (outline). */
|
|
11
|
+
fill?: string
|
|
12
|
+
/** Accessible title — sets `role="img"` + `<title>`. Omit for a decorative icon (hidden from AT). */
|
|
13
|
+
title?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SvgIconProps extends IconProps {
|
|
17
|
+
children: ReactNode
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Base SVG wrapper for the titan icon set — a 24×24 web-`<svg>` with the shared
|
|
22
|
+
* accessibility contract (titled → `role="img"` + `<title>`; untitled →
|
|
23
|
+
* `aria-hidden`) and `currentColor` inheritance. Icons pass their paths as
|
|
24
|
+
* children plus their stroke/fill defaults.
|
|
25
|
+
*/
|
|
26
|
+
export function SvgIcon({
|
|
27
|
+
size = 24,
|
|
28
|
+
color = 'currentColor',
|
|
29
|
+
strokeWidth = 2,
|
|
30
|
+
fill = 'none',
|
|
31
|
+
title,
|
|
32
|
+
children,
|
|
33
|
+
}: SvgIconProps) {
|
|
34
|
+
return (
|
|
35
|
+
<svg
|
|
36
|
+
width={size}
|
|
37
|
+
height={size}
|
|
38
|
+
viewBox="0 0 24 24"
|
|
39
|
+
fill={fill}
|
|
40
|
+
stroke={color}
|
|
41
|
+
strokeWidth={strokeWidth}
|
|
42
|
+
strokeLinecap="round"
|
|
43
|
+
strokeLinejoin="round"
|
|
44
|
+
{...(title ? { role: 'img', 'aria-label': title } : { 'aria-hidden': true })}
|
|
45
|
+
>
|
|
46
|
+
{title ? <title>{title}</title> : null}
|
|
47
|
+
{children}
|
|
48
|
+
</svg>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { View, Text } from 'react-native'
|
|
4
|
+
import { VoltrasMark, BluetoothIcon, DumbbellIcon, StarIcon } from './icons'
|
|
5
|
+
|
|
6
|
+
const meta: Meta = {
|
|
7
|
+
title: 'Icons',
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
'The shared icon primitive — 24×24 web-`<svg>` glyphs on a common `SvgIcon` base ' +
|
|
14
|
+
'(a11y contract: titled → `role="img"`, untitled → `aria-hidden`; `currentColor` inheritance). ' +
|
|
15
|
+
'Reusable design-system-wide (the shell, Workout badges, etc. compose these).',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
export default meta
|
|
21
|
+
type Story = StoryObj
|
|
22
|
+
|
|
23
|
+
function Swatch({ label, children }: { label: string; children: ReactNode }) {
|
|
24
|
+
return (
|
|
25
|
+
<View className="items-center gap-2">
|
|
26
|
+
{children}
|
|
27
|
+
<Text className="font-mono text-[10px] text-text-tertiary">{label}</Text>
|
|
28
|
+
</View>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const All: Story = {
|
|
33
|
+
render: () => (
|
|
34
|
+
<View className="flex-row gap-8 p-4 items-start">
|
|
35
|
+
<Swatch label="VoltrasMark">
|
|
36
|
+
<View className="text-brand-primary">
|
|
37
|
+
<VoltrasMark size={28} />
|
|
38
|
+
</View>
|
|
39
|
+
</Swatch>
|
|
40
|
+
<Swatch label="BluetoothIcon">
|
|
41
|
+
<View className="text-status-success">
|
|
42
|
+
<BluetoothIcon size={28} />
|
|
43
|
+
</View>
|
|
44
|
+
</Swatch>
|
|
45
|
+
<Swatch label="DumbbellIcon">
|
|
46
|
+
<View className="text-text-primary">
|
|
47
|
+
<DumbbellIcon size={28} />
|
|
48
|
+
</View>
|
|
49
|
+
</Swatch>
|
|
50
|
+
<Swatch label="StarIcon (fill)">
|
|
51
|
+
<View className="text-brand-primary">
|
|
52
|
+
<StarIcon size={28} fill="currentColor" />
|
|
53
|
+
</View>
|
|
54
|
+
</Swatch>
|
|
55
|
+
</View>
|
|
56
|
+
),
|
|
57
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { VoltrasMark, BluetoothIcon, DumbbellIcon, StarIcon } from './icons'
|
|
5
|
+
import { SvgIcon } from './SvgIcon'
|
|
6
|
+
|
|
7
|
+
describe('icon primitives', () => {
|
|
8
|
+
it('every icon renders an svg', () => {
|
|
9
|
+
;[VoltrasMark, BluetoothIcon, DumbbellIcon, StarIcon].forEach((Icon) => {
|
|
10
|
+
const { container, unmount } = render(<Icon />)
|
|
11
|
+
expect(container.querySelector('svg')).toBeInTheDocument()
|
|
12
|
+
unmount()
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('a titled icon is exposed as an accessible image', () => {
|
|
17
|
+
render(<BluetoothIcon title="Devices" />)
|
|
18
|
+
expect(screen.getByRole('img', { name: 'Devices' })).toBeInTheDocument()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('an untitled icon is decorative (aria-hidden, no role)', () => {
|
|
22
|
+
const { container } = render(<VoltrasMark />)
|
|
23
|
+
const svg = container.querySelector('svg')!
|
|
24
|
+
expect(svg.getAttribute('aria-hidden')).toBe('true')
|
|
25
|
+
expect(svg.getAttribute('role')).toBeNull()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('honors size + strokeWidth via the SvgIcon base', () => {
|
|
29
|
+
const { container } = render(
|
|
30
|
+
<SvgIcon size={32} strokeWidth={3}>
|
|
31
|
+
<path d="M0 0" />
|
|
32
|
+
</SvgIcon>
|
|
33
|
+
)
|
|
34
|
+
const svg = container.querySelector('svg')!
|
|
35
|
+
expect(svg.getAttribute('width')).toBe('32')
|
|
36
|
+
expect(svg.getAttribute('stroke-width')).toBe('3')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('a decorative icon has no accessibility violations', async () => {
|
|
40
|
+
const { container } = render(<StarIcon />)
|
|
41
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { SvgIcon, type IconProps } from './SvgIcon'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Voltras brand mark — the ◇ diamond (outline). Pass `fill` for a solid mark.
|
|
5
|
+
*/
|
|
6
|
+
export function VoltrasMark({ size = 14, ...props }: IconProps) {
|
|
7
|
+
return (
|
|
8
|
+
<SvgIcon size={size} {...props}>
|
|
9
|
+
<path d="M12 2 22 12 12 22 2 12Z" />
|
|
10
|
+
</SvgIcon>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Bluetooth glyph (stroked) — e.g. device-connection state (color via `currentColor`).
|
|
16
|
+
*/
|
|
17
|
+
export function BluetoothIcon({ size = 18, ...props }: IconProps) {
|
|
18
|
+
return (
|
|
19
|
+
<SvgIcon size={size} {...props}>
|
|
20
|
+
<path d="M7 7 17 17 12 22 12 2 17 7 7 17" />
|
|
21
|
+
</SvgIcon>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Dumbbell glyph (mirrors lucide-react `Dumbbell`). Outline-only by default. */
|
|
26
|
+
export function DumbbellIcon(props: IconProps) {
|
|
27
|
+
return (
|
|
28
|
+
<SvgIcon {...props}>
|
|
29
|
+
<path d="M14.4 14.4 9.6 9.6" />
|
|
30
|
+
<path d="M18.657 21.485a2 2 0 1 1-2.829-2.828l-1.767 1.768a2 2 0 1 1-2.829-2.829l6.364-6.364a2 2 0 1 1 2.829 2.829l-1.768 1.767a2 2 0 1 1 2.828 2.829z" />
|
|
31
|
+
<path d="m21.5 21.5-1.4-1.4" />
|
|
32
|
+
<path d="M3.9 3.9 2.5 2.5" />
|
|
33
|
+
<path d="M6.404 12.768a2 2 0 1 1-2.829-2.829l1.768-1.767a2 2 0 1 1-2.828-2.829l2.828-2.828a2 2 0 1 1 2.829 2.828l1.767-1.768a2 2 0 1 1 2.829 2.829z" />
|
|
34
|
+
</SvgIcon>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Star glyph (mirrors lucide-react `Star`). Pass `fill` for a solid star. */
|
|
39
|
+
export function StarIcon(props: IconProps) {
|
|
40
|
+
return (
|
|
41
|
+
<SvgIcon {...props}>
|
|
42
|
+
<path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" />
|
|
43
|
+
</SvgIcon>
|
|
44
|
+
)
|
|
45
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
// Icon primitives (shared, design-system-wide)
|
|
2
|
+
export * from './icons'
|
|
3
|
+
|
|
1
4
|
// Re-export all UI components
|
|
2
5
|
export * from './ui'
|
|
3
6
|
|
|
4
7
|
// Re-export custom components
|
|
5
8
|
export * from './custom'
|
|
9
|
+
|
|
10
|
+
// Re-export shell chrome components
|
|
11
|
+
export * from './shell'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { BrandLockup } from './BrandLockup'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof BrandLockup> = {
|
|
5
|
+
title: 'Shell/TopBar/BrandLockup',
|
|
6
|
+
component: BrandLockup,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { subtitle: 'wall dashboard', showSubtitle: true },
|
|
9
|
+
argTypes: {
|
|
10
|
+
subtitle: { control: 'text' },
|
|
11
|
+
showSubtitle: { control: 'boolean' },
|
|
12
|
+
},
|
|
13
|
+
parameters: {
|
|
14
|
+
docs: {
|
|
15
|
+
description: {
|
|
16
|
+
component:
|
|
17
|
+
'**Molecule.** Composes [VoltrasMark](?path=/docs/icons--docs) (icon) + ' +
|
|
18
|
+
'[Typography](?path=/docs/custom-typography--docs) (wordmark + subtitle). ' +
|
|
19
|
+
'Toggle `showSubtitle` to see the responsive collapse.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
export default meta
|
|
25
|
+
type Story = StoryObj<typeof BrandLockup>
|
|
26
|
+
|
|
27
|
+
export const Default: Story = {}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { BrandLockup } from './BrandLockup'
|
|
5
|
+
|
|
6
|
+
describe('BrandLockup', () => {
|
|
7
|
+
it('renders the wordmark', () => {
|
|
8
|
+
render(<BrandLockup />)
|
|
9
|
+
expect(screen.getByText('VOLTRAS')).toBeInTheDocument()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('shows the subtitle by default and hides it when showSubtitle is false', () => {
|
|
13
|
+
const { rerender } = render(<BrandLockup subtitle="wall dashboard" />)
|
|
14
|
+
expect(screen.getByText('/ wall dashboard')).toBeInTheDocument()
|
|
15
|
+
rerender(<BrandLockup subtitle="wall dashboard" showSubtitle={false} />)
|
|
16
|
+
expect(screen.queryByText('/ wall dashboard')).not.toBeInTheDocument()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('has no accessibility violations', async () => {
|
|
20
|
+
const { container } = render(<BrandLockup />)
|
|
21
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { View, type ViewProps } from 'react-native'
|
|
2
|
+
import { cn } from '../../utils/cn'
|
|
3
|
+
import { Typography } from '../custom/Typography'
|
|
4
|
+
import { VoltrasMark } from '../icons'
|
|
5
|
+
|
|
6
|
+
export interface BrandLockupProps extends ViewProps {
|
|
7
|
+
/** The "/ subtitle" that follows the wordmark. Default "wall dashboard". */
|
|
8
|
+
subtitle?: string
|
|
9
|
+
/** Show the subtitle. Hidden at tablet width and below (S1-Q4 responsive). */
|
|
10
|
+
showSubtitle?: boolean
|
|
11
|
+
className?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Product identity lockup for the top bar: ◇ mark + VOLTRAS wordmark + optional
|
|
16
|
+
* subtitle. Composes the VoltrasMark icon + Typography. S1 · BrandLockup.
|
|
17
|
+
*/
|
|
18
|
+
export function BrandLockup({
|
|
19
|
+
subtitle = 'wall dashboard',
|
|
20
|
+
showSubtitle = true,
|
|
21
|
+
className,
|
|
22
|
+
...props
|
|
23
|
+
}: BrandLockupProps) {
|
|
24
|
+
return (
|
|
25
|
+
<View className={cn('flex-row items-center gap-[7px]', className)} {...props}>
|
|
26
|
+
<View className="text-brand-primary">
|
|
27
|
+
<VoltrasMark size={14} color="currentColor" />
|
|
28
|
+
</View>
|
|
29
|
+
<Typography
|
|
30
|
+
variant="button"
|
|
31
|
+
color="primary"
|
|
32
|
+
className="text-[12px] font-extrabold tracking-[1.5px]"
|
|
33
|
+
>
|
|
34
|
+
VOLTRAS
|
|
35
|
+
</Typography>
|
|
36
|
+
{showSubtitle && subtitle ? (
|
|
37
|
+
<Typography
|
|
38
|
+
variant="button"
|
|
39
|
+
color="tertiary"
|
|
40
|
+
className="text-[10px] font-semibold tracking-[0.4px]"
|
|
41
|
+
>
|
|
42
|
+
/ {subtitle}
|
|
43
|
+
</Typography>
|
|
44
|
+
) : null}
|
|
45
|
+
</View>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { DeviceIndicator } from './DeviceIndicator'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof DeviceIndicator> = {
|
|
5
|
+
title: 'Shell/TopBar/DeviceMenu/DeviceIndicator',
|
|
6
|
+
component: DeviceIndicator,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { status: 'connected' },
|
|
9
|
+
argTypes: {
|
|
10
|
+
status: { control: 'select', options: ['connected', 'degraded', 'lost'] },
|
|
11
|
+
label: { control: 'text' },
|
|
12
|
+
onPress: { control: false },
|
|
13
|
+
},
|
|
14
|
+
parameters: {
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component:
|
|
18
|
+
'**Molecule.** Composes [BluetoothIcon](?path=/docs/icons--docs), color-coded by ' +
|
|
19
|
+
'connection state via a `text-status-*-vivid` token — the glyph color alone carries the fault ' +
|
|
20
|
+
'(no separate badge). Switch the `status` control to see connected / degraded / lost.',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
export default meta
|
|
26
|
+
type Story = StoryObj<typeof DeviceIndicator>
|
|
27
|
+
|
|
28
|
+
export const Default: Story = {}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { DeviceIndicator } from './DeviceIndicator'
|
|
4
|
+
|
|
5
|
+
describe('DeviceIndicator', () => {
|
|
6
|
+
it('renders each connection state', () => {
|
|
7
|
+
const states = ['connected', 'degraded', 'lost'] as const
|
|
8
|
+
states.forEach((status) => {
|
|
9
|
+
const { unmount, container } = render(<DeviceIndicator status={status} />)
|
|
10
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
11
|
+
unmount()
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('exposes a button role and fires onPress when interactive', () => {
|
|
16
|
+
const onPress = vi.fn()
|
|
17
|
+
render(<DeviceIndicator onPress={onPress} />)
|
|
18
|
+
fireEvent.click(screen.getByRole('button', { name: 'Devices' }))
|
|
19
|
+
expect(onPress).toHaveBeenCalledTimes(1)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('renders the lost state (red glyph, no separate badge)', () => {
|
|
23
|
+
const { container } = render(<DeviceIndicator status="lost" />)
|
|
24
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { View, Pressable, type ViewProps } from 'react-native'
|
|
2
|
+
import { cn } from '../../utils/cn'
|
|
3
|
+
import { BluetoothIcon } from '../icons'
|
|
4
|
+
|
|
5
|
+
export type DeviceConnState = 'connected' | 'degraded' | 'lost'
|
|
6
|
+
|
|
7
|
+
export interface DeviceIndicatorProps extends ViewProps {
|
|
8
|
+
/** Aggregate connection state (worst-of the bound devices) → glyph color. */
|
|
9
|
+
status?: DeviceConnState
|
|
10
|
+
/** Tap handler. When omitted the glyph is non-interactive (e.g. inside a PopoverTrigger). */
|
|
11
|
+
onPress?: () => void
|
|
12
|
+
/** Accessible label (names the glyph / the wrapping trigger button). */
|
|
13
|
+
label?: string
|
|
14
|
+
className?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// currentColor drives the stroke → color the wrapper's text token by state (vivid, matches LIVE).
|
|
18
|
+
// The glyph color alone carries the fault (red on lost) — no separate alert badge.
|
|
19
|
+
const statusTextClass: Record<DeviceConnState, string> = {
|
|
20
|
+
connected: 'text-status-success',
|
|
21
|
+
degraded: 'text-status-warning',
|
|
22
|
+
lost: 'text-status-error-vivid',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The device/BLE connection glyph — a color-coded bluetooth mark, no label.
|
|
27
|
+
* Opens the DeviceMenu when tapped. S1 · DeviceIndicator.
|
|
28
|
+
*/
|
|
29
|
+
export function DeviceIndicator({
|
|
30
|
+
status = 'connected',
|
|
31
|
+
onPress,
|
|
32
|
+
label = 'Devices',
|
|
33
|
+
className,
|
|
34
|
+
...props
|
|
35
|
+
}: DeviceIndicatorProps) {
|
|
36
|
+
const Wrapper = onPress ? Pressable : View
|
|
37
|
+
return (
|
|
38
|
+
<Wrapper
|
|
39
|
+
accessibilityLabel={label}
|
|
40
|
+
{...(onPress ? { onPress, accessibilityRole: 'button' } : {})}
|
|
41
|
+
className={cn(
|
|
42
|
+
'w-[26px] h-[28px] items-center justify-center rounded-[7px]',
|
|
43
|
+
statusTextClass[status],
|
|
44
|
+
className
|
|
45
|
+
)}
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
48
|
+
{/* the SVG title names the wrapping trigger button (RNW drops aria-label on non-accessible Views) */}
|
|
49
|
+
<BluetoothIcon size={18} color="currentColor" title={label} />
|
|
50
|
+
</Wrapper>
|
|
51
|
+
)
|
|
52
|
+
}
|