@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
|
@@ -8,7 +8,10 @@ const meta: Meta<typeof Indicator> = {
|
|
|
8
8
|
tags: ['autodocs'],
|
|
9
9
|
argTypes: {
|
|
10
10
|
size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] },
|
|
11
|
-
color: {
|
|
11
|
+
color: {
|
|
12
|
+
control: 'select',
|
|
13
|
+
options: ['default', 'primary', 'success', 'error', 'warning', 'info'],
|
|
14
|
+
},
|
|
12
15
|
glow: { control: 'boolean' },
|
|
13
16
|
ring: { control: 'boolean' },
|
|
14
17
|
},
|
|
@@ -72,3 +75,36 @@ export const CustomColor: Story = {
|
|
|
72
75
|
</View>
|
|
73
76
|
),
|
|
74
77
|
}
|
|
78
|
+
|
|
79
|
+
export const Pulse: Story = {
|
|
80
|
+
render: () => (
|
|
81
|
+
<View className="flex-row gap-6 items-center">
|
|
82
|
+
<Indicator size="md" color="success" pulse />
|
|
83
|
+
<Indicator size="md" color="success" pulse="ping" />
|
|
84
|
+
<Indicator size="md" color="warning" />
|
|
85
|
+
<Indicator size="md" color="default" />
|
|
86
|
+
</View>
|
|
87
|
+
),
|
|
88
|
+
parameters: {
|
|
89
|
+
docs: {
|
|
90
|
+
description: {
|
|
91
|
+
story:
|
|
92
|
+
'Opacity pulse (default), expanding-ring pulse (`pulse="ping"`, green for live), solid, dim.',
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const Vivid: Story = {
|
|
99
|
+
render: () => (
|
|
100
|
+
<View className="flex-row gap-4 items-center">
|
|
101
|
+
<Indicator size="md" color="success" />
|
|
102
|
+
<Indicator size="md" color="success" />
|
|
103
|
+
<Indicator size="md" color="error" />
|
|
104
|
+
<Indicator size="md" color="error-vivid" />
|
|
105
|
+
</View>
|
|
106
|
+
),
|
|
107
|
+
parameters: {
|
|
108
|
+
docs: { description: { story: 'Standard status vs the vivid palette (success/error).' } },
|
|
109
|
+
},
|
|
110
|
+
}
|
|
@@ -18,7 +18,15 @@ describe('Indicator', () => {
|
|
|
18
18
|
})
|
|
19
19
|
|
|
20
20
|
it('renders with all color options', () => {
|
|
21
|
-
const colors = [
|
|
21
|
+
const colors = [
|
|
22
|
+
'default',
|
|
23
|
+
'primary',
|
|
24
|
+
'success',
|
|
25
|
+
'error',
|
|
26
|
+
'warning',
|
|
27
|
+
'info',
|
|
28
|
+
'error-vivid',
|
|
29
|
+
] as const
|
|
22
30
|
colors.forEach((color) => {
|
|
23
31
|
const { unmount } = render(<Indicator color={color} />)
|
|
24
32
|
unmount()
|
|
@@ -45,6 +53,16 @@ describe('Indicator', () => {
|
|
|
45
53
|
expect(container.firstChild).toBeInTheDocument()
|
|
46
54
|
})
|
|
47
55
|
|
|
56
|
+
it('supports pulse prop (opacity)', () => {
|
|
57
|
+
const { container } = render(<Indicator pulse color="success" />)
|
|
58
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('supports the ping pulse variant', () => {
|
|
62
|
+
const { container } = render(<Indicator pulse="ping" color="success" />)
|
|
63
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
64
|
+
})
|
|
65
|
+
|
|
48
66
|
describe('accessibility', () => {
|
|
49
67
|
it('has no accessibility violations', async () => {
|
|
50
68
|
const { container } = render(<Indicator color="success" />)
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
1
|
import { View, type ViewProps } from 'react-native'
|
|
3
2
|
import { cn } from '../../../utils/cn'
|
|
4
3
|
|
|
5
4
|
export type IndicatorSize = 'xs' | 'sm' | 'md' | 'lg'
|
|
6
|
-
export type IndicatorColor =
|
|
5
|
+
export type IndicatorColor =
|
|
6
|
+
| 'default'
|
|
7
|
+
| 'primary'
|
|
8
|
+
| 'success'
|
|
9
|
+
| 'error'
|
|
10
|
+
| 'warning'
|
|
11
|
+
| 'info'
|
|
12
|
+
| 'error-vivid'
|
|
13
|
+
|
|
14
|
+
/** How a pulsing indicator animates: a subtle opacity fade, or an expanding ring. */
|
|
15
|
+
export type IndicatorPulse = 'opacity' | 'ping'
|
|
7
16
|
|
|
8
17
|
export interface IndicatorProps extends ViewProps {
|
|
9
18
|
/** Dot size */
|
|
@@ -16,6 +25,8 @@ export interface IndicatorProps extends ViewProps {
|
|
|
16
25
|
glow?: boolean
|
|
17
26
|
/** Add a ring border */
|
|
18
27
|
ring?: boolean
|
|
28
|
+
/** Animate for live/active status. `true` = opacity fade; `'ping'` = expanding ring. */
|
|
29
|
+
pulse?: boolean | IndicatorPulse
|
|
19
30
|
/** Additional className */
|
|
20
31
|
className?: string
|
|
21
32
|
}
|
|
@@ -34,6 +45,7 @@ const colorStyles: Record<IndicatorColor, string> = {
|
|
|
34
45
|
error: 'bg-status-error',
|
|
35
46
|
warning: 'bg-status-warning',
|
|
36
47
|
info: 'bg-status-info',
|
|
48
|
+
'error-vivid': 'bg-status-error-vivid',
|
|
37
49
|
}
|
|
38
50
|
|
|
39
51
|
export function Indicator({
|
|
@@ -42,14 +54,39 @@ export function Indicator({
|
|
|
42
54
|
customColor,
|
|
43
55
|
glow = false,
|
|
44
56
|
ring = false,
|
|
57
|
+
pulse = false,
|
|
45
58
|
className,
|
|
46
59
|
style,
|
|
47
60
|
...props
|
|
48
61
|
}: IndicatorProps) {
|
|
62
|
+
const pulseMode: IndicatorPulse | false = pulse === true ? 'opacity' : pulse || false
|
|
63
|
+
const colorClass = !customColor ? colorStyles[color] : undefined
|
|
64
|
+
const colorStyle = customColor ? { backgroundColor: customColor } : undefined
|
|
65
|
+
|
|
66
|
+
// Expanding-ring pulse: a ping layer behind the solid dot (for live/active status).
|
|
67
|
+
if (pulseMode === 'ping') {
|
|
68
|
+
return (
|
|
69
|
+
<View className={cn('relative', sizeStyles[size], className)} style={style} {...props}>
|
|
70
|
+
<View
|
|
71
|
+
className={cn('absolute inset-0 rounded-full animate-ping', colorClass)}
|
|
72
|
+
style={colorStyle}
|
|
73
|
+
/>
|
|
74
|
+
<View
|
|
75
|
+
className={cn(
|
|
76
|
+
'relative rounded-full',
|
|
77
|
+
sizeStyles[size],
|
|
78
|
+
colorClass,
|
|
79
|
+
ring && 'border-2 border-background-base'
|
|
80
|
+
)}
|
|
81
|
+
style={colorStyle}
|
|
82
|
+
/>
|
|
83
|
+
</View>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
49
87
|
const dynamicStyle = {
|
|
50
88
|
...(typeof style === 'object' ? style : {}),
|
|
51
|
-
...(
|
|
52
|
-
...(glow && !customColor ? {} : {}),
|
|
89
|
+
...(colorStyle ?? {}),
|
|
53
90
|
...(glow && customColor ? { boxShadow: `0 0 6px ${customColor}` } : {}),
|
|
54
91
|
}
|
|
55
92
|
|
|
@@ -58,12 +95,14 @@ export function Indicator({
|
|
|
58
95
|
className={cn(
|
|
59
96
|
'rounded-full shrink-0',
|
|
60
97
|
sizeStyles[size],
|
|
61
|
-
|
|
98
|
+
colorClass,
|
|
99
|
+
pulseMode === 'opacity' && 'animate-pulse',
|
|
62
100
|
ring && 'border-2 border-background-base',
|
|
63
|
-
glow && !customColor && color === 'success' && 'shadow-[0_0_6px_rgba(
|
|
101
|
+
glow && !customColor && color === 'success' && 'shadow-[0_0_6px_rgba(46,213,115,0.6)]',
|
|
64
102
|
glow && !customColor && color === 'error' && 'shadow-[0_0_6px_rgba(239,68,68,0.6)]',
|
|
65
103
|
glow && !customColor && color === 'warning' && 'shadow-[0_0_6px_rgba(245,158,11,0.6)]',
|
|
66
104
|
glow && !customColor && color === 'primary' && 'shadow-[0_0_6px_rgba(255,121,0,0.6)]',
|
|
105
|
+
glow && !customColor && color === 'error-vivid' && 'shadow-[0_0_6px_rgba(255,71,87,0.6)]',
|
|
67
106
|
className
|
|
68
107
|
)}
|
|
69
108
|
style={Object.keys(dynamicStyle).length > 0 ? dynamicStyle : style}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { Indicator } from './Indicator'
|
|
2
|
-
export type { IndicatorProps, IndicatorSize, IndicatorColor } from './Indicator'
|
|
2
|
+
export type { IndicatorProps, IndicatorSize, IndicatorColor, IndicatorPulse } from './Indicator'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { ActivityIndicator, View, type ViewProps } from 'react-native'
|
|
3
3
|
import { cn } from '../../../utils/cn'
|
|
4
|
+
import { SPINNER_PRIMARY, SPINNER_SECONDARY } from '../../../theme/extracted-colors-ui'
|
|
4
5
|
|
|
5
6
|
export type SpinnerSize = 'sm' | 'md' | 'lg' | 'xl'
|
|
6
7
|
export type SpinnerColor = 'primary' | 'secondary' | 'white' | 'default'
|
|
@@ -24,8 +25,8 @@ const sizeMap: Record<SpinnerSize, 'small' | 'large'> = {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const colorMap: Record<SpinnerColor, string> = {
|
|
27
|
-
primary:
|
|
28
|
-
secondary:
|
|
28
|
+
primary: SPINNER_PRIMARY,
|
|
29
|
+
secondary: SPINNER_SECONDARY,
|
|
29
30
|
white: '#FFFFFF',
|
|
30
31
|
default: '#6B7280',
|
|
31
32
|
}
|
|
@@ -2,6 +2,7 @@ import React, { useState, useCallback, createContext, useContext } from 'react'
|
|
|
2
2
|
import { View, Text, Pressable, type ViewProps, StyleSheet, Platform } from 'react-native'
|
|
3
3
|
import { cn } from '../../../utils/cn'
|
|
4
4
|
import { neumorphicShadows, getHoverColors } from '../../../theme'
|
|
5
|
+
import { resolveColor } from '../../../theme/resolve-color'
|
|
5
6
|
|
|
6
7
|
export type ToolbarButtonVariant = 'default' | 'neumorphic'
|
|
7
8
|
export type ToolbarButtonSize = 'sm' | 'md' | 'lg'
|
|
@@ -129,7 +130,7 @@ export function ToolbarButton({
|
|
|
129
130
|
|
|
130
131
|
// Determine icon color based on state
|
|
131
132
|
const getIconColor = () => {
|
|
132
|
-
if (isActive === true) return '
|
|
133
|
+
if (isActive === true) return resolveColor('brand-primary')
|
|
133
134
|
return showActive ? '#FFFFFF' : '#D1D1D1'
|
|
134
135
|
}
|
|
135
136
|
|
package/src/index.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// Components
|
|
4
4
|
export * from './components'
|
|
5
5
|
|
|
6
|
-
// Theme
|
|
7
|
-
|
|
6
|
+
// Theme (nativewind-free surface; ThemeProvider value lives on the /theme
|
|
7
|
+
// subpath so the root barrel does not pull the nativewind import chain)
|
|
8
|
+
export * from './theme/barrel'
|
|
8
9
|
|
|
9
10
|
// Utilities
|
|
10
11
|
export * from './utils'
|
package/src/pages.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Subpath entry: @titan-design/react-ui/pages
|
|
2
|
+
//
|
|
3
|
+
// Page-level organisms — phone-shaped reference screens (ActiveWorkoutPage,
|
|
4
|
+
// ExerciseDetailPage, ProgramPlanningPage). Demoted out of the root barrel for
|
|
5
|
+
// titan 0.5.0: they are full app screens, orphaned by both consumers, and their
|
|
6
|
+
// successors are the form-factor-responsive level views (plan B2). Their VALUE
|
|
7
|
+
// exports (components + derivation helpers) live HERE; type-only re-exports
|
|
8
|
+
// stay on the root barrel so consumers can still type props without importing
|
|
9
|
+
// the screens. Mirrors the `/bodymap` isolation pattern.
|
|
10
|
+
//
|
|
11
|
+
// TrainingStatusPage is NOT here — it depends on react-native-body-highlighter
|
|
12
|
+
// and lives under `@titan-design/react-ui/bodymap`.
|
|
13
|
+
export {
|
|
14
|
+
ActiveWorkoutPage,
|
|
15
|
+
countCompletedSets,
|
|
16
|
+
deriveWorkoutProgress,
|
|
17
|
+
findActiveExercise,
|
|
18
|
+
statusToCardState,
|
|
19
|
+
toActiveCardProps,
|
|
20
|
+
groupExercises,
|
|
21
|
+
type ActiveWorkoutPageProps,
|
|
22
|
+
type ActiveWorkoutExercise,
|
|
23
|
+
type ActiveWorkoutSuperset,
|
|
24
|
+
type ActiveWorkoutInput,
|
|
25
|
+
type ActiveWorkoutRest,
|
|
26
|
+
type ActiveExerciseStatus,
|
|
27
|
+
type WorkoutProgress,
|
|
28
|
+
type WorkoutGroup,
|
|
29
|
+
} from './components/custom/Workout/ActiveWorkoutPage'
|
|
30
|
+
export {
|
|
31
|
+
ExerciseDetailPage,
|
|
32
|
+
deriveExerciseStats,
|
|
33
|
+
toExerciseCardProps,
|
|
34
|
+
summarizeVbt,
|
|
35
|
+
EXERCISE_DETAIL_TABS,
|
|
36
|
+
type ExerciseDetailPageProps,
|
|
37
|
+
type ExerciseDetailTab,
|
|
38
|
+
type ExerciseDetailHeader,
|
|
39
|
+
type ExerciseDetailEntry,
|
|
40
|
+
type ExerciseTrend,
|
|
41
|
+
type ExerciseVbtSet,
|
|
42
|
+
type ExerciseVbt,
|
|
43
|
+
type ExerciseDetailStats,
|
|
44
|
+
type VbtSummary,
|
|
45
|
+
} from './components/custom/Workout/ExerciseDetailPage'
|
|
46
|
+
export {
|
|
47
|
+
ProgramPlanningPage,
|
|
48
|
+
deriveNavLevel,
|
|
49
|
+
toProgressBarMesos,
|
|
50
|
+
findMeso,
|
|
51
|
+
findWeek,
|
|
52
|
+
findWorkout,
|
|
53
|
+
buildBreadcrumbs,
|
|
54
|
+
type ProgramPlanningPageProps,
|
|
55
|
+
type PlanMeso,
|
|
56
|
+
type PlanWeek,
|
|
57
|
+
type PlanWorkout,
|
|
58
|
+
type ProgramNavLevel,
|
|
59
|
+
type ProgramSelection,
|
|
60
|
+
type ProgramBreadcrumb,
|
|
61
|
+
} from './components/custom/Workout/ProgramPlanningPage'
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render } from '@testing-library/react'
|
|
3
|
+
import { composeStories } from '@storybook/react'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Storybook → render-test bridge (TD-04.11 / VW-20).
|
|
7
|
+
*
|
|
8
|
+
* The design system has a complete `*.stories.tsx` set but no test consumed it.
|
|
9
|
+
* This uses Storybook's portable-stories `composeStories` to mount EVERY story
|
|
10
|
+
* (with its args/decorators applied) through the same react-native-web + jsdom
|
|
11
|
+
* pipeline the unit tests use, asserting each renders without throwing and emits
|
|
12
|
+
* output. It's broad, near-free coverage that catches "this organism/variant
|
|
13
|
+
* crashes on render" regressions across the whole component set — the long tail
|
|
14
|
+
* the hand-written per-organism tests don't enumerate.
|
|
15
|
+
*
|
|
16
|
+
* Project annotations (the preview's theme decorator) are intentionally NOT
|
|
17
|
+
* applied: a smoke test only needs to prove each story renders, and skipping
|
|
18
|
+
* them avoids importing global.css / addon-themes into the test env.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
type StoryModule = Record<string, any>
|
|
23
|
+
|
|
24
|
+
const storyModules = import.meta.glob<StoryModule>('../components/**/*.stories.tsx', {
|
|
25
|
+
eager: true,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('storybook stories render (composeStories smoke)', () => {
|
|
29
|
+
const entries = Object.entries(storyModules)
|
|
30
|
+
|
|
31
|
+
it('discovers a non-trivial number of story modules', () => {
|
|
32
|
+
expect(entries.length).toBeGreaterThan(10)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
for (const [file, mod] of entries) {
|
|
36
|
+
const label = file.replace('../components/', '')
|
|
37
|
+
const composed = composeStories(mod)
|
|
38
|
+
for (const [name, Story] of Object.entries(composed)) {
|
|
39
|
+
it(`${label} › ${name} renders`, () => {
|
|
40
|
+
// Smoke level: every story must MOUNT without throwing. `firstChild` is
|
|
41
|
+
// not asserted — portal/modal organisms (PrHistoryModal) render into
|
|
42
|
+
// document.body, and empty-state stories legitimately render null; the
|
|
43
|
+
// hand-written per-organism tests own the detailed output assertions.
|
|
44
|
+
const result = render(<Story />)
|
|
45
|
+
expect(result.unmount).toBeTypeOf('function')
|
|
46
|
+
result.unmount()
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
})
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
3
|
+
import { View, Text } from 'react-native'
|
|
4
|
+
import {
|
|
5
|
+
primitiveColors,
|
|
6
|
+
primitiveRamps,
|
|
7
|
+
categoricalPalette,
|
|
8
|
+
getCategoricalColor,
|
|
9
|
+
CATEGORICAL_CVD_SAFE_MAX,
|
|
10
|
+
divergingScale,
|
|
11
|
+
sequentialEffort,
|
|
12
|
+
bestTextColor,
|
|
13
|
+
} from './tokens/primitives'
|
|
14
|
+
|
|
15
|
+
const meta: Meta = {
|
|
16
|
+
title: 'Design Tokens/Color System',
|
|
17
|
+
tags: ['autodocs'],
|
|
18
|
+
}
|
|
19
|
+
export default meta
|
|
20
|
+
|
|
21
|
+
// ---- color math (self-contained; drives the accessibility panel) ----
|
|
22
|
+
const hexToRgb = (hex: string): [number, number, number] => {
|
|
23
|
+
const h = hex.replace('#', '')
|
|
24
|
+
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255) as [number, number, number]
|
|
25
|
+
}
|
|
26
|
+
const toHex = (v: number) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, '0')
|
|
27
|
+
const lin = (c: number) => (c >= 0.04045 ? ((c + 0.055) / 1.055) ** 2.4 : c / 12.92)
|
|
28
|
+
const gamma = (c: number) => (c >= 0.0031308 ? 1.055 * Math.max(0, c) ** (1 / 2.4) - 0.055 : 12.92 * c)
|
|
29
|
+
const relLum = (hex: string) => {
|
|
30
|
+
const [r, g, b] = hexToRgb(hex).map(lin)
|
|
31
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
32
|
+
}
|
|
33
|
+
const contrast = (a: string, b: string) => {
|
|
34
|
+
const [l1, l2] = [relLum(a), relLum(b)].sort((x, y) => y - x)
|
|
35
|
+
return (l1 + 0.05) / (l2 + 0.05)
|
|
36
|
+
}
|
|
37
|
+
const bestText = (hex: string) => (contrast(hex, '#000000') >= contrast(hex, '#FFFFFF') ? '#000000' : '#FFFFFF')
|
|
38
|
+
// Machado-2009 dichromacy simulation (severity 1.0)
|
|
39
|
+
const CVD = {
|
|
40
|
+
deuteranopia: [0.367322, 0.860646, -0.227968, 0.280085, 0.672501, 0.047413, -0.01182, 0.04294, 0.968881],
|
|
41
|
+
protanopia: [0.152286, 1.052583, -0.204868, 0.114503, 0.786281, 0.099216, -0.003882, -0.048116, 1.051998],
|
|
42
|
+
} as const
|
|
43
|
+
const simulate = (M: readonly number[], hex: string) => {
|
|
44
|
+
const [r, g, b] = hexToRgb(hex).map(lin)
|
|
45
|
+
const out = [
|
|
46
|
+
M[0] * r + M[1] * g + M[2] * b,
|
|
47
|
+
M[3] * r + M[4] * g + M[5] * b,
|
|
48
|
+
M[6] * r + M[7] * g + M[8] * b,
|
|
49
|
+
].map(gamma)
|
|
50
|
+
return '#' + out.map(toHex).join('')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const
|
|
54
|
+
|
|
55
|
+
function SectionTitle({ children }: { children: string }) {
|
|
56
|
+
return <Text className="text-lg font-bold text-text-primary mb-3 mt-2">{children}</Text>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function Ramp({ name, ramp }: { name: string; ramp: Record<number, string> }) {
|
|
60
|
+
return (
|
|
61
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 6 }}>
|
|
62
|
+
<Text className="text-text-secondary text-xs" style={{ width: 150 }}>
|
|
63
|
+
{name}
|
|
64
|
+
</Text>
|
|
65
|
+
<View style={{ flexDirection: 'row', flex: 1, borderRadius: 6, overflow: 'hidden', borderWidth: 1, borderColor: '#2C2C2C' }}>
|
|
66
|
+
{STEPS.map((s) => (
|
|
67
|
+
<View key={s} style={{ flex: 1, height: 30, backgroundColor: ramp[s] }} />
|
|
68
|
+
))}
|
|
69
|
+
</View>
|
|
70
|
+
</View>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function Chip({ hex, label, sub }: { hex: string; label?: string; sub?: string }) {
|
|
75
|
+
return (
|
|
76
|
+
<View style={{ alignItems: 'center' }}>
|
|
77
|
+
<View
|
|
78
|
+
style={{
|
|
79
|
+
width: 58,
|
|
80
|
+
height: 44,
|
|
81
|
+
borderRadius: 8,
|
|
82
|
+
backgroundColor: hex,
|
|
83
|
+
borderWidth: 1,
|
|
84
|
+
borderColor: '#2C2C2C',
|
|
85
|
+
alignItems: 'center',
|
|
86
|
+
justifyContent: 'center',
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
{label ? <Text style={{ color: bestText(hex), fontSize: 11, fontWeight: '700' }}>{label}</Text> : null}
|
|
90
|
+
</View>
|
|
91
|
+
{sub ? <Text className="text-text-secondary" style={{ fontSize: 8, marginTop: 2 }}>{sub}</Text> : null}
|
|
92
|
+
</View>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const RAMP_NAMES: Array<[keyof typeof primitiveRamps, string]> = [
|
|
97
|
+
['red', 'Red'],
|
|
98
|
+
['orange', 'Orange'],
|
|
99
|
+
['amber', 'Amber'],
|
|
100
|
+
['green', 'Green'],
|
|
101
|
+
['cyan', 'Cyan'],
|
|
102
|
+
['blue', 'Blue'],
|
|
103
|
+
['magenta', 'Magenta'],
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
const SEM = {
|
|
107
|
+
success: primitiveRamps.green[300],
|
|
108
|
+
warning: primitiveRamps.amber[300],
|
|
109
|
+
error: primitiveRamps.red[600],
|
|
110
|
+
info: primitiveRamps.blue[500],
|
|
111
|
+
deload: primitiveRamps.magenta[600],
|
|
112
|
+
brand: primitiveRamps.orange[400],
|
|
113
|
+
brandDark: primitiveRamps.orange[600],
|
|
114
|
+
brandSecondary: primitiveRamps.cyan[600],
|
|
115
|
+
spinner: primitiveRamps.cyan[600],
|
|
116
|
+
neutral: primitiveColors.neutral[500],
|
|
117
|
+
} as const
|
|
118
|
+
|
|
119
|
+
// BodyMap training-status diverging scale (from the divergingScale token).
|
|
120
|
+
const D3_STATUS = divergingScale
|
|
121
|
+
|
|
122
|
+
export const Overview: StoryObj = {
|
|
123
|
+
render: () => (
|
|
124
|
+
<View style={{ padding: 24, gap: 6 }}>
|
|
125
|
+
<Text className="text-2xl font-bold text-text-primary mb-1">Color System</Text>
|
|
126
|
+
<Text className="text-text-secondary mb-4">
|
|
127
|
+
Seven OKLCH tonal ramps and an ordered, colorblind-safe categorical palette. Ramps are the single
|
|
128
|
+
source of truth; the categorical references ramp steps.
|
|
129
|
+
</Text>
|
|
130
|
+
|
|
131
|
+
<SectionTitle>Tonal ramps</SectionTitle>
|
|
132
|
+
{RAMP_NAMES.map(([key, name]) => (
|
|
133
|
+
<Ramp key={key} name={name} ramp={primitiveRamps[key]} />
|
|
134
|
+
))}
|
|
135
|
+
|
|
136
|
+
<SectionTitle>Semantic tokens</SectionTitle>
|
|
137
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 18 }}>
|
|
138
|
+
{Object.entries(SEM).map(([label, hex]) => (
|
|
139
|
+
<View key={label} style={{ alignItems: 'center' }}>
|
|
140
|
+
<Chip hex={hex} />
|
|
141
|
+
<Text className="text-text-secondary" style={{ fontSize: 9, marginTop: 3 }}>
|
|
142
|
+
{label}
|
|
143
|
+
</Text>
|
|
144
|
+
</View>
|
|
145
|
+
))}
|
|
146
|
+
</View>
|
|
147
|
+
|
|
148
|
+
<SectionTitle>Categorical — ordered, nested, CVD-safe to 6</SectionTitle>
|
|
149
|
+
{(['default', 'dark'] as const).map((variant) => (
|
|
150
|
+
<View key={variant} style={{ marginBottom: 10 }}>
|
|
151
|
+
<Text className="text-text-secondary text-xs mb-1" style={{ textTransform: 'capitalize' }}>
|
|
152
|
+
{variant}
|
|
153
|
+
</Text>
|
|
154
|
+
<View style={{ flexDirection: 'row', gap: 6 }}>
|
|
155
|
+
{categoricalPalette[variant].map((hex, i) => (
|
|
156
|
+
<Chip key={i} hex={hex} label={`${i + 1}`} sub={i < CATEGORICAL_CVD_SAFE_MAX ? undefined : 'ext'} />
|
|
157
|
+
))}
|
|
158
|
+
</View>
|
|
159
|
+
</View>
|
|
160
|
+
))}
|
|
161
|
+
|
|
162
|
+
<SectionTitle>Data-viz scales</SectionTitle>
|
|
163
|
+
<Text className="text-text-secondary text-xs mb-2">
|
|
164
|
+
Diverging (BodyMap training-status) — light-centered so it reads in lightness and stays
|
|
165
|
+
colorblind-stable; direction is the blue↔red axis. Labels use per-cell best-contrast text.
|
|
166
|
+
</Text>
|
|
167
|
+
<View style={{ flexDirection: 'row', gap: 6, marginBottom: 12 }}>
|
|
168
|
+
{['under', 'maint', 'optimal', 'approach', 'over'].map((l, i) => (
|
|
169
|
+
<View key={l} style={{ flex: 1, height: 40, borderRadius: 6, backgroundColor: divergingScale[i], alignItems: 'center', justifyContent: 'center' }}>
|
|
170
|
+
<Text style={{ color: bestTextColor(divergingScale[i]), fontSize: 10, fontWeight: '700' }}>{l}</Text>
|
|
171
|
+
</View>
|
|
172
|
+
))}
|
|
173
|
+
</View>
|
|
174
|
+
<Text className="text-text-secondary text-xs mb-2">
|
|
175
|
+
Sequential effort (low → high) — an ordinal green→yellow→orange→red scale that IntensityBar uses
|
|
176
|
+
in full and VelocityStrip/RPE sub-samples.
|
|
177
|
+
</Text>
|
|
178
|
+
<View style={{ flexDirection: 'row', gap: 4, marginBottom: 4 }}>
|
|
179
|
+
{sequentialEffort.map((hex, i) => (
|
|
180
|
+
<View key={i} style={{ flex: 1, height: 34, borderRadius: 5, backgroundColor: hex, alignItems: 'center', justifyContent: 'center' }}>
|
|
181
|
+
<Text style={{ color: bestTextColor(hex), fontSize: 10, fontWeight: '700' }}>{i + 1}</Text>
|
|
182
|
+
</View>
|
|
183
|
+
))}
|
|
184
|
+
</View>
|
|
185
|
+
|
|
186
|
+
<SectionTitle>Component examples — correct color roles</SectionTitle>
|
|
187
|
+
<View style={{ flexDirection: 'row', gap: 20, flexWrap: 'wrap', alignItems: 'flex-start' }}>
|
|
188
|
+
{/* BodyMap muscle heatmap — training-status diverging (D3) */}
|
|
189
|
+
<Demo label="BodyMap · training status">
|
|
190
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', width: 132, gap: 4 }}>
|
|
191
|
+
{[
|
|
192
|
+
['Chest', 4], ['Quads', 2], ['Back', 3],
|
|
193
|
+
['Delts', 0], ['Biceps', 4], ['Calves', 1],
|
|
194
|
+
].map(([m, idx]) => (
|
|
195
|
+
<View key={m as string} style={{ width: 40, backgroundColor: D3_STATUS[idx as number], borderRadius: 5, paddingVertical: 6 }}>
|
|
196
|
+
<Text style={{ color: bestTextColor(D3_STATUS[idx as number]), fontSize: 8, fontWeight: '700', textAlign: 'center' }}>{m as string}</Text>
|
|
197
|
+
</View>
|
|
198
|
+
))}
|
|
199
|
+
</View>
|
|
200
|
+
</Demo>
|
|
201
|
+
{/* StatusDot — semantic */}
|
|
202
|
+
<Demo label="StatusDot · semantic">
|
|
203
|
+
<View style={{ gap: 6 }}>
|
|
204
|
+
{[['ok', SEM.success], ['warn', SEM.warning], ['err', SEM.error], ['neutral', SEM.neutral]].map(([l, c]) => (
|
|
205
|
+
<View key={l as string} style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
|
206
|
+
<View style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: c as string }} />
|
|
207
|
+
<Text className="text-text-secondary" style={{ fontSize: 11 }}>{l as string}</Text>
|
|
208
|
+
</View>
|
|
209
|
+
))}
|
|
210
|
+
</View>
|
|
211
|
+
</Demo>
|
|
212
|
+
{/* WorkoutPill — status */}
|
|
213
|
+
<Demo label="WorkoutPill">
|
|
214
|
+
<View style={{ flexDirection: 'row', gap: 6, flexWrap: 'wrap', maxWidth: 220 }}>
|
|
215
|
+
{[['Done', SEM.success], ['Now', SEM.brand], ['Next', SEM.neutral], ['Deload', SEM.deload]].map(([t, c]) => (
|
|
216
|
+
<View key={t as string} style={{ borderRadius: 99, borderWidth: 1, borderColor: c as string, paddingHorizontal: 10, paddingVertical: 3 }}>
|
|
217
|
+
<Text style={{ color: c as string, fontSize: 11, fontWeight: '700' }}>{t as string}</Text>
|
|
218
|
+
</View>
|
|
219
|
+
))}
|
|
220
|
+
</View>
|
|
221
|
+
</Demo>
|
|
222
|
+
{/* StrengthTrend legend — series → semantic */}
|
|
223
|
+
<Demo label="StrengthTrend · series">
|
|
224
|
+
<View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap' }}>
|
|
225
|
+
{[['1RM', SEM.brand], ['vel', SEM.success], ['miss', SEM.error], ['RPE', SEM.warning]].map(([l, c]) => (
|
|
226
|
+
<View key={l as string} style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
|
|
227
|
+
<View style={{ width: 14, height: 3, backgroundColor: c as string, borderRadius: 2 }} />
|
|
228
|
+
<Text className="text-text-secondary" style={{ fontSize: 10 }}>{l as string}</Text>
|
|
229
|
+
</View>
|
|
230
|
+
))}
|
|
231
|
+
</View>
|
|
232
|
+
</Demo>
|
|
233
|
+
{/* Treemap — ordered categorical */}
|
|
234
|
+
<Demo label="Treemap · categorical">
|
|
235
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', width: 140, gap: 2 }}>
|
|
236
|
+
{[46, 30, 30, 24, 24, 24, 20].map((h, i) => (
|
|
237
|
+
<View key={i} style={{ height: h, flexGrow: 1, flexBasis: h > 40 ? 60 : 40, backgroundColor: getCategoricalColor(i), borderRadius: 3 }} />
|
|
238
|
+
))}
|
|
239
|
+
</View>
|
|
240
|
+
</Demo>
|
|
241
|
+
{/* Spinner */}
|
|
242
|
+
<Demo label="Spinner">
|
|
243
|
+
<View style={{ width: 24, height: 24, borderRadius: 12, borderWidth: 3, borderColor: '#2C2C2C', borderTopColor: SEM.spinner }} />
|
|
244
|
+
</Demo>
|
|
245
|
+
{/* IntensityBar — full effort scale */}
|
|
246
|
+
<Demo label="IntensityBar · effort">
|
|
247
|
+
<View style={{ flexDirection: 'row', gap: 2, width: 132 }}>
|
|
248
|
+
{sequentialEffort.map((hex, i) => (
|
|
249
|
+
<View key={i} style={{ flex: 1, height: 20, backgroundColor: hex, borderRadius: 2 }} />
|
|
250
|
+
))}
|
|
251
|
+
</View>
|
|
252
|
+
</Demo>
|
|
253
|
+
{/* VelocityStrip / RPE — effort sub-sample */}
|
|
254
|
+
<Demo label="VelocityStrip · RPE">
|
|
255
|
+
<View style={{ flexDirection: 'row', gap: 3 }}>
|
|
256
|
+
{[sequentialEffort[0], sequentialEffort[2], sequentialEffort[3], sequentialEffort[5]].map((hex, i) => (
|
|
257
|
+
<View key={i} style={{ width: 30, height: 26, borderRadius: 4, backgroundColor: hex, alignItems: 'center', justifyContent: 'center' }}>
|
|
258
|
+
<Text style={{ color: bestTextColor(hex), fontSize: 9, fontWeight: '700' }}>{['S', 'M', 'F', 'X'][i]}</Text>
|
|
259
|
+
</View>
|
|
260
|
+
))}
|
|
261
|
+
</View>
|
|
262
|
+
</Demo>
|
|
263
|
+
</View>
|
|
264
|
+
</View>
|
|
265
|
+
),
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function Demo({ label, children }: { label: string; children: ReactNode }) {
|
|
269
|
+
return (
|
|
270
|
+
<View style={{ padding: 10, backgroundColor: '#1A1A1A', borderRadius: 8 }}>
|
|
271
|
+
<Text className="text-text-secondary" style={{ fontSize: 9, marginBottom: 6 }}>{label}</Text>
|
|
272
|
+
{children}
|
|
273
|
+
</View>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export const Accessibility: StoryObj = {
|
|
278
|
+
render: () => (
|
|
279
|
+
<View style={{ padding: 24 }}>
|
|
280
|
+
<Text className="text-2xl font-bold text-text-primary mb-1">Categorical Accessibility</Text>
|
|
281
|
+
<Text className="text-text-secondary mb-5">
|
|
282
|
+
In-place text contrast and colorblind simulation for the categorical series. Converging strips across
|
|
283
|
+
two swatches signal a collision for that viewer; all pairs hold ΔE ≥ 8 through the first {CATEGORICAL_CVD_SAFE_MAX}.
|
|
284
|
+
</Text>
|
|
285
|
+
|
|
286
|
+
{(['default', 'dark'] as const).map((variant) => {
|
|
287
|
+
const colors = categoricalPalette[variant]
|
|
288
|
+
const textColor = variant === 'dark' ? '#FFFFFF' : '#000000'
|
|
289
|
+
return (
|
|
290
|
+
<View key={variant} style={{ marginBottom: 22 }}>
|
|
291
|
+
<Text className="text-lg font-bold text-text-primary mb-2" style={{ textTransform: 'capitalize' }}>
|
|
292
|
+
{variant}
|
|
293
|
+
</Text>
|
|
294
|
+
{/* swatches with in-place text + contrast ratio */}
|
|
295
|
+
<View style={{ flexDirection: 'row', gap: 6, marginBottom: 6 }}>
|
|
296
|
+
{colors.map((hex, i) => (
|
|
297
|
+
<View key={i} style={{ alignItems: 'center' }}>
|
|
298
|
+
<View style={{ width: 56, height: 40, borderRadius: 6, backgroundColor: hex, alignItems: 'center', justifyContent: 'center' }}>
|
|
299
|
+
<Text style={{ color: textColor, fontSize: 11, fontWeight: '700' }}>Aa</Text>
|
|
300
|
+
</View>
|
|
301
|
+
<Text className="text-text-secondary" style={{ fontSize: 8, marginTop: 2 }}>
|
|
302
|
+
{contrast(hex, textColor).toFixed(1)}:1
|
|
303
|
+
</Text>
|
|
304
|
+
</View>
|
|
305
|
+
))}
|
|
306
|
+
</View>
|
|
307
|
+
{/* CVD simulation strips */}
|
|
308
|
+
{(['deuteranopia', 'protanopia'] as const).map((mode) => (
|
|
309
|
+
<View key={mode} style={{ flexDirection: 'row', alignItems: 'center', marginTop: 3 }}>
|
|
310
|
+
<Text className="text-text-secondary" style={{ fontSize: 9, width: 96 }}>{mode}</Text>
|
|
311
|
+
<View style={{ flexDirection: 'row', gap: 6 }}>
|
|
312
|
+
{colors.map((hex, i) => (
|
|
313
|
+
<View key={i} style={{ width: 56, height: 14, borderRadius: 3, backgroundColor: simulate(CVD[mode], hex) }} />
|
|
314
|
+
))}
|
|
315
|
+
</View>
|
|
316
|
+
</View>
|
|
317
|
+
))}
|
|
318
|
+
</View>
|
|
319
|
+
)
|
|
320
|
+
})}
|
|
321
|
+
</View>
|
|
322
|
+
),
|
|
323
|
+
}
|