@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.
Files changed (84) hide show
  1. package/dist/index.d.mts +399 -4
  2. package/dist/index.d.ts +399 -4
  3. package/dist/index.js +2295 -895
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +2279 -905
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/{pages-DyEx-lBf.d.ts → pages-B1hqnGIC.d.ts} +149 -15
  8. package/dist/{pages-D7hOD4Vj.d.mts → pages-CvsFqNtx.d.mts} +149 -15
  9. package/dist/pages.d.mts +1 -1
  10. package/dist/pages.d.ts +1 -1
  11. package/dist/pages.js +1974 -1740
  12. package/dist/pages.js.map +1 -1
  13. package/dist/pages.mjs +1975 -1741
  14. package/dist/pages.mjs.map +1 -1
  15. package/package.json +8 -3
  16. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
  17. package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
  18. package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
  19. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
  20. package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
  21. package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
  22. package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
  23. package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
  24. package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
  25. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
  26. package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
  27. package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
  28. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
  29. package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
  30. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
  31. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
  32. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
  33. package/src/components/custom/Fatigue/README.md +93 -0
  34. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
  35. package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
  36. package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
  37. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
  38. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
  39. package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
  40. package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
  41. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
  42. package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
  43. package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
  44. package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
  45. package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
  46. package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
  47. package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
  48. package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
  49. package/src/components/custom/Fatigue/index.ts +41 -0
  50. package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
  51. package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
  52. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
  53. package/src/components/custom/Workout/ExerciseCard.test.tsx +4 -4
  54. package/src/components/custom/Workout/ExerciseCard.tsx +3 -1
  55. package/src/components/custom/Workout/ExerciseDetailPage.tsx +7 -1
  56. package/src/components/custom/Workout/README.md +62 -16
  57. package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
  58. package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
  59. package/src/components/custom/Workout/SessionRail.tsx +50 -13
  60. package/src/components/custom/Workout/SetRow.test.tsx +6 -6
  61. package/src/components/custom/Workout/SetRow.tsx +9 -2
  62. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
  63. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
  64. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
  65. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
  66. package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
  67. package/src/components/custom/Workout/VelocityStrip.test.tsx +196 -79
  68. package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
  69. package/src/components/custom/Workout/index.ts +3 -0
  70. package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
  71. package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
  72. package/src/components/custom/charts/SetBarChart.tsx +651 -0
  73. package/src/components/custom/charts/live-rep-growth.ts +91 -0
  74. package/src/components/custom/index.ts +1 -0
  75. package/src/test/raw-color-patterns.test.ts +71 -0
  76. package/src/theme/ColorPalettes.stories.tsx +871 -0
  77. package/src/theme/ColorPrimitives.stories.tsx +300 -0
  78. package/src/theme/bar-paper.ts +59 -0
  79. package/src/theme/color-stories.coverage.test.ts +130 -0
  80. package/src/theme/color-story-kit.tsx +170 -0
  81. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
  82. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
  83. package/src/theme/ColorSystem.stories.tsx +0 -508
  84. package/src/theme/Colors.stories.tsx +0 -323
@@ -27,6 +27,9 @@ export {
27
27
  getVelocityZoneName,
28
28
  calculateVelocityLoss,
29
29
  calculateMeanVelocity,
30
+ DualVelocityStrip,
31
+ type DualVelocityStripProps,
32
+ type DualVelocityStream,
30
33
  } from './VelocityStrip'
31
34
  export { MuscleGroupChip, type MuscleGroupChipProps, type VolumeStatus } from './MuscleGroupChip'
32
35
  export { Sparkline, type SparklineProps } from './Sparkline'
