@titan-design/react-ui 0.11.0 → 0.12.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 (142) hide show
  1. package/dist/bodymap.js +173 -343
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +173 -343
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +24 -21
  6. package/dist/index.d.ts +24 -21
  7. package/dist/index.js +366 -561
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +360 -553
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/pages.js +242 -407
  12. package/dist/pages.js.map +1 -1
  13. package/dist/pages.mjs +242 -407
  14. package/dist/pages.mjs.map +1 -1
  15. package/dist/theme/index.d.mts +409 -415
  16. package/dist/theme/index.d.ts +409 -415
  17. package/dist/theme/index.js +234 -408
  18. package/dist/theme/index.js.map +1 -1
  19. package/dist/theme/index.mjs +227 -399
  20. package/dist/theme/index.mjs.map +1 -1
  21. package/dist/theme/tokens-css.js +141 -140
  22. package/dist/theme/tokens-css.js.map +1 -1
  23. package/dist/theme/tokens-css.mjs +141 -140
  24. package/dist/theme/tokens-css.mjs.map +1 -1
  25. package/package.json +1 -1
  26. package/src/arch/Architecture.stories.tsx +9 -9
  27. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  28. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +3 -3
  29. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +2 -2
  30. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +3 -3
  31. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +1 -1
  32. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +2 -2
  33. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +2 -2
  34. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +4 -4
  35. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +3 -3
  36. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +2 -2
  37. package/src/components/custom/Fatigue/fatigue-tokens.ts +12 -12
  38. package/src/components/custom/Sidebar/Sidebar.tsx +4 -4
  39. package/src/components/custom/Table/Table.tsx +2 -2
  40. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +3 -3
  41. package/src/components/custom/Workout/BaseBadge.test.tsx +6 -1
  42. package/src/components/custom/Workout/BaseBadge.tsx +1 -1
  43. package/src/components/custom/Workout/BodyMap.tsx +2 -2
  44. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +2 -2
  45. package/src/components/custom/Workout/DeviationBar.test.tsx +2 -1
  46. package/src/components/custom/Workout/DeviationBar.tsx +3 -2
  47. package/src/components/custom/Workout/ExerciseCard.test.tsx +3 -3
  48. package/src/components/custom/Workout/ExerciseCard.tsx +2 -2
  49. package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -4
  50. package/src/components/custom/Workout/InputBar.tsx +2 -2
  51. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +1 -1
  52. package/src/components/custom/Workout/LiveAuraFrame.test.tsx +1 -1
  53. package/src/components/custom/Workout/LiveAuraFrame.tsx +3 -3
  54. package/src/components/custom/Workout/MesoCard.tsx +3 -1
  55. package/src/components/custom/Workout/MuscleGroupChip.tsx +4 -3
  56. package/src/components/custom/Workout/PrHistoryModal.tsx +2 -2
  57. package/src/components/custom/Workout/ProgramPlanningPage.tsx +1 -1
  58. package/src/components/custom/Workout/ReadinessCheck.tsx +2 -2
  59. package/src/components/custom/Workout/RestTimer.tsx +2 -2
  60. package/src/components/custom/Workout/S3SetTypes.stories.tsx +3 -3
  61. package/src/components/custom/Workout/SessionHeader.stories.tsx +2 -2
  62. package/src/components/custom/Workout/SessionHeader.tsx +1 -1
  63. package/src/components/custom/Workout/SessionRail.stories.tsx +2 -2
  64. package/src/components/custom/Workout/SessionRail.tsx +13 -5
  65. package/src/components/custom/Workout/SetBar.tsx +3 -3
  66. package/src/components/custom/Workout/SetRow.test.tsx +6 -3
  67. package/src/components/custom/Workout/StatusDot.tsx +5 -4
  68. package/src/components/custom/Workout/StrengthTrendChart.tsx +1 -1
  69. package/src/components/custom/Workout/TrainingStatusPage.tsx +2 -2
  70. package/src/components/custom/Workout/VelocityStrip.test.tsx +5 -2
  71. package/src/components/custom/Workout/VelocityStrip.tsx +4 -4
  72. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +3 -3
  73. package/src/components/custom/Workout/WeightBadge.test.tsx +6 -1
  74. package/src/components/custom/Workout/WeightBadge.tsx +2 -1
  75. package/src/components/custom/Workout/WorkoutCard.tsx +3 -1
  76. package/src/components/custom/Workout/WorkoutPill.tsx +5 -2
  77. package/src/components/custom/Workout/ZoneTrack.tsx +4 -4
  78. package/src/components/custom/Workout/setHeadingKit.tsx +14 -8
  79. package/src/components/custom/Workout/velocity-story-kit.tsx +2 -2
  80. package/src/components/custom/charts/SetBarChart.tsx +3 -3
  81. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  82. package/src/components/custom/stepper/Stepper.tsx +3 -3
  83. package/src/components/shell/DeviceMenu.tsx +2 -2
  84. package/src/components/shell/SideNav.tsx +1 -1
  85. package/src/components/shell/TopBar.tsx +1 -1
  86. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  87. package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
  88. package/src/components/ui/avatar/Avatar.tsx +2 -2
  89. package/src/components/ui/badge/Badge.tsx +2 -2
  90. package/src/components/ui/card/Card.tsx +3 -3
  91. package/src/components/ui/checkbox/Checkbox.tsx +1 -1
  92. package/src/components/ui/collapse/Collapse.tsx +1 -1
  93. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  94. package/src/components/ui/drawer/Drawer.tsx +3 -3
  95. package/src/components/ui/form-field/FormField.tsx +1 -1
  96. package/src/components/ui/help-tip/HelpTip.tsx +1 -1
  97. package/src/components/ui/menu/Menu.tsx +2 -2
  98. package/src/components/ui/pill/Pill.tsx +2 -2
  99. package/src/components/ui/popover/Popover.stories.tsx +2 -2
  100. package/src/components/ui/popover/Popover.tsx +1 -1
  101. package/src/components/ui/progress/Progress.tsx +3 -3
  102. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  103. package/src/components/ui/radio/Radio.tsx +4 -4
  104. package/src/components/ui/select/Select.tsx +3 -3
  105. package/src/components/ui/surface/Surface.stories.tsx +3 -3
  106. package/src/components/ui/surface/Surface.test.tsx +32 -19
  107. package/src/components/ui/surface/Surface.tsx +3 -3
  108. package/src/components/ui/surface/SurfaceContext.ts +19 -29
  109. package/src/components/ui/surface/surface.contract.test.ts +57 -44
  110. package/src/components/ui/switch/Switch.tsx +1 -1
  111. package/src/components/ui/tabs/Tabs.tsx +2 -2
  112. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +5 -5
  113. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +1 -1
  114. package/src/components/ui/toolbar-button/ToolbarButton.tsx +36 -22
  115. package/src/stories/PresetShowcase.stories.tsx +21 -21
  116. package/src/theme/ColorPalettes.stories.tsx +41 -38
  117. package/src/theme/ColorPrimitives.stories.tsx +89 -41
  118. package/src/theme/Depth.stories.tsx +312 -0
  119. package/src/theme/DepthCalibration.stories.tsx +655 -0
  120. package/src/theme/ThemeProvider.tsx +1 -1
  121. package/src/theme/barrel.ts +7 -14
  122. package/src/theme/color-stories.coverage.test.ts +15 -20
  123. package/src/theme/color-story-kit.tsx +1 -1
  124. package/src/theme/color-utils.ts +197 -0
  125. package/src/theme/config.ts +0 -8
  126. package/src/theme/depth-calibration.test.tsx +97 -0
  127. package/src/theme/elevation.test.ts +62 -1
  128. package/src/theme/elevation.ts +34 -8
  129. package/src/theme/global.css +47 -53
  130. package/src/theme/grey-ramp.test.ts +166 -0
  131. package/src/theme/materials.test.ts +106 -0
  132. package/src/theme/materials.ts +211 -0
  133. package/src/theme/presets/audiobook.ts +4 -4
  134. package/src/theme/presets/types.ts +3 -3
  135. package/src/theme/semantic-contrast.test.ts +83 -0
  136. package/src/theme/tokens/primitives.ts +62 -115
  137. package/src/theme/tokens/semantic.ts +130 -82
  138. package/src/utils/colors.ts +1 -1
  139. package/tailwind.config.js +2 -5
  140. package/src/theme/bar-paper.ts +0 -59
  141. package/src/theme/shadows.stories.tsx +0 -523
  142. package/src/theme/shadows.ts +0 -439
