@titan-design/react-ui 0.5.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/bodymap.js +232 -185
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +232 -185
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +164 -8
- package/dist/index.d.ts +164 -8
- package/dist/index.js +835 -342
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +816 -343
- package/dist/index.mjs.map +1 -1
- package/dist/pages.js +570 -522
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +570 -522
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +461 -199
- package/dist/theme/index.d.ts +461 -199
- package/dist/theme/index.js +311 -97
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +303 -98
- 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 +148 -139
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +3 -1
- 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/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/ExerciseDetailPage.tsx +2 -1
- package/src/components/custom/Workout/InputBar.tsx +2 -1
- package/src/components/custom/Workout/IntensityBar.test.tsx +8 -8
- package/src/components/custom/Workout/IntensityBar.tsx +10 -6
- 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/PrBadge.tsx +5 -2
- package/src/components/custom/Workout/PrHistoryModal.tsx +4 -3
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +2 -1
- package/src/components/custom/Workout/ReadinessCheck.tsx +7 -4
- package/src/components/custom/Workout/RestTimer.tsx +2 -1
- package/src/components/custom/Workout/SetRow.tsx +2 -1
- 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.tsx +6 -3
- package/src/components/custom/Workout/VelocityStrip.test.tsx +3 -3
- package/src/components/custom/Workout/WeekRow.tsx +3 -2
- package/src/components/custom/Workout/WeightBadge.tsx +6 -2
- 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 +6 -67
- 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/theme/ColorSystem.stories.tsx +323 -0
- package/src/theme/Colors.stories.tsx +69 -1
- package/src/theme/barrel.ts +1 -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/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
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
2
|
import { View, Text } from 'react-native'
|
|
3
|
-
import { primitiveColors, discreteRainbow } from './tokens/primitives'
|
|
3
|
+
import { primitiveColors, discreteRainbow, primitiveRamps, categoricalPalette } from './tokens/primitives'
|
|
4
4
|
import { semanticColorsLight, semanticColorsDark } from './tokens/semantic'
|
|
5
5
|
|
|
6
6
|
const meta: Meta = {
|
|
@@ -236,6 +236,74 @@ export const DataVisualizationColors: StoryObj = {
|
|
|
236
236
|
),
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
function CategoricalRow({ name, colors }: { name: string; colors: readonly string[] }) {
|
|
240
|
+
return (
|
|
241
|
+
<View style={{ marginBottom: 24 }}>
|
|
242
|
+
<Text className="text-lg font-bold text-text-primary mb-3">
|
|
243
|
+
{name} <Text className="text-text-secondary text-sm">({colors.length})</Text>
|
|
244
|
+
</Text>
|
|
245
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
|
|
246
|
+
{colors.map((color, index) => (
|
|
247
|
+
<View key={index} style={{ alignItems: 'center' }}>
|
|
248
|
+
<View
|
|
249
|
+
style={{
|
|
250
|
+
width: 56,
|
|
251
|
+
height: 56,
|
|
252
|
+
borderRadius: 8,
|
|
253
|
+
backgroundColor: color,
|
|
254
|
+
borderWidth: 1,
|
|
255
|
+
borderColor: '#374151',
|
|
256
|
+
}}
|
|
257
|
+
/>
|
|
258
|
+
<Text className="text-text-secondary text-xs mt-1">{color.toUpperCase()}</Text>
|
|
259
|
+
</View>
|
|
260
|
+
))}
|
|
261
|
+
</View>
|
|
262
|
+
</View>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export const TonalRamps: StoryObj = {
|
|
267
|
+
render: () => (
|
|
268
|
+
<View style={{ padding: 24 }}>
|
|
269
|
+
<Text className="text-2xl font-bold text-text-primary mb-6">
|
|
270
|
+
Tonal Ramps (Foundations)
|
|
271
|
+
</Text>
|
|
272
|
+
<Text className="text-text-secondary mb-6">
|
|
273
|
+
Seven OKLCH-generated hue ramps, 11 perceptual steps each (50 → 950). Built with hue-torsion and a
|
|
274
|
+
chroma arc; amber absorbs the former yellow (lemon → warm body) and cyan absorbs the former steel.
|
|
275
|
+
</Text>
|
|
276
|
+
|
|
277
|
+
<ColorScale name="Red" colors={primitiveRamps.red} />
|
|
278
|
+
<ColorScale name="Orange" colors={primitiveRamps.orange} />
|
|
279
|
+
<ColorScale name="Amber" colors={primitiveRamps.amber} />
|
|
280
|
+
<ColorScale name="Green" colors={primitiveRamps.green} />
|
|
281
|
+
<ColorScale name="Cyan" colors={primitiveRamps.cyan} />
|
|
282
|
+
<ColorScale name="Blue" colors={primitiveRamps.blue} />
|
|
283
|
+
<ColorScale name="Magenta" colors={primitiveRamps.magenta} />
|
|
284
|
+
</View>
|
|
285
|
+
),
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export const CategoricalPalette: StoryObj = {
|
|
289
|
+
render: () => (
|
|
290
|
+
<View style={{ padding: 24 }}>
|
|
291
|
+
<Text className="text-2xl font-bold text-text-primary mb-6">
|
|
292
|
+
Categorical Palette
|
|
293
|
+
</Text>
|
|
294
|
+
<Text className="text-text-secondary mb-6">
|
|
295
|
+
Ordered, colorblind-safe qualitative series (worst-case deut/protanopia floor 8 through the first
|
|
296
|
+
six colors; the 7th is extended — pair it with a legend). Series index is stable. Use{' '}
|
|
297
|
+
<Text className="font-semibold">default</Text> on neutral/light surfaces (also legible under black
|
|
298
|
+
text) and <Text className="font-semibold">dark</Text> where white text sits on the swatch.
|
|
299
|
+
</Text>
|
|
300
|
+
|
|
301
|
+
<CategoricalRow name="Default" colors={categoricalPalette.default} />
|
|
302
|
+
<CategoricalRow name="Dark (white text)" colors={categoricalPalette.dark} />
|
|
303
|
+
</View>
|
|
304
|
+
),
|
|
305
|
+
}
|
|
306
|
+
|
|
239
307
|
export const BorderColors: StoryObj = {
|
|
240
308
|
render: () => (
|
|
241
309
|
<View style={{ padding: 24 }}>
|
package/src/theme/barrel.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from './shadows'
|
|
|
12
12
|
export * from './elevation'
|
|
13
13
|
export * from './manifest'
|
|
14
14
|
export { resolveColor } from './resolve-color'
|
|
15
|
+
export { linearGradient, surfaceGradient, type GradientStyle } from './gradients'
|
|
15
16
|
|
|
16
17
|
// ThemeProvider TYPES may stay on the root (erased at build — no nativewind
|
|
17
18
|
// pull). Only the ThemeProvider VALUE moves to the `/theme` subpath.
|
package/src/theme/config.ts
CHANGED
|
@@ -14,10 +14,11 @@ import { semanticColorsLight, semanticColorsDark, type ThemeMode } from './token
|
|
|
14
14
|
const themeIndependentCSSVars = {
|
|
15
15
|
// RGB decomposed values for glow shadows
|
|
16
16
|
'--color-brand-primary-rgb': '255, 121, 0',
|
|
17
|
-
'--color-brand-secondary-rgb': '
|
|
18
|
-
'--color-status-success-rgb': '
|
|
17
|
+
'--color-brand-secondary-rgb': '48, 123, 155',
|
|
18
|
+
'--color-status-success-rgb': '46, 213, 115',
|
|
19
19
|
'--color-status-error-rgb': '209, 67, 67',
|
|
20
|
-
'--color-status-
|
|
20
|
+
'--color-status-error-vivid-rgb': '255, 71, 87',
|
|
21
|
+
'--color-status-warning-rgb': '249, 180, 21',
|
|
21
22
|
'--color-status-info-rgb': '33, 150, 243',
|
|
22
23
|
'--color-background-base-rgb': '16, 16, 16',
|
|
23
24
|
|
|
@@ -59,6 +60,11 @@ export const lightThemeCSSVars = {
|
|
|
59
60
|
'--color-status-info': semanticColorsLight['status-info'],
|
|
60
61
|
'--color-status-info-subtle': semanticColorsLight['status-info-subtle'],
|
|
61
62
|
|
|
63
|
+
'--color-status-error-vivid': semanticColorsLight['status-error-vivid'],
|
|
64
|
+
'--color-status-error-vivid-light': semanticColorsLight['status-error-vivid-light'],
|
|
65
|
+
'--color-status-error-vivid-dark': semanticColorsLight['status-error-vivid-dark'],
|
|
66
|
+
'--color-status-error-vivid-subtle': semanticColorsLight['status-error-vivid-subtle'],
|
|
67
|
+
|
|
62
68
|
'--color-text-primary': semanticColorsLight['text-primary'],
|
|
63
69
|
'--color-text-secondary': semanticColorsLight['text-secondary'],
|
|
64
70
|
'--color-text-tertiary': semanticColorsLight['text-tertiary'],
|
|
@@ -77,6 +83,7 @@ export const lightThemeCSSVars = {
|
|
|
77
83
|
'--color-border-default': semanticColorsLight['border-default'],
|
|
78
84
|
'--color-border-subtle': semanticColorsLight['border-subtle'],
|
|
79
85
|
'--color-border-strong': semanticColorsLight['border-strong'],
|
|
86
|
+
'--color-border-prominent': semanticColorsLight['border-prominent'],
|
|
80
87
|
'--color-border-focus': semanticColorsLight['border-focus'],
|
|
81
88
|
|
|
82
89
|
'--color-interactive-hover': semanticColorsLight['interactive-hover'],
|
|
@@ -172,6 +179,11 @@ export const darkThemeCSSVars = {
|
|
|
172
179
|
'--color-status-info': semanticColorsDark['status-info'],
|
|
173
180
|
'--color-status-info-subtle': semanticColorsDark['status-info-subtle'],
|
|
174
181
|
|
|
182
|
+
'--color-status-error-vivid': semanticColorsDark['status-error-vivid'],
|
|
183
|
+
'--color-status-error-vivid-light': semanticColorsDark['status-error-vivid-light'],
|
|
184
|
+
'--color-status-error-vivid-dark': semanticColorsDark['status-error-vivid-dark'],
|
|
185
|
+
'--color-status-error-vivid-subtle': semanticColorsDark['status-error-vivid-subtle'],
|
|
186
|
+
|
|
175
187
|
'--color-text-primary': semanticColorsDark['text-primary'],
|
|
176
188
|
'--color-text-secondary': semanticColorsDark['text-secondary'],
|
|
177
189
|
'--color-text-tertiary': semanticColorsDark['text-tertiary'],
|
|
@@ -190,6 +202,7 @@ export const darkThemeCSSVars = {
|
|
|
190
202
|
'--color-border-default': semanticColorsDark['border-default'],
|
|
191
203
|
'--color-border-subtle': semanticColorsDark['border-subtle'],
|
|
192
204
|
'--color-border-strong': semanticColorsDark['border-strong'],
|
|
205
|
+
'--color-border-prominent': semanticColorsDark['border-prominent'],
|
|
193
206
|
'--color-border-focus': semanticColorsDark['border-focus'],
|
|
194
207
|
|
|
195
208
|
'--color-interactive-hover': semanticColorsDark['interactive-hover'],
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data-viz / status / brand fills, sourced from the color foundations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { categoricalPalette, primitiveRamps as ramp } from './tokens/primitives'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Titan categorical fallback palette shared by Treemap and Scatter — the
|
|
9
|
+
* canonical {@link categoricalPalette} (ordered, nested-stable, CVD-safe).
|
|
10
|
+
* Components index into it modulo length, so the 7-color series is sufficient.
|
|
11
|
+
*/
|
|
12
|
+
export const DATAVIZ_CATEGORICAL_PALETTE = categoricalPalette.default
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* MesoCard / MesoStatusCard 3px top-accent gradient stops (dark → primary →
|
|
16
|
+
* light), on the orange ramp that brand-primary (orange-400) lives in.
|
|
17
|
+
*/
|
|
18
|
+
export const MESO_ACCENT_GRADIENT_DARK = ramp.orange[500]
|
|
19
|
+
export const MESO_ACCENT_GRADIENT_LIGHT = ramp.orange[300]
|
|
20
|
+
|
|
21
|
+
export const WORKOUT_PILL_DELOAD = ramp.magenta[600]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI-atom fills, sourced from the color foundations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { categoricalPalette, primitiveRamps as ramp } from './tokens/primitives'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Spinner `color="primary"` fill — keeps the spinner a cool, on-palette
|
|
9
|
+
* loading accent.
|
|
10
|
+
*/
|
|
11
|
+
export const SPINNER_PRIMARY = ramp.cyan[600]
|
|
12
|
+
|
|
13
|
+
export const SPINNER_SECONDARY = ramp.green[400]
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Categorical avatar palette — deterministic per-name fill, the canonical
|
|
17
|
+
* ordered {@link categoricalPalette} (CVD-safe). Order is preserved for stable
|
|
18
|
+
* hashing.
|
|
19
|
+
*/
|
|
20
|
+
export const AVATAR_CATEGORICAL_COLORS = categoricalPalette.default
|