@titan-design/react-ui 0.13.0 → 0.14.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 (146) hide show
  1. package/README.md +14 -0
  2. package/dist/bodymap.js +129 -76
  3. package/dist/bodymap.js.map +1 -1
  4. package/dist/bodymap.mjs +129 -76
  5. package/dist/bodymap.mjs.map +1 -1
  6. package/dist/index.d.mts +117 -26
  7. package/dist/index.d.ts +117 -26
  8. package/dist/index.js +677 -450
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +673 -451
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/{pages-_3Ajpiqu.d.ts → pages-CsjsUn0G.d.ts} +30 -13
  13. package/dist/{pages-CpW3OB3g.d.mts → pages-ZbT_pD_U.d.mts} +30 -13
  14. package/dist/pages.d.mts +1 -1
  15. package/dist/pages.d.ts +1 -1
  16. package/dist/pages.js +676 -701
  17. package/dist/pages.js.map +1 -1
  18. package/dist/pages.mjs +676 -701
  19. package/dist/pages.mjs.map +1 -1
  20. package/dist/theme/index.d.mts +31 -7
  21. package/dist/theme/index.d.ts +31 -7
  22. package/dist/theme/index.js +84 -20
  23. package/dist/theme/index.js.map +1 -1
  24. package/dist/theme/index.mjs +83 -21
  25. package/dist/theme/index.mjs.map +1 -1
  26. package/dist/theme/tokens-css.js +84 -20
  27. package/dist/theme/tokens-css.js.map +1 -1
  28. package/dist/theme/tokens-css.mjs +84 -20
  29. package/dist/theme/tokens-css.mjs.map +1 -1
  30. package/dist/theme/tokens.d.mts +56 -7
  31. package/dist/theme/tokens.d.ts +56 -7
  32. package/dist/theme/tokens.js +72 -20
  33. package/dist/theme/tokens.js.map +1 -1
  34. package/dist/theme/tokens.mjs +71 -21
  35. package/dist/theme/tokens.mjs.map +1 -1
  36. package/docs/render-testing-pattern.md +130 -0
  37. package/package.json +1 -1
  38. package/src/arch/Architecture.stories.tsx +3 -1
  39. package/src/arch/arch-graph.json +74 -72
  40. package/src/components/custom/ActiveWork/InitiativeBrief.tsx +2 -1
  41. package/src/components/custom/CircularTimer/CircularTimer.stories.tsx +8 -3
  42. package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
  43. package/src/components/custom/Gauge/Gauge.stories.tsx +4 -1
  44. package/src/components/custom/Gauge/Gauge.tsx +3 -1
  45. package/src/components/custom/Scatter/Scatter.stories.tsx +10 -7
  46. package/src/components/custom/Scatter/Scatter.tsx +6 -4
  47. package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
  48. package/src/components/custom/Table/Table.stories.tsx +1 -1
  49. package/src/components/custom/Table/Table.tsx +2 -2
  50. package/src/components/custom/Treemap/Treemap.stories.tsx +13 -10
  51. package/src/components/custom/Treemap/Treemap.tsx +4 -1
  52. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +2 -1
  53. package/src/components/custom/Workout/BaseBadge.stories.tsx +11 -7
  54. package/src/components/custom/Workout/BaseBadge.tsx +3 -2
  55. package/src/components/custom/Workout/BodyMap.tsx +5 -3
  56. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +13 -3
  57. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +5 -3
  58. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +2 -1
  59. package/src/components/custom/Workout/CapacityBandChart.tsx +5 -3
  60. package/src/components/custom/Workout/DeviationBar.test.tsx +5 -2
  61. package/src/components/custom/Workout/DeviationBar.tsx +5 -2
  62. package/src/components/custom/Workout/DualVelocityStrip.stories.tsx +7 -6
  63. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +29 -16
  64. package/src/components/custom/Workout/ExerciseCard.stories.tsx +8 -8
  65. package/src/components/custom/Workout/ExerciseCard.test.tsx +39 -19
  66. package/src/components/custom/Workout/ExerciseCard.tsx +92 -167
  67. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +89 -6
  68. package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +189 -1
  69. package/src/components/custom/Workout/ExerciseCardHeading.tsx +167 -18
  70. package/src/components/custom/Workout/ExerciseDetailPage.tsx +3 -2
  71. package/src/components/custom/Workout/ExerciseHeading.tsx +192 -47
  72. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +6 -5
  73. package/src/components/custom/Workout/InputBar.tsx +4 -2
  74. package/src/components/custom/Workout/IntensityBar.tsx +4 -1
  75. package/src/components/custom/Workout/MesoCard.tsx +2 -1
  76. package/src/components/custom/Workout/MesoStatusCard.tsx +11 -10
  77. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +8 -3
  78. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +12 -2
  79. package/src/components/custom/Workout/README.md +58 -12
  80. package/src/components/custom/Workout/ReadinessCheck.tsx +4 -2
  81. package/src/components/custom/Workout/RestTimer.stories.tsx +13 -2
  82. package/src/components/custom/Workout/RestTimer.tsx +4 -2
  83. package/src/components/custom/Workout/SegmentedBar.stories.tsx +16 -12
  84. package/src/components/custom/Workout/SegmentedBar.tsx +4 -3
  85. package/src/components/custom/Workout/SetStrip.stories.tsx +3 -2
  86. package/src/components/custom/Workout/SetsRepsLoad.tsx +23 -9
  87. package/src/components/custom/Workout/Sparkline.stories.tsx +13 -10
  88. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +2 -1
  89. package/src/components/custom/Workout/StrengthTrendChart.tsx +8 -6
  90. package/src/components/custom/Workout/TempoDisplay.stories.tsx +4 -3
  91. package/src/components/custom/Workout/TempoDisplay.tsx +2 -0
  92. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +3 -2
  93. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +1 -1
  94. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +40 -0
  95. package/src/components/custom/Workout/VelocityStrip.test.tsx +48 -6
  96. package/src/components/custom/Workout/VelocityStrip.tsx +23 -14
  97. package/src/components/custom/Workout/ZoneTrack.stories.tsx +3 -1
  98. package/src/components/custom/Workout/exerciseRowState.ts +29 -0
  99. package/src/components/custom/Workout/index.ts +12 -2
  100. package/src/components/custom/Workout/metricText.stories.tsx +9 -8
  101. package/src/components/custom/Workout/metricText.tsx +4 -2
  102. package/src/components/custom/Workout/setHeadingKit.tsx +12 -10
  103. package/src/components/custom/Workout/velocity-story-kit.tsx +2 -2
  104. package/src/components/custom/charts/README.md +5 -0
  105. package/src/components/custom/charts/SetBarChart.test.tsx +16 -0
  106. package/src/components/custom/charts/SetBarChart.tsx +14 -5
  107. package/src/components/custom/charts/flatBarGeometry.test.ts +41 -0
  108. package/src/components/custom/charts/flatBarGeometry.ts +32 -0
  109. package/src/components/custom/stepper/Stepper.tsx +8 -6
  110. package/src/components/ui/alert/Alert.stories.tsx +14 -4
  111. package/src/components/ui/alert/Alert.tsx +24 -3
  112. package/src/components/ui/button/Button.tsx +12 -12
  113. package/src/components/ui/checkbox/Checkbox.tsx +2 -2
  114. package/src/components/ui/data-row/DataRow.stories.tsx +6 -2
  115. package/src/components/ui/drawer/Drawer.tsx +4 -1
  116. package/src/components/ui/icon-box/IconBox.tsx +8 -6
  117. package/src/components/ui/indicator/Indicator.stories.tsx +4 -3
  118. package/src/components/ui/list-item/ListItem.stories.tsx +7 -4
  119. package/src/components/ui/modal/Modal.tsx +2 -0
  120. package/src/components/ui/pill/Pill.stories.tsx +53 -0
  121. package/src/components/ui/pill/Pill.tsx +10 -6
  122. package/src/components/ui/popover/Popover.stories.tsx +1 -1
  123. package/src/components/ui/progress/Progress.tsx +9 -6
  124. package/src/components/ui/radio/Radio.tsx +1 -1
  125. package/src/components/ui/section/Section.stories.tsx +10 -10
  126. package/src/components/ui/select/Select.tsx +2 -1
  127. package/src/components/ui/spinner/Spinner.tsx +3 -1
  128. package/src/components/ui/stack/Stack.stories.tsx +7 -7
  129. package/src/components/ui/surface/Surface.stories.tsx +75 -28
  130. package/src/components/ui/switch/Switch.tsx +1 -1
  131. package/src/components/ui/tabs/Tabs.tsx +4 -2
  132. package/src/components/ui/toast/Toast.tsx +6 -4
  133. package/src/components/ui/toolbar-button/ToolbarButton.tsx +13 -6
  134. package/src/stories/PresetShowcase.stories.tsx +1 -1
  135. package/src/test/no-raw-color.test.ts +39 -0
  136. package/src/theme/Alpha.stories.tsx +103 -0
  137. package/src/theme/ProposedTokensVW82.stories.tsx +414 -0
  138. package/src/theme/config.ts +12 -0
  139. package/src/theme/global.css +15 -3
  140. package/src/theme/subtle-tone-weight.test.ts +163 -0
  141. package/src/theme/tailwind-var-opacity.test.ts +163 -0
  142. package/src/theme/tokens/primitives.ts +33 -0
  143. package/src/theme/tokens/semantic-hex-guard.test.ts +322 -0
  144. package/src/theme/tokens/semantic.ts +56 -20
  145. package/src/utils/colors.alpha-adoption.test.ts +408 -0
  146. package/tailwind.config.js +50 -9
