@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
@@ -1,145 +0,0 @@
1
- /**
2
- * Notch across widths — the between-group notch (drop sub-loads / myo clusters /
3
- * cluster intra-rests) is a fixed 8px gap, held constant at every strip width. This
4
- * page renders the real {@link VelocityStrip} across a ladder of widths (session-rail
5
- * → wall dashboard) and across rep / rep-grouping counts, so the notch can be judged
6
- * at the size it will actually ship at. Same set, stacked and left-aligned across
7
- * widths: the notch stays put while the bars grow, so it reads as a group break at
8
- * rail scale without ballooning at wall scale.
9
- */
10
- import type { Meta, StoryObj } from '@storybook/react-vite'
11
- import { View, Text } from 'react-native'
12
- import { VelocityStrip, type VelocitySet } from './VelocityStrip'
13
- import { T_PRIMARY, T_SECONDARY, T_TERTIARY, INSET, BORDER_SUBTLE, Page } from './setHeadingKit'
14
-
15
- const INTER = 'Inter, sans-serif'
16
- const MONO = 'monospace'
17
-
18
- // The width ladder: a session-rail row, two mid app widths, a modality card, a wall
19
- // dashboard. These bracket everywhere the strip renders.
20
- const WIDTHS = [130, 200, 300, 440, 620] as const
21
-
22
- // Preview height — the production mini strip is 3px tall; shown a touch taller here so
23
- // the horizontal notch is legible on screen. Width (the axis under test) is untouched.
24
- const PREVIEW_HEIGHT = 8
25
-
26
- // Velocity colour buckets (match the modalities exploration: fastest green → slowest red).
27
- const G = 1.1
28
- const A = 0.85
29
- const O = 0.6
30
- const R = 0.4
31
-
32
- /** A descending velocity ramp of length n (1.15 → 0.40), for realistic fatigue. */
33
- function ramp(n: number): number[] {
34
- if (n <= 1) return [G]
35
- return Array.from({ length: n }, (_, i) => {
36
- const t = i / (n - 1)
37
- return Math.round((1.15 - t * 0.75) * 100) / 100
38
- })
39
- }
40
-
41
- type Config = { name: string; detail: string; set: VelocitySet }
42
-
43
- const CONFIGS: Config[] = [
44
- {
45
- name: 'Straight · 5',
46
- detail: '5 reps · one group · no notch',
47
- set: { type: 'straight', velocities: [G, A, A, O, R], planned: 5 },
48
- },
49
- {
50
- name: 'Straight · 12',
51
- detail: '12 reps · one group · no notch',
52
- set: { type: 'straight', velocities: ramp(12), planned: 12 },
53
- },
54
- {
55
- name: 'Drop · 3 loads',
56
- detail: '3 groups (5 / 3 / 2) · 2 notches',
57
- set: { type: 'drop', subloads: [[G, A, O, O, R], [A, O, R], [O, R]] },
58
- },
59
- {
60
- name: 'Cluster · groups of 2',
61
- detail: '10 reps · 5 groups · 4 notches',
62
- set: { type: 'cluster', velocities: ramp(10), groupSize: 2, planned: 10 },
63
- },
64
- {
65
- name: 'Cluster · groups of 3',
66
- detail: '15 reps · 5 groups · 4 notches (dense)',
67
- set: { type: 'cluster', velocities: ramp(15), groupSize: 3, planned: 15 },
68
- },
69
- {
70
- name: 'Myo · open',
71
- detail: 'activation + 2 clusters · 2 notches + cyan tail',
72
- set: { type: 'myo', activation: [G, A, A, O, O, R], clusters: [[A, O, R], [O, R]], open: true },
73
- },
74
- ]
75
-
76
- function WidthRow({ set, width }: { set: VelocitySet; width: number }) {
77
- return (
78
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
79
- <Text style={{ fontFamily: MONO, fontSize: 10, color: T_TERTIARY, width: 44, textAlign: 'right' }}>
80
- {width}px
81
- </Text>
82
- <View style={{ width }}>
83
- <VelocityStrip variant="mini" set={set} style={{ height: PREVIEW_HEIGHT }} />
84
- </View>
85
- </View>
86
- )
87
- }
88
-
89
- function ConfigPanel({ c }: { c: Config }) {
90
- return (
91
- <View
92
- style={{
93
- backgroundColor: INSET,
94
- borderWidth: 1,
95
- borderColor: '#2A2A2E',
96
- borderRadius: 12,
97
- padding: 18,
98
- gap: 14,
99
- width: 620 + 44 + 12 + 36, // widest strip + label col + gap + panel padding
100
- }}
101
- >
102
- <View style={{ gap: 3 }}>
103
- <Text style={{ fontFamily: INTER, fontSize: 15, fontWeight: '700', color: T_PRIMARY }}>{c.name}</Text>
104
- <Text style={{ fontFamily: INTER, fontSize: 12, color: T_TERTIARY }}>{c.detail}</Text>
105
- </View>
106
- <View style={{ height: 1, backgroundColor: BORDER_SUBTLE }} />
107
- <View style={{ gap: 12 }}>
108
- {WIDTHS.map((w) => (
109
- <WidthRow key={w} set={c.set} width={w} />
110
- ))}
111
- </View>
112
- </View>
113
- )
114
- }
115
-
116
- const meta: Meta = {
117
- title: 'Workout/DataViz/VelocityStrip/Responsive Width',
118
- parameters: { layout: 'fullscreen' },
119
- }
120
- export default meta
121
- type Story = StoryObj
122
-
123
- export const NotchAcrossWidths: Story = {
124
- name: 'Between-group notch · across widths',
125
- render: () => (
126
- <Page
127
- title="Notch across widths — a fixed 8px group break"
128
- note="The notch that separates drop sub-loads, myo clusters and cluster intra-rests is a fixed 8px gap, held constant at every strip width — so it reads as a group break at session-rail scale AND at wall-dashboard scale without ballooning as the strip grows. Each panel is one set type, stacked and left-aligned across the width ladder (130px rail → 620px wall); both the 2px rep gap and the 8px notch hold fixed while only the bars grow. Straight sets carry no notch (baseline). Preview strips are shown at 8px tall for legibility; the production mini strip is 3px."
129
- >
130
- <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 18 }}>
131
- {CONFIGS.map((c) => (
132
- <ConfigPanel key={c.name} c={c} />
133
- ))}
134
- </View>
135
-
136
- <Text style={{ fontFamily: INTER, fontSize: 12.5, color: T_SECONDARY, lineHeight: 19, maxWidth: 760, marginTop: 4 }}>
137
- Reading the ladder: the notch holds at 8px everywhere, so at rail scale it is a clear group break
138
- (4× the rep gap) and at wall scale it stays a tidy break rather than a canyon. Watch the dense
139
- cluster (groups of 3, 15 reps): even where the bars are thin, the 8px notch separates groups without
140
- a proportional gap swallowing the strip at large sizes. The single knob is WIDE_GAP in
141
- VelocityStrip.tsx.
142
- </Text>
143
- </Page>
144
- ),
145
- }
@@ -1,508 +0,0 @@
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
- import { getSemanticColors } from './tokens/semantic'
15
-
16
- const t = getSemanticColors('dark')
17
-
18
- const meta: Meta = {
19
- title: 'Foundations/Color/System',
20
- tags: ['autodocs'],
21
- }
22
- export default meta
23
-
24
- // ---- color math (self-contained; drives the accessibility panel) ----
25
- const hexToRgb = (hex: string): [number, number, number] => {
26
- const h = hex.replace('#', '')
27
- return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255) as [number, number, number]
28
- }
29
- const toHex = (v: number) =>
30
- Math.round(Math.min(1, Math.max(0, v)) * 255)
31
- .toString(16)
32
- .padStart(2, '0')
33
- const lin = (c: number) => (c >= 0.04045 ? ((c + 0.055) / 1.055) ** 2.4 : c / 12.92)
34
- const gamma = (c: number) =>
35
- c >= 0.0031308 ? 1.055 * Math.max(0, c) ** (1 / 2.4) - 0.055 : 12.92 * c
36
- const relLum = (hex: string) => {
37
- const [r, g, b] = hexToRgb(hex).map(lin)
38
- return 0.2126 * r + 0.7152 * g + 0.0722 * b
39
- }
40
- const contrast = (a: string, b: string) => {
41
- const [l1, l2] = [relLum(a), relLum(b)].sort((x, y) => y - x)
42
- return (l1 + 0.05) / (l2 + 0.05)
43
- }
44
- const bestText = (hex: string) =>
45
- contrast(hex, '#000000') >= contrast(hex, '#FFFFFF') ? '#000000' : '#FFFFFF'
46
- // Machado-2009 dichromacy simulation (severity 1.0)
47
- const CVD = {
48
- deuteranopia: [
49
- 0.367322, 0.860646, -0.227968, 0.280085, 0.672501, 0.047413, -0.01182, 0.04294, 0.968881,
50
- ],
51
- protanopia: [
52
- 0.152286, 1.052583, -0.204868, 0.114503, 0.786281, 0.099216, -0.003882, -0.048116, 1.051998,
53
- ],
54
- } as const
55
- const simulate = (M: readonly number[], hex: string) => {
56
- const [r, g, b] = hexToRgb(hex).map(lin)
57
- const out = [
58
- M[0] * r + M[1] * g + M[2] * b,
59
- M[3] * r + M[4] * g + M[5] * b,
60
- M[6] * r + M[7] * g + M[8] * b,
61
- ].map(gamma)
62
- return '#' + out.map(toHex).join('')
63
- }
64
-
65
- const STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const
66
-
67
- function SectionTitle({ children }: { children: string }) {
68
- return <Text className="text-lg font-bold text-text-primary mb-3 mt-2">{children}</Text>
69
- }
70
-
71
- function Ramp({ name, ramp }: { name: string; ramp: Record<number, string> }) {
72
- return (
73
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 6 }}>
74
- <Text className="text-text-secondary text-xs" style={{ width: 150 }}>
75
- {name}
76
- </Text>
77
- <View
78
- style={{
79
- flexDirection: 'row',
80
- flex: 1,
81
- borderRadius: 6,
82
- overflow: 'hidden',
83
- borderWidth: 1,
84
- borderColor: '#2C2C2C',
85
- }}
86
- >
87
- {STEPS.map((s) => (
88
- <View key={s} style={{ flex: 1, height: 30, backgroundColor: ramp[s] }} />
89
- ))}
90
- </View>
91
- </View>
92
- )
93
- }
94
-
95
- function Chip({ hex, label, sub }: { hex: string; label?: string; sub?: string }) {
96
- return (
97
- <View style={{ alignItems: 'center' }}>
98
- <View
99
- style={{
100
- width: 58,
101
- height: 44,
102
- borderRadius: 8,
103
- backgroundColor: hex,
104
- borderWidth: 1,
105
- borderColor: '#2C2C2C',
106
- alignItems: 'center',
107
- justifyContent: 'center',
108
- }}
109
- >
110
- {label ? (
111
- <Text style={{ color: bestText(hex), fontSize: 11, fontWeight: '700' }}>{label}</Text>
112
- ) : null}
113
- </View>
114
- {sub ? (
115
- <Text className="text-text-secondary" style={{ fontSize: 8, marginTop: 2 }}>
116
- {sub}
117
- </Text>
118
- ) : null}
119
- </View>
120
- )
121
- }
122
-
123
- const RAMP_NAMES: Array<[keyof typeof primitiveRamps, string]> = [
124
- ['red', 'Red'],
125
- ['orange', 'Orange'],
126
- ['amber', 'Amber'],
127
- ['green', 'Green'],
128
- ['cyan', 'Cyan'],
129
- ['blue', 'Blue'],
130
- ['magenta', 'Magenta'],
131
- ]
132
-
133
- const SEM = {
134
- success: primitiveRamps.green[300],
135
- warning: primitiveRamps.amber[300],
136
- error: primitiveRamps.red[600],
137
- info: primitiveRamps.blue[500],
138
- deload: primitiveRamps.magenta[600],
139
- brand: primitiveRamps.orange[400],
140
- brandDark: primitiveRamps.orange[600],
141
- brandSecondary: primitiveRamps.cyan[600],
142
- spinner: primitiveRamps.cyan[600],
143
- neutral: primitiveColors.neutral[500],
144
- } as const
145
-
146
- // BodyMap training-status diverging scale (from the divergingScale token).
147
- const D3_STATUS = divergingScale
148
-
149
- export const Overview: StoryObj = {
150
- render: () => (
151
- <View style={{ padding: 24, gap: 6 }}>
152
- <Text className="text-2xl font-bold text-text-primary mb-1">Color System</Text>
153
- <Text className="text-text-secondary mb-4">
154
- Seven OKLCH tonal ramps and an ordered, colorblind-safe categorical palette. Ramps are the
155
- single source of truth; the categorical references ramp steps.
156
- </Text>
157
-
158
- <SectionTitle>Tonal ramps</SectionTitle>
159
- {RAMP_NAMES.map(([key, name]) => (
160
- <Ramp key={key} name={name} ramp={primitiveRamps[key]} />
161
- ))}
162
-
163
- <SectionTitle>Semantic tokens</SectionTitle>
164
- <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 18 }}>
165
- {Object.entries(SEM).map(([label, hex]) => (
166
- <View key={label} style={{ alignItems: 'center' }}>
167
- <Chip hex={hex} />
168
- <Text className="text-text-secondary" style={{ fontSize: 9, marginTop: 3 }}>
169
- {label}
170
- </Text>
171
- </View>
172
- ))}
173
- </View>
174
-
175
- <SectionTitle>Categorical — ordered, nested, CVD-safe to 6</SectionTitle>
176
- {(['default', 'dark'] as const).map((variant) => (
177
- <View key={variant} style={{ marginBottom: 10 }}>
178
- <Text
179
- className="text-text-secondary text-xs mb-1"
180
- style={{ textTransform: 'capitalize' }}
181
- >
182
- {variant}
183
- </Text>
184
- <View style={{ flexDirection: 'row', gap: 6 }}>
185
- {categoricalPalette[variant].map((hex, i) => (
186
- <Chip
187
- key={i}
188
- hex={hex}
189
- label={`${i + 1}`}
190
- sub={i < CATEGORICAL_CVD_SAFE_MAX ? undefined : 'ext'}
191
- />
192
- ))}
193
- </View>
194
- </View>
195
- ))}
196
-
197
- <SectionTitle>Data-viz scales</SectionTitle>
198
- <Text className="text-text-secondary text-xs mb-2">
199
- Diverging (BodyMap training-status) — light-centered so it reads in lightness and stays
200
- colorblind-stable; direction is the blue↔red axis. Labels use per-cell best-contrast text.
201
- </Text>
202
- <View style={{ flexDirection: 'row', gap: 6, marginBottom: 12 }}>
203
- {['under', 'maint', 'optimal', 'approach', 'over'].map((l, i) => (
204
- <View
205
- key={l}
206
- style={{
207
- flex: 1,
208
- height: 40,
209
- borderRadius: 6,
210
- backgroundColor: divergingScale[i],
211
- alignItems: 'center',
212
- justifyContent: 'center',
213
- }}
214
- >
215
- <Text
216
- style={{ color: bestTextColor(divergingScale[i]), fontSize: 10, fontWeight: '700' }}
217
- >
218
- {l}
219
- </Text>
220
- </View>
221
- ))}
222
- </View>
223
- <Text className="text-text-secondary text-xs mb-2">
224
- Sequential effort (low → high) — an ordinal green→yellow→orange→red scale that IntensityBar
225
- uses in full and VelocityStrip/RPE sub-samples.
226
- </Text>
227
- <View style={{ flexDirection: 'row', gap: 4, marginBottom: 4 }}>
228
- {sequentialEffort.map((hex, i) => (
229
- <View
230
- key={i}
231
- style={{
232
- flex: 1,
233
- height: 34,
234
- borderRadius: 5,
235
- backgroundColor: hex,
236
- alignItems: 'center',
237
- justifyContent: 'center',
238
- }}
239
- >
240
- <Text style={{ color: bestTextColor(hex), fontSize: 10, fontWeight: '700' }}>
241
- {i + 1}
242
- </Text>
243
- </View>
244
- ))}
245
- </View>
246
-
247
- <SectionTitle>Component examples — correct color roles</SectionTitle>
248
- <View style={{ flexDirection: 'row', gap: 20, flexWrap: 'wrap', alignItems: 'flex-start' }}>
249
- {/* BodyMap muscle heatmap — training-status diverging (D3) */}
250
- <Demo label="BodyMap · training status">
251
- <View style={{ flexDirection: 'row', flexWrap: 'wrap', width: 132, gap: 4 }}>
252
- {[
253
- ['Chest', 4],
254
- ['Quads', 2],
255
- ['Back', 3],
256
- ['Delts', 0],
257
- ['Biceps', 4],
258
- ['Calves', 1],
259
- ].map(([m, idx]) => (
260
- <View
261
- key={m as string}
262
- style={{
263
- width: 40,
264
- backgroundColor: D3_STATUS[idx as number],
265
- borderRadius: 5,
266
- paddingVertical: 6,
267
- }}
268
- >
269
- <Text
270
- style={{
271
- color: bestTextColor(D3_STATUS[idx as number]),
272
- fontSize: 8,
273
- fontWeight: '700',
274
- textAlign: 'center',
275
- }}
276
- >
277
- {m as string}
278
- </Text>
279
- </View>
280
- ))}
281
- </View>
282
- </Demo>
283
- {/* StatusDot — semantic */}
284
- <Demo label="StatusDot · semantic">
285
- <View style={{ gap: 6 }}>
286
- {[
287
- ['ok', SEM.success],
288
- ['warn', SEM.warning],
289
- ['err', SEM.error],
290
- ['neutral', SEM.neutral],
291
- ].map(([l, c]) => (
292
- <View
293
- key={l as string}
294
- style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}
295
- >
296
- <View
297
- style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: c as string }}
298
- />
299
- <Text className="text-text-secondary" style={{ fontSize: 11 }}>
300
- {l as string}
301
- </Text>
302
- </View>
303
- ))}
304
- </View>
305
- </Demo>
306
- {/* WorkoutPill — status */}
307
- <Demo label="WorkoutPill">
308
- <View style={{ flexDirection: 'row', gap: 6, flexWrap: 'wrap', maxWidth: 220 }}>
309
- {[
310
- ['Done', SEM.success],
311
- ['Now', SEM.brand],
312
- ['Next', SEM.neutral],
313
- ['Deload', SEM.deload],
314
- ].map(([t, c]) => (
315
- <View
316
- key={t as string}
317
- style={{
318
- borderRadius: 99,
319
- borderWidth: 1,
320
- borderColor: c as string,
321
- paddingHorizontal: 10,
322
- paddingVertical: 3,
323
- }}
324
- >
325
- <Text style={{ color: c as string, fontSize: 11, fontWeight: '700' }}>
326
- {t as string}
327
- </Text>
328
- </View>
329
- ))}
330
- </View>
331
- </Demo>
332
- {/* StrengthTrend legend — series → semantic */}
333
- <Demo label="StrengthTrend · series">
334
- <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap' }}>
335
- {[
336
- ['1RM', SEM.brand],
337
- ['vel', SEM.success],
338
- ['miss', SEM.error],
339
- ['RPE', SEM.warning],
340
- ].map(([l, c]) => (
341
- <View
342
- key={l as string}
343
- style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}
344
- >
345
- <View
346
- style={{ width: 14, height: 3, backgroundColor: c as string, borderRadius: 2 }}
347
- />
348
- <Text className="text-text-secondary" style={{ fontSize: 10 }}>
349
- {l as string}
350
- </Text>
351
- </View>
352
- ))}
353
- </View>
354
- </Demo>
355
- {/* Treemap — ordered categorical */}
356
- <Demo label="Treemap · categorical">
357
- <View style={{ flexDirection: 'row', flexWrap: 'wrap', width: 140, gap: 2 }}>
358
- {[46, 30, 30, 24, 24, 24, 20].map((h, i) => (
359
- <View
360
- key={i}
361
- style={{
362
- height: h,
363
- flexGrow: 1,
364
- flexBasis: h > 40 ? 60 : 40,
365
- backgroundColor: getCategoricalColor(i),
366
- borderRadius: 3,
367
- }}
368
- />
369
- ))}
370
- </View>
371
- </Demo>
372
- {/* Spinner */}
373
- <Demo label="Spinner">
374
- <View
375
- style={{
376
- width: 24,
377
- height: 24,
378
- borderRadius: 12,
379
- borderWidth: 3,
380
- borderColor: '#2C2C2C',
381
- borderTopColor: SEM.spinner,
382
- }}
383
- />
384
- </Demo>
385
- {/* IntensityBar — full effort scale */}
386
- <Demo label="IntensityBar · effort">
387
- <View style={{ flexDirection: 'row', gap: 2, width: 132 }}>
388
- {sequentialEffort.map((hex, i) => (
389
- <View
390
- key={i}
391
- style={{ flex: 1, height: 20, backgroundColor: hex, borderRadius: 2 }}
392
- />
393
- ))}
394
- </View>
395
- </Demo>
396
- {/* VelocityStrip / RPE — effort sub-sample */}
397
- <Demo label="VelocityStrip · RPE">
398
- <View style={{ flexDirection: 'row', gap: 3 }}>
399
- {[
400
- sequentialEffort[0],
401
- sequentialEffort[2],
402
- sequentialEffort[3],
403
- sequentialEffort[5],
404
- ].map((hex, i) => (
405
- <View
406
- key={i}
407
- style={{
408
- width: 30,
409
- height: 26,
410
- borderRadius: 4,
411
- backgroundColor: hex,
412
- alignItems: 'center',
413
- justifyContent: 'center',
414
- }}
415
- >
416
- <Text style={{ color: bestTextColor(hex), fontSize: 9, fontWeight: '700' }}>
417
- {['S', 'M', 'F', 'X'][i]}
418
- </Text>
419
- </View>
420
- ))}
421
- </View>
422
- </Demo>
423
- </View>
424
- </View>
425
- ),
426
- }
427
-
428
- function Demo({ label, children }: { label: string; children: ReactNode }) {
429
- return (
430
- <View style={{ padding: 10, backgroundColor: t['surface-raised'], borderRadius: 8 }}>
431
- <Text className="text-text-secondary" style={{ fontSize: 9, marginBottom: 6 }}>
432
- {label}
433
- </Text>
434
- {children}
435
- </View>
436
- )
437
- }
438
-
439
- export const Accessibility: StoryObj = {
440
- render: () => (
441
- <View style={{ padding: 24 }}>
442
- <Text className="text-2xl font-bold text-text-primary mb-1">Categorical Accessibility</Text>
443
- <Text className="text-text-secondary mb-5">
444
- In-place text contrast and colorblind simulation for the categorical series. Converging
445
- strips across two swatches signal a collision for that viewer; all pairs hold ΔE ≥ 8 through
446
- the first {CATEGORICAL_CVD_SAFE_MAX}.
447
- </Text>
448
-
449
- {(['default', 'dark'] as const).map((variant) => {
450
- const colors = categoricalPalette[variant]
451
- const textColor = variant === 'dark' ? '#FFFFFF' : '#000000'
452
- return (
453
- <View key={variant} style={{ marginBottom: 22 }}>
454
- <Text
455
- className="text-lg font-bold text-text-primary mb-2"
456
- style={{ textTransform: 'capitalize' }}
457
- >
458
- {variant}
459
- </Text>
460
- {/* swatches with in-place text + contrast ratio */}
461
- <View style={{ flexDirection: 'row', gap: 6, marginBottom: 6 }}>
462
- {colors.map((hex, i) => (
463
- <View key={i} style={{ alignItems: 'center' }}>
464
- <View
465
- style={{
466
- width: 56,
467
- height: 40,
468
- borderRadius: 6,
469
- backgroundColor: hex,
470
- alignItems: 'center',
471
- justifyContent: 'center',
472
- }}
473
- >
474
- <Text style={{ color: textColor, fontSize: 11, fontWeight: '700' }}>Aa</Text>
475
- </View>
476
- <Text className="text-text-secondary" style={{ fontSize: 8, marginTop: 2 }}>
477
- {contrast(hex, textColor).toFixed(1)}:1
478
- </Text>
479
- </View>
480
- ))}
481
- </View>
482
- {/* CVD simulation strips */}
483
- {(['deuteranopia', 'protanopia'] as const).map((mode) => (
484
- <View key={mode} style={{ flexDirection: 'row', alignItems: 'center', marginTop: 3 }}>
485
- <Text className="text-text-secondary" style={{ fontSize: 9, width: 96 }}>
486
- {mode}
487
- </Text>
488
- <View style={{ flexDirection: 'row', gap: 6 }}>
489
- {colors.map((hex, i) => (
490
- <View
491
- key={i}
492
- style={{
493
- width: 56,
494
- height: 14,
495
- borderRadius: 3,
496
- backgroundColor: simulate(CVD[mode], hex),
497
- }}
498
- />
499
- ))}
500
- </View>
501
- </View>
502
- ))}
503
- </View>
504
- )
505
- })}
506
- </View>
507
- ),
508
- }