@titan-design/react-ui 0.9.3 → 0.10.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 (49) 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 +31 -8
  6. package/dist/index.d.ts +31 -8
  7. package/dist/index.js +214 -113
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +210 -114
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/pages.js +214 -157
  12. package/dist/pages.js.map +1 -1
  13. package/dist/pages.mjs +214 -157
  14. package/dist/pages.mjs.map +1 -1
  15. package/dist/theme/index.d.mts +129 -34
  16. package/dist/theme/index.d.ts +129 -34
  17. package/dist/theme/index.js +95 -19
  18. package/dist/theme/index.js.map +1 -1
  19. package/dist/theme/index.mjs +92 -20
  20. package/dist/theme/index.mjs.map +1 -1
  21. package/dist/theme/tokens-css.js +88 -24
  22. package/dist/theme/tokens-css.js.map +1 -1
  23. package/dist/theme/tokens-css.mjs +88 -24
  24. package/dist/theme/tokens-css.mjs.map +1 -1
  25. package/package.json +1 -1
  26. package/src/components/custom/Workout/BaseBadge.tsx +5 -5
  27. package/src/components/custom/Workout/RowInventory.stories.tsx +5 -2
  28. package/src/components/custom/Workout/S3FullRail.stories.tsx +13 -1
  29. package/src/components/custom/Workout/SessionHeader.tsx +2 -2
  30. package/src/components/custom/Workout/SessionRail.stories.tsx +4 -1
  31. package/src/components/custom/Workout/SessionRail.tsx +5 -5
  32. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +16 -25
  33. package/src/components/custom/Workout/SupersetWrapper.tsx +1 -1
  34. package/src/components/custom/Workout/TempoDisplay.tsx +3 -7
  35. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +4 -1
  36. package/src/components/custom/Workout/WorkoutPill.tsx +2 -2
  37. package/src/components/ui/surface/Surface.stories.tsx +195 -1
  38. package/src/components/ui/surface/Surface.test.tsx +148 -9
  39. package/src/components/ui/surface/Surface.tsx +37 -9
  40. package/src/components/ui/surface/SurfaceContext.ts +48 -8
  41. package/src/components/ui/surface/index.ts +1 -0
  42. package/src/components/ui/surface/surface.contract.test.ts +283 -0
  43. package/src/theme/ColorSystem.stories.tsx +232 -47
  44. package/src/theme/config.ts +12 -0
  45. package/src/theme/elevation.ts +90 -67
  46. package/src/theme/global.css +35 -9
  47. package/src/theme/tokens/primitives.ts +40 -0
  48. package/src/theme/tokens/semantic.ts +63 -14
  49. package/tailwind.config.js +8 -0
@@ -22,6 +22,9 @@ import { WeightBadge } from './WeightBadge'
22
22
  import { PrBadge } from './PrBadge'
23
23
  import { PlaceholderStrip } from './PlaceholderStrip'
24
24
  import { SupersetWrapper } from './SupersetWrapper'
25
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
26
+
27
+ const t = getSemanticColors('dark')
25
28
 
26
29
  // ----------------------------------------------------------------------------- gallery scaffold