@@ -0,0 +1,438 @@
1
+ /**
2
+ * Shared fixtures and framing for the VelocityStrip story groups.
3
+ *
4
+ * The strip's stories are split by VIEW (compact / expanded / hero), and each
5
+ * group walks the SAME scenario set against the SAME datasets. That only proves
6
+ * anything if every group is genuinely showing the same data — so the datasets,
7
+ * the single-above-dual pairing, and the set-type board all live here rather
8
+ * than being re-typed per file.
9
+ *
10
+ * Dual is not a fourth view. It is the same strip when the exercise used two
11
+ * Voltras, so every scenario renders single ABOVE dual on one dataset and the
12
+ * pairing itself carries the point: dual = single + a lower half.
13
+ *
14
+ * Not a `.stories` file on purpose — it exports no stories, and keeping it out
15
+ * of the story glob means the chrome below is held to the same token rules as
16
+ * component code.
17
+ */
18
+
19
+ import type { ReactNode } from 'react'
20
+ import type { Decorator } from '@storybook/react-vite'
21
+ import { View, Text } from 'react-native'
22
+ import { VelocityStrip, DualVelocityStrip, type VelocitySet } from './VelocityStrip'
23
+ import { surfaceRampDark } from '../../../theme/tokens/primitives'
24
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
25
+
26
+ const t = getSemanticColors('dark')
27
+
28
+ /** The real dark surface-background plane — stories sit on the token, not an ad-hoc grey. */
29
+ export const SURFACE_BG = surfaceRampDark.background
30
+
31
+ export const VIEW_LABEL = t['text-secondary']
32
+ export const VIEW_NOTE = t['text-tertiary']
33
+ export const VIEW_HEADING = t['text-primary']
34
+
35
+ export const LEFT_SLOT = 'Left'
36
+ export const RIGHT_SLOT = 'Right'
37
+
38
+ // ── Datasets ────────────────────────────────────────────────────────────────
39
+ // One representative set reused across every view, so a drift in one view is
40
+ // visible as a mismatch against the others rather than hiding behind new data.
41
+
42
+ /** The representative set: a clean, mildly-declining working set. */
43
+ export const REP_SET = [0.95, 0.9, 0.86, 0.8, 0.72]
44
+ /** The lagging partner — 4 reps to REP_SET's 5, so the index-lock shows an empty. */
45
+ export const REP_SET_LAGGING = [0.9, 0.83, 0.75, 0.66]
46
+
47
+ /** A hard fatigue decline — the loss colouring and VL bands have something to say. */
48
+ export const FATIGUE_SET = [0.96, 0.91, 0.83, 0.72, 0.61, 0.5]
49
+ export const FATIGUE_SET_LAGGING = [0.94, 0.87, 0.76, 0.63, 0.48]
50
+
51
+ /** Mid-set, still going: 4 done of 8 planned, newest bar live. */
52
+ export const IN_PROGRESS_SET = [0.82, 0.79, 0.81, 0.76]
53
+ export const IN_PROGRESS_LAGGING = [0.8, 0.77, 0.75]
54
+
55
+ export const dualOf = (velocities: number[], label: string) => ({ velocities, label })
56
+
57
+ // ── Framing ─────────────────────────────────────────────────────────────────
58
+
59
+ export function ViewLabel({ text }: { text: string }) {
60
+ return (
61
+ <Text
62
+ style={{
63
+ color: VIEW_LABEL,
64
+ fontSize: 10,
65
+ fontWeight: '700',
66
+ letterSpacing: 1,
67
+ textTransform: 'uppercase',
68
+ }}
69
+ >
70
+ {text}
71
+ </Text>
72
+ )
73
+ }
74
+
75
+ export function Note({ children }: { children: ReactNode }) {
76
+ return <Text style={{ color: VIEW_NOTE, fontSize: 10, lineHeight: 15 }}>{children}</Text>
77
+ }
78
+
79
+ /** Page frame every scenario story sits in — one surface, one rhythm. */
80
+ export function Sheet({ children, width = 640 }: { children: ReactNode; width?: number }) {
81
+ return (
82
+ <View style={{ padding: 24, backgroundColor: SURFACE_BG, gap: 26, width }}>{children}</View>
83
+ )
84
+ }
85
+
86
+ /**
87
+ * Wall frame for hero stories. The eyebrow is ORGANISM chrome — the north-star
88
+ * live page renders it, the primitive does not. It sits here only so the hero
89
+ * is judged in the context it will live in.
90
+ */
91
+ export const wallDecorator: Decorator = (Story) => (
92
+ <View style={{ width: 620, padding: 28, backgroundColor: SURFACE_BG }}>
93
+ <Text
94
+ style={{
95
+ color: VIEW_NOTE,
96
+ fontSize: 10,
97
+ fontWeight: '700',
98
+ letterSpacing: 1,
99
+ marginBottom: 12,
100
+ }}
101
+ >
102
+ CONCENTRIC VELOCITY · THIS SET · (organism chrome — not the component)
103
+ </Text>
104
+ <Story />
105
+ </View>
106
+ )
107
+
108
+ // ── The single-above-dual pairing ───────────────────────────────────────────
109
+
110
+ /** Per-view heights. Dual expanded is taller because value-height genuinely needs 2×. */
111
+ export const VIEW_HEIGHT: Record<StripView, { single: number; dual: number }> = {
112
+ compact: { single: 11.5, dual: 11.5 },
113
+ expanded: { single: 60, dual: 64 },
114
+ hero: { single: 180, dual: 200 },
115
+ }
116
+
117
+ export type StripView = 'compact' | 'expanded' | 'hero'
118
+
119
+ /** The dual renderer for a view. `expanded`'s dual is the lean `rail` (a verified misnomer). */
120
+ export const dualVariantFor = (view: StripView) => (view === 'expanded' ? 'rail' : view)
121
+
122
+ /**
123
+ * One scenario, rendered single above dual on the same dataset.
124
+ *
125
+ * Takes either raw velocities or full `VelocitySet` descriptors, because the
126
+ * set-type scenarios need the descriptor form and the rest do not.
127
+ */
128
+ export function ScenarioPair({
129
+ view,
130
+ title,
131
+ note,
132
+ single,
133
+ left,
134
+ right,
135
+ singleSet,
136
+ leftSet,
137
+ rightSet,
138
+ scale = 'peak',
139
+ width,
140
+ ...rest
141
+ }: {
142
+ view: StripView
143
+ title: string
144
+ note?: string
145
+ single?: number[]
146
+ left?: number[]
147
+ right?: number[]
148
+ singleSet?: VelocitySet
149
+ leftSet?: VelocitySet
150
+ rightSet?: VelocitySet
151
+ scale?: 'peak' | 'fixed'
152
+ /** Constrains the pair, for the width-degrade ladders. Unset = fill the sheet. */
153
+ width?: number
154
+ targetReps?: number
155
+ liveRepIndex?: number
156
+ barColor?: 'zone' | 'loss'
157
+ }) {
158
+ const h = VIEW_HEIGHT[view]
159
+ const heroProps = view === 'hero' ? { label: 'Set' } : {}
160
+ const bareExpanded = view === 'expanded' ? { showNumbers: false, showInfo: false } : {}
161
+
162
+ return (
163
+ <View style={{ gap: 10, ...(width == null ? null : { width }) }}>
164
+ <View style={{ gap: 3 }}>
165
+ <ViewLabel text={title} />
166
+ {note ? <Note>{note}</Note> : null}
167
+ </View>
168
+
169
+ <ViewLabel text="single" />
170
+ <VelocityStrip
171
+ {...(singleSet ? { set: singleSet } : { velocities: single ?? [] })}
172
+ variant={view}
173
+ height={h.single}
174
+ scale={scale}
175
+ {...heroProps}
176
+ {...bareExpanded}
177
+ {...rest}
178
+ />
179
+
180
+ <ViewLabel text="dual" />
181
+ <DualVelocityStrip
182
+ left={leftSet ? { set: leftSet, label: LEFT_SLOT } : dualOf(left ?? [], LEFT_SLOT)}
183
+ right={rightSet ? { set: rightSet, label: RIGHT_SLOT } : dualOf(right ?? [], RIGHT_SLOT)}
184
+ variant={dualVariantFor(view)}
185
+ height={h.dual}
186
+ scale={scale}
187
+ {...rest}
188
+ />
189
+ </View>
190
+ )
191
+ }
192
+
193
+ // ── Set-type board ──────────────────────────────────────────────────────────
194
+ // The full set-type vocabulary, one row per type, each paired single-above-dual.
195
+ // Generalised over `view` so compact / expanded / hero all walk the same board
196
+ // instead of the hero owning a private copy.
197
+
198
+ export type BoardRow = {
199
+ type: string
200
+ note: string
201
+ single: VelocitySet
202
+ left: VelocitySet
203
+ right: VelocitySet
204
+ }
205
+
206
+ export const SET_TYPE_BOARD: BoardRow[] = [
207
+ {
208
+ type: 'straight',
209
+ note: 'done reps + solid to-do remainder to the planned count',
210
+ single: { type: 'straight', velocities: [0.9, 0.86, 0.82, 0.78], planned: 6 },
211
+ left: { type: 'straight', velocities: [0.9, 0.86, 0.82, 0.78], planned: 6 },
212
+ right: { type: 'straight', velocities: [0.84, 0.79], planned: 6 },
213
+ },
214
+ {
215
+ type: 'range',
216
+ note: 'committed rep bars + solid floor + the cyan variable window (floor..max)',
217
+ single: { type: 'range', velocities: [0.9, 0.86, 0.82, 0.8], floor: 6, max: 8 },
218
+ left: { type: 'range', velocities: [0.9, 0.86, 0.82, 0.8], floor: 6, max: 8 },
219
+ right: { type: 'range', velocities: [0.83, 0.78], floor: 6, max: 8 },
220
+ },
221
+ {
222
+ type: 'amrap',
223
+ note: 'performed reps + the trailing cyan-outline "continue" window',
224
+ single: { type: 'amrap', velocities: [0.9, 0.85, 0.8, 0.75] },
225
+ left: { type: 'amrap', velocities: [0.9, 0.85, 0.8, 0.75] },
226
+ right: { type: 'amrap', velocities: [0.82, 0.76] },
227
+ },
228
+ {
229
+ type: 'drop',
230
+ note: 'one bar per rep with WIDE chunk-notch gaps splitting each sub-load',
231
+ single: {
232
+ type: 'drop',
233
+ subloads: [
234
+ [0.95, 0.9],
235
+ [0.82, 0.76],
236
+ [0.7, 0.64],
237
+ ],
238
+ },
239
+ left: {
240
+ type: 'drop',
241
+ subloads: [
242
+ [0.95, 0.9],
243
+ [0.82, 0.76],
244
+ [0.7, 0.64],
245
+ ],
246
+ },
247
+ right: { type: 'drop', subloads: [[0.88, 0.83], [0.75, 0.68], [0.62]] },
248
+ },
249
+ ]
250
+
251
+ export function SetTypeBoard({ view }: { view: StripView }) {
252
+ const h = VIEW_HEIGHT[view]
253
+ return (
254
+ <View style={{ gap: 30 }}>
255
+ {SET_TYPE_BOARD.map((row) => (
256
+ <View key={row.type} style={{ gap: 8 }}>
257
+ <View style={{ flexDirection: 'row', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
258
+ <Text
259
+ style={{
260
+ color: VIEW_HEADING,
261
+ fontSize: 13,
262
+ fontWeight: '800',
263
+ letterSpacing: 1,
264
+ textTransform: 'uppercase',
265
+ }}
266
+ >
267
+ {row.type}
268
+ </Text>
269
+ <Note>{row.note}</Note>
270
+ </View>
271
+
272
+ <ViewLabel text="single" />
273
+ <VelocityStrip
274
+ variant={view}
275
+ set={row.single}
276
+ height={h.single}
277
+ scale="fixed"
278
+ {...(view === 'hero' ? { label: 'This Set' } : {})}
279
+ {...(view === 'expanded' ? { showNumbers: false, showInfo: false } : {})}
280
+ />
281
+
282
+ <ViewLabel text="dual · symmetric structure, right logs fewer → aligned empties" />
283
+ <DualVelocityStrip
284
+ left={{ set: row.left, label: LEFT_SLOT }}
285
+ right={{ set: row.right, label: RIGHT_SLOT }}
286
+ variant={dualVariantFor(view)}
287
+ height={h.dual}
288
+ scale="fixed"
289
+ />
290
+ </View>
291
+ ))}
292
+ </View>
293
+ )
294
+ }
295
+
296
+ // ── Rep-type coverage ───────────────────────────────────────────────────────
297
+ // Every state a single rep column can be in, in one board. The velocity zones
298
+ // are only three of them; the rest come from the set descriptor (planned
299
+ // remainder, the repeatable windows, drop sub-load splits) or from the dual's
300
+ // index-lock (a rep one side logged and the other did not).
301
+ //
302
+ // Generalised over `view` for the same reason the set-type board is: a state
303
+ // that renders on hero but not compact is exactly the class of bug this is
304
+ // meant to surface.
305
+
306
+ export type RepTypeRow = {
307
+ key: string
308
+ label: string
309
+ note: string
310
+ single: VelocitySet
311
+ left: VelocitySet
312
+ right: VelocitySet
313
+ }
314
+
315
+ /** Velocities chosen to land in each zone: fast, moderate, slow, grinding. */
316
+ const ZONE_SET = [1.15, 0.88, 0.66, 0.42]
317
+
318
+ export const REP_TYPE_ROWS: RepTypeRow[] = [
319
+ {
320
+ key: 'zones',
321
+ label: 'Logged reps · the velocity zones',
322
+ note: 'fast → moderate → slow → grinding. One bar per rep, coloured by the zone it landed in.',
323
+ single: { type: 'straight', velocities: ZONE_SET },
324
+ left: { type: 'straight', velocities: ZONE_SET },
325
+ right: { type: 'straight', velocities: ZONE_SET },
326
+ },
327
+ {
328
+ key: 'upcoming',
329
+ label: 'Upcoming · the planned remainder',
330
+ note: 'Two logged of six planned. The remainder holds its columns so the set never reflows as reps land.',
331
+ single: { type: 'straight', velocities: [0.9, 0.86], planned: 6 },
332
+ left: { type: 'straight', velocities: [0.9, 0.86], planned: 6 },
333
+ right: { type: 'straight', velocities: [0.88, 0.84], planned: 6 },
334
+ },
335
+ {
336
+ key: 'missed-bottom',
337
+ label: 'Missed rep · lower slot',
338
+ note: 'Both slots logged four, but the LOWER one missed rep 3. Its column stays as an index-locked empty, fainter than a planned rep — so the gap reads as missing, not upcoming.',
339
+ single: { type: 'straight', velocities: [0.9, 0.86, 0.82, 0.78] },
340
+ left: { type: 'straight', velocities: [0.9, 0.86, 0.82, 0.78] },
341
+ right: { type: 'straight', velocities: [0.88, 0.84, 0.76] },
342
+ },
343
+ {
344
+ key: 'missed-top',
345
+ label: 'Missed rep · upper slot',
346
+ note: 'The mirror case — the UPPER slot is the one short a rep. The empty sits above the axis instead of below it.',
347
+ single: { type: 'straight', velocities: [0.9, 0.86, 0.82, 0.78] },
348
+ left: { type: 'straight', velocities: [0.9, 0.86, 0.82] },
349
+ right: { type: 'straight', velocities: [0.88, 0.84, 0.8, 0.76] },
350
+ },
351
+ {
352
+ key: 'variable',
353
+ label: 'Variable · a rep range',
354
+ note: 'A range set: committed reps to the floor, then the variable window up to the max. Outlined, never filled — a rep that MIGHT happen must not read as one that did.',
355
+ single: { type: 'range', velocities: [0.9, 0.86, 0.82], floor: 4, max: 6 },
356
+ left: { type: 'range', velocities: [0.9, 0.86, 0.82], floor: 4, max: 6 },
357
+ right: { type: 'range', velocities: [0.88, 0.84, 0.8], floor: 4, max: 6 },
358
+ },
359
+ {
360
+ key: 'repeatable',
361
+ label: 'Repeatable · AMRAP / myo continue',
362
+ note: 'No planned ceiling — the trailing continue window says another rep is allowed at any time.',
363
+ single: { type: 'amrap', velocities: [0.9, 0.85, 0.8] },
364
+ left: { type: 'amrap', velocities: [0.9, 0.85, 0.8] },
365
+ right: { type: 'amrap', velocities: [0.88, 0.83, 0.78] },
366
+ },
367
+ {
368
+ key: 'drop',
369
+ label: 'Drop · sub-load splits',
370
+ note: 'One bar per rep, with a wide chunk-notch separating each sub-load. The notch is a gap in the columns, so it survives even at compact height.',
371
+ single: {
372
+ type: 'drop',
373
+ subloads: [
374
+ [0.95, 0.9],
375
+ [0.82, 0.76],
376
+ [0.7, 0.64],
377
+ ],
378
+ },
379
+ left: {
380
+ type: 'drop',
381
+ subloads: [
382
+ [0.95, 0.9],
383
+ [0.82, 0.76],
384
+ [0.7, 0.64],
385
+ ],
386
+ },
387
+ right: {
388
+ type: 'drop',
389
+ subloads: [
390
+ [0.93, 0.88],
391
+ [0.8, 0.74],
392
+ [0.68, 0.62],
393
+ ],
394
+ },
395
+ },
396
+ ]
397
+
398
+ export function RepTypeBoard({ view }: { view: StripView }) {
399
+ const h = VIEW_HEIGHT[view]
400
+ return (
401
+ <View style={{ gap: 30 }}>
402
+ {REP_TYPE_ROWS.map((row) => (
403
+ <View key={row.key} style={{ gap: 8 }}>
404
+ <Text
405
+ style={{
406
+ color: VIEW_HEADING,
407
+ fontSize: 13,
408
+ fontWeight: '800',
409
+ letterSpacing: 0.5,
410
+ }}
411
+ >
412
+ {row.label}
413
+ </Text>
414
+ <Note>{row.note}</Note>
415
+
416
+ <ViewLabel text="single" />
417
+ <VelocityStrip
418
+ variant={view}
419
+ set={row.single}
420
+ height={h.single}
421
+ scale="fixed"
422
+ {...(view === 'hero' ? { label: 'Set' } : {})}
423
+ {...(view === 'expanded' ? { showNumbers: false, showInfo: false } : {})}
424
+ />
425
+
426
+ <ViewLabel text="dual" />
427
+ <DualVelocityStrip
428
+ left={{ set: row.left, label: LEFT_SLOT }}
429
+ right={{ set: row.right, label: RIGHT_SLOT }}
430
+ variant={dualVariantFor(view)}
431
+ height={h.dual}
432
+ scale="fixed"
433
+ />
434
+ </View>
435
+ ))}
436
+ </View>
437
+ )
438
+ }