@titan-design/react-ui 0.9.3 → 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 (120) hide show
  1. package/dist/bodymap.js +78 -24
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +78 -24
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +429 -11
  6. package/dist/index.d.ts +429 -11
  7. package/dist/index.js +2386 -885
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2306 -836
  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 +1900 -1609
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1901 -1610
  18. package/dist/pages.mjs.map +1 -1
  19. package/dist/theme/index.d.mts +129 -34
  20. package/dist/theme/index.d.ts +129 -34
  21. package/dist/theme/index.js +95 -19
  22. package/dist/theme/index.js.map +1 -1
  23. package/dist/theme/index.mjs +92 -20
  24. package/dist/theme/index.mjs.map +1 -1
  25. package/dist/theme/tokens-css.js +88 -24
  26. package/dist/theme/tokens-css.js.map +1 -1
  27. package/dist/theme/tokens-css.mjs +88 -24
  28. package/dist/theme/tokens-css.mjs.map +1 -1
  29. package/package.json +8 -3
  30. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
  31. package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
  32. package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
  33. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
  34. package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
  35. package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
  36. package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
  37. package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
  38. package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
  39. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
  40. package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
  41. package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
  42. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
  43. package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
  44. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
  45. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
  46. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
  47. package/src/components/custom/Fatigue/README.md +93 -0
  48. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
  49. package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
  50. package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
  51. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
  52. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
  53. package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
  54. package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
  55. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
  56. package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
  57. package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
  58. package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
  59. package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
  60. package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
  61. package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
  62. package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
  63. package/src/components/custom/Fatigue/index.ts +41 -0
  64. package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
  65. package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
  66. package/src/components/custom/Workout/BaseBadge.tsx +5 -5
  67. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
  68. package/src/components/custom/Workout/ExerciseCard.test.tsx +4 -4
  69. package/src/components/custom/Workout/ExerciseCard.tsx +3 -1
  70. package/src/components/custom/Workout/ExerciseDetailPage.tsx +7 -1
  71. package/src/components/custom/Workout/README.md +62 -16
  72. package/src/components/custom/Workout/RowInventory.stories.tsx +5 -2
  73. package/src/components/custom/Workout/S3FullRail.stories.tsx +13 -1
  74. package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
  75. package/src/components/custom/Workout/SessionHeader.tsx +2 -2
  76. package/src/components/custom/Workout/SessionRail.stories.tsx +4 -1
  77. package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
  78. package/src/components/custom/Workout/SessionRail.tsx +55 -18
  79. package/src/components/custom/Workout/SetRow.test.tsx +6 -6
  80. package/src/components/custom/Workout/SetRow.tsx +9 -2
  81. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +16 -25
  82. package/src/components/custom/Workout/SupersetWrapper.tsx +1 -1
  83. package/src/components/custom/Workout/TempoDisplay.tsx +3 -7
  84. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
  85. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
  86. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
  87. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
  88. package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
  89. package/src/components/custom/Workout/VelocityStrip.test.tsx +196 -79
  90. package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
  91. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +4 -1
  92. package/src/components/custom/Workout/WorkoutPill.tsx +2 -2
  93. package/src/components/custom/Workout/index.ts +3 -0
  94. package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
  95. package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
  96. package/src/components/custom/charts/SetBarChart.tsx +651 -0
  97. package/src/components/custom/charts/live-rep-growth.ts +91 -0
  98. package/src/components/custom/index.ts +1 -0
  99. package/src/components/ui/surface/Surface.stories.tsx +195 -1
  100. package/src/components/ui/surface/Surface.test.tsx +148 -9
  101. package/src/components/ui/surface/Surface.tsx +37 -9
  102. package/src/components/ui/surface/SurfaceContext.ts +48 -8
  103. package/src/components/ui/surface/index.ts +1 -0
  104. package/src/components/ui/surface/surface.contract.test.ts +283 -0
  105. package/src/test/raw-color-patterns.test.ts +71 -0
  106. package/src/theme/ColorPalettes.stories.tsx +871 -0
  107. package/src/theme/ColorPrimitives.stories.tsx +300 -0
  108. package/src/theme/bar-paper.ts +59 -0
  109. package/src/theme/color-stories.coverage.test.ts +130 -0
  110. package/src/theme/color-story-kit.tsx +170 -0
  111. package/src/theme/config.ts +12 -0
  112. package/src/theme/elevation.ts +90 -67
  113. package/src/theme/global.css +35 -9
  114. package/src/theme/tokens/primitives.ts +40 -0
  115. package/src/theme/tokens/semantic.ts +63 -14
  116. package/tailwind.config.js +8 -0
  117. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
  118. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
  119. package/src/theme/ColorSystem.stories.tsx +0 -323
  120. package/src/theme/Colors.stories.tsx +0 -323
