@titan-design/react-ui 0.10.0 → 0.11.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/index.d.mts +399 -4
- package/dist/index.d.ts +399 -4
- package/dist/index.js +2295 -895
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2279 -905
- package/dist/index.mjs.map +1 -1
- package/dist/{pages-DyEx-lBf.d.ts → pages-B1hqnGIC.d.ts} +149 -15
- package/dist/{pages-D7hOD4Vj.d.mts → pages-CvsFqNtx.d.mts} +149 -15
- package/dist/pages.d.mts +1 -1
- package/dist/pages.d.ts +1 -1
- package/dist/pages.js +1974 -1740
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +1975 -1741
- package/dist/pages.mjs.map +1 -1
- package/package.json +8 -3
- package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
- package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
- package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
- package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
- package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
- package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
- package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
- package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
- package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
- package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
- package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
- package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
- package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
- package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
- package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
- package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
- package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
- package/src/components/custom/Fatigue/README.md +93 -0
- package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
- package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
- package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
- package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
- package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
- package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
- package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
- package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
- package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
- package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
- package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
- package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
- package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
- package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
- package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
- package/src/components/custom/Fatigue/index.ts +41 -0
- package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
- package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
- package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
- package/src/components/custom/Workout/ExerciseCard.test.tsx +4 -4
- package/src/components/custom/Workout/ExerciseCard.tsx +3 -1
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +7 -1
- package/src/components/custom/Workout/README.md +62 -16
- package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
- package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
- package/src/components/custom/Workout/SessionRail.tsx +50 -13
- package/src/components/custom/Workout/SetRow.test.tsx +6 -6
- package/src/components/custom/Workout/SetRow.tsx +9 -2
- package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
- package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
- package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
- package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
- package/src/components/custom/Workout/VelocityStrip.test.tsx +196 -79
- package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
- package/src/components/custom/Workout/index.ts +3 -0
- package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
- package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
- package/src/components/custom/charts/SetBarChart.tsx +651 -0
- package/src/components/custom/charts/live-rep-growth.ts +91 -0
- package/src/components/custom/index.ts +1 -0
- package/src/test/raw-color-patterns.test.ts +71 -0
- package/src/theme/ColorPalettes.stories.tsx +871 -0
- package/src/theme/ColorPrimitives.stories.tsx +300 -0
- package/src/theme/bar-paper.ts +59 -0
- package/src/theme/color-stories.coverage.test.ts +130 -0
- package/src/theme/color-story-kit.tsx +170 -0
- package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
- package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
- package/src/theme/ColorSystem.stories.tsx +0 -508
- package/src/theme/Colors.stories.tsx +0 -323
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import {
|
|
4
|
+
primitiveColors,
|
|
5
|
+
surfaceRampDark,
|
|
6
|
+
backgroundFrameDark,
|
|
7
|
+
categoricalPalette,
|
|
8
|
+
getCategoricalColor,
|
|
9
|
+
CATEGORICAL_CVD_SAFE_MAX,
|
|
10
|
+
divergingScale,
|
|
11
|
+
sequentialEffort,
|
|
12
|
+
discreteRainbow,
|
|
13
|
+
bestTextColor,
|
|
14
|
+
} from './tokens/primitives'
|
|
15
|
+
import { semanticColorsLight, semanticColorsDark } from './tokens/semantic'
|
|
16
|
+
import { WORKOUT_TOKENS } from './workout-tokens'
|
|
17
|
+
import { WORKOUT_PILL_DELOAD } from './extracted-colors-dataviz'
|
|
18
|
+
import { SPINNER_PRIMARY } from './extracted-colors-ui'
|
|
19
|
+
import {
|
|
20
|
+
SILVER,
|
|
21
|
+
RED_LIGHT,
|
|
22
|
+
RED_MID,
|
|
23
|
+
RED_DEEP,
|
|
24
|
+
DRIFT_GREY,
|
|
25
|
+
GRIND_THRESHOLD,
|
|
26
|
+
ghostLineColor,
|
|
27
|
+
} from '../components/custom/Fatigue/fatigue-tokens'
|
|
28
|
+
import { SWATCH_BORDER, SectionIntro, SectionTitle, ColorSwatch, Demo } from './color-story-kit'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Foundations/Color/Palettes — the organisations of the primitives into meaning.
|
|
32
|
+
*
|
|
33
|
+
* The raw scales and their accessibility validation live in the sibling
|
|
34
|
+
* `Foundations/Color/Primitives`. Everything here is an ASSIGNMENT: a depth
|
|
35
|
+
* ladder, a qualitative series, an ordered magnitude scale, a semantic role.
|
|
36
|
+
* Ordered so the assignments that the most surfaces depend on come first.
|
|
37
|
+
*
|
|
38
|
+
* `color-stories.coverage.test.ts` reads this file as text (together with the
|
|
39
|
+
* Primitives story) and fails if a semantic token ships without a swatch, or
|
|
40
|
+
* if a surface token lands off the ramp.
|
|
41
|
+
*/
|
|
42
|
+
const meta: Meta = {
|
|
43
|
+
title: 'Foundations/Color/Palettes',
|
|
44
|
+
tags: ['autodocs'],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default meta
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// 1. Surface ramp
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The dark surface ramp, darkest plane first.
|
|
55
|
+
*
|
|
56
|
+
* Order and `lStar` mirror `surfaceRampDark` in tokens/primitives.ts, plus the
|
|
57
|
+
* out-of-ramp `backgroundFrameDark` bezel that sits below it. Written as an
|
|
58
|
+
* explicit ordered list because object key order is not a contract and depth
|
|
59
|
+
* order is the whole point of this story. `color-stories.coverage.test.ts`
|
|
60
|
+
* asserts every ramp step appears here.
|
|
61
|
+
*/
|
|
62
|
+
const SURFACE_PLANES = [
|
|
63
|
+
{ hex: backgroundFrameDark, lStar: 3.79, role: 'Frame / bezel — chrome outside the ramp' },
|
|
64
|
+
{ hex: surfaceRampDark.inset, lStar: 4.5, role: 'Sub-shell well / pressed' },
|
|
65
|
+
{ hex: surfaceRampDark.background, lStar: 9, role: 'Shell' },
|
|
66
|
+
{ hex: surfaceRampDark.base, lStar: 13.5, role: 'Main content plane' },
|
|
67
|
+
{ hex: surfaceRampDark.elevated, lStar: 17, role: 'Nav / rail' },
|
|
68
|
+
{ hex: surfaceRampDark.raised, lStar: 20, role: 'Cards' },
|
|
69
|
+
{ hex: surfaceRampDark.overlay, lStar: 22.5, role: 'Hero / popover' },
|
|
70
|
+
] as const
|
|
71
|
+
|
|
72
|
+
/** Semantic tokens resolving to `hex`, derived at render so the mapping can't drift. */
|
|
73
|
+
function tokensResolvingTo(hex: string): string[] {
|
|
74
|
+
return Object.entries(semanticColorsDark)
|
|
75
|
+
.filter(([name, value]) => /^(surface|background)-/.test(name) && value === hex)
|
|
76
|
+
.map(([name]) => name)
|
|
77
|
+
.sort()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function SurfacePlaneRow({ hex, lStar, role }: { hex: string; lStar: number; role: string }) {
|
|
81
|
+
const tokens = tokensResolvingTo(hex)
|
|
82
|
+
return (
|
|
83
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 16 }}>
|
|
84
|
+
<View
|
|
85
|
+
style={{
|
|
86
|
+
width: 96,
|
|
87
|
+
height: 56,
|
|
88
|
+
borderRadius: 8,
|
|
89
|
+
backgroundColor: hex,
|
|
90
|
+
borderWidth: 1,
|
|
91
|
+
borderColor: SWATCH_BORDER,
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
<View style={{ flex: 1 }}>
|
|
95
|
+
<Text className="font-semibold text-text-primary text-sm">
|
|
96
|
+
{hex.toUpperCase()}{' '}
|
|
97
|
+
<Text className="text-text-tertiary text-xs font-normal">L*{lStar}</Text>
|
|
98
|
+
</Text>
|
|
99
|
+
<Text className="text-text-secondary text-xs">{role}</Text>
|
|
100
|
+
<Text className="text-text-tertiary text-xs mt-1">
|
|
101
|
+
{tokens.length > 0 ? tokens.join(' · ') : '— no semantic alias'}
|
|
102
|
+
</Text>
|
|
103
|
+
</View>
|
|
104
|
+
</View>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const SurfaceRamp: StoryObj = {
|
|
109
|
+
name: '1. Surface Ramp (Dark)',
|
|
110
|
+
render: () => (
|
|
111
|
+
<View style={{ padding: 24 }}>
|
|
112
|
+
<Text className="text-2xl font-bold text-text-primary mb-2">Surface Ramp (Dark Mode)</Text>
|
|
113
|
+
<SectionIntro>
|
|
114
|
+
The v0.10.0 surface foundation, and the assignment the most surfaces depend on. Depth is
|
|
115
|
+
an <Text className="font-semibold">ordered ramp</Text>, not a set of unrelated fills —
|
|
116
|
+
planes are spaced by perceptual lightness (L*) on a diminishing taper, so "one step
|
|
117
|
+
up" is the same visual distance anywhere in the stack. Listed darkest first. Several
|
|
118
|
+
semantic tokens deliberately share a plane (shown per row); that aliasing is what lets a
|
|
119
|
+
component say what it <Text className="italic">is</Text> rather than how deep it sits.
|
|
120
|
+
</SectionIntro>
|
|
121
|
+
|
|
122
|
+
<View style={{ gap: 14 }}>
|
|
123
|
+
{SURFACE_PLANES.map((plane) => (
|
|
124
|
+
<SurfacePlaneRow key={plane.hex} {...plane} />
|
|
125
|
+
))}
|
|
126
|
+
</View>
|
|
127
|
+
|
|
128
|
+
<Text className="text-text-secondary text-xs mt-8">
|
|
129
|
+
This ramp is derived, not hand-picked, which is why it sits here rather than with the raw
|
|
130
|
+
scales — the greys it is built from are in Foundations/Color/Primitives. To apply it at
|
|
131
|
+
runtime use <Text className="font-semibold text-text-primary"><Surface level></Text>{' '}
|
|
132
|
+
and <Text className="font-semibold text-text-primary">useOnSurfaceColor</Text> rather than
|
|
133
|
+
reading tokens directly — see Components/Atoms/Surface. Shadow and glow treatments layered
|
|
134
|
+
on top of these planes are in Foundations/Shadows; the legacy numeric elevation scale is
|
|
135
|
+
in Foundations/Elevation.
|
|
136
|
+
</Text>
|
|
137
|
+
</View>
|
|
138
|
+
),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// 2. Categorical palette
|
|
143
|
+
// ============================================================================
|
|
144
|
+
|
|
145
|
+
function CategoricalRow({ name, colors }: { name: string; colors: readonly string[] }) {
|
|
146
|
+
return (
|
|
147
|
+
<View style={{ marginBottom: 24 }}>
|
|
148
|
+
<Text className="text-lg font-bold text-text-primary mb-3">
|
|
149
|
+
{name} <Text className="text-text-secondary text-sm">({colors.length})</Text>
|
|
150
|
+
</Text>
|
|
151
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
|
|
152
|
+
{colors.map((color, index) => (
|
|
153
|
+
<View key={index} style={{ alignItems: 'center' }}>
|
|
154
|
+
<View
|
|
155
|
+
style={{
|
|
156
|
+
width: 56,
|
|
157
|
+
height: 56,
|
|
158
|
+
borderRadius: 8,
|
|
159
|
+
backgroundColor: color,
|
|
160
|
+
borderWidth: 1,
|
|
161
|
+
borderColor: SWATCH_BORDER,
|
|
162
|
+
alignItems: 'center',
|
|
163
|
+
justifyContent: 'center',
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
<Text style={{ color: bestTextColor(color), fontSize: 12, fontWeight: '700' }}>
|
|
167
|
+
{index + 1}
|
|
168
|
+
</Text>
|
|
169
|
+
</View>
|
|
170
|
+
<Text className="text-text-secondary text-xs mt-1">{color.toUpperCase()}</Text>
|
|
171
|
+
<Text className="text-text-tertiary" style={{ fontSize: 9 }}>
|
|
172
|
+
{index < CATEGORICAL_CVD_SAFE_MAX ? 'CVD-safe' : 'extended'}
|
|
173
|
+
</Text>
|
|
174
|
+
</View>
|
|
175
|
+
))}
|
|
176
|
+
</View>
|
|
177
|
+
</View>
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export const CategoricalPalette: StoryObj = {
|
|
182
|
+
name: '2. Categorical Palette',
|
|
183
|
+
render: () => (
|
|
184
|
+
<View style={{ padding: 24 }}>
|
|
185
|
+
<Text className="text-2xl font-bold text-text-primary mb-2">Categorical Palette</Text>
|
|
186
|
+
<SectionIntro>
|
|
187
|
+
Seven hues in one canonical order —{' '}
|
|
188
|
+
<Text className="font-semibold">blue → magenta → red → orange → green → cyan → amber</Text>{' '}
|
|
189
|
+
— expressed as references into the tonal ramps. The sequence is nested-stable: a chart
|
|
190
|
+
with N series takes the first N colors and a series' index does not shift as N
|
|
191
|
+
changes. The worst-case deuteranopia/protanopia ΔE floor of 8 holds through the first{' '}
|
|
192
|
+
{CATEGORICAL_CVD_SAFE_MAX} (
|
|
193
|
+
<Text className="font-semibold">CATEGORICAL_CVD_SAFE_MAX</Text>); the 7th is extended and
|
|
194
|
+
wants a legend or a second encoding. Two variants ship:{' '}
|
|
195
|
+
<Text className="font-semibold">default</Text> (vivid, for neutral/light surfaces, legible
|
|
196
|
+
under black text) and <Text className="font-semibold">dark</Text> (deeper, for white text
|
|
197
|
+
on a filled swatch). A third "light" variant was dropped — `default` doubles as
|
|
198
|
+
it.
|
|
199
|
+
</SectionIntro>
|
|
200
|
+
|
|
201
|
+
<CategoricalRow name="Default (black text)" colors={categoricalPalette.default} />
|
|
202
|
+
<CategoricalRow name="Dark (white text)" colors={categoricalPalette.dark} />
|
|
203
|
+
|
|
204
|
+
<Text className="text-text-secondary text-xs">
|
|
205
|
+
Read a series color with{' '}
|
|
206
|
+
<Text className="font-semibold text-text-primary">getCategoricalColor(index, variant)</Text>{' '}
|
|
207
|
+
rather than indexing the array — it wraps past the end. The contrast and colorblind
|
|
208
|
+
validation for this palette is in Foundations/Color/Primitives → Accessibility.
|
|
209
|
+
</Text>
|
|
210
|
+
</View>
|
|
211
|
+
),
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================================================
|
|
215
|
+
// 3. Diverging scale
|
|
216
|
+
// ============================================================================
|
|
217
|
+
|
|
218
|
+
const DIVERGING_LABELS = ['under', 'maintenance', 'optimal', 'approaching', 'over'] as const
|
|
219
|
+
|
|
220
|
+
export const DivergingScale: StoryObj = {
|
|
221
|
+
name: '3. Diverging Scale — Training Status',
|
|
222
|
+
render: () => (
|
|
223
|
+
<View style={{ padding: 24 }}>
|
|
224
|
+
<Text className="text-2xl font-bold text-text-primary mb-2">Diverging Scale</Text>
|
|
225
|
+
<SectionIntro>
|
|
226
|
+
The BodyMap training-status scale (under → optimal → over), as ramp references. A true
|
|
227
|
+
diverging shape — `optimal` is the lightest center and the extremes go darker — so the
|
|
228
|
+
signal reads in <Text className="font-semibold">lightness</Text> (colorblind-robust,
|
|
229
|
+
worst-case ΔE ≈ 10.9) and the direction reads on the blue↔red axis, the CVD-safe hue pair.
|
|
230
|
+
Every stop clears 4.5:1 under black text, so there is no per-cell white/black flipping.
|
|
231
|
+
</SectionIntro>
|
|
232
|
+
|
|
233
|
+
<View style={{ flexDirection: 'row', gap: 6, marginBottom: 12 }}>
|
|
234
|
+
{DIVERGING_LABELS.map((label, i) => (
|
|
235
|
+
<View
|
|
236
|
+
key={label}
|
|
237
|
+
style={{
|
|
238
|
+
flex: 1,
|
|
239
|
+
height: 56,
|
|
240
|
+
borderRadius: 6,
|
|
241
|
+
backgroundColor: divergingScale[i],
|
|
242
|
+
alignItems: 'center',
|
|
243
|
+
justifyContent: 'center',
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
<Text
|
|
247
|
+
style={{ color: bestTextColor(divergingScale[i]), fontSize: 11, fontWeight: '700' }}
|
|
248
|
+
>
|
|
249
|
+
{label}
|
|
250
|
+
</Text>
|
|
251
|
+
<Text style={{ color: bestTextColor(divergingScale[i]), fontSize: 9 }}>
|
|
252
|
+
{divergingScale[i].toUpperCase()}
|
|
253
|
+
</Text>
|
|
254
|
+
</View>
|
|
255
|
+
))}
|
|
256
|
+
</View>
|
|
257
|
+
</View>
|
|
258
|
+
),
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// 4. Sequential effort scale
|
|
263
|
+
// ============================================================================
|
|
264
|
+
|
|
265
|
+
export const SequentialEffortScale: StoryObj = {
|
|
266
|
+
name: '4. Sequential Effort Scale',
|
|
267
|
+
render: () => (
|
|
268
|
+
<View style={{ padding: 24 }}>
|
|
269
|
+
<Text className="text-2xl font-bold text-text-primary mb-2">Sequential Effort Scale</Text>
|
|
270
|
+
<SectionIntro>
|
|
271
|
+
Ordinal low → high intensity (green → amber → orange → red), as ramp references.
|
|
272
|
+
IntensityBar uses all six; VelocityStrip/RPE sub-samples it. Adjacent pairs clear a
|
|
273
|
+
deut/protan ΔE ≥ 4.5 while the hue walk stays smooth — the amber-200 → amber-300 →
|
|
274
|
+
orange-400 run steps the yellow→orange transition gradually rather than lurching. Order
|
|
275
|
+
encodes magnitude, which is what lets it tolerate the green↔red CVD pair.
|
|
276
|
+
</SectionIntro>
|
|
277
|
+
|
|
278
|
+
<View style={{ flexDirection: 'row', gap: 4, marginBottom: 8 }}>
|
|
279
|
+
{sequentialEffort.map((hex, i) => (
|
|
280
|
+
<View
|
|
281
|
+
key={i}
|
|
282
|
+
style={{
|
|
283
|
+
flex: 1,
|
|
284
|
+
height: 48,
|
|
285
|
+
borderRadius: 5,
|
|
286
|
+
backgroundColor: hex,
|
|
287
|
+
alignItems: 'center',
|
|
288
|
+
justifyContent: 'center',
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
<Text style={{ color: bestTextColor(hex), fontSize: 11, fontWeight: '700' }}>
|
|
292
|
+
{i + 1}
|
|
293
|
+
</Text>
|
|
294
|
+
<Text style={{ color: bestTextColor(hex), fontSize: 9 }}>{hex.toUpperCase()}</Text>
|
|
295
|
+
</View>
|
|
296
|
+
))}
|
|
297
|
+
</View>
|
|
298
|
+
|
|
299
|
+
<SectionTitle>Production 4-band sub-sample (VelocityStrip / RPE)</SectionTitle>
|
|
300
|
+
<View style={{ flexDirection: 'row', gap: 6 }}>
|
|
301
|
+
{Object.entries(WORKOUT_TOKENS.scale).map(([band, hex]) => (
|
|
302
|
+
<View key={band} style={{ alignItems: 'center' }}>
|
|
303
|
+
<View
|
|
304
|
+
style={{
|
|
305
|
+
width: 78,
|
|
306
|
+
height: 40,
|
|
307
|
+
borderRadius: 5,
|
|
308
|
+
backgroundColor: hex,
|
|
309
|
+
alignItems: 'center',
|
|
310
|
+
justifyContent: 'center',
|
|
311
|
+
}}
|
|
312
|
+
>
|
|
313
|
+
<Text style={{ color: bestTextColor(hex), fontSize: 10, fontWeight: '700' }}>
|
|
314
|
+
{band}
|
|
315
|
+
</Text>
|
|
316
|
+
</View>
|
|
317
|
+
</View>
|
|
318
|
+
))}
|
|
319
|
+
</View>
|
|
320
|
+
</View>
|
|
321
|
+
),
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ============================================================================
|
|
325
|
+
// 5. Fatigue / ROM palette — silver → red
|
|
326
|
+
// ============================================================================
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Sourced from the SHIPPED `fatigue-tokens.ts` exports, not re-derived here: the
|
|
330
|
+
* ROM chart and the ghost-spark line share one scheme, and a story that copied
|
|
331
|
+
* the values would be free to drift from the components it documents.
|
|
332
|
+
*/
|
|
333
|
+
const FATIGUE_SWATCHES = [
|
|
334
|
+
{ name: 'SILVER — neutral[300]', value: SILVER, role: 'on-track / at-or-above working range' },
|
|
335
|
+
{ name: 'DRIFT_GREY — neutral[600]', value: DRIFT_GREY, role: 'controlled but drifting' },
|
|
336
|
+
{ name: 'RED_LIGHT — red[400]', value: RED_LIGHT, role: 'below working range' },
|
|
337
|
+
{ name: 'RED_MID — red[600]', value: RED_MID, role: 'collapsing' },
|
|
338
|
+
{ name: 'RED_DEEP — red[800]', value: RED_DEEP, role: 'below the short threshold' },
|
|
339
|
+
] as const
|
|
340
|
+
|
|
341
|
+
/** The real `ghostLineColor()` output sampled across the two control signals. */
|
|
342
|
+
const GHOST_CONTROLLED = [0, 0.25, 0.5, 0.75, 1].map((deviation) => ({
|
|
343
|
+
label: `drift ${deviation.toFixed(2)}`,
|
|
344
|
+
hex: ghostLineColor(deviation, 0),
|
|
345
|
+
}))
|
|
346
|
+
const GHOST_COLLAPSING = [0.35, 0.5, 0.65, 0.8, 1].map((grind) => ({
|
|
347
|
+
label: `grind ${grind.toFixed(2)}`,
|
|
348
|
+
hex: ghostLineColor(0, grind),
|
|
349
|
+
}))
|
|
350
|
+
|
|
351
|
+
function FatigueRampStrip({
|
|
352
|
+
title,
|
|
353
|
+
stops,
|
|
354
|
+
}: {
|
|
355
|
+
title: string
|
|
356
|
+
stops: Array<{ label: string; hex: string }>
|
|
357
|
+
}) {
|
|
358
|
+
return (
|
|
359
|
+
<View style={{ flex: 1 }}>
|
|
360
|
+
<Text className="text-text-secondary text-xs mb-2">{title}</Text>
|
|
361
|
+
<View style={{ flexDirection: 'row', gap: 4 }}>
|
|
362
|
+
{stops.map(({ label, hex }) => (
|
|
363
|
+
<View key={label} style={{ alignItems: 'center' }}>
|
|
364
|
+
<View
|
|
365
|
+
style={{
|
|
366
|
+
width: 64,
|
|
367
|
+
height: 40,
|
|
368
|
+
borderRadius: 6,
|
|
369
|
+
backgroundColor: hex,
|
|
370
|
+
borderWidth: 1,
|
|
371
|
+
borderColor: SWATCH_BORDER,
|
|
372
|
+
}}
|
|
373
|
+
/>
|
|
374
|
+
<Text className="text-text-tertiary" style={{ fontSize: 8, marginTop: 2 }}>
|
|
375
|
+
{label}
|
|
376
|
+
</Text>
|
|
377
|
+
</View>
|
|
378
|
+
))}
|
|
379
|
+
</View>
|
|
380
|
+
</View>
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export const FatigueRomPalette: StoryObj = {
|
|
385
|
+
name: '5. Fatigue / ROM Palette — Silver → Red',
|
|
386
|
+
render: () => (
|
|
387
|
+
<View style={{ padding: 24 }}>
|
|
388
|
+
<Text className="text-2xl font-bold text-text-primary mb-2">Fatigue / ROM Palette</Text>
|
|
389
|
+
<SectionIntro>
|
|
390
|
+
<Text className="font-semibold">
|
|
391
|
+
Silver when the rep is right, shades of red when there is an issue.
|
|
392
|
+
</Text>{' '}
|
|
393
|
+
One scheme shared by the two live-quality readouts — the ROM progression bars and the
|
|
394
|
+
ghost-spark current line. Deliberately no greens and no ambers: those languages belong to
|
|
395
|
+
the verdict tones and the velocity-loss bands (stories 3 and 4), and reusing them here
|
|
396
|
+
would make "fine" and "fast" look like the same claim. Every stop is a
|
|
397
|
+
reference into the tonal ramps.
|
|
398
|
+
</SectionIntro>
|
|
399
|
+
|
|
400
|
+
<View style={{ gap: 14, marginBottom: 28 }}>
|
|
401
|
+
{FATIGUE_SWATCHES.map(({ name, value, role }) => (
|
|
402
|
+
<View key={name} style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
|
|
403
|
+
<View
|
|
404
|
+
style={{
|
|
405
|
+
width: 48,
|
|
406
|
+
height: 48,
|
|
407
|
+
borderRadius: 8,
|
|
408
|
+
backgroundColor: value,
|
|
409
|
+
borderWidth: 1,
|
|
410
|
+
borderColor: SWATCH_BORDER,
|
|
411
|
+
}}
|
|
412
|
+
/>
|
|
413
|
+
<View>
|
|
414
|
+
<Text className="font-semibold text-text-primary text-sm">{name}</Text>
|
|
415
|
+
<Text className="text-text-secondary text-xs">
|
|
416
|
+
{value.toUpperCase()} · {role}
|
|
417
|
+
</Text>
|
|
418
|
+
</View>
|
|
419
|
+
</View>
|
|
420
|
+
))}
|
|
421
|
+
</View>
|
|
422
|
+
|
|
423
|
+
<SectionTitle>ghostLineColor() — the shipped mix, sampled</SectionTitle>
|
|
424
|
+
<Text className="text-text-secondary text-xs mb-3">
|
|
425
|
+
A controlled rep (grind signature below GRIND_THRESHOLD = {GRIND_THRESHOLD}) stays in the
|
|
426
|
+
silver family, dimming toward DRIFT_GREY with tempo deviation — a "drifting but not
|
|
427
|
+
failing" cue that never becomes a color. At or above the threshold the line runs
|
|
428
|
+
light → mid → deep by severity.
|
|
429
|
+
</Text>
|
|
430
|
+
<View style={{ flexDirection: 'row', gap: 32 }}>
|
|
431
|
+
<FatigueRampStrip
|
|
432
|
+
title="controlled — silver dimming toward drift"
|
|
433
|
+
stops={GHOST_CONTROLLED}
|
|
434
|
+
/>
|
|
435
|
+
<FatigueRampStrip title="collapsing — light → mid → deep" stops={GHOST_COLLAPSING} />
|
|
436
|
+
</View>
|
|
437
|
+
|
|
438
|
+
<Text className="text-text-secondary text-xs mt-8">
|
|
439
|
+
<Text className="font-semibold text-text-primary">Open question (TD-07.14):</Text>{' '}
|
|
440
|
+
DRIFT_GREY is `neutral[600]` #4B5563, and the unified warm grey ramp assigns it no
|
|
441
|
+
destination — the 15-row fold maps the other greys but skips this one. It needs an
|
|
442
|
+
assignment before that migration lands. Note that the recovered spec records DRIFT_GREY as
|
|
443
|
+
`charcoal[500]` #1C1C1C; that is stale, `fatigue-tokens.ts` on main reads `neutral[600]`,
|
|
444
|
+
which puts it in the expensive half of the fold rather than the cheap one.
|
|
445
|
+
</Text>
|
|
446
|
+
</View>
|
|
447
|
+
),
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ============================================================================
|
|
451
|
+
// 6–9. Semantic roles
|
|
452
|
+
// ============================================================================
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Modifier suffixes a semantic token family can carry.
|
|
456
|
+
*
|
|
457
|
+
* Rendered as a matrix rather than a flat swatch list: the useful question is
|
|
458
|
+
* almost always "what is the subtle/dark form of THIS role", which a flat list
|
|
459
|
+
* buries. Columns are derived from the shipped tokens, so a new modifier shows
|
|
460
|
+
* up here without editing the story.
|
|
461
|
+
*/
|
|
462
|
+
const VARIANT_SUFFIXES = ['light', 'dark', 'subtle', 'hover', 'active', 'muted'] as const
|
|
463
|
+
|
|
464
|
+
function variantsOf(base: string, palette: Record<string, string>) {
|
|
465
|
+
return [
|
|
466
|
+
{ suffix: 'base', name: base },
|
|
467
|
+
...VARIANT_SUFFIXES.map((s) => ({ suffix: s, name: `${base}-${s}` })),
|
|
468
|
+
].filter(({ name }) => name in palette)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function VariantMatrix({
|
|
472
|
+
bases,
|
|
473
|
+
palette,
|
|
474
|
+
}: {
|
|
475
|
+
bases: readonly string[]
|
|
476
|
+
palette: Record<string, string>
|
|
477
|
+
}) {
|
|
478
|
+
return (
|
|
479
|
+
<View style={{ gap: 20 }}>
|
|
480
|
+
{bases.map((base) => (
|
|
481
|
+
<View key={base}>
|
|
482
|
+
<Text className="font-semibold text-text-primary text-sm mb-2">{base}</Text>
|
|
483
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 10 }}>
|
|
484
|
+
{variantsOf(base, palette).map(({ suffix, name }) => (
|
|
485
|
+
<View key={name} style={{ alignItems: 'center', width: 76 }}>
|
|
486
|
+
<View
|
|
487
|
+
style={{
|
|
488
|
+
width: 76,
|
|
489
|
+
height: 44,
|
|
490
|
+
borderRadius: 6,
|
|
491
|
+
backgroundColor: palette[name],
|
|
492
|
+
borderWidth: 1,
|
|
493
|
+
borderColor: SWATCH_BORDER,
|
|
494
|
+
}}
|
|
495
|
+
/>
|
|
496
|
+
<Text className="text-text-secondary text-xs mt-1">{suffix}</Text>
|
|
497
|
+
<Text className="text-text-tertiary text-xs">{palette[name].toUpperCase()}</Text>
|
|
498
|
+
</View>
|
|
499
|
+
))}
|
|
500
|
+
</View>
|
|
501
|
+
</View>
|
|
502
|
+
))}
|
|
503
|
+
</View>
|
|
504
|
+
)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export const StatusColors: StoryObj = {
|
|
508
|
+
name: '6. Status Colors',
|
|
509
|
+
render: () => (
|
|
510
|
+
<View style={{ padding: 24 }}>
|
|
511
|
+
<Text className="text-2xl font-bold text-text-primary mb-2">Status Colors</Text>
|
|
512
|
+
<SectionIntro>
|
|
513
|
+
Status roles and their modifiers. <Text className="font-semibold">status-error-vivid</Text>{' '}
|
|
514
|
+
is a separate, higher-chroma role (not a modifier of status-error) reserved for
|
|
515
|
+
destructive emphasis. It is the one status family still sourced from the redVivid support
|
|
516
|
+
scale rather than the OKLCH ramps, and it is on its way out: under TD-07.14 redVivid
|
|
517
|
+
retires, status-error-vivid collapses onto status-error's red[600], and the
|
|
518
|
+
Indicator's emphasis moves to a glow derived from red[500].
|
|
519
|
+
</SectionIntro>
|
|
520
|
+
<VariantMatrix
|
|
521
|
+
bases={[
|
|
522
|
+
'status-success',
|
|
523
|
+
'status-error',
|
|
524
|
+
'status-error-vivid',
|
|
525
|
+
'status-warning',
|
|
526
|
+
'status-info',
|
|
527
|
+
'status-live',
|
|
528
|
+
]}
|
|
529
|
+
palette={semanticColorsLight}
|
|
530
|
+
/>
|
|
531
|
+
</View>
|
|
532
|
+
),
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export const BrandColors: StoryObj = {
|
|
536
|
+
name: '7. Brand Colors',
|
|
537
|
+
render: () => (
|
|
538
|
+
<View style={{ padding: 24 }}>
|
|
539
|
+
<Text className="text-2xl font-bold text-text-primary mb-2">Brand Colors</Text>
|
|
540
|
+
<SectionIntro>
|
|
541
|
+
Each brand role and its modifiers. <Text className="font-semibold">hover</Text> and{' '}
|
|
542
|
+
<Text className="font-semibold">active</Text> are interaction states —{' '}
|
|
543
|
+
<Text className="font-semibold">subtle</Text> is a tint for fills behind text.
|
|
544
|
+
</SectionIntro>
|
|
545
|
+
<VariantMatrix bases={['brand-primary', 'brand-secondary']} palette={semanticColorsLight} />
|
|
546
|
+
</View>
|
|
547
|
+
),
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export const ResultColors: StoryObj = {
|
|
551
|
+
name: '8. Result / Outcome Colors',
|
|
552
|
+
render: () => (
|
|
553
|
+
<View style={{ padding: 24 }}>
|
|
554
|
+
<Text className="text-2xl font-bold text-text-primary mb-2">Result / Outcome Colors</Text>
|
|
555
|
+
<SectionIntro>
|
|
556
|
+
Colors for indicating positive, negative, or neutral outcomes. Distinct from status: these
|
|
557
|
+
encode the <Text className="italic">direction of a change</Text> (did this get better?),
|
|
558
|
+
not a system state.
|
|
559
|
+
</SectionIntro>
|
|
560
|
+
<VariantMatrix
|
|
561
|
+
bases={['result-improve', 'result-degrade', 'result-inconclusive', 'result-neutral']}
|
|
562
|
+
palette={semanticColorsLight}
|
|
563
|
+
/>
|
|
564
|
+
</View>
|
|
565
|
+
),
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export const TextAndBorderColors: StoryObj = {
|
|
569
|
+
name: '9. Text & Border Colors (Dark)',
|
|
570
|
+
render: () => (
|
|
571
|
+
<View style={{ padding: 24 }}>
|
|
572
|
+
<Text className="text-2xl font-bold text-text-primary mb-2">Text & Border Colors</Text>
|
|
573
|
+
<SectionIntro>
|
|
574
|
+
The dark-mode text and border roles. Today the border family is the charcoal grey ramp
|
|
575
|
+
assigned to line-work, and text-tertiary and the link colors come from the neutral and
|
|
576
|
+
blue support scales — all three of which are pending migration (TD-07.14): the greys fold
|
|
577
|
+
into one warm grey ramp and the links move to the OKLCH blue. These are the roles that
|
|
578
|
+
will move most, because the mid-grey text steps shift furthest. Prefer{' '}
|
|
579
|
+
<Text className="font-semibold">useOnSurfaceColor</Text> for text sitting on a{' '}
|
|
580
|
+
<Text className="font-semibold"><Surface></Text> — it picks the right text role for
|
|
581
|
+
the plane instead of hardcoding one, which is also what makes the migration survivable.
|
|
582
|
+
</SectionIntro>
|
|
583
|
+
|
|
584
|
+
<SectionTitle>Text</SectionTitle>
|
|
585
|
+
<View style={{ gap: 16, marginBottom: 28 }}>
|
|
586
|
+
<ColorSwatch name="text-primary" value={semanticColorsDark['text-primary']} />
|
|
587
|
+
<ColorSwatch name="text-secondary" value={semanticColorsDark['text-secondary']} />
|
|
588
|
+
<ColorSwatch name="text-tertiary" value={semanticColorsDark['text-tertiary']} />
|
|
589
|
+
<ColorSwatch name="text-disabled" value={semanticColorsDark['text-disabled']} />
|
|
590
|
+
<ColorSwatch name="text-link" value={semanticColorsDark['text-link']} />
|
|
591
|
+
<ColorSwatch name="text-link-hover" value={semanticColorsDark['text-link-hover']} />
|
|
592
|
+
<ColorSwatch name="text-inverse" value={semanticColorsDark['text-inverse']} />
|
|
593
|
+
</View>
|
|
594
|
+
|
|
595
|
+
<SectionTitle>Border</SectionTitle>
|
|
596
|
+
<View style={{ gap: 16 }}>
|
|
597
|
+
<ColorSwatch name="border-default" value={semanticColorsDark['border-default']} />
|
|
598
|
+
<ColorSwatch name="border-subtle" value={semanticColorsDark['border-subtle']} />
|
|
599
|
+
<ColorSwatch name="border-strong" value={semanticColorsDark['border-strong']} />
|
|
600
|
+
<ColorSwatch name="border-prominent" value={semanticColorsDark['border-prominent']} />
|
|
601
|
+
<ColorSwatch name="border-focus" value={semanticColorsDark['border-focus']} />
|
|
602
|
+
<ColorSwatch name="border-input" value={semanticColorsDark['border-input']} />
|
|
603
|
+
<ColorSwatch name="border-input-hover" value={semanticColorsDark['border-input-hover']} />
|
|
604
|
+
<ColorSwatch name="border-input-focus" value={semanticColorsDark['border-input-focus']} />
|
|
605
|
+
<ColorSwatch name="border-input-error" value={semanticColorsDark['border-input-error']} />
|
|
606
|
+
<ColorSwatch name="divider" value={semanticColorsDark['divider']} />
|
|
607
|
+
</View>
|
|
608
|
+
</View>
|
|
609
|
+
),
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ============================================================================
|
|
613
|
+
// 10. Roles in use
|
|
614
|
+
// ============================================================================
|
|
615
|
+
|
|
616
|
+
// Sourced from the real semantic-token layer (not re-derived) so the story can't
|
|
617
|
+
// silently desync if semantic.ts changes. deload/spinner come from the same
|
|
618
|
+
// extracted-colors modules the components consume.
|
|
619
|
+
const SEM = {
|
|
620
|
+
success: semanticColorsDark['status-success'],
|
|
621
|
+
warning: semanticColorsDark['status-warning'],
|
|
622
|
+
error: semanticColorsDark['status-error'],
|
|
623
|
+
info: semanticColorsDark['status-info'],
|
|
624
|
+
deload: WORKOUT_PILL_DELOAD,
|
|
625
|
+
brand: semanticColorsDark['brand-primary'],
|
|
626
|
+
brandDark: semanticColorsDark['brand-primary-dark'],
|
|
627
|
+
brandSecondary: semanticColorsDark['brand-secondary'],
|
|
628
|
+
spinner: SPINNER_PRIMARY,
|
|
629
|
+
neutral: primitiveColors.neutral[500],
|
|
630
|
+
} as const
|
|
631
|
+
|
|
632
|
+
export const RolesInUse: StoryObj = {
|
|
633
|
+
name: '10. Roles in Use — Component Examples',
|
|
634
|
+
render: () => (
|
|
635
|
+
<View style={{ padding: 24 }}>
|
|
636
|
+
<Text className="text-2xl font-bold text-text-primary mb-2">Roles in Use</Text>
|
|
637
|
+
<SectionIntro>
|
|
638
|
+
The same palettes as they land in shipped components — which palette a surface reaches for
|
|
639
|
+
is as much of the system as the values themselves.
|
|
640
|
+
</SectionIntro>
|
|
641
|
+
|
|
642
|
+
<View style={{ flexDirection: 'row', gap: 20, flexWrap: 'wrap', alignItems: 'flex-start' }}>
|
|
643
|
+
{/* BodyMap muscle heatmap — training-status diverging */}
|
|
644
|
+
<Demo label="BodyMap · training status (diverging)">
|
|
645
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', width: 132, gap: 4 }}>
|
|
646
|
+
{(
|
|
647
|
+
[
|
|
648
|
+
['Chest', 4],
|
|
649
|
+
['Quads', 2],
|
|
650
|
+
['Back', 3],
|
|
651
|
+
['Delts', 0],
|
|
652
|
+
['Biceps', 4],
|
|
653
|
+
['Calves', 1],
|
|
654
|
+
] as const
|
|
655
|
+
).map(([muscle, idx]) => (
|
|
656
|
+
<View
|
|
657
|
+
key={muscle}
|
|
658
|
+
style={{
|
|
659
|
+
width: 40,
|
|
660
|
+
backgroundColor: divergingScale[idx],
|
|
661
|
+
borderRadius: 5,
|
|
662
|
+
paddingVertical: 6,
|
|
663
|
+
}}
|
|
664
|
+
>
|
|
665
|
+
<Text
|
|
666
|
+
style={{
|
|
667
|
+
color: bestTextColor(divergingScale[idx]),
|
|
668
|
+
fontSize: 8,
|
|
669
|
+
fontWeight: '700',
|
|
670
|
+
textAlign: 'center',
|
|
671
|
+
}}
|
|
672
|
+
>
|
|
673
|
+
{muscle}
|
|
674
|
+
</Text>
|
|
675
|
+
</View>
|
|
676
|
+
))}
|
|
677
|
+
</View>
|
|
678
|
+
</Demo>
|
|
679
|
+
{/* StatusDot — semantic */}
|
|
680
|
+
<Demo label="StatusDot · semantic">
|
|
681
|
+
<View style={{ gap: 6 }}>
|
|
682
|
+
{(
|
|
683
|
+
[
|
|
684
|
+
['ok', SEM.success],
|
|
685
|
+
['warn', SEM.warning],
|
|
686
|
+
['err', SEM.error],
|
|
687
|
+
['info', SEM.info],
|
|
688
|
+
['neutral', SEM.neutral],
|
|
689
|
+
] as const
|
|
690
|
+
).map(([label, color]) => (
|
|
691
|
+
<View key={label} style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
|
692
|
+
<View style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: color }} />
|
|
693
|
+
<Text className="text-text-secondary" style={{ fontSize: 11 }}>
|
|
694
|
+
{label}
|
|
695
|
+
</Text>
|
|
696
|
+
</View>
|
|
697
|
+
))}
|
|
698
|
+
</View>
|
|
699
|
+
</Demo>
|
|
700
|
+
{/* WorkoutPill — status */}
|
|
701
|
+
<Demo label="WorkoutPill · status">
|
|
702
|
+
<View style={{ flexDirection: 'row', gap: 6, flexWrap: 'wrap', maxWidth: 220 }}>
|
|
703
|
+
{(
|
|
704
|
+
[
|
|
705
|
+
['Done', SEM.success],
|
|
706
|
+
['Now', SEM.brand],
|
|
707
|
+
['Next', SEM.neutral],
|
|
708
|
+
['Deload', SEM.deload],
|
|
709
|
+
] as const
|
|
710
|
+
).map(([label, color]) => (
|
|
711
|
+
<View
|
|
712
|
+
key={label}
|
|
713
|
+
style={{
|
|
714
|
+
borderRadius: 99,
|
|
715
|
+
borderWidth: 1,
|
|
716
|
+
borderColor: color,
|
|
717
|
+
paddingHorizontal: 10,
|
|
718
|
+
paddingVertical: 3,
|
|
719
|
+
}}
|
|
720
|
+
>
|
|
721
|
+
<Text style={{ color, fontSize: 11, fontWeight: '700' }}>{label}</Text>
|
|
722
|
+
</View>
|
|
723
|
+
))}
|
|
724
|
+
</View>
|
|
725
|
+
</Demo>
|
|
726
|
+
{/* StrengthTrend legend — series → semantic */}
|
|
727
|
+
<Demo label="StrengthTrend · series">
|
|
728
|
+
<View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap' }}>
|
|
729
|
+
{(
|
|
730
|
+
[
|
|
731
|
+
['1RM', SEM.brand],
|
|
732
|
+
['vel', SEM.success],
|
|
733
|
+
['miss', SEM.error],
|
|
734
|
+
['RPE', SEM.warning],
|
|
735
|
+
['alt', SEM.brandSecondary],
|
|
736
|
+
['prev', SEM.brandDark],
|
|
737
|
+
] as const
|
|
738
|
+
).map(([label, color]) => (
|
|
739
|
+
<View key={label} style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
|
|
740
|
+
<View style={{ width: 14, height: 3, backgroundColor: color, borderRadius: 2 }} />
|
|
741
|
+
<Text className="text-text-secondary" style={{ fontSize: 10 }}>
|
|
742
|
+
{label}
|
|
743
|
+
</Text>
|
|
744
|
+
</View>
|
|
745
|
+
))}
|
|
746
|
+
</View>
|
|
747
|
+
</Demo>
|
|
748
|
+
{/* Treemap — ordered categorical */}
|
|
749
|
+
<Demo label="Treemap · categorical">
|
|
750
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', width: 140, gap: 2 }}>
|
|
751
|
+
{[46, 30, 30, 24, 24, 24, 20].map((h, i) => (
|
|
752
|
+
<View
|
|
753
|
+
key={i}
|
|
754
|
+
style={{
|
|
755
|
+
height: h,
|
|
756
|
+
flexGrow: 1,
|
|
757
|
+
flexBasis: h > 40 ? 60 : 40,
|
|
758
|
+
backgroundColor: getCategoricalColor(i),
|
|
759
|
+
borderRadius: 3,
|
|
760
|
+
}}
|
|
761
|
+
/>
|
|
762
|
+
))}
|
|
763
|
+
</View>
|
|
764
|
+
</Demo>
|
|
765
|
+
{/* Spinner */}
|
|
766
|
+
<Demo label="Spinner">
|
|
767
|
+
<View
|
|
768
|
+
style={{
|
|
769
|
+
width: 24,
|
|
770
|
+
height: 24,
|
|
771
|
+
borderRadius: 12,
|
|
772
|
+
borderWidth: 3,
|
|
773
|
+
borderColor: SWATCH_BORDER,
|
|
774
|
+
borderTopColor: SEM.spinner,
|
|
775
|
+
}}
|
|
776
|
+
/>
|
|
777
|
+
</Demo>
|
|
778
|
+
{/* IntensityBar — full effort scale */}
|
|
779
|
+
<Demo label="IntensityBar · effort">
|
|
780
|
+
<View style={{ flexDirection: 'row', gap: 2, width: 132 }}>
|
|
781
|
+
{sequentialEffort.map((hex, i) => (
|
|
782
|
+
<View key={i} style={{ flex: 1, height: 20, backgroundColor: hex, borderRadius: 2 }} />
|
|
783
|
+
))}
|
|
784
|
+
</View>
|
|
785
|
+
</Demo>
|
|
786
|
+
{/* ROM bars — the fatigue silver/red scheme */}
|
|
787
|
+
<Demo label="RomProgressionChart · fatigue">
|
|
788
|
+
<View style={{ flexDirection: 'row', gap: 3, alignItems: 'flex-end', height: 40 }}>
|
|
789
|
+
{(
|
|
790
|
+
[
|
|
791
|
+
[34, SILVER],
|
|
792
|
+
[32, SILVER],
|
|
793
|
+
[33, SILVER],
|
|
794
|
+
[26, RED_LIGHT],
|
|
795
|
+
[22, RED_LIGHT],
|
|
796
|
+
[14, RED_DEEP],
|
|
797
|
+
] as const
|
|
798
|
+
).map(([h, color], i) => (
|
|
799
|
+
<View
|
|
800
|
+
key={i}
|
|
801
|
+
style={{ width: 14, height: h, backgroundColor: color, borderRadius: 2 }}
|
|
802
|
+
/>
|
|
803
|
+
))}
|
|
804
|
+
</View>
|
|
805
|
+
</Demo>
|
|
806
|
+
</View>
|
|
807
|
+
</View>
|
|
808
|
+
),
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// ============================================================================
|
|
812
|
+
// 11. Data-viz rainbow — superseded assignment, tokens still defined
|
|
813
|
+
// ============================================================================
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* SUPERSEDED ASSIGNMENT — kept because the `data-1..10` semantic tokens are
|
|
817
|
+
* still defined and still emit `--color-data-N` CSS vars, and an undocumented
|
|
818
|
+
* shipped token is exactly what `color-stories.coverage.test.ts` exists to
|
|
819
|
+
* prevent. This is not a legacy PRIMITIVE shelf — it is a live token family
|
|
820
|
+
* whose assignment has been replaced by {@link CategoricalPalette} for all new
|
|
821
|
+
* work. Retiring the `data-N` tokens would let this story go.
|
|
822
|
+
*/
|
|
823
|
+
export const LegacyDataVisualizationColors: StoryObj = {
|
|
824
|
+
name: '11. Data Visualization (Superseded)',
|
|
825
|
+
render: () => (
|
|
826
|
+
<View style={{ padding: 24 }}>
|
|
827
|
+
<Text className="text-2xl font-bold text-text-primary mb-2">
|
|
828
|
+
Data Visualization — Superseded
|
|
829
|
+
</Text>
|
|
830
|
+
<SectionIntro>
|
|
831
|
+
<Text className="font-semibold">Do not use for new work.</Text> Paul Tol's
|
|
832
|
+
discrete-rainbow scale behind the `data-1..10` tokens — no longer consumed by any
|
|
833
|
+
component. Use the Categorical Palette (story 2) for charts and qualitative series. This
|
|
834
|
+
story exists because the tokens are still shipped, and a shipped token without a swatch is
|
|
835
|
+
the drift the coverage test guards against; it goes away when the tokens do.
|
|
836
|
+
</SectionIntro>
|
|
837
|
+
|
|
838
|
+
<View style={{ gap: 16 }}>
|
|
839
|
+
<ColorSwatch name="data-1" value={semanticColorsLight['data-1']} />
|
|
840
|
+
<ColorSwatch name="data-2" value={semanticColorsLight['data-2']} />
|
|
841
|
+
<ColorSwatch name="data-3" value={semanticColorsLight['data-3']} />
|
|
842
|
+
<ColorSwatch name="data-4" value={semanticColorsLight['data-4']} />
|
|
843
|
+
<ColorSwatch name="data-5" value={semanticColorsLight['data-5']} />
|
|
844
|
+
<ColorSwatch name="data-6" value={semanticColorsLight['data-6']} />
|
|
845
|
+
<ColorSwatch name="data-7" value={semanticColorsLight['data-7']} />
|
|
846
|
+
<ColorSwatch name="data-8" value={semanticColorsLight['data-8']} />
|
|
847
|
+
<ColorSwatch name="data-9" value={semanticColorsLight['data-9']} />
|
|
848
|
+
<ColorSwatch name="data-10" value={semanticColorsLight['data-10']} />
|
|
849
|
+
</View>
|
|
850
|
+
|
|
851
|
+
<Text className="text-lg font-bold text-text-primary mt-8 mb-3">
|
|
852
|
+
Full Discrete Rainbow Palette
|
|
853
|
+
</Text>
|
|
854
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 4 }}>
|
|
855
|
+
{discreteRainbow.map((color, index) => (
|
|
856
|
+
<View
|
|
857
|
+
key={index}
|
|
858
|
+
style={{
|
|
859
|
+
width: 32,
|
|
860
|
+
height: 32,
|
|
861
|
+
borderRadius: 4,
|
|
862
|
+
backgroundColor: color,
|
|
863
|
+
borderWidth: 1,
|
|
864
|
+
borderColor: SWATCH_BORDER,
|
|
865
|
+
}}
|
|
866
|
+
/>
|
|
867
|
+
))}
|
|
868
|
+
</View>
|
|
869
|
+
</View>
|
|
870
|
+
),
|
|
871
|
+
}
|