@@ -1,6 +1,7 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import { View } from 'react-native'
3
3
  import { Indicator } from './Indicator'
4
+ import { primitiveRamps } from '../../../theme/tokens/primitives'
4
5
 
5
6
  const meta: Meta<typeof Indicator> = {
6
7
  title: 'Components/Atoms/Indicator',
@@ -69,9 +70,9 @@ export const WithRing: Story = {
69
70
  export const CustomColor: Story = {
70
71
  render: () => (
71
72
  <View className="flex-row gap-4 items-center">
72
- <Indicator size="md" customColor="#FF6B6B" />
73
- <Indicator size="md" customColor="#4ECDC4" />
74
- <Indicator size="md" customColor="#45B7D1" glow />
73
+ <Indicator size="md" customColor={primitiveRamps.red[400]} />
74
+ <Indicator size="md" customColor={primitiveRamps.cyan[300]} />
75
+ <Indicator size="md" customColor={primitiveRamps.blue[400]} glow />
75
76
  </View>
76
77
  ),
77
78
  }
@@ -7,6 +7,7 @@ import {
7
7
  ListItemTrailing,
8
8
  ListItemDivider,
9
9
  } from './ListItem'
10
+ import { greyRamp } from '../../../theme/tokens/primitives'
10
11
 
11
12
  function UserIcon({ size = 20 }: { size?: number; className?: string }) {
12
13
  return (
@@ -15,12 +16,14 @@ function UserIcon({ size = 20 }: { size?: number; className?: string }) {
15
16
  width: size,
16
17
  height: size,
17
18
  borderRadius: size / 2,
18
- backgroundColor: '#6366f1',
19
+ backgroundColor: 'var(--color-avatar-background)',
19
20
  justifyContent: 'center',
20
21
  alignItems: 'center',
21
22
  }}
22
23
  >
23
- <Text style={{ color: '#fff', fontSize: size * 0.5, fontWeight: '600' }}>U</Text>
24
+ <Text style={{ color: 'var(--color-avatar-text)', fontSize: size * 0.5, fontWeight: '600' }}>
25
+ U
26
+ </Text>
24
27
  </View>
25
28
  )
26
29
  }
@@ -50,7 +53,7 @@ function LockIcon({ size = 20 }: { size?: number; className?: string }) {
50
53
  }
51
54
 
52
55
  function ChevronRight({ size = 16 }: { size?: number }) {
53
- return <Text style={{ color: '#9ca3af', fontSize: size }}>{'>'}</Text>
56
+ return <Text style={{ color: greyRamp[400], fontSize: size }}>{'>'}</Text>
54
57
  }
55
58
 
56
59
  const meta: Meta<typeof ListItem> = {
@@ -133,7 +136,7 @@ export const SettingsList: Story = {
133
136
  <ListItemIcon icon={WifiIcon} />
134
137
  <ListItemContent title="Wi-Fi" subtitle="HomeNetwork" />
135
138
  <ListItemTrailing>
136
- <Text style={{ color: '#22c55e', fontSize: 12 }}>Connected</Text>
139
+ <Text style={{ color: 'var(--color-status-success)', fontSize: 12 }}>Connected</Text>
137
140
  </ListItemTrailing>
138
141
  </ListItem>
139
142
  </View>
@@ -92,6 +92,8 @@ export function Modal({
92
92
  onPress={handleBackdropPress}
93
93
  className={cn(
94
94
  'flex-1 items-center justify-center',
95
+ // VW-82: scrims stay raw — Tailwind v3 drops the `/n` modifier on a
96
+ // `var()` colour, so a translucent role has to be its own token (proposed).
95
97
  backdropBlur ? 'bg-black/30 web:backdrop-blur-sm' : 'bg-black/50',
96
98
  backdropClassName
97
99
  )}
@@ -2,6 +2,8 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import { View } from 'react-native'
3
3
  import { Pill } from './Pill'
4
4
  import { Indicator } from '../indicator'
5
+ import { Surface } from '../surface'
6
+ import { Typography } from '../../custom/Typography'
5
7
 
6
8
  const meta: Meta<typeof Pill> = {
7
9
  title: 'Components/Atoms/Pill',
@@ -54,6 +56,57 @@ export const AllTones: Story = {
54
56
  ),
55
57
  }
56
58
 
59
+ /**
60
+ * The same row on both planes a subtle pill actually lands on. A `-subtle` fill is
61
+ * alpha, so it composites against whatever is behind it: the raised card lifts every
62
+ * capsule and costs each label contrast. It is the harder case and the one AW-133 is
63
+ * measured against, so judge the tone weight here, not only on the page plane.
64
+ */
65
+ export const OnBothPlanes: Story = {
66
+ parameters: {
67
+ docs: {
68
+ description: {
69
+ story:
70
+ 'Page plane (`grey-925`) above, raised card (`grey-875`) below. Every tone loses ' +
71
+ 'roughly 0.8-1.0 of contrast ratio on the card. `brand` is the deliberate exception ' +
72
+ 'from AW-133 — its label stays the exact `orange[400]` brand hue rather than ' +
73
+ 'levelling with the other five, which leaves it the one tone under AA here.',
74
+ },
75
+ },
76
+ },
77
+ render: () => (
78
+ <View className="gap-4">
79
+ <View className="gap-2">
80
+ <Typography variant="caption" color="tertiary">
81
+ page plane · grey-925
82
+ </Typography>
83
+ <View className="flex-row gap-2 flex-wrap">
84
+ <Pill tone="brand">Brand</Pill>
85
+ <Pill tone="brand-secondary">Accent</Pill>
86
+ <Pill tone="success">Success</Pill>
87
+ <Pill tone="warning">Warning</Pill>
88
+ <Pill tone="error">Error</Pill>
89
+ <Pill tone="info">Info</Pill>
90
+ </View>
91
+ </View>
92
+
93
+ <Surface level="raised" className="gap-2 rounded-xl border-hairline p-4">
94
+ <Typography variant="caption" color="tertiary">
95
+ raised card · grey-875
96
+ </Typography>
97
+ <View className="flex-row gap-2 flex-wrap">
98
+ <Pill tone="brand">Brand</Pill>
99
+ <Pill tone="brand-secondary">Accent</Pill>
100
+ <Pill tone="success">Success</Pill>
101
+ <Pill tone="warning">Warning</Pill>
102
+ <Pill tone="error">Error</Pill>
103
+ <Pill tone="info">Info</Pill>
104
+ </View>
105
+ </Surface>
106
+ </View>
107
+ ),
108
+ }
109
+
57
110
  export const AllSizes: Story = {
58
111
  render: () => (
59
112
  <View className="flex-row gap-2 items-center">
@@ -83,13 +83,17 @@ const toneStyles: Record<PillVariant, Record<PillTone, string>> = {
83
83
  // host is, and an alpha fill composites by the same amount on all of them.
84
84
  // A `surface-*` fill would vanish on the plane it names. Borderless like the
85
85
  // six coloured tones — the fill carries the capsule, not a ring.
86
+ // Label comes from `on-*-subtle`, not the base tone token: the base is tuned to
87
+ // carry a white label as a solid fill, which left error and accent unreadable
88
+ // here (AW-133). The on-subtle tokens level the family at one weight.
86
89
  neutral: 'bg-hairline-subtle border-transparent text-text-secondary',
87
- brand: 'bg-brand-primary-subtle border-transparent text-brand-primary',
88
- 'brand-secondary': 'bg-brand-secondary-subtle border-transparent text-brand-secondary',
89
- success: 'bg-status-success-subtle border-transparent text-status-success',
90
- warning: 'bg-status-warning-subtle border-transparent text-status-warning',
91
- error: 'bg-status-error-subtle border-transparent text-status-error',
92
- info: 'bg-status-info-subtle border-transparent text-status-info',
90
+ brand: 'bg-brand-primary-subtle border-transparent text-on-brand-primary-subtle',
91
+ 'brand-secondary':
92
+ 'bg-brand-secondary-subtle border-transparent text-on-brand-secondary-subtle',
93
+ success: 'bg-status-success-subtle border-transparent text-on-status-success-subtle',
94
+ warning: 'bg-status-warning-subtle border-transparent text-on-status-warning-subtle',
95
+ error: 'bg-status-error-subtle border-transparent text-on-status-error-subtle',
96
+ info: 'bg-status-info-subtle border-transparent text-on-status-info-subtle',
93
97
  },
94
98
  outline: {
95
99
  neutral: 'border-hairline text-text-secondary',
@@ -160,7 +160,7 @@ export const WithFormContent: Story = {
160
160
  </View>
161
161
  </PopoverCloseButton>
162
162
  <View className="px-3 py-1.5 rounded-md bg-brand-primary">
163
- <Text className="text-white text-sm font-medium">Save</Text>
163
+ <Text className="text-on-brand-primary text-sm font-medium">Save</Text>
164
164
  </View>
165
165
  </View>
166
166
  </View>
@@ -56,13 +56,16 @@ const colorStyles: Record<ProgressColor, string> = {
56
56
  info: 'bg-status-info',
57
57
  }
58
58
 
59
+ // `-muted` rather than `-subtle`: the track is structural — it has to show the
60
+ // bar's full extent behind the fill — and the lightest rung disappears on the
61
+ // dark base. Replaces a `/20` modifier that emitted no rule (VW-308).
59
62
  const trackColorStyles: Record<ProgressColor, string> = {
60
- primary: 'bg-brand-primary/20',
61
- secondary: 'bg-brand-secondary/20',
62
- success: 'bg-status-success/20',
63
- error: 'bg-status-error/20',
64
- warning: 'bg-status-warning/20',
65
- info: 'bg-status-info/20',
63
+ primary: 'bg-brand-primary-muted',
64
+ secondary: 'bg-brand-secondary-muted',
65
+ success: 'bg-status-success-muted',
66
+ error: 'bg-status-error-muted',
67
+ warning: 'bg-status-warning-muted',
68
+ info: 'bg-status-info-muted',
66
69
  }
67
70
 
68
71
  /**
@@ -191,7 +191,7 @@ export function Radio({
191
191
  )}
192
192
  >
193
193
  {/* Inner dot */}
194
- {isChecked && <View className={cn('rounded-full bg-white', inner)} />}
194
+ {isChecked && <View className={cn('rounded-full bg-on-brand-primary', inner)} />}
195
195
  </View>
196
196
 
197
197
  {/* Label */}
@@ -16,7 +16,7 @@ export const Default: Story = {
16
16
  <Section>
17
17
  <SectionHeader title="Recent Workouts" />
18
18
  <SectionContent>
19
- <Text style={{ color: '#a0a0a0' }}>Workout items would go here</Text>
19
+ <Text style={{ color: 'var(--color-text-secondary)' }}>Workout items would go here</Text>
20
20
  </SectionContent>
21
21
  </Section>
22
22
  ),
@@ -27,7 +27,7 @@ export const WithSubtitle: Story = {
27
27
  <Section>
28
28
  <SectionHeader title="Workout Stats" subtitle="Last 7 days" />
29
29
  <SectionContent>
30
- <Text style={{ color: '#a0a0a0' }}>Stats content</Text>
30
+ <Text style={{ color: 'var(--color-text-secondary)' }}>Stats content</Text>
31
31
  </SectionContent>
32
32
  </Section>
33
33
  ),
@@ -38,10 +38,10 @@ export const WithTrailingAction: Story = {
38
38
  <Section>
39
39
  <SectionHeader
40
40
  title="Exercises"
41
- trailing={<Text style={{ color: '#6366f1', fontSize: 13 }}>View All</Text>}
41
+ trailing={<Text style={{ color: 'var(--color-text-link)', fontSize: 13 }}>View All</Text>}
42
42
  />
43
43
  <SectionContent>
44
- <Text style={{ color: '#a0a0a0' }}>Exercise list</Text>
44
+ <Text style={{ color: 'var(--color-text-secondary)' }}>Exercise list</Text>
45
45
  </SectionContent>
46
46
  </Section>
47
47
  ),
@@ -53,10 +53,10 @@ export const FullExample: Story = {
53
53
  <SectionHeader
54
54
  title="Performance"
55
55
  subtitle="Average velocity by set"
56
- trailing={<Text style={{ color: '#6366f1', fontSize: 13 }}>Details</Text>}
56
+ trailing={<Text style={{ color: 'var(--color-text-link)', fontSize: 13 }}>Details</Text>}
57
57
  />
58
58
  <SectionContent className="rounded-lg bg-surface-elevated p-4">
59
- <Text style={{ color: '#a0a0a0' }}>Chart or data visualization</Text>
59
+ <Text style={{ color: 'var(--color-text-secondary)' }}>Chart or data visualization</Text>
60
60
  </SectionContent>
61
61
  </Section>
62
62
  ),
@@ -68,7 +68,7 @@ export const MultipleSections: Story = {
68
68
  <Section>
69
69
  <SectionHeader title="Today" subtitle="3 exercises" />
70
70
  <SectionContent className="rounded-lg bg-surface-elevated p-4">
71
- <Text style={{ color: '#a0a0a0' }}>Today&apos;s workout data</Text>
71
+ <Text style={{ color: 'var(--color-text-secondary)' }}>Today&apos;s workout data</Text>
72
72
  </SectionContent>
73
73
  </Section>
74
74
 
@@ -76,17 +76,17 @@ export const MultipleSections: Story = {
76
76
  <SectionHeader
77
77
  title="This Week"
78
78
  subtitle="12 sets completed"
79
- trailing={<Text style={{ color: '#6366f1', fontSize: 13 }}>See All</Text>}
79
+ trailing={<Text style={{ color: 'var(--color-text-link)', fontSize: 13 }}>See All</Text>}
80
80
  />
81
81
  <SectionContent className="rounded-lg bg-surface-elevated p-4">
82
- <Text style={{ color: '#a0a0a0' }}>Weekly summary</Text>
82
+ <Text style={{ color: 'var(--color-text-secondary)' }}>Weekly summary</Text>
83
83
  </SectionContent>
84
84
  </Section>
85
85
 
86
86
  <Section>
87
87
  <SectionHeader title="Personal Records" />
88
88
  <SectionContent className="rounded-lg bg-surface-elevated p-4">
89
- <Text style={{ color: '#a0a0a0' }}>PR list</Text>
89
+ <Text style={{ color: 'var(--color-text-secondary)' }}>PR list</Text>
90
90
  </SectionContent>
91
91
  </Section>
92
92
  </View>
@@ -140,6 +140,7 @@ export function Select<T extends string = string>({
140
140
  accessibilityState={{ expanded: isOpen, disabled: isDisabled }}
141
141
  className={cn(
142
142
  'flex-row items-center justify-between px-4 py-2.5 rounded-md border',
143
+ // VW-82: scrim-style fill, left raw (see Modal.tsx).
143
144
  variant === 'filled' ? 'bg-black/30' : 'bg-surface-base',
144
145
  isInvalid
145
146
  ? 'border-border-input-error'
@@ -227,7 +228,7 @@ function SelectOption<T>({ option }: SelectOptionComponentProps<T>) {
227
228
  selected ? 'bg-brand-primary border-brand-primary' : 'border-hairline-strong'
228
229
  )}
229
230
  >
230
- {selected && <Text className="text-white text-xs text-center">✓</Text>}
231
+ {selected && <Text className="text-on-brand-primary text-xs text-center">✓</Text>}
231
232
  </View>
232
233
  )}
233
234
  <Text
@@ -2,6 +2,7 @@ import React from 'react'
2
2
  import { ActivityIndicator, View, type ViewProps } from 'react-native'
3
3
  import { cn } from '../../../utils/cn'
4
4
  import { SPINNER_PRIMARY, SPINNER_SECONDARY } from '../../../theme/extracted-colors-ui'
5
+ import { primitiveColors } from '../../../theme/tokens/primitives'
5
6
 
6
7
  export type SpinnerSize = 'sm' | 'md' | 'lg' | 'xl'
7
8
  export type SpinnerColor = 'primary' | 'secondary' | 'white' | 'default'
@@ -27,7 +28,8 @@ const sizeMap: Record<SpinnerSize, 'small' | 'large'> = {
27
28
  const colorMap: Record<SpinnerColor, string> = {
28
29
  primary: SPINNER_PRIMARY,
29
30
  secondary: SPINNER_SECONDARY,
30
- white: '#FFFFFF',
31
+ white: primitiveColors.white,
32
+ // VW-82: Tailwind gray-500; no titan grey matches. Left raw, proposed in the PR.
31
33
  default: '#6B7280',
32
34
  }
33
35
 
@@ -35,7 +35,7 @@ type Story = StoryObj<typeof Stack>
35
35
  function Box({ children }: { children: string }) {
36
36
  return (
37
37
  <View className="rounded bg-brand-primary px-4 py-2">
38
- <Text className="text-sm text-white">{children}</Text>
38
+ <Text className="text-sm text-on-brand-primary">{children}</Text>
39
39
  </View>
40
40
  )
41
41
  }
@@ -76,23 +76,23 @@ export const Vertical: Story = {
76
76
  export const WithAlignment: Story = {
77
77
  render: () => (
78
78
  <VStack gap={6}>
79
- <Text className="text-sm font-medium text-neutral-400">justify=&quot;between&quot;</Text>
79
+ <Text className="text-sm font-medium text-text-secondary">justify=&quot;between&quot;</Text>
80
80
  <HStack gap={2} justify="between" className="w-full">
81
81
  <Box>A</Box>
82
82
  <Box>B</Box>
83
83
  <Box>C</Box>
84
84
  </HStack>
85
85
 
86
- <Text className="text-sm font-medium text-neutral-400">align=&quot;center&quot;</Text>
86
+ <Text className="text-sm font-medium text-text-secondary">align=&quot;center&quot;</Text>
87
87
  <HStack gap={2} align="center">
88
88
  <View className="rounded bg-brand-primary px-4 py-1">
89
- <Text className="text-sm text-white">Short</Text>
89
+ <Text className="text-sm text-on-brand-primary">Short</Text>
90
90
  </View>
91
91
  <View className="rounded bg-brand-primary px-4 py-4">
92
- <Text className="text-sm text-white">Tall</Text>
92
+ <Text className="text-sm text-on-brand-primary">Tall</Text>
93
93
  </View>
94
94
  <View className="rounded bg-brand-primary px-4 py-2">
95
- <Text className="text-sm text-white">Medium</Text>
95
+ <Text className="text-sm text-on-brand-primary">Medium</Text>
96
96
  </View>
97
97
  </HStack>
98
98
  </VStack>
@@ -114,7 +114,7 @@ export const GapScale: Story = {
114
114
  <VStack gap={6}>
115
115
  {([0, 1, 2, 4, 6, 8, 12] as const).map((g) => (
116
116
  <VStack key={g} gap={1}>
117
- <Text className="text-xs text-neutral-400">gap={g}</Text>
117
+ <Text className="text-xs text-text-secondary">gap={g}</Text>
118
118
  <HStack gap={g}>
119
119
  <Box>A</Box>
120
120
  <Box>B</Box>
@@ -3,6 +3,8 @@ import { View, Text } from 'react-native'
3
3
  import { Surface } from './Surface'
4
4
  import { useOnSurfaceColor, type SurfaceLevel } from './SurfaceContext'
5
5
  import type { ElevationLevel } from '../../../theme/elevation'
6
+ import { semanticColorsLight, semanticColorsDark } from '../../../theme/tokens/semantic'
7
+ import { primitiveRamps } from '../../../theme/tokens/primitives'
6
8
 
7
9
  const meta: Meta<typeof Surface> = {
8
10
  title: 'Components/Atoms/Surface',
@@ -56,11 +58,18 @@ export const Default: Story = {
56
58
  elevation: 2,
57
59
  theme: 'dark',
58
60
  },
59
- render: (args) => (
60
- <Surface {...args} style={{ padding: 24 }}>
61
- <Text style={{ color: '#fff' }}>Default Surface</Text>
62
- </Surface>
63
- ),
61
+ render: function Render(args) {
62
+ return (
63
+ <Surface {...args} style={{ padding: 24 }}>
64
+ <Text style={{ color: useOnSurfaceColor('primary') }}>Default Surface</Text>
65
+ </Surface>
66
+ )
67
+ },
68
+ }
69
+
70
+ /** On-surface elevation label — resolves from whichever Surface encloses it. */
71
+ function ElevationLabel({ level }: { level: ElevationLevel }) {
72
+ return <Text style={{ color: useOnSurfaceColor('primary') }}>Elevation {level}</Text>
64
73
  }
65
74
 
66
75
  // Every level is a ramp plane. Negative levels recess, 0 sits flat on the page,
@@ -71,7 +80,7 @@ export const ElevationLevels: Story = {
71
80
  <Surface level="base" style={{ gap: 16, padding: 24 }}>
72
81
  {([-2, -1, 0, 1, 2, 3, 4, 5] as ElevationLevel[]).map((level) => (
73
82
  <Surface key={level} elevation={level} style={{ padding: 16 }}>
74
- <Text style={{ color: '#fff' }}>Elevation {level}</Text>
83
+ <ElevationLabel level={level} />
75
84
  </Surface>
76
85
  ))}
77
86
  </Surface>
@@ -79,25 +88,58 @@ export const ElevationLevels: Story = {
79
88
  }
80
89
 
81
90
  export const WithGlow: Story = {
82
- render: () => (
83
- <View style={{ gap: 16, padding: 24 }}>
84
- <Surface elevation={2} glowColor="#FF7900" glowIntensity="subtle" style={{ padding: 16 }}>
85
- <Text style={{ color: '#fff' }}>Subtle Orange Glow</Text>
86
- </Surface>
87
- <Surface elevation={2} glowColor="#FF7900" glowIntensity="medium" style={{ padding: 16 }}>
88
- <Text style={{ color: '#fff' }}>Medium Orange Glow</Text>
89
- </Surface>
90
- <Surface elevation={2} glowColor="#FF7900" glowIntensity="strong" style={{ padding: 16 }}>
91
- <Text style={{ color: '#fff' }}>Strong Orange Glow</Text>
92
- </Surface>
93
- <Surface elevation={2} glowColor="#22C55E" glowIntensity="medium" style={{ padding: 16 }}>
94
- <Text style={{ color: '#fff' }}>Green Glow (Success)</Text>
95
- </Surface>
96
- <Surface elevation={2} glowColor="#EF4444" glowIntensity="medium" style={{ padding: 16 }}>
97
- <Text style={{ color: '#fff' }}>Red Glow (Error)</Text>
98
- </Surface>
99
- </View>
100
- ),
91
+ render: function Render() {
92
+ const onSurface = useOnSurfaceColor('primary')
93
+ return (
94
+ <View style={{ gap: 16, padding: 24 }}>
95
+ <Surface
96
+ elevation={2}
97
+ glowColor={primitiveRamps.orange[400]}
98
+ glowIntensity="subtle"
99
+ style={{ padding: 16 }}
100
+ >
101
+ <Text style={{ color: onSurface }}>Subtle Orange Glow</Text>
102
+ </Surface>
103
+ <Surface
104
+ elevation={2}
105
+ glowColor={primitiveRamps.orange[400]}
106
+ glowIntensity="medium"
107
+ style={{ padding: 16 }}
108
+ >
109
+ <Text style={{ color: onSurface }}>Medium Orange Glow</Text>
110
+ </Surface>
111
+ <Surface
112
+ elevation={2}
113
+ glowColor={primitiveRamps.orange[400]}
114
+ glowIntensity="strong"
115
+ style={{ padding: 16 }}
116
+ >
117
+ <Text style={{ color: onSurface }}>Strong Orange Glow</Text>
118
+ </Surface>
119
+ <Surface
120
+ elevation={2}
121
+ glowColor={semanticColorsDark['status-success']}
122
+ glowIntensity="medium"
123
+ style={{ padding: 16 }}
124
+ >
125
+ <Text style={{ color: onSurface }}>Green Glow (Success)</Text>
126
+ </Surface>
127
+ <Surface
128
+ elevation={2}
129
+ glowColor={semanticColorsDark['status-error']}
130
+ glowIntensity="medium"
131
+ style={{ padding: 16 }}
132
+ >
133
+ <Text style={{ color: onSurface }}>Red Glow (Error)</Text>
134
+ </Surface>
135
+ </View>
136
+ )
137
+ },
138
+ }
139
+
140
+ /** On-surface plane label — resolves from whichever Surface encloses it. */
141
+ function NamedPlaneLabel({ level }: { level: SurfaceLevel }) {
142
+ return <Text style={{ color: useOnSurfaceColor('primary') }}>level=&quot;{level}&quot;</Text>
101
143
  }
102
144
 
103
145
  // The named-plane model: flat, full-bleed grey planes straight from the
@@ -108,7 +150,7 @@ export const NamedPlanes: Story = {
108
150
  <View style={{ gap: 12, padding: 24 }}>
109
151
  {(['background', 'base', 'elevated', 'raised', 'overlay'] as SurfaceLevel[]).map((level) => (
110
152
  <Surface key={level} level={level} style={{ padding: 16 }}>
111
- <Text style={{ color: '#fff' }}>level=&quot;{level}&quot;</Text>
153
+ <NamedPlaneLabel level={level} />
112
154
  </Surface>
113
155
  ))}
114
156
  </View>
@@ -199,12 +241,17 @@ export const OnSurfaceText: Story = {
199
241
  ),
200
242
  }
201
243
 
244
+ /** On-surface elevation label for the forced-light demo below. */
245
+ function LightElevationLabel({ level }: { level: ElevationLevel }) {
246
+ return <Text style={{ color: useOnSurfaceColor('primary') }}>Light Elevation {level}</Text>
247
+ }
248
+
202
249
  export const LightTheme: Story = {
203
250
  render: () => (
204
- <View style={{ gap: 16, padding: 24, backgroundColor: '#F3F4F6' }}>
251
+ <View style={{ gap: 16, padding: 24, backgroundColor: semanticColorsLight['background-base'] }}>
205
252
  {([0, 1, 2, 3, 4, 5] as ElevationLevel[]).map((level) => (
206
253
  <Surface key={level} elevation={level} theme="light" style={{ padding: 16 }}>
207
- <Text style={{ color: '#111' }}>Light Elevation {level}</Text>
254
+ <LightElevationLabel level={level} />
208
255
  </Surface>
209
256
  ))}
210
257
  </View>
@@ -72,7 +72,7 @@ export const Switch = forwardRef<View, SwitchProps>(function Switch(
72
72
  {/* The thumb is a raised control resting in its track: one plane of lift. */}
73
73
  <View
74
74
  className={cn(
75
- 'rounded-full bg-white transition-transform',
75
+ 'rounded-full bg-on-brand-primary transition-transform',
76
76
  styles.thumb,
77
77
  isChecked && styles.translate
78
78
  )}
@@ -169,7 +169,9 @@ export function Tab({ index = 0, isDisabled = false, className, children }: TabP
169
169
  ),
170
170
  'soft-rounded': cn(
171
171
  'px-4 py-2 rounded-full',
172
- isActive ? 'bg-brand-primary text-white' : 'text-text-secondary web:hover:text-text-primary'
172
+ isActive
173
+ ? 'bg-brand-primary text-on-brand-primary'
174
+ : 'text-text-secondary web:hover:text-text-primary'
173
175
  ),
174
176
  }
175
177
 
@@ -191,7 +193,7 @@ export function Tab({ index = 0, isDisabled = false, className, children }: TabP
191
193
  'font-medium',
192
194
  isActive
193
195
  ? variant === 'soft-rounded'
194
- ? 'text-white'
196
+ ? 'text-on-brand-primary'
195
197
  : 'text-text-primary'
196
198
  : 'text-text-secondary'
197
199
  )}
@@ -157,24 +157,26 @@ interface ToastItemProps extends ToastConfig {
157
157
  onClose: () => void
158
158
  }
159
159
 
160
+ // `-subtle` is the wash ladder's lightest rung. It replaces a `/10` opacity
161
+ // modifier, which emitted no rule at all against a var()-backed token (VW-308).
160
162
  const statusStyles: Record<ToastStatus, { bg: string; border: string; icon: string }> = {
161
163
  success: {
162
- bg: 'bg-status-success/10',
164
+ bg: 'bg-status-success-subtle',
163
165
  border: 'border-status-success',
164
166
  icon: '✓',
165
167
  },
166
168
  error: {
167
- bg: 'bg-status-error/10',
169
+ bg: 'bg-status-error-subtle',
168
170
  border: 'border-status-error',
169
171
  icon: '✕',
170
172
  },
171
173
  warning: {
172
- bg: 'bg-status-warning/10',
174
+ bg: 'bg-status-warning-subtle',
173
175
  border: 'border-status-warning',
174
176
  icon: '⚠',
175
177
  },
176
178
  info: {
177
- bg: 'bg-status-info/10',
179
+ bg: 'bg-status-info-subtle',
178
180
  border: 'border-status-info',
179
181
  icon: 'ℹ',
180
182
  },
@@ -2,10 +2,11 @@ import React, { useState, useCallback, createContext, useContext } from 'react'
2
2
  import { View, Text, Pressable, type ViewProps, StyleSheet, Platform } from 'react-native'
3
3
  import { cn } from '../../../utils/cn'
4
4
  import { getHoverColors } from '../../../theme'
5
- import { greyRamp } from '../../../theme/tokens/primitives'
5
+ import { greyRamp, primitiveColors } from '../../../theme/tokens/primitives'
6
6
  import { resolveColor } from '../../../theme/resolve-color'
7
7
  import { getPressedRecessShadow } from '../../../theme/elevation'
8
8
  import { liftStyle } from '../../../theme/lift'
9
+ import { alpha } from '../../../utils/colors'
9
10
  import { Surface, useSurfaceMode } from '../surface'
10
11
 
11
12
  export type ToolbarButtonVariant = 'default' | 'raised'
@@ -16,6 +17,14 @@ interface ToolbarButtonContextType {
16
17
  setIsOpen: (open: boolean) => void
17
18
  }
18
19
 
20
+ /**
21
+ * Idle icon/label tone. VW-82: no token resolves to this — it sits between
22
+ * `text-secondary` (#A29F9D) and `text-primary` (#F9F6F3) and is cooler than
23
+ * either, so snapping it to the warm grey ramp is a visual change awaiting the
24
+ * proposed `on-control-idle` role rather than a silent swap.
25
+ */
26
+ const ICON_IDLE = '#D1D1D1'
27
+
19
28
  const ToolbarButtonContext = createContext<ToolbarButtonContextType>({
20
29
  isOpen: false,
21
30
  setIsOpen: () => {},
@@ -51,9 +60,7 @@ export interface ToolbarButtonProps extends ViewProps {
51
60
  className?: string
52
61
  }
53
62
 
54
- // Base button colours. These were raw `#3C3C3C`/`#2C2C2C` literals — old cold
55
- // charcoal steps that survived the grey migration only because they were plain
56
- // strings rather than scale references.
63
+ // Base button colours.
57
64
  const BUTTON_BG = greyRamp[800]
58
65
 
59
66
  // Calculate hover colors using color math
@@ -137,7 +144,7 @@ export function ToolbarButton({
137
144
  // Determine icon color based on state
138
145
  const getIconColor = () => {
139
146
  if (isActive === true) return resolveColor('brand-primary')
140
- return showActive ? '#FFFFFF' : '#D1D1D1'
147
+ return showActive ? primitiveColors.white : ICON_IDLE
141
148
  }
142
149
 
143
150
  /**
@@ -246,7 +253,7 @@ export function ToolbarButton({
246
253
  const styles = StyleSheet.create({
247
254
  // Disabled - flat gray background, no shadows
248
255
  disabledBg: {
249
- backgroundColor: 'rgba(255, 255, 255, 0.12)',
256
+ backgroundColor: alpha(primitiveColors.white, 0.12),
250
257
  ...Platform.select({
251
258
  web: { boxShadow: 'none' } as any,
252
259
  default: { shadowOpacity: 0, elevation: 0 },
@@ -324,7 +324,7 @@ function TypographyShowcase() {
324
324
  function ColorSwatch({
325
325
  label,
326
326
  className,
327
- textClass = 'text-white',
327
+ textClass = 'text-on-brand-primary',
328
328
  }: {
329
329
  label: string
330
330
  className: string