@@ -12,7 +12,7 @@ export interface SupersetWrapperProps {
12
12
  const DEFAULTS = {
13
13
  label: 'SS',
14
14
  color: getSemanticColors('dark')['brand-primary'],
15
- bgBase: '#101010',
15
+ bgBase: getSemanticColors('dark')['background-base'],
16
16
  } as const
17
17
 
18
18
  export function SupersetWrapper({
@@ -84,11 +84,7 @@ function activeNumberTone(elapsedMs: number, targetMs: number | null): string {
84
84
  }
85
85
 
86
86
  /** The active number: `countup` elapsed (→ target) or `countdown` remaining (→ 0.0, then −). */
87
- function liveReadoutText(
88
- elapsedMs: number,
89
- targetMs: number,
90
- readout: TempoLiveReadout,
91
- ): string {
87
+ function liveReadoutText(elapsedMs: number, targetMs: number, readout: TempoLiveReadout): string {
92
88
  const seconds = readout === 'countup' ? elapsedMs / 1000 : (targetMs - elapsedMs) / 1000
93
89
  return seconds.toFixed(1)
94
90
  }
@@ -308,7 +304,7 @@ export function TempoDisplay({
308
304
  style={{
309
305
  flexDirection: 'row',
310
306
  alignItems: 'center',
311
- backgroundColor: '#1C1C1C',
307
+ backgroundColor: t['surface-raised'],
312
308
  paddingHorizontal: chromePadX,
313
309
  paddingVertical: chromePadY,
314
310
  borderRadius: chromeRadius,
@@ -378,7 +374,7 @@ export function TempoDisplay({
378
374
  >
379
375
  <View
380
376
  style={{
381
- backgroundColor: '#191919',
377
+ backgroundColor: t['surface-overlay'],
382
378
  borderRadius: 6,
383
379
  paddingVertical: 8,
384
380
  paddingHorizontal: 12,
@@ -0,0 +1,191 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { VelocityStrip } from './VelocityStrip'
3
+ import {
4
+ Sheet,
5
+ Note,
6
+ ScenarioPair,
7
+ SetTypeBoard,
8
+ RepTypeBoard,
9
+ REP_SET,
10
+ REP_SET_LAGGING,
11
+ FATIGUE_SET,
12
+ FATIGUE_SET_LAGGING,
13
+ } from './velocity-story-kit'
14
+
15
+ /**
16
+ * `compact` — the flat resting strip. SetBarChart in flat mode: uniform short
17
+ * bars, no labels, no axis. The glance used in the SetRow table and on cards,
18
+ * where the bar is colour-encoded rather than height-encoded.
19
+ *
20
+ * The compact dual FOLDS into one footprint rather than stacking to 2x: each
21
+ * rep column splits at the centre into an L top pill and an R bottom pill.
22
+ * Because resting bars carry their meaning in colour, the pair costs the same
23
+ * vertical space as a single — 2x is only forced for the value-height expanded.
24
+ */
25
+ const meta: Meta<typeof VelocityStrip> = {
26
+ title: 'Workout/DataViz/VelocityStrip/Compact',
27
+ component: VelocityStrip,
28
+ tags: ['autodocs'],
29
+ }
30
+ export default meta
31
+ type Story = StoryObj<typeof VelocityStrip>
32
+
33
+ export const Default: Story = {
34
+ name: 'Default',
35
+ render: () => (
36
+ <Sheet>
37
+ <Note>
38
+ The representative set at rest. Single fills its plot; the dual folds the same reps into
39
+ rounded L/R pills sharing that footprint.
40
+ </Note>
41
+ <ScenarioPair
42
+ view="compact"
43
+ title="Default"
44
+ single={REP_SET}
45
+ left={REP_SET}
46
+ right={REP_SET_LAGGING}
47
+ />
48
+ </Sheet>
49
+ ),
50
+ }
51
+
52
+ export const SetTypes: Story = {
53
+ name: 'Set Types',
54
+ render: () => (
55
+ <Sheet>
56
+ <Note>
57
+ The full set-type vocabulary at resting scale. The chunk-notch still separates drop
58
+ sub-loads even when every bar is the same height.
59
+ </Note>
60
+ <SetTypeBoard view="compact" />
61
+ </Sheet>
62
+ ),
63
+ }
64
+
65
+ export const FatigueDecline: Story = {
66
+ name: 'Fatigue Decline',
67
+ render: () => (
68
+ <Sheet>
69
+ <Note>
70
+ Loss colouring is the default: each bar is coloured by its drop from the set&apos;s own
71
+ best, so a fatiguing set reads green-to-red regardless of absolute speed. At compact scale
72
+ this colour IS the signal — there is no height to read.
73
+ </Note>
74
+ <ScenarioPair
75
+ view="compact"
76
+ title="Fatigue decline"
77
+ single={FATIGUE_SET}
78
+ left={FATIGUE_SET}
79
+ right={FATIGUE_SET_LAGGING}
80
+ barColor="loss"
81
+ />
82
+ </Sheet>
83
+ ),
84
+ }
85
+
86
+ export const EmptyColdBoot: Story = {
87
+ name: 'Empty / Cold Boot',
88
+ render: () => (
89
+ <Sheet>
90
+ <Note>
91
+ Before the first rep lands. A planned set draws its upcoming reps, so the strip holds both
92
+ its footprint AND its column count — nothing reflows when rep one arrives.
93
+ </Note>
94
+ <ScenarioPair
95
+ view="compact"
96
+ title="Cold boot · 6 planned, none logged"
97
+ singleSet={{ type: 'straight', velocities: [], planned: 6 }}
98
+ leftSet={{ type: 'straight', velocities: [], planned: 6 }}
99
+ rightSet={{ type: 'straight', velocities: [], planned: 6 }}
100
+ />
101
+ <ScenarioPair
102
+ view="compact"
103
+ title="Truly empty · no plan to draw"
104
+ note="No planned count either — the only case where there is nothing to hold."
105
+ single={[]}
106
+ left={[]}
107
+ right={[]}
108
+ />
109
+ </Sheet>
110
+ ),
111
+ }
112
+
113
+ export const DualLaggingSide: Story = {
114
+ name: 'Dual · Lagging Side',
115
+ render: () => (
116
+ <Sheet>
117
+ <Note>
118
+ Index-locked: the right slot has logged fewer reps, so its trailing columns render as faint
119
+ empties directly under the left&apos;s. Columns never shift — the next rep a lagging side
120
+ performs lands in its own column.
121
+ </Note>
122
+ <ScenarioPair
123
+ view="compact"
124
+ title="Equal counts · the common case"
125
+ note="Both slots logged the same reps. This is what most of a session looks like."
126
+ single={REP_SET}
127
+ left={REP_SET}
128
+ right={[0.93, 0.88, 0.84, 0.78, 0.7]}
129
+ />
130
+ <ScenarioPair
131
+ view="compact"
132
+ title="Missed rep · lower slot"
133
+ note="Rep 3 missing from the LOWER slot — its column holds as a faint empty."
134
+ single={REP_SET}
135
+ left={REP_SET}
136
+ right={[0.93, 0.88, 0.78, 0.7]}
137
+ />
138
+ <ScenarioPair
139
+ view="compact"
140
+ title="Missed rep · upper slot"
141
+ note="The mirror: the UPPER slot is short a rep."
142
+ single={REP_SET}
143
+ left={[0.95, 0.9, 0.8, 0.72]}
144
+ right={[0.93, 0.88, 0.84, 0.78, 0.7]}
145
+ />
146
+ <ScenarioPair
147
+ view="compact"
148
+ title="One slot well behind"
149
+ single={REP_SET}
150
+ left={REP_SET}
151
+ right={REP_SET_LAGGING.slice(0, 2)}
152
+ />
153
+ </Sheet>
154
+ ),
155
+ }
156
+
157
+ export const Responsive: Story = {
158
+ name: 'Responsive',
159
+ render: () => (
160
+ <Sheet width={520}>
161
+ <Note>
162
+ Compact degrades by WIDTH, not height — the bar layout is shared geometry, so bars and gaps
163
+ scale together and the chunk-notch stays proportional.
164
+ </Note>
165
+ {[440, 300, 180, 110].map((w) => (
166
+ <ScenarioPair
167
+ key={w}
168
+ view="compact"
169
+ title={`${w}px`}
170
+ width={w}
171
+ single={REP_SET}
172
+ left={REP_SET}
173
+ right={REP_SET_LAGGING}
174
+ />
175
+ ))}
176
+ </Sheet>
177
+ ),
178
+ }
179
+
180
+ export const RepTypes: Story = {
181
+ name: 'Rep Types',
182
+ render: () => (
183
+ <Sheet>
184
+ <Note>
185
+ Every state a rep column can be in at resting scale. Colour is the only channel compact has,
186
+ so a state that needs height to read must find another way here.
187
+ </Note>
188
+ <RepTypeBoard view="compact" />
189
+ </Sheet>
190
+ ),
191
+ }
@@ -0,0 +1,186 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { DualVelocityStrip } from './VelocityStrip'
3
+ import {
4
+ LEFT_SLOT,
5
+ RIGHT_SLOT,
6
+ REP_SET,
7
+ REP_SET_LAGGING,
8
+ IN_PROGRESS_SET,
9
+ IN_PROGRESS_LAGGING,
10
+ dualOf,
11
+ wallDecorator,
12
+ } from './velocity-story-kit'
13
+
14
+ /**
15
+ * `DualVelocityStrip` — the strip when the exercise used TWO Voltras.
16
+ *
17
+ * Dual is **not a fourth view**, so its scenario coverage lives in the
18
+ * per-view groups, where every scenario renders single ABOVE dual on one
19
+ * dataset:
20
+ * [Compact](?path=/docs/workout-dataviz-velocitystrip-compact--docs) ·
21
+ * [Expanded](?path=/docs/workout-dataviz-velocitystrip-expanded--docs) ·
22
+ * [Hero](?path=/docs/workout-dataviz-velocitystrip-hero--docs).
23
+ *
24
+ * This group exists for the two things a pairing cannot carry: the exported
25
+ * **API** (`DualVelocityStripProps`, in the args table below) and the layout
26
+ * **extremes** — heights at which the labels have to degrade, and mismatched
27
+ * rep counts that must stay index-locked.
28
+ *
29
+ * The `hero` variant is literally two composed `VelocityStrip` heroes — an
30
+ * `orientation="up"` hero over a mirrored `orientation="down"` one, sharing ONE
31
+ * height scale and meeting at one centre axis — so every hero improvement
32
+ * reaches the dual for free. The up wing grows UP and the down wing grows DOWN,
33
+ * so asymmetry reads pre-attentively as the silhouette: a stronger side reads
34
+ * TALLER, while each wing still colours by its OWN best. **Side is POSITION
35
+ * only, never hue.** Each side takes a `DualVelocityStream` (`velocities` OR a
36
+ * structured `set`, plus an optional `label`); the vertical edge label is DATA,
37
+ * not a hardcoded side. Single-slot sets keep using `VelocityStrip`.
38
+ */
39
+ const meta: Meta<typeof DualVelocityStrip> = {
40
+ title: 'Workout/DataViz/VelocityStrip/Dual',
41
+ component: DualVelocityStrip,
42
+ tags: ['autodocs'],
43
+ decorators: [wallDecorator],
44
+ argTypes: {
45
+ left: {
46
+ control: 'object',
47
+ description:
48
+ 'Up-wing stream — `{ velocities | set, label }`. Edit `label` here to change the slot name.',
49
+ },
50
+ right: {
51
+ control: 'object',
52
+ description:
53
+ 'Down-wing stream — `{ velocities | set, label }`. Edit `label` here to change the slot name.',
54
+ },
55
+ variant: {
56
+ control: 'inline-radio',
57
+ options: ['hero', 'compact', 'rail'],
58
+ description:
59
+ 'hero (wall: labels + reference lines), compact (flat resting fold), or rail (lean, neither)',
60
+ },
61
+ scale: {
62
+ control: 'inline-radio',
63
+ options: ['peak', 'fixed'],
64
+ description:
65
+ 'bar scaling, shared across both wings: peak (pair max) or fixed (cross-set ceiling)',
66
+ },
67
+ barColor: {
68
+ control: 'inline-radio',
69
+ options: ['zone', 'loss'],
70
+ description: 'per-wing loss (default) or the shared absolute zone scale; never encodes side',
71
+ },
72
+ targetReps: {
73
+ control: 'number',
74
+ description:
75
+ 'planned rep count — reps beyond a side’s done count draw as mirrored dashed stubs',
76
+ },
77
+ liveRepIndex: {
78
+ control: 'number',
79
+ description: 'index of the newest rep; that mirrored pair animates in (hero only)',
80
+ },
81
+ height: {
82
+ control: { type: 'number', min: 60, max: 260, step: 4 },
83
+ description: 'total plot height (px), split evenly into the up (L) and down (R) wings',
84
+ },
85
+ zones: { control: false, description: 'Velocity-zone bands (WA); default scale when absent' },
86
+ },
87
+ }
88
+ export default meta
89
+ type Story = StoryObj<typeof DualVelocityStrip>
90
+
91
+ // ── The API surface ─────────────────────────────────────────────────────────
92
+
93
+ /** Controls-driven: flip `variant` / `scale` / `barColor` / `targetReps` / `height`, edit each stream's `label` + data. */
94
+ export const Playground: Story = {
95
+ args: {
96
+ left: dualOf(REP_SET, LEFT_SLOT),
97
+ right: dualOf(REP_SET_LAGGING, RIGHT_SLOT),
98
+ variant: 'hero',
99
+ scale: 'peak',
100
+ height: 200,
101
+ },
102
+ }
103
+
104
+ // ── The two canonical states ────────────────────────────────────────────────
105
+ // Deliberately minimal — the scenario pairs in the view groups carry breadth.
106
+
107
+ /** Both sides done, the lagging side one rep short — the everyday finished dual set. */
108
+ export const Default: Story = {
109
+ args: {
110
+ left: dualOf(REP_SET, LEFT_SLOT),
111
+ right: dualOf(REP_SET_LAGGING, RIGHT_SLOT),
112
+ variant: 'hero',
113
+ scale: 'fixed',
114
+ height: 200,
115
+ },
116
+ }
117
+
118
+ /**
119
+ * Mid-set and live: 4 of 8 planned on the up wing, 3 on the down, newest rep
120
+ * animating in. The remainder draws as mirrored dashed stubs on both wings, so
121
+ * the columns are reserved and the set never reflows as reps land — which is
122
+ * also why a live dual should run `scale="fixed"`.
123
+ */
124
+ export const InProgress: Story = {
125
+ args: {
126
+ left: dualOf(IN_PROGRESS_SET, LEFT_SLOT),
127
+ right: dualOf(IN_PROGRESS_LAGGING, RIGHT_SLOT),
128
+ variant: 'hero',
129
+ scale: 'fixed',
130
+ targetReps: 8,
131
+ liveRepIndex: 3,
132
+ height: 200,
133
+ },
134
+ }
135
+
136
+ // ── Layout extremes ─────────────────────────────────────────────────────────
137
+ // Recovered from the pre-reorg DualVelocityStrip stories. Unit tests cover the
138
+ // behaviour; these cover the SURFACE — label placement and collision at full
139
+ // width is a class of bug the tests have already missed once (TD-07.10).
140
+
141
+ /**
142
+ * Symmetric index-lock — the up wing logs 5 reps, the down wing 3, same
143
+ * structure. The dual renders 5 aligned columns and the down side's reps 4–5
144
+ * render as EMPTY cells directly under the up side's (index-locked, never
145
+ * shifted). The next rep a lagging side performs lands at its own column.
146
+ */
147
+ export const SymmetricEmpties: Story = {
148
+ args: {
149
+ left: dualOf([0.9, 0.88, 0.86, 0.84, 0.82], LEFT_SLOT),
150
+ right: dualOf([0.85, 0.8, 0.75], RIGHT_SLOT),
151
+ variant: 'hero',
152
+ scale: 'fixed',
153
+ },
154
+ }
155
+
156
+ /**
157
+ * Small height (120px) — each wing gets ~59px rather than the full hero
158
+ * sentinel, and the responsive text DEGRADES: the per-side slot names collapse
159
+ * to initials ("Left"/"Right" → "L"/"R") and the VL20 / VL30 band labels drop
160
+ * entirely, while the dashed lines + washes stay.
161
+ */
162
+ export const SmallHeightLabels: Story = {
163
+ args: {
164
+ left: dualOf([0.96, 0.9, 0.83, 0.72], LEFT_SLOT),
165
+ right: dualOf([0.9, 0.82, 0.71, 0.6], RIGHT_SLOT),
166
+ variant: 'hero',
167
+ scale: 'fixed',
168
+ height: 120,
169
+ },
170
+ }
171
+
172
+ /**
173
+ * Very small height (72px) — the floor of graceful degradation. Below the
174
+ * thresholds the VL20 / VL30 labels are gone and the slot names are down to
175
+ * initials (a multi-word name would show its initials, e.g. "L A"). No
176
+ * shrinking into an illegible smear.
177
+ */
178
+ export const TinyHeight: Story = {
179
+ args: {
180
+ left: dualOf([0.96, 0.9, 0.83, 0.72], LEFT_SLOT),
181
+ right: dualOf([0.9, 0.82, 0.71, 0.6], RIGHT_SLOT),
182
+ variant: 'hero',
183
+ scale: 'fixed',
184
+ height: 72,
185
+ },
186
+ }