27
30
  class Boundary extends Component<{ children: ReactNode }, { err: string | null }> {
@@ -364,10 +367,10 @@ export const AllRows: Story = {
364
367
  </Cell>
365
368
  <Cell name="SupersetWrapper" entity="grouping bracket" expand="no">
366
369
  <SupersetWrapper label="SS1" color="#FF7900">
367
- <View style={{ padding: 10, backgroundColor: '#1C1C1C', borderRadius: 8 }}>
370
+ <View style={{ padding: 10, backgroundColor: t['surface-raised'], borderRadius: 8 }}>
368
371
  <Text style={{ color: '#F3F4F6' }}>Exercise A</Text>
369
372
  </View>
370
- <View style={{ padding: 10, backgroundColor: '#1C1C1C', borderRadius: 8 }}>
373
+ <View style={{ padding: 10, backgroundColor: t['surface-raised'], borderRadius: 8 }}>
371
374
  <Text style={{ color: '#F3F4F6' }}>Exercise B</Text>
372
375
  </View>
373
376
  </SupersetWrapper>
@@ -4,6 +4,9 @@
4
4
  */
5
5
  import type { Meta, StoryObj } from '@storybook/react-vite'
6
6
  import { NavStub, Rail, Page, T_TERTIARY } from './setHeadingKit'
7
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
8
+
9
+ const t = getSemanticColors('dark')
7
10
 
8
11
  type Args = { stripHeight: number }
9
12
  const meta: Meta<Args> = {
@@ -25,7 +28,16 @@ export const NextToNav: Story = {
25
28
  <div style={{ display: 'flex', alignItems: 'stretch', width: 'fit-content' }}>
26
29
  <NavStub />
27
30
  <Rail stripH={args.stripHeight} />
28
- <div style={{ width: 140, background: '#161616', alignSelf: 'stretch', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
31
+ <div
32
+ style={{
33
+ width: 140,
34
+ background: t['background-base'],
35
+ alignSelf: 'stretch',
36
+ display: 'flex',
37
+ alignItems: 'center',
38
+ justifyContent: 'center',
39
+ }}
40
+ >
29
41
  <span style={{ fontSize: 10, color: T_TERTIARY, fontFamily: 'monospace' }}>stage</span>
30
42
  </div>
31
43
  </div>
@@ -8,8 +8,8 @@ import { MetricTiles, type MetricTileData } from './MetricTiles'
8
8
  import { ScheduleTiles } from './ScheduleTiles'
9
9
  import { paceTone, paceToneColor } from './paceTone'
10
10
 
11
- // The header shares the SideNav's `background-base` (charcoal 900, #101010) so the nav and
12
- // the rail header read as ONE continuous dark plane on the left — the sunk exercise list
11
+ // The header shares the SideNav's `background-base` (warm-tapered ramp shell, #1C1916) so the
12
+ // nav and the rail header read as ONE continuous dark plane on the left — the sunk exercise list
13
13
  // sits raised off it. A `<Surface level="background">` owns that plane so the header no
14
14
  // longer hand-sets it, and its title/label text resolve on-surface colours from context.
15
15
 
@@ -1,8 +1,11 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import { View, Text } from 'react-native'
3
3
  import { primitiveRamps } from '../../../theme/tokens/primitives'
4
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
4
5
  import { SessionRail, type SessionRailExercise } from './SessionRail'
5
6
 
7
+ const t = getSemanticColors('dark')
8
+
6
9
  /**
7
10
  * `SessionRail` (shell organism) — the live-workout exercise list: a flat raised
8
11
  * `SessionHeader` glance (title, stat tiles, chunked pace bar) over a sunk, inset list
@@ -20,7 +23,7 @@ const meta: Meta<typeof SessionRail> = {
20
23
  },
21
24
  decorators: [
22
25
  (Story) => (
23
- <View className="min-h-screen flex-row" style={{ backgroundColor: '#101010' }}>
26
+ <View className="min-h-screen flex-row" style={{ backgroundColor: t['background-base'] }}>
24
27
  <Story />
25
28
  <View className="flex-1 items-center justify-center">
26
29
  <Text style={{ color: '#6B7280', fontSize: 12 }}>main viewport</Text>
@@ -9,13 +9,13 @@ import type { MetricTileData } from './MetricTiles'
9
9
  import type { SetStripSet } from './SetStrip'
10
10
  import type { ExerciseIndicatorKind } from './ExerciseIndicator'
11
11
 
12
- // Charcoal-ramp surfaces (the locked S3 shell shades): the nav + the heading plane read as
13
- // ONE dark plane (charcoal 900, #101010, owned by SessionHeader's `<Surface level="background">`).
14
- // The exercise list is a LIGHTER inset panel — `<Surface level="elevated">` (charcoal 600,
15
- // #191919) — recessed via its inner shadow yet paler than the header, so the transparent
12
+ // Warm-tapered ramp surfaces (the locked S3 shell shades): the nav + the heading plane read as
13
+ // ONE dark plane (background shell, #1C1916, owned by SessionHeader's `<Surface level="background">`).
14
+ // The exercise list is a LIGHTER inset panel — `<Surface level="elevated">` (#2A2827)
15
+ // recessed via its inner shadow yet paler than the header, so the transparent
16
16
  // exercise headings on it never blend into the header plane. Surface owns both backgrounds,
17
17
  // so the rail no longer hand-sets them.
18
- const DIVIDER = primitiveColors.charcoal[300] // #2C2C2C — row divider (raised to read on #191919)
18
+ const DIVIDER = primitiveColors.charcoal[300] // #2C2C2C — row divider (raised to read on surface-elevated #2A2827)
19
19
 
20
20
  const DEFAULT_WIDTH = 246
21
21
 
@@ -2,6 +2,9 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import { View, Text } from 'react-native'
3
3
  import { SupersetWrapper } from './SupersetWrapper'
4
4
  import { ExerciseCard } from './ExerciseCard'
5
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
6
+
7
+ const t = getSemanticColors('dark')
5
8
 
6
9
  const meta: Meta<typeof SupersetWrapper> = {
7
10
  title: 'Workout/SupersetWrapper',
@@ -9,7 +12,7 @@ const meta: Meta<typeof SupersetWrapper> = {
9
12
  tags: ['autodocs'],
10
13
  decorators: [
11
14
  (Story) => (
12
- <View style={{ maxWidth: 400, padding: 16, backgroundColor: '#121212' }}>
15
+ <View style={{ maxWidth: 400, padding: 16, backgroundColor: t['background-base'] }}>
13
16
  <Story />
14
17
  </View>
15
18
  ),
@@ -26,24 +29,20 @@ export const Default: Story = {
26
29
  <View
27
30
  style={{
28
31
  padding: 12,
29
- backgroundColor: '#1C1C1C',
32
+ backgroundColor: t['surface-raised'],
30
33
  borderRadius: 8,
31
34
  }}
32
35
  >
33
- <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
34
- Exercise A
35
- </Text>
36
+ <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>Exercise A</Text>
36
37
  </View>
37
38
  <View
38
39
  style={{
39
40
  padding: 12,
40
- backgroundColor: '#1C1C1C',
41
+ backgroundColor: t['surface-raised'],
41
42
  borderRadius: 8,
42
43
  }}
43
44
  >
44
- <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
45
- Exercise B
46
- </Text>
45
+ <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>Exercise B</Text>
47
46
  </View>
48
47
  </>
49
48
  ),
@@ -58,24 +57,20 @@ export const CustomLabel: Story = {
58
57
  <View
59
58
  style={{
60
59
  padding: 12,
61
- backgroundColor: '#1C1C1C',
60
+ backgroundColor: t['surface-raised'],
62
61
  borderRadius: 8,
63
62
  }}
64
63
  >
65
- <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
66
- Exercise A1
67
- </Text>
64
+ <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>Exercise A1</Text>
68
65
  </View>
69
66
  <View
70
67
  style={{
71
68
  padding: 12,
72
- backgroundColor: '#1C1C1C',
69
+ backgroundColor: t['surface-raised'],
73
70
  borderRadius: 8,
74
71
  }}
75
72
  >
76
- <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
77
- Exercise A2
78
- </Text>
73
+ <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>Exercise A2</Text>
79
74
  </View>
80
75
  </>
81
76
  ),
@@ -90,24 +85,20 @@ export const CustomColor: Story = {
90
85
  <View
91
86
  style={{
92
87
  padding: 12,
93
- backgroundColor: '#1C1C1C',
88
+ backgroundColor: t['surface-raised'],
94
89
  borderRadius: 8,
95
90
  }}
96
91
  >
97
- <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
98
- Exercise A
99
- </Text>
92
+ <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>Exercise A</Text>
100
93
  </View>
101
94
  <View
102
95
  style={{
103
96
  padding: 12,
104
- backgroundColor: '#1C1C1C',
97
+ backgroundColor: t['surface-raised'],
105
98
  borderRadius: 8,
106
99
  }}
107
100
  >
108
- <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
109
- Exercise B
110
- </Text>
101
+ <Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>Exercise B</Text>
111
102
  </View>
112
103
  </>
113
104
  ),
@@ -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,
@@ -1,6 +1,9 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import { View } from 'react-native'
3
3
  import { VolumeLandmarkBar } from './VolumeLandmarkBar'
4
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
5
+
6
+ const t = getSemanticColors('dark')
4
7
 
5
8
  const meta: Meta<typeof VolumeLandmarkBar> = {
6
9
  title: 'Workout/DataViz/VolumeLandmarkBar',
@@ -28,7 +31,7 @@ const meta: Meta<typeof VolumeLandmarkBar> = {
28
31
  },
29
32
  decorators: [
30
33
  (Story) => (
31
- <View style={{ padding: 24, paddingBottom: 40, backgroundColor: '#101010' }}>
34
+ <View style={{ padding: 24, paddingBottom: 40, backgroundColor: t['background-base'] }}>
32
35
  <Story />
33
36
  </View>
34
37
  ),
@@ -26,7 +26,7 @@ const statusBgColors: Record<WorkoutPillStatus, string> = {
26
26
  completed: 'rgba(46,213,115,0.15)',
27
27
  current: 'rgba(255,121,0,0.15)',
28
28
  next: 'transparent',
29
- upcoming: '#1C1C1C',
29
+ upcoming: t['surface-raised'],
30
30
  missed: 'rgba(209,67,67,0.1)',
31
31
  deload: 'rgba(186,41,150,0.15)',
32
32
  }
@@ -72,7 +72,7 @@ function usePulse(enabled: boolean) {
72
72
  easing: Easing.inOut(Easing.ease),
73
73
  useNativeDriver: true,
74
74
  }),
75
- ]),
75
+ ])
76
76
  )
77
77
  animation.start()
78
78
 
@@ -15,9 +15,13 @@ const meta: Meta<typeof Surface> = {
15
15
  },
16
16
  level: {
17
17
  control: 'select',
18
- options: ['background', 'base', 'elevated', 'raised', 'overlay'],
18
+ options: ['inset', 'background', 'base', 'elevated', 'raised', 'overlay'],
19
19
  description: 'Named charcoal plane (flat background from a semantic token)',
20
20
  },
21
+ pressed: {
22
+ control: 'boolean',
23
+ description: 'Sunken well: one ramp step DOWN from the parent level + inner-shadow recess',
24
+ },
21
25
  theme: {
22
26
  control: 'select',
23
27
  options: ['light', 'dark'],
@@ -99,6 +103,62 @@ export const NamedPlanes: Story = {
99
103
  ),
100
104
  }
101
105
 
106
+ // The pressed (sunken well) model: a `<Surface pressed>` renders ONE ramp step
107
+ // DOWN from its parent's level — the symmetric twin of a raised surface stepping
108
+ // up — plus an inner-shadow recess. It's RELATIVE: the same `pressed` well reads
109
+ // as a consistent recess on every host plane (base → background, raised →
110
+ // elevated, overlay → raised), not a single fixed black hole. It clamps at the
111
+ // `inset` floor so a well in `background` bottoms out at the ramp's pit.
112
+ function WellCard({ level }: { level: SurfaceLevel }) {
113
+ return (
114
+ <Surface level={level} style={{ padding: 16, gap: 12, flex: 1, borderRadius: 14 }}>
115
+ <Eyebrow>HOST · LEVEL = {level.toUpperCase()}</Eyebrow>
116
+ <Surface pressed style={{ padding: 16, gap: 4 }}>
117
+ <Eyebrow>PRESSED WELL</Eyebrow>
118
+ <OnSurfaceBody title="Sunken panel" sub="one step down + inner recess" />
119
+ </Surface>
120
+ </Surface>
121
+ )
122
+ }
123
+
124
+ export const PressedWells: Story = {
125
+ render: () => (
126
+ <Surface level="background" style={{ flexDirection: 'row', gap: 16, padding: 24 }}>
127
+ {(['base', 'raised', 'overlay'] as SurfaceLevel[]).map((level) => (
128
+ <WellCard key={level} level={level} />
129
+ ))}
130
+ </Surface>
131
+ ),
132
+ }
133
+
134
+ // Clamp + nesting — a well in `background` bottoms out at the inset floor
135
+ // (#13100D), and pressing again inside it stays at the floor (no underflow),
136
+ // while a well in a lighter plane still steps down normally.
137
+ export const PressedClampAndNesting: Story = {
138
+ render: () => (
139
+ <View style={{ flexDirection: 'row', gap: 16, padding: 24 }}>
140
+ <Surface level="background" style={{ padding: 16, gap: 12, flex: 1, borderRadius: 14 }}>
141
+ <Eyebrow>HOST · LEVEL = BACKGROUND</Eyebrow>
142
+ <Surface pressed style={{ padding: 16, gap: 10 }}>
143
+ <Eyebrow>PRESSED → INSET FLOOR</Eyebrow>
144
+ <Surface pressed style={{ padding: 12 }}>
145
+ <Eyebrow>PRESSED AGAIN → CLAMPED</Eyebrow>
146
+ </Surface>
147
+ </Surface>
148
+ </Surface>
149
+ <Surface level="raised" style={{ padding: 16, gap: 12, flex: 1, borderRadius: 14 }}>
150
+ <Eyebrow>HOST · LEVEL = RAISED</Eyebrow>
151
+ <Surface pressed style={{ padding: 16, gap: 10 }}>
152
+ <Eyebrow>PRESSED → ELEVATED</Eyebrow>
153
+ <Surface pressed style={{ padding: 12 }}>
154
+ <Eyebrow>PRESSED AGAIN → BASE</Eyebrow>
155
+ </Surface>
156
+ </Surface>
157
+ </Surface>
158
+ </View>
159
+ ),
160
+ }
161
+
102
162
  // On-surface colour context: descendant text reads its colour from the Surface
103
163
  // via `useOnSurfaceColor` — no hard-coded hex, so it never renders black when the
104
164
  // tree mounts as raw RN in the standalone wall SPA.
@@ -138,3 +198,137 @@ export const LightTheme: Story = {
138
198
  </View>
139
199
  ),
140
200
  }
201
+
202
+ // ===========================================================================
203
+ // The Surface WORK — composition, context inheritance, and the real wall lockup.
204
+ // The stories above exercise the primitive in isolation; these show it doing the
205
+ // job it was built for (TD-05.12): stacking planes, propagating the on-surface
206
+ // colour context, and backing the migrated shell / rail / stage.
207
+ // ===========================================================================
208
+
209
+ /** An all-caps eyebrow whose colour resolves on whatever Surface encloses it. */
210
+ function Eyebrow({ children }: { children: string }) {
211
+ return (
212
+ <Text
213
+ style={{
214
+ color: useOnSurfaceColor('tertiary'),
215
+ fontSize: 10,
216
+ fontWeight: '700',
217
+ letterSpacing: 1,
218
+ }}
219
+ >
220
+ {children}
221
+ </Text>
222
+ )
223
+ }
224
+
225
+ /** Primary + secondary body text, both on-surface — no hard-coded hex. */
226
+ function OnSurfaceBody({ title, sub }: { title: string; sub: string }) {
227
+ return (
228
+ <View style={{ gap: 2 }}>
229
+ <Text style={{ color: useOnSurfaceColor('primary'), fontSize: 14, fontWeight: '600' }}>
230
+ {title}
231
+ </Text>
232
+ <Text style={{ color: useOnSurfaceColor('secondary'), fontSize: 12 }}>{sub}</Text>
233
+ </View>
234
+ )
235
+ }
236
+
237
+ // Nesting + context inheritance — the core mechanic. Each Surface publishes its
238
+ // level; the planes read darkest → lightest as they nest (background < base <
239
+ // elevated < raised). The labels call `useOnSurfaceColor` and stay legible on
240
+ // every plane WITHOUT a hard-coded hex — on-surface text resolves from the mode,
241
+ // so one set of roles reads correctly across all the dark planes.
242
+ export const NestedPlanes: Story = {
243
+ render: () => (
244
+ <Surface level="background" style={{ padding: 20, gap: 14 }}>
245
+ <Eyebrow>LEVEL = BACKGROUND</Eyebrow>
246
+ <Surface level="base" style={{ padding: 16, gap: 14, borderRadius: 14 }}>
247
+ <Eyebrow>LEVEL = BASE</Eyebrow>
248
+ <Surface level="elevated" style={{ padding: 16, gap: 14, borderRadius: 12 }}>
249
+ <Eyebrow>LEVEL = ELEVATED</Eyebrow>
250
+ <Surface level="raised" style={{ padding: 16, gap: 6, borderRadius: 10 }}>
251
+ <Eyebrow>LEVEL = RAISED</Eyebrow>
252
+ <OnSurfaceBody title="On-surface text" sub="resolves from the enclosing Surface" />
253
+ </Surface>
254
+ </Surface>
255
+ </Surface>
256
+ </Surface>
257
+ ),
258
+ }
259
+
260
+ // Theme inheritance — a Surface seeds the mode; the nested Surfaces and text with
261
+ // NO explicit `theme` inherit it. The SAME subtree renders on both grounds and
262
+ // stays legible, because on-surface colours resolve from the inherited mode
263
+ // rather than a fixed hex. (A numeric `elevation` Surface inherits mode too.)
264
+ function InheritingCard() {
265
+ return (
266
+ <Surface level="base" style={{ padding: 14, gap: 10, borderRadius: 12 }}>
267
+ <OnSurfaceBody title="Inherited mode" sub="no theme prop on this card" />
268
+ <Surface level="raised" style={{ padding: 12, borderRadius: 8 }}>
269
+ <Eyebrow>NESTED · STILL INHERITS</Eyebrow>
270
+ </Surface>
271
+ </Surface>
272
+ )
273
+ }
274
+
275
+ export const ThemeInheritance: Story = {
276
+ render: () => (
277
+ <View style={{ flexDirection: 'row', gap: 16, padding: 24 }}>
278
+ <Surface theme="dark" level="background" style={{ padding: 16, gap: 10, flex: 1 }}>
279
+ <Eyebrow>THEME = DARK (SEEDS CONTEXT)</Eyebrow>
280
+ <InheritingCard />
281
+ </Surface>
282
+ <Surface theme="light" level="background" style={{ padding: 16, gap: 10, flex: 1 }}>
283
+ <Eyebrow>THEME = LIGHT (SEEDS CONTEXT)</Eyebrow>
284
+ <InheritingCard />
285
+ </Surface>
286
+ </View>
287
+ ),
288
+ }
289
+
290
+ // The wall lockup, as migrated (DashboardShell / SessionRail / stage). The shell
291
+ // is a `background` plane; the page a `base` plane; the session rail an
292
+ // `elevated` inset; the live stage a `raised` panel. Every eyebrow/label reads
293
+ // on-surface — this composition is what replaced the inline `getSemanticColors`
294
+ // text patches the standalone wall SPA used to carry.
295
+ function RailRow({ name, sets }: { name: string; sets: string }) {
296
+ return (
297
+ <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
298
+ <Text style={{ color: useOnSurfaceColor('primary'), fontSize: 13 }}>{name}</Text>
299
+ <Text style={{ color: useOnSurfaceColor('tertiary'), fontSize: 13 }}>{sets}</Text>
300
+ </View>
301
+ )
302
+ }
303
+
304
+ export const WallLockup: Story = {
305
+ render: () => (
306
+ <Surface level="background" style={{ flexDirection: 'row', height: 320, padding: 12, gap: 12 }}>
307
+ {/* left nav — flush to the shell edge, on the same background plane */}
308
+ <View style={{ width: 56, alignItems: 'center', paddingTop: 10 }}>
309
+ <Eyebrow>NAV</Eyebrow>
310
+ </View>
311
+ {/* page — a base plane holding the rail + stage */}
312
+ <Surface
313
+ level="base"
314
+ style={{ flex: 1, flexDirection: 'row', padding: 12, gap: 12, borderRadius: 14 }}
315
+ >
316
+ {/* session rail — an elevated inset */}
317
+ <Surface level="elevated" style={{ width: 150, padding: 12, gap: 10, borderRadius: 12 }}>
318
+ <Eyebrow>SESSION</Eyebrow>
319
+ <RailRow name="Bench" sets="3/3" />
320
+ <RailRow name="Cable Row" sets="1/3" />
321
+ <RailRow name="Curl" sets="0/3" />
322
+ </Surface>
323
+ {/* live stage — a raised panel */}
324
+ <Surface
325
+ level="raised"
326
+ style={{ flex: 1, padding: 16, gap: 6, justifyContent: 'center', borderRadius: 12 }}
327
+ >
328
+ <Eyebrow>LIVE</Eyebrow>
329
+ <OnSurfaceBody title="Seated Cable Row" sub="set 2 · 8 reps · 0.42 m/s" />
330
+ </Surface>
331
+ </Surface>
332
+ </Surface>
333
+ ),
334
+ }