@titan-design/react-ui 0.9.2 → 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 (53) 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 +215 -123
  6. package/dist/index.d.ts +215 -123
  7. package/dist/index.js +408 -259
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +258 -121
  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 +895 -795
  16. package/dist/theme/index.d.ts +895 -795
  17. package/dist/theme/index.js +101 -20
  18. package/dist/theme/index.js.map +1 -1
  19. package/dist/theme/index.mjs +99 -22
  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 +11 -13
  30. package/src/components/custom/Workout/SessionRail.stories.tsx +4 -1
  31. package/src/components/custom/Workout/SessionRail.tsx +16 -14
  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/shell/DashboardShell.tsx +6 -3
  38. package/src/components/ui/surface/Surface.stories.tsx +244 -1
  39. package/src/components/ui/surface/Surface.test.tsx +244 -1
  40. package/src/components/ui/surface/Surface.tsx +98 -20
  41. package/src/components/ui/surface/SurfaceContext.ts +126 -0
  42. package/src/components/ui/surface/index.ts +13 -0
  43. package/src/components/ui/surface/surface.contract.test.ts +283 -0
  44. package/src/test/nativewind-stub.ts +13 -0
  45. package/src/theme/ColorSystem.stories.tsx +232 -47
  46. package/src/theme/ThemeProvider.test.tsx +24 -0
  47. package/src/theme/ThemeProvider.tsx +23 -6
  48. package/src/theme/config.ts +12 -0
  49. package/src/theme/elevation.ts +90 -67
  50. package/src/theme/global.css +35 -9
  51. package/src/theme/tokens/primitives.ts +40 -0
  52. package/src/theme/tokens/semantic.ts +63 -14
  53. 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>
@@ -1,6 +1,6 @@
1
1
  // Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
2
2
  import { View, Text, type ViewProps } from 'react-native'
3
- import { getSemanticColors } from '../../../theme/tokens/semantic'
3
+ import { Surface, onSurfaceColors, useSurfaceMode } from '../../ui/surface'
4
4
  import { Typography } from '../Typography'
5
5
  import { TimerReadout } from '../TimerReadout'
6
6
  import { SegmentedProgressBar } from './SegmentedProgressBar'
@@ -8,13 +8,10 @@ import { MetricTiles, type MetricTileData } from './MetricTiles'
8
8
  import { ScheduleTiles } from './ScheduleTiles'
9
9
  import { paceTone, paceToneColor } from './paceTone'
10
10
 
11
- const t = getSemanticColors('dark')
12
-
13
- // The header shares the SideNav's `background-base` (charcoal 900, #101010) so the nav and
14
- // the rail header read as ONE continuous dark plane on the left — the sunk exercise list
15
- // (charcoal 800) sits raised off it. Previously charcoal 400 (#1F1F1F), which made the header
16
- // the lightest rail surface and let it blend with the rows instead of anchoring to the nav.
17
- const HEADER_BG = t['background-base'] // #101010 — matches SideNav bg-background-base
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
+ // sits raised off it. A `<Surface level="background">` owns that plane so the header no
14
+ // longer hand-sets it, and its title/label text resolve on-surface colours from context.
18
15
 
19
16
  // The chunked pace bar matches the SetStrip language but sits tighter than the default
20
17
  // SetStrip gap (5) — the locked design uses a 3px gap and a 9px track.