@@ -253,7 +253,7 @@ export function FormActions({
253
253
  <View
254
254
  className={cn(
255
255
  'flex-row items-center gap-3 pt-4',
256
- 'border-t border-border mt-4',
256
+ 'border-t border-hairline mt-4',
257
257
  alignStyles[align],
258
258
  className
259
259
  )}
@@ -155,7 +155,7 @@ export function HelpTip({
155
155
  className={cn(
156
156
  'absolute z-50',
157
157
  'bg-surface-elevated rounded-lg shadow-lg',
158
- 'border border-border',
158
+ 'border border-hairline',
159
159
  'px-3 py-2',
160
160
  placementStyles[placement],
161
161
  tooltipClassName
@@ -116,7 +116,7 @@ export function MenuList({ children, className }: MenuListProps) {
116
116
  <View
117
117
  className={cn(
118
118
  'absolute z-50 top-full left-0 mt-1 min-w-[160px]',
119
- 'bg-surface-elevated rounded-lg shadow-lg border border-border',
119
+ 'bg-surface-elevated rounded-lg shadow-lg border border-hairline',
120
120
  'py-1 overflow-hidden',
121
121
  className
122
122
  )}
@@ -195,7 +195,7 @@ export interface MenuDividerProps {
195
195
  * Divider between menu items.
196
196
  */
197
197
  export function MenuDivider({ className }: MenuDividerProps) {
198
- return <View className={cn('h-px bg-border my-1', className)} />
198
+ return <View className={cn('h-px bg-hairline my-1', className)} />
199
199
  }
200
200
 
201
201
  export interface MenuGroupProps {
@@ -27,7 +27,7 @@ export interface PillProps extends ViewProps {
27
27
 
28
28
  const variantColorStyles: Record<PillVariant, Record<PillColor, string>> = {
29
29
  subtle: {
30
- default: 'bg-surface-raised border-border-subtle text-text-secondary',
30
+ default: 'bg-surface-raised border-hairline-subtle text-text-secondary',
31
31
  primary: 'bg-brand-primary/10 border-brand-primary/25 text-brand-primary',
32
32
  secondary: 'bg-brand-secondary/10 border-brand-secondary/25 text-brand-secondary',
33
33
  success: 'bg-status-success/10 border-status-success/25 text-status-success',
@@ -36,7 +36,7 @@ const variantColorStyles: Record<PillVariant, Record<PillColor, string>> = {
36
36
  info: 'bg-status-info/10 border-status-info/25 text-status-info',
37
37
  },
38
38
  outline: {
39
- default: 'border-border text-text-secondary',
39
+ default: 'border-hairline text-text-secondary',
40
40
  primary: 'border-brand-primary text-brand-primary',
41
41
  secondary: 'border-brand-secondary text-brand-secondary',
42
42
  success: 'border-status-success text-status-success',
@@ -152,12 +152,12 @@ export const WithFormContent: Story = {
152
152
  <Text className="text-text-secondary text-xs">
153
153
  Enter your new display name below.
154
154
  </Text>
155
- <View className="bg-surface-input border border-border rounded-md px-3 py-2">
155
+ <View className="bg-surface-input border border-hairline rounded-md px-3 py-2">
156
156
  <Text className="text-text-secondary text-sm">John Doe</Text>
157
157
  </View>
158
158
  <View style={{ flexDirection: 'row', gap: 8, justifyContent: 'flex-end' }}>
159
159
  <PopoverCloseButton>
160
- <View className="px-3 py-1.5 rounded-md bg-surface-elevated border border-border">
160
+ <View className="px-3 py-1.5 rounded-md bg-surface-elevated border border-hairline">
161
161
  <Text className="text-text-primary text-sm">Cancel</Text>
162
162
  </View>
163
163
  </PopoverCloseButton>
@@ -188,7 +188,7 @@ export function PopoverContent({ children, className }: PopoverContentProps) {
188
188
  <View
189
189
  className={cn(
190
190
  'absolute z-50 min-w-[200px]',
191
- 'bg-surface-elevated rounded-lg shadow-lg border border-border',
191
+ 'bg-surface-elevated rounded-lg shadow-lg border border-hairline',
192
192
  'p-4',
193
193
  placementStyles[placement],
194
194
  className
@@ -230,7 +230,7 @@ export function CircularProgress({
230
230
  cy={center}
231
231
  r={radius}
232
232
  fill="none"
233
- stroke="var(--color-border-default)"
233
+ stroke="var(--color-hairline-default)"
234
234
  strokeWidth={strokeWidth}
235
235
  />
236
236
  {/* Progress arc */}
@@ -325,7 +325,7 @@ export function ProgressSteps({
325
325
  className={cn(
326
326
  'rounded-full',
327
327
  dot,
328
- index <= currentStep ? colorStyles[color] : 'bg-border-strong'
328
+ index <= currentStep ? colorStyles[color] : 'bg-hairline-strong'
329
329
  )}
330
330
  />
331
331
 
@@ -335,7 +335,7 @@ export function ProgressSteps({
335
335
  className={cn(
336
336
  'flex-1 mx-1',
337
337
  connector,
338
- index < currentStep ? colorStyles[color] : 'bg-border-strong'
338
+ index < currentStep ? colorStyles[color] : 'bg-hairline-strong'
339
339
  )}
340
340
  />
341
341
  )}
@@ -188,7 +188,7 @@ export const FormExample: Story = {
188
188
  <Radio value="yearly">Yearly (save 20%)</Radio>
189
189
  </RadioGroup>
190
190
 
191
- <View className="pt-4 border-t border-border">
191
+ <View className="pt-4 border-t border-hairline">
192
192
  <Text className="text-sm text-text-secondary">
193
193
  Selected: {plan} plan, billed {billing}
194
194
  </Text>
@@ -117,10 +117,10 @@ const sizeConfig: Record<RadioSize, { outer: string; inner: string; text: string
117
117
  }
118
118
 
119
119
  const colorStyles: Record<RadioColor, { checked: string; unchecked: string }> = {
120
- primary: { checked: 'border-brand-primary bg-brand-primary', unchecked: 'border-border-strong' },
121
- secondary: { checked: 'border-brand-secondary bg-brand-secondary', unchecked: 'border-border-strong' },
122
- success: { checked: 'border-status-success bg-status-success', unchecked: 'border-border-strong' },
123
- error: { checked: 'border-status-error bg-status-error', unchecked: 'border-border-strong' },
120
+ primary: { checked: 'border-brand-primary bg-brand-primary', unchecked: 'border-hairline-strong' },
121
+ secondary: { checked: 'border-brand-secondary bg-brand-secondary', unchecked: 'border-hairline-strong' },
122
+ success: { checked: 'border-status-success bg-status-success', unchecked: 'border-hairline-strong' },
123
+ error: { checked: 'border-status-error bg-status-error', unchecked: 'border-hairline-strong' },
124
124
  }
125
125
 
126
126
  /**
@@ -145,7 +145,7 @@ export function Select<T extends string = string>({
145
145
  isInvalid
146
146
  ? 'border-border-input-error'
147
147
  : variant === 'filled'
148
- ? 'border-border-subtle'
148
+ ? 'border-hairline-subtle'
149
149
  : 'border-border-input',
150
150
  !isDisabled && !isInvalid && 'web:hover:border-border-input-hover',
151
151
  isOpen && 'border-border-input-focus',
@@ -187,7 +187,7 @@ export function Select<T extends string = string>({
187
187
  <View
188
188
  className={cn(
189
189
  'absolute z-50 top-full left-0 right-0 mt-1',
190
- 'bg-surface-elevated rounded-md shadow-lg border border-border',
190
+ 'bg-surface-elevated rounded-md shadow-lg border border-hairline',
191
191
  'max-h-60 overflow-hidden'
192
192
  )}
193
193
  >
@@ -229,7 +229,7 @@ function SelectOption<T>({ option }: SelectOptionComponentProps<T>) {
229
229
  <View
230
230
  className={cn(
231
231
  'w-4 h-4 mr-3 rounded border',
232
- selected ? 'bg-brand-primary border-brand-primary' : 'border-border-strong'
232
+ selected ? 'bg-brand-primary border-brand-primary' : 'border-hairline-strong'
233
233
  )}
234
234
  >
235
235
  {selected && <Text className="text-white text-xs text-center">✓</Text>}
@@ -15,8 +15,8 @@ const meta: Meta<typeof Surface> = {
15
15
  },
16
16
  level: {
17
17
  control: 'select',
18
- options: ['inset', 'background', 'base', 'elevated', 'raised', 'overlay'],
19
- description: 'Named charcoal plane (flat background from a semantic token)',
18
+ options: ['frame', 'background', 'base', 'elevated', 'raised', 'overlay'],
19
+ description: 'Named grey plane (flat background from a semantic token)',
20
20
  },
21
21
  pressed: {
22
22
  control: 'boolean',
@@ -88,7 +88,7 @@ export const WithGlow: Story = {
88
88
  ),
89
89
  }
90
90
 
91
- // The named-plane model: flat, full-bleed charcoal planes straight from the
91
+ // The named-plane model: flat, full-bleed grey planes straight from the
92
92
  // semantic surface tokens — reaching the darker nav/page shades the numeric
93
93
  // lighten model can't. These back the shell / rail / stage.
94
94
  export const NamedPlanes: Story = {
@@ -11,6 +11,7 @@ import {
11
11
  useSurface,
12
12
  useSurfaceMode,
13
13
  } from './SurfaceContext'
14
+ import { getSemanticColors } from '../../../theme/tokens/semantic'
14
15
  import { getPressedRecessShadow } from '../../../theme/elevation'
15
16
 
16
17
  // A descendant probe that renders the on-surface colour + mode it resolves from
@@ -157,17 +158,17 @@ describe('Surface (pressed well)', () => {
157
158
  expect(screen.getByTestId('well')).toHaveStyle({ backgroundColor: '#1C1916' })
158
159
  })
159
160
 
160
- it('clamps at the inset floor: pressed directly in background does not underflow', () => {
161
+ it('clamps at the frame floor: pressed directly in background does not underflow', () => {
161
162
  render(
162
163
  <Surface level="background">
163
164
  <Surface pressed testID="well" />
164
165
  </Surface>
165
166
  )
166
- // background (#1C1916) steps down to the inset floor (#13100D), the pit.
167
- expect(screen.getByTestId('well')).toHaveStyle({ backgroundColor: '#13100D' })
167
+ // background (#1C1916) steps down to the frame floor (#100D0A), the bezel.
168
+ expect(screen.getByTestId('well')).toHaveStyle({ backgroundColor: '#100D0A' })
168
169
  })
169
170
 
170
- it('does not step below the floor: pressed within a floor-pressed well stays at inset', () => {
171
+ it('does not step below the floor: pressed within a floor-pressed well stays at frame', () => {
171
172
  render(
172
173
  <Surface level="background">
173
174
  <Surface pressed>
@@ -175,7 +176,7 @@ describe('Surface (pressed well)', () => {
175
176
  </Surface>
176
177
  </Surface>
177
178
  )
178
- expect(screen.getByTestId('deeper')).toHaveStyle({ backgroundColor: '#13100D' })
179
+ expect(screen.getByTestId('deeper')).toHaveStyle({ backgroundColor: '#100D0A' })
179
180
  })
180
181
 
181
182
  it('publishes the stepped-down level to descendants so a nested press steps again', () => {
@@ -230,9 +231,9 @@ describe('pressedLevel helper', () => {
230
231
  ['raised', 'elevated'],
231
232
  ['elevated', 'base'],
232
233
  ['base', 'background'],
233
- ['background', 'inset'],
234
- ['inset', 'inset'],
235
- ] as const)('steps %s down to %s (clamped at inset)', (parent, expected) => {
234
+ ['background', 'frame'],
235
+ ['frame', 'frame'],
236
+ ] as const)('steps %s down to %s (clamped at frame)', (parent, expected) => {
236
237
  expect(pressedLevel(parent)).toBe(expected)
237
238
  })
238
239
  })
@@ -253,14 +254,15 @@ describe('Surface on-surface colour context', () => {
253
254
  <Probe role="tertiary" label="t" />
254
255
  </Surface>
255
256
  )
256
- expect(screen.getByTestId('p')).toHaveStyle({ color: '#F3F4F6' })
257
- expect(screen.getByTestId('s')).toHaveStyle({ color: '#9CA3AF' })
258
- expect(screen.getByTestId('t')).toHaveStyle({ color: '#6B7280' })
257
+ const t = getSemanticColors('dark')
258
+ expect(screen.getByTestId('p')).toHaveStyle({ color: t['text-primary'] })
259
+ expect(screen.getByTestId('s')).toHaveStyle({ color: t['text-secondary'] })
260
+ expect(screen.getByTestId('t')).toHaveStyle({ color: t['text-tertiary'] })
259
261
  })
260
262
 
261
263
  it('resolves dark on-surface colours even with no enclosing Surface', () => {
262
264
  render(<Probe role="primary" label="p" />)
263
- expect(screen.getByTestId('p')).toHaveStyle({ color: '#F3F4F6' })
265
+ expect(screen.getByTestId('p')).toHaveStyle({ color: getSemanticColors('dark')['text-primary'] })
264
266
  })
265
267
 
266
268
  it('flows an overridden theme to descendant text', () => {
@@ -292,15 +294,26 @@ describe('surface colour helpers', () => {
292
294
  expect(surfaceBackground('base', 'light')).toBe('#FFFFFF')
293
295
  })
294
296
 
295
- it('resolves the inset floor from the surfaceRampDark.inset primitive (no token yet)', () => {
296
- expect(surfaceBackground('inset', 'dark')).toBe('#13100D')
297
+ it('resolves the frame floor from its own semantic token', () => {
298
+ // The old `inset` level had no token and fell back to a primitive. `frame`
299
+ // has `background-frame`, which is what let SurfaceContext drop the special
300
+ // case in surfaceBackground().
301
+ expect(surfaceBackground('frame', 'dark')).toBe(getSemanticColors('dark')['background-frame'])
297
302
  })
298
303
 
299
- it('onSurfaceColors returns the neutral text ramp as literal hex', () => {
300
- expect(onSurfaceColors('dark')).toEqual({
301
- primary: '#F3F4F6',
302
- secondary: '#9CA3AF',
303
- tertiary: '#6B7280',
304
+ it('onSurfaceColors returns the text ramp as literal hex', () => {
305
+ const t = getSemanticColors('dark')
306
+ const got = onSurfaceColors('dark')
307
+ expect(got).toEqual({
308
+ primary: t['text-primary'],
309
+ secondary: t['text-secondary'],
310
+ tertiary: t['text-tertiary'],
304
311
  })
312
+ // The actual contract this test guards: LITERAL hex, never a var() — under
313
+ // the RNW alias resolveColor() returns 'var(--color-…)', which silently
314
+ // breaks any consumer doing colour maths on the result.
315
+ for (const [role, value] of Object.entries(got)) {
316
+ expect(value, role).toMatch(/^#[0-9A-F]{6}$/i)
317
+ }
305
318
  })
306
319
  })
@@ -28,7 +28,7 @@ export interface SurfaceProps extends ViewProps {
28
28
  */
29
29
  elevation?: ElevationLevel
30
30
  /**
31
- * Named charcoal plane (shell / rail / stage). Sets the background directly
31
+ * Named grey plane (shell / rail / stage). Sets the background directly
32
32
  * from a semantic surface token — reaching the darker nav/page planes the
33
33
  * lighten model can't — and drops the default rounding + auto shadow so the
34
34
  * plane reads full-bleed. Composable with `elevation`'s glow.
@@ -61,7 +61,7 @@ export interface SurfaceProps extends ViewProps {
61
61
  * A container that OWNS its background and establishes an on-surface colour
62
62
  * context. Two grounded ways to pick the background:
63
63
  * - `elevation` (numeric): the raised-card model — lighten-from-base + shadow.
64
- * - `level` (named): a flat charcoal plane straight from a semantic token,
64
+ * - `level` (named): a flat grey plane straight from a semantic token,
65
65
  * for the shell / rail / stage backgrounds.
66
66
  * Either way, descendants read the surface via {@link useOnSurfaceColor} /
67
67
  * {@link useSurfaceMode} rather than hard-coding `getSemanticColors('dark')`.
@@ -86,7 +86,7 @@ export function Surface({
86
86
  // Three grounded ways to own a background, in precedence order:
87
87
  // - pressed: a well ONE ramp step DOWN from the parent level (relative),
88
88
  // with an inner-shadow recess — clamps at the `inset` floor.
89
- // - level (named plane): a flat charcoal plane straight from a token.
89
+ // - level (named plane): a flat grey plane straight from a token.
90
90
  // - elevation (numeric): the raised-card model — lighten-from-base + shadow.
91
91
  const baseColor = getBaseSurfaceColor(mode)
92
92
  const resolvedLevel = pressed ? pressedLevel(inherited.level) : (level ?? inherited.level)
@@ -7,7 +7,6 @@
7
7
  // (no global.css, no nativewind) — the bug class this primitive retires.
8
8
  import { createContext, useContext } from 'react'
9
9
  import { getSemanticColors, type ThemeMode } from '../../../theme/tokens/semantic'
10
- import { surfaceRampDark } from '../../../theme/tokens/primitives'
11
10
 
12
11
  type ColorToken = keyof ReturnType<typeof getSemanticColors>
13
12
 
@@ -18,32 +17,35 @@ type ColorToken = keyof ReturnType<typeof getSemanticColors>
18
17
  * no new hexes. Darkest → lightest:
19
18
  * inset (#13100D) < background (#1C1916) < base (#252321) < elevated (#2A2827) < raised (#2D2C2B) < overlay (#302F2E)
20
19
  *
21
- * `inset` is the ramp's deepest pit — the sub-shell well. It's the floor a
22
- * `<Surface pressed>` clamps at (see {@link pressedLevel}); it has no shipped
23
- * semantic token yet, so {@link surfaceBackground} resolves it from the
24
- * `surface-inset` token when present, else the `surfaceRampDark.inset` primitive.
20
+ * `frame` is the bezel the ramp sits inside — the top bar / side nav chrome —
21
+ * and the floor a `<Surface pressed>` clamps at (see {@link pressedLevel}).
22
+ *
23
+ * It replaced a separate `inset` level in TD-07.14. That level was RETIRED, not
24
+ * renamed: at ΔE 1.10 from the frame it was an imperceptible duplicate, which is
25
+ * exactly why pressed (surface − 1) kept collapsing into it.
25
26
  */
26
- export type SurfaceLevel = 'inset' | 'background' | 'base' | 'elevated' | 'raised' | 'overlay'
27
+ export type SurfaceLevel = 'frame' | 'background' | 'base' | 'elevated' | 'raised' | 'overlay'
27
28
 
28
29
  /** On-surface neutral text roles, resolved for the current surface + theme. */
29
30
  export type OnSurfaceRole = 'primary' | 'secondary' | 'tertiary'
30
31
 
31
- // Which semantic token backs each ADDRESSABLE level. Values live in `semantic.ts`
32
- // and stay in sync with the charcoal ramp — this map never carries literal hexes.
33
- // `inset` is intentionally absent: it has no shipped semantic token yet, so it's
34
- // resolved separately (token-or-primitive) in `surfaceBackground`.
32
+ // Which semantic token backs each level. Values live in `semantic.ts` and stay
33
+ // in sync with the grey ramp — this map never carries literal hexes. Every level
34
+ // is present now: swapping `inset` (which had no token) for `frame` (which does)
35
+ // is what made this a total map and removed `surfaceBackground`'s special case.
35
36
  export const SURFACE_LEVEL_TOKEN = {
37
+ frame: 'background-frame',
36
38
  background: 'background-base',
37
39
  base: 'surface-base',
38
40
  elevated: 'surface-elevated',
39
41
  raised: 'surface-raised',
40
42
  overlay: 'surface-overlay',
41
- } as const satisfies Record<Exclude<SurfaceLevel, 'inset'>, ColorToken>
43
+ } as const satisfies Record<SurfaceLevel, ColorToken>
42
44
 
43
- // Darkest → lightest, INCLUDING the inset floor. A `<Surface pressed>` renders
44
- // one index DOWN from its parent's level and clamps at `inset` (index 0). Kept
45
- // in lockstep with the derived ramp and the surface.contract.test PLANE_ORDER.
46
- const PRESSED_RAMP = ['inset', 'background', 'base', 'elevated', 'raised', 'overlay'] as const
45
+ // Darkest → lightest. A `<Surface pressed>` renders one index DOWN from its
46
+ // parent's level and clamps at `frame` (index 0). Kept in lockstep with the grey
47
+ // ramp and the surface.contract.test PLANE_ORDER.
48
+ const PRESSED_RAMP = ['frame', 'background', 'base', 'elevated', 'raised', 'overlay'] as const
47
49
 
48
50
  const ON_SURFACE_TOKEN = {
49
51
  primary: 'text-primary',
@@ -94,29 +96,17 @@ export function useOnSurfaceColor(role: OnSurfaceRole = 'primary'): string {
94
96
  return getSemanticColors(useSurfaceMode())[ON_SURFACE_TOKEN[role]]
95
97
  }
96
98
 
97
- /**
98
- * The inset floor hex — the ramp's deepest pit that a pressed surface clamps at.
99
- * Prefers the `surface-inset` semantic token (theme-aware; being promoted in a
100
- * parallel token change) and falls back to the `surfaceRampDark.inset` primitive
101
- * (#13100D, dark-ramp only) until that token ships.
102
- */
103
- function insetFloor(mode: ThemeMode): string {
104
- const colors = getSemanticColors(mode) as Record<string, string>
105
- return colors['surface-inset'] ?? surfaceRampDark.inset
106
- }
107
-
108
99
  /** The background hex for a surface level under a theme mode (literal hex). */
109
100
  export function surfaceBackground(level: SurfaceLevel, mode: ThemeMode): string {
110
- if (level === 'inset') return insetFloor(mode)
111
101
  return getSemanticColors(mode)[SURFACE_LEVEL_TOKEN[level]]
112
102
  }
113
103
 
114
104
  /**
115
105
  * The level a `<Surface pressed>` resolves to: its parent's level stepped one
116
106
  * index DOWN the ramp — the symmetric twin of a raised surface stepping up —
117
- * clamped at the `inset` floor so a pressed surface never underflows the pit.
107
+ * clamped at the `frame` floor so a pressed surface never underflows the bezel.
118
108
  * overlay→raised, raised→elevated, elevated→base, base→background,
119
- * background→inset, insetinset (clamped).
109
+ * background→frame, frameframe (clamped).
120
110
  * Pressed exposes ONE level of depth only (no pressed-2).
121
111
  */
122
112
  export function pressedLevel(parent: SurfaceLevel): SurfaceLevel {
@@ -20,7 +20,7 @@ import { getSemanticColors } from '../../../theme/tokens/semantic'
20
20
  * derived, not hand-picked") deliberately supersedes that with DIMINISHING
21
21
  * steps — the frame->content jump is biggest, each plane above adds less.
22
22
  * As of S-3 (this re-space) the steps are 4.5 / 4.5 / 3.5 / 3 / 2.5
23
- * (inset->background / background->base / base->elevated / elevated->raised /
23
+ * (frame->background / background->base / base->elevated / elevated->raised /
24
24
  * raised->overlay) — the top three widened from the original 2.5/2/1.5 taper
25
25
  * so the content planes read as distinct without leaning on the hairline
26
26
  * alone (R3 still carries the rest, per "lightness is a *secondary* cue").
@@ -73,10 +73,10 @@ function compositeOver(baseHex: string, overlay: string): string {
73
73
 
74
74
  const dark = getSemanticColors('dark')
75
75
 
76
- // The 6-plane derivation, in darkest -> lightest order. `inset` is now a real
77
- // shipped token (`surface-inset`, promoted at S-3 see semantic.ts comment).
76
+ // The 6-plane derivation, in darkest -> lightest order. The floor is `frame`;
77
+ // the old `inset` plane was retired in TD-07.14 for being ΔE 1.10 from it.
78
78
  const RAMP_HEX = {
79
- inset: dark['surface-inset'],
79
+ frame: dark['background-frame'],
80
80
  background: dark['background-base'],
81
81
  base: dark['surface-base'],
82
82
  elevated: dark['surface-elevated'],
@@ -84,14 +84,14 @@ const RAMP_HEX = {
84
84
  overlay: dark['surface-overlay'],
85
85
  } as const
86
86
 
87
- const PLANE_ORDER = ['inset', 'background', 'base', 'elevated', 'raised', 'overlay'] as const
87
+ const PLANE_ORDER = ['frame', 'background', 'base', 'elevated', 'raised', 'overlay'] as const
88
88
 
89
89
  describe('surface ramp contract (dark) — token-value guardrails', () => {
90
90
  it('ships the deriveSurfaceRamp() output verbatim for the addressable planes', () => {
91
91
  // Locked values from surface-system-north-star.md (re-spaced S-3) — see
92
92
  // surfaceRampDark in primitives.ts for the derivation this must match
93
- // byte-for-byte. background/base/inset are unchanged by the re-space.
94
- expect(dark['surface-inset']).toBe('#13100D')
93
+ // byte-for-byte. background/base are unchanged by the re-space.
94
+ expect(dark['background-frame']).toBe('#100D0A')
95
95
  expect(dark['background-base']).toBe('#1C1916')
96
96
  expect(dark['surface-base']).toBe('#252321')
97
97
  expect(dark['surface-elevated']).toBe('#2C2A28')
@@ -110,8 +110,8 @@ describe('surface ramp contract (dark) — token-value guardrails', () => {
110
110
  }
111
111
  })
112
112
 
113
- it('clears ΔL* >= 4 at the two foundational steps (inset->background, background->base)', () => {
114
- const dL_insetToBackground = lstar(RAMP_HEX.background) - lstar(RAMP_HEX.inset)
113
+ it('clears ΔL* >= 4 at the two foundational steps (frame->background, background->base)', () => {
114
+ const dL_insetToBackground = lstar(RAMP_HEX.background) - lstar(RAMP_HEX.frame)
115
115
  const dL_backgroundToBase = lstar(RAMP_HEX.base) - lstar(RAMP_HEX.background)
116
116
  expect(dL_insetToBackground).toBeGreaterThanOrEqual(4)
117
117
  expect(dL_backgroundToBase).toBeGreaterThanOrEqual(4)
@@ -158,14 +158,10 @@ describe('surface ramp contract (dark) — token-value guardrails', () => {
158
158
  dark['surface-raised'],
159
159
  dark['surface-overlay'],
160
160
  dark['surface-input'],
161
- dark['surface-inset'],
162
161
  ])
163
- const solidBorders = [
164
- 'border-default',
165
- 'border-subtle',
166
- 'border-strong',
167
- 'border-prominent',
168
- ] as const
162
+ // `border-prominent` is the only solid border left; the quiet three are
163
+ // alpha hairlines now and cannot equal a fill.
164
+ const solidBorders = ['border-prominent'] as const
169
165
  for (const token of solidBorders) {
170
166
  expect(
171
167
  surfaceHexes.has(dark[token]),
@@ -185,10 +181,17 @@ describe('surface ramp contract (dark) — token-value guardrails', () => {
185
181
  // the S-3 ramp (default dropped 8->7 — the `overlay` plane now measures
186
182
  // ~7.97, just under the old floor); a wall-display calibration pass (S-6)
187
183
  // may retune the alpha values themselves.
184
+ //
185
+ // S-6 DID (VW-99). The alphas went up ×1.5 to .09/.135/.21: on the wall at
186
+ // ~3 m `subtle` rendered but was indiscernible, and `default` — which has had
187
+ // no solid-border fallback since TD-07.14 — went weak on the lightest plane.
188
+ // Floors below re-measured against the new alphas. Note new `subtle` now
189
+ // measures exactly where old `default` did (min 7.97), and the new `default`
190
+ // where old `strong` did (min 12.26) — each tier onto a measured-good rung.
188
191
  const HAIRLINES = {
189
- subtle: { token: 'hairline-subtle', floor: 5 },
190
- default: { token: 'hairline-default', floor: 7 },
191
- strong: { token: 'hairline-strong', floor: 12 },
192
+ subtle: { token: 'hairline-subtle', floor: 7 },
193
+ default: { token: 'hairline-default', floor: 12 },
194
+ strong: { token: 'hairline-strong', floor: 18 },
192
195
  } as const
193
196
 
194
197
  for (const [name, { token, floor }] of Object.entries(HAIRLINES)) {
@@ -203,30 +206,40 @@ describe('surface ramp contract (dark) — token-value guardrails', () => {
203
206
  })
204
207
  }
205
208
 
206
- it('is near-constant (self-normalizing) across all planes: spread < 4 L* per tier', () => {
207
- // Measured spread (inset..overlay): subtle ~1.6, default ~2.2, strong ~3.2 —
208
- // still far tighter than a solid border token (which swings ~5.6 -> 0
209
- // across this same ramp, see R4). "Near-constant" is relative to that,
210
- // not perfectly flat — alpha-over-white is sublinear in L* as the base
211
- // lightens, which is exactly the R3 floor-vs-doc discrepancy noted above.
209
+ it('is near-constant (self-normalizing) across all planes: spread stays proportional', () => {
210
+ // Measured RELATIVE spread ((max-min)/min): subtle 0.27, default 0.29,
211
+ // strong 0.29 and, importantly, those are the same figures the OLD
212
+ // .06/.09/.14 family produced (0.28 / 0.27 / 0.28). Self-normalization
213
+ // survived the S-6 retune untouched.
214
+ //
215
+ // This assertion used to be an ABSOLUTE `spread < 4 L*`, which was the
216
+ // wrong metric: absolute spread scales with the alpha, so ANY strengthening
217
+ // fails it no matter how well the property holds. The S-6 ×1.5 retune
218
+ // tripped it (strong 3.46 -> 5.29) purely arithmetically. A ratio is
219
+ // scale-invariant, which is what "self-normalizing" actually claims.
220
+ //
221
+ // Contrast a solid border token, which swings ~5.6 -> 0 across this same
222
+ // ramp (see R4): the point is not a tight absolute band, it is that no
223
+ // plane ever loses the cue. Alpha-over-white is sublinear in L* as the
224
+ // base lightens, which is the R3 floor-vs-doc discrepancy noted above.
212
225
  for (const { token } of Object.values(HAIRLINES)) {
213
226
  const hairlineColor = dark[token as keyof typeof dark]
214
227
  const deltas = PLANE_ORDER.map((plane) => {
215
228
  const planeHex = RAMP_HEX[plane]
216
229
  return lstar(compositeOver(planeHex, hairlineColor)) - lstar(planeHex)
217
230
  })
218
- expect(Math.max(...deltas) - Math.min(...deltas)).toBeLessThan(4)
231
+ const min = Math.min(...deltas)
232
+ expect((Math.max(...deltas) - min) / min, `${token} relative spread`).toBeLessThan(0.35)
219
233
  }
220
234
  })
221
235
  })
222
236
 
223
- describe('R4 border-on-surface clears ΔL* >= 3 (fixes the invisible-hairline collision)', () => {
224
- it('border-subtle is no longer invisible on surface-raised (the original bug)', () => {
225
- const dL = Math.abs(lstar(dark['border-subtle']) - lstar(dark['surface-raised']))
226
- expect(dL).toBeGreaterThanOrEqual(3)
227
- })
228
-
229
- it('border-subtle clears ΔL* >= 3 against base/elevated/raised/overlay', () => {
237
+ // R4 used to check that the solid `border-subtle` hex did not match the plane
238
+ // it sat on. Those solid borders are gone (TD-07.14) separation is R3's
239
+ // alpha hairlines, which cannot collide with a plane by construction. What is
240
+ // left to guard is the one border still solid.
241
+ describe('R4 — border-prominent, the last solid border', () => {
242
+ it('clears ΔL* >= 3 on every content plane', () => {
230
243
  const planes = [
231
244
  'surface-base',
232
245
  'surface-elevated',
@@ -234,13 +247,13 @@ describe('surface ramp contract (dark) — token-value guardrails', () => {
234
247
  'surface-overlay',
235
248
  ] as const
236
249
  for (const plane of planes) {
237
- const dL = Math.abs(lstar(dark['border-subtle']) - lstar(dark[plane]))
238
- expect(dL, `border-subtle vs ${plane}`).toBeGreaterThanOrEqual(3)
250
+ const dL = Math.abs(lstar(dark['border-prominent']) - lstar(dark[plane]))
251
+ expect(dL, `border-prominent vs ${plane}`).toBeGreaterThanOrEqual(3)
239
252
  }
240
253
  })
241
254
  })
242
255
 
243
- describe('R5 — background-frame and surface-inset (S-3 new tokens)', () => {
256
+ describe('R5 — background-frame, the ramp floor', () => {
244
257
  it('background-frame is darker than background-base', () => {
245
258
  expect(lstar(dark['background-frame'])).toBeLessThan(lstar(dark['background-base']))
246
259
  })
@@ -255,14 +268,18 @@ describe('surface ramp contract (dark) — token-value guardrails', () => {
255
268
  'surface-raised',
256
269
  'surface-overlay',
257
270
  'surface-input',
258
- 'surface-inset',
259
271
  ] as const
260
272
  for (const token of otherTokens) {
261
273
  expect(dark['background-frame'], `background-frame vs ${token}`).not.toBe(dark[token])
262
274
  }
263
275
  })
264
276
 
265
- it('surface-inset is the darkest surface-family token', () => {
277
+ // `surface-inset` used to sit between frame and background as the darkest
278
+ // surface-family token. TD-07.14 retired it: at delta-E 1.10 from the frame
279
+ // it was an imperceptible duplicate, which is why `<Surface pressed>`
280
+ // (surface - 1) kept collapsing into it. `frame` is the floor now, so the
281
+ // rule that matters is that nothing in the surface family goes below it.
282
+ it('background-frame is darker than every surface-family token', () => {
266
283
  const surfaceFamily = [
267
284
  'surface-base',
268
285
  'surface-elevated',
@@ -270,14 +287,10 @@ describe('surface ramp contract (dark) — token-value guardrails', () => {
270
287
  'surface-overlay',
271
288
  'surface-input',
272
289
  ] as const
273
- const insetL = lstar(dark['surface-inset'])
290
+ const frameL = lstar(dark['background-frame'])
274
291
  for (const token of surfaceFamily) {
275
- expect(insetL, `surface-inset vs ${token}`).toBeLessThan(lstar(dark[token]))
292
+ expect(frameL, `background-frame vs ${token}`).toBeLessThan(lstar(dark[token]))
276
293
  }
277
294
  })
278
-
279
- it('surface-inset is distinct from background-frame', () => {
280
- expect(dark['surface-inset']).not.toBe(dark['background-frame'])
281
- })
282
295
  })
283
296
  })
@@ -63,7 +63,7 @@ export const Switch = forwardRef<View, SwitchProps>(function Switch(
63
63
  className={cn(
64
64
  'rounded-full p-0.5 transition-colors',
65
65
  styles.track,
66
- isChecked ? 'bg-brand-primary' : 'bg-border-strong'
66
+ isChecked ? 'bg-brand-primary' : 'bg-hairline-strong'
67
67
  )}
68
68
  >
69
69
  <View
@@ -100,8 +100,8 @@ export function TabList({ children, className }: TabListProps) {
100
100
 
101
101
  const variantStyles = {
102
102
  line: orientation === 'horizontal'
103
- ? 'border-b border-border'
104
- : 'border-r border-border',
103
+ ? 'border-b border-hairline'
104
+ : 'border-r border-hairline',
105
105
  enclosed: 'bg-surface-raised rounded-lg p-1',
106
106
  'soft-rounded': 'bg-surface-raised rounded-full p-1',
107
107
  }