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