@@ -73,21 +70,22 @@ export function SessionHeader({
73
70
  }: SessionHeaderProps) {
74
71
  const upcoming = next != null
75
72
  const totalSets = plan.reduce((sum, e) => sum + e.sets, 0)
73
+ const onSurface = onSurfaceColors(useSurfaceMode())
76
74
 
77
75
  const hasPace = !upcoming && budgetMs != null && elapsedMs != null
78
76
  const target = hasPace ? (elapsedMs as number) / (budgetMs as number) : undefined
79
77
 
80
78
  const setsLabel = upcoming ? `${totalSets} sets` : `${Math.floor(setsDone)}/${totalSets} sets`
81
79
  const setsLabelColor = upcoming
82
- ? t['text-secondary']
80
+ ? onSurface.secondary
83
81
  : paceToneColor(paceTone(totalSets > 0 ? setsDone / totalSets : 0, target))
84
82
 
85
83
  return (
86
- <View
84
+ <Surface
85
+ level="background"
87
86
  className={className}
88
87
  style={[
89
88
  {
90
- backgroundColor: HEADER_BG,
91
89
  paddingTop: 11,
92
90
  paddingRight: 12,
93
91
  paddingBottom: 12,
@@ -106,7 +104,7 @@ export function SessionHeader({
106
104
  lineHeight: 18,
107
105
  fontWeight: '700',
108
106
  fontFamily: '"Space Grotesk", sans-serif',
109
- color: t['text-primary'],
107
+ color: onSurface.primary,
110
108
  marginBottom: 10,
111
109
  }}
112
110
  testID="session-rail-title"
@@ -148,6 +146,6 @@ export function SessionHeader({
148
146
  />
149
147
  </View>
150
148
  </View>
151
- </View>
149
+ </Surface>
152
150
  )
153
151
  }
@@ -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>
@@ -2,20 +2,20 @@
2
2
  import { View, type ViewProps } from 'react-native'
3
3
  import { primitiveColors } from '../../../theme/tokens/primitives'
4
4
  import { neumorphicShadows } from '../../../theme/shadows'
5
+ import { Surface } from '../../ui/surface'
5
6
  import { ExerciseCardHeading } from './ExerciseCardHeading'
6
7
  import { SessionHeader } from './SessionHeader'
7
8
  import type { MetricTileData } from './MetricTiles'
8
9
  import type { SetStripSet } from './SetStrip'
9
10
  import type { ExerciseIndicatorKind } from './ExerciseIndicator'
10
11
 
11
- // Charcoal-ramp surfaces (the locked S3 shell shades): the nav + the heading plane read as
12
- // ONE dark plane (charcoal 900, #101010, set in SessionHeader). The exercise list is a
13
- // LIGHTER inset panel (charcoal 600) recessed via its inner shadow yet paler than the
14
- // header, so the transparent exercise headings on it never blend into the header plane.
15
- // (Band-aid: the list is picked to clear the components; the durable fix is a `<Surface>`
16
- // primitive whose cards own their own raised background — see the rail's PR notes.)
17
- const INSET = primitiveColors.charcoal[600] // #191919lighter sunk exercise-list panel
18
- const DIVIDER = primitiveColors.charcoal[300] // #2C2C2C — row divider (raised to read on #191919)
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
+ // exercise headings on it never blend into the header plane. Surface owns both backgrounds,
17
+ // so the rail no longer hand-sets them.
18
+ const DIVIDER = primitiveColors.charcoal[300] // #2C2C2Crow divider (raised to read on surface-elevated #2A2827)
19
19
 
20
20
  const DEFAULT_WIDTH = 246
21
21
 
@@ -87,9 +87,10 @@ export function SessionRail({
87
87
  ...props
88
88
  }: SessionRailProps) {
89
89
  return (
90
- <View
90
+ <Surface
91
+ level="elevated"
91
92
  className={className}
92
- style={[{ width, flexDirection: 'column', backgroundColor: INSET }, style]}
93
+ style={[{ width, flexDirection: 'column' }, style]}
93
94
  testID="session-rail"
94
95
  {...props}
95
96
  >
@@ -104,8 +105,9 @@ export function SessionRail({
104
105
  next={next}
105
106
  />
106
107
 
107
- <View
108
- style={[{ flex: 1, backgroundColor: INSET }, neumorphicShadows.charcoal.pressed.subtle]}
108
+ <Surface
109
+ level="elevated"
110
+ style={[{ flex: 1 }, neumorphicShadows.charcoal.pressed.subtle]}
109
111
  testID="session-rail-list"
110
112
  >
111
113
  {exercises.map((ex, i) => (
@@ -131,7 +133,7 @@ export function SessionRail({
131
133
  />
132
134
  </View>
133
135
  ))}
134
- </View>
135
- </View>
136
+ </Surface>
137
+ </Surface>
136
138
  )
137
139
  }
@@ -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
 
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from 'react'
2
2
  import { View, Text } from 'react-native'
3
3
  import { cn } from '../../utils/cn'
4
+ import { Surface } from '../ui/surface'
4
5
  import { SideNav, type SideNavItem } from './SideNav'
5
6
  import { TopBar } from './TopBar'
6
7
  import { type Device } from './DeviceRow'
@@ -60,13 +61,15 @@ export function DashboardShell({
60
61
  return (
61
62
  // Column: the TopBar spans the FULL width across the top, and the SideNav sits BELOW it
62
63
  // in the content row (not a full-height left rail). This keeps the brand/status band
63
- // unbroken edge-to-edge and lets the nav align under it.
64
- <View className={cn('flex-1 bg-surface-base', className)}>
64
+ // unbroken edge-to-edge and lets the nav align under it. The shell is the outermost
65
+ // Surface — it owns the base plane and seeds the on-surface colour context (mode) for
66
+ // the whole dashboard tree.
67
+ <Surface level="base" className={cn('flex-1', className)}>
65
68
  <TopBar state={state} devices={devices} subtitle={subtitle} onSelectDevice={onSelectDevice} />
66
69
  <View className="flex-1 flex-row">
67
70
  <SideNav activeKey={activeKey} items={navItems} liveKey={liveKey} onNavigate={onNavigate} />
68
71
  <View className="flex-1">{children ?? <ContentPlaceholder />}</View>
69
72
  </View>
70
- </View>
73
+ </Surface>
71
74
  )
72
75
  }