@titan-design/react-ui 0.9.3 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/bodymap.js +78 -24
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +78 -24
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +429 -11
  6. package/dist/index.d.ts +429 -11
  7. package/dist/index.js +2386 -885
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2306 -836
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/{pages-DyEx-lBf.d.ts → pages-B1hqnGIC.d.ts} +149 -15
  12. package/dist/{pages-D7hOD4Vj.d.mts → pages-CvsFqNtx.d.mts} +149 -15
  13. package/dist/pages.d.mts +1 -1
  14. package/dist/pages.d.ts +1 -1
  15. package/dist/pages.js +1900 -1609
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1901 -1610
  18. package/dist/pages.mjs.map +1 -1
  19. package/dist/theme/index.d.mts +129 -34
  20. package/dist/theme/index.d.ts +129 -34
  21. package/dist/theme/index.js +95 -19
  22. package/dist/theme/index.js.map +1 -1
  23. package/dist/theme/index.mjs +92 -20
  24. package/dist/theme/index.mjs.map +1 -1
  25. package/dist/theme/tokens-css.js +88 -24
  26. package/dist/theme/tokens-css.js.map +1 -1
  27. package/dist/theme/tokens-css.mjs +88 -24
  28. package/dist/theme/tokens-css.mjs.map +1 -1
  29. package/package.json +8 -3
  30. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
  31. package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
  32. package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
  33. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
  34. package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
  35. package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
  36. package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
  37. package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
  38. package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
  39. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
  40. package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
  41. package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
  42. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
  43. package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
  44. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
  45. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
  46. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
  47. package/src/components/custom/Fatigue/README.md +93 -0
  48. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
  49. package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
  50. package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
  51. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
  52. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
  53. package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
  54. package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
  55. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
  56. package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
  57. package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
  58. package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
  59. package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
  60. package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
  61. package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
  62. package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
  63. package/src/components/custom/Fatigue/index.ts +41 -0
  64. package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
  65. package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
  66. package/src/components/custom/Workout/BaseBadge.tsx +5 -5
  67. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
  68. package/src/components/custom/Workout/ExerciseCard.test.tsx +4 -4
  69. package/src/components/custom/Workout/ExerciseCard.tsx +3 -1
  70. package/src/components/custom/Workout/ExerciseDetailPage.tsx +7 -1
  71. package/src/components/custom/Workout/README.md +62 -16
  72. package/src/components/custom/Workout/RowInventory.stories.tsx +5 -2
  73. package/src/components/custom/Workout/S3FullRail.stories.tsx +13 -1
  74. package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
  75. package/src/components/custom/Workout/SessionHeader.tsx +2 -2
  76. package/src/components/custom/Workout/SessionRail.stories.tsx +4 -1
  77. package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
  78. package/src/components/custom/Workout/SessionRail.tsx +55 -18
  79. package/src/components/custom/Workout/SetRow.test.tsx +6 -6
  80. package/src/components/custom/Workout/SetRow.tsx +9 -2
  81. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +16 -25
  82. package/src/components/custom/Workout/SupersetWrapper.tsx +1 -1
  83. package/src/components/custom/Workout/TempoDisplay.tsx +3 -7
  84. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
  85. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
  86. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
  87. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
  88. package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
  89. package/src/components/custom/Workout/VelocityStrip.test.tsx +196 -79
  90. package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
  91. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +4 -1
  92. package/src/components/custom/Workout/WorkoutPill.tsx +2 -2
  93. package/src/components/custom/Workout/index.ts +3 -0
  94. package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
  95. package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
  96. package/src/components/custom/charts/SetBarChart.tsx +651 -0
  97. package/src/components/custom/charts/live-rep-growth.ts +91 -0
  98. package/src/components/custom/index.ts +1 -0
  99. package/src/components/ui/surface/Surface.stories.tsx +195 -1
  100. package/src/components/ui/surface/Surface.test.tsx +148 -9
  101. package/src/components/ui/surface/Surface.tsx +37 -9
  102. package/src/components/ui/surface/SurfaceContext.ts +48 -8
  103. package/src/components/ui/surface/index.ts +1 -0
  104. package/src/components/ui/surface/surface.contract.test.ts +283 -0
  105. package/src/test/raw-color-patterns.test.ts +71 -0
  106. package/src/theme/ColorPalettes.stories.tsx +871 -0
  107. package/src/theme/ColorPrimitives.stories.tsx +300 -0
  108. package/src/theme/bar-paper.ts +59 -0
  109. package/src/theme/color-stories.coverage.test.ts +130 -0
  110. package/src/theme/color-story-kit.tsx +170 -0
  111. package/src/theme/config.ts +12 -0
  112. package/src/theme/elevation.ts +90 -67
  113. package/src/theme/global.css +35 -9
  114. package/src/theme/tokens/primitives.ts +40 -0
  115. package/src/theme/tokens/semantic.ts +63 -14
  116. package/tailwind.config.js +8 -0
  117. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
  118. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
  119. package/src/theme/ColorSystem.stories.tsx +0 -323
  120. package/src/theme/Colors.stories.tsx +0 -323
@@ -77,10 +77,12 @@ export const lightThemeCSSVars = {
77
77
  '--color-surface-base': semanticColorsLight['surface-base'],
78
78
  '--color-surface-elevated': semanticColorsLight['surface-elevated'],
79
79
  '--color-surface-raised': semanticColorsLight['surface-raised'],
80
+ '--color-surface-inset': semanticColorsLight['surface-inset'],
80
81
 
81
82
  '--color-background-base': semanticColorsLight['background-base'],
82
83
  '--color-background-default': semanticColorsLight['background-default'],
83
84
  '--color-background-subtle': semanticColorsLight['background-subtle'],
85
+ '--color-background-frame': semanticColorsLight['background-frame'],
84
86
 
85
87
  '--color-border-default': semanticColorsLight['border-default'],
86
88
  '--color-border-subtle': semanticColorsLight['border-subtle'],
@@ -150,6 +152,10 @@ export const lightThemeCSSVars = {
150
152
  '--color-border-input-focus': semanticColorsLight['border-input-focus'],
151
153
  '--color-border-input-error': semanticColorsLight['border-input-error'],
152
154
 
155
+ '--color-hairline-subtle': semanticColorsLight['hairline-subtle'],
156
+ '--color-hairline-default': semanticColorsLight['hairline-default'],
157
+ '--color-hairline-strong': semanticColorsLight['hairline-strong'],
158
+
153
159
  '--color-interactive-disabled-text': semanticColorsLight['interactive-disabled-text'],
154
160
 
155
161
  '--color-avatar-background': semanticColorsLight['avatar-background'],
@@ -198,10 +204,12 @@ export const darkThemeCSSVars = {
198
204
  '--color-surface-base': semanticColorsDark['surface-base'],
199
205
  '--color-surface-elevated': semanticColorsDark['surface-elevated'],
200
206
  '--color-surface-raised': semanticColorsDark['surface-raised'],
207
+ '--color-surface-inset': semanticColorsDark['surface-inset'],
201
208
 
202
209
  '--color-background-base': semanticColorsDark['background-base'],
203
210
  '--color-background-default': semanticColorsDark['background-default'],
204
211
  '--color-background-subtle': semanticColorsDark['background-subtle'],
212
+ '--color-background-frame': semanticColorsDark['background-frame'],
205
213
 
206
214
  '--color-border-default': semanticColorsDark['border-default'],
207
215
  '--color-border-subtle': semanticColorsDark['border-subtle'],
@@ -271,6 +279,10 @@ export const darkThemeCSSVars = {
271
279
  '--color-border-input-focus': semanticColorsDark['border-input-focus'],
272
280
  '--color-border-input-error': semanticColorsDark['border-input-error'],
273
281
 
282
+ '--color-hairline-subtle': semanticColorsDark['hairline-subtle'],
283
+ '--color-hairline-default': semanticColorsDark['hairline-default'],
284
+ '--color-hairline-strong': semanticColorsDark['hairline-strong'],
285
+
274
286
  '--color-interactive-disabled-text': semanticColorsDark['interactive-disabled-text'],
275
287
 
276
288
  '--color-avatar-background': semanticColorsDark['avatar-background'],
@@ -1,21 +1,21 @@
1
1
  /**
2
2
  * Elevation System
3
- *
3
+ *
4
4
  * A unified depth system where each elevation level corresponds to:
5
5
  * - Calculated surface color (derived from base using lighten)
6
6
  * - Shadow intensity and style (raised/pressed)
7
7
  * - Dynamically calculated shadow colors (derived from surface color)
8
8
  * - Semantic name for reference
9
- *
9
+ *
10
10
  * Elevation range: -2 (deep inset) to +5 (floating overlay)
11
- *
11
+ *
12
12
  * Negative elevations: Inset/pressed elements (sunken into surface)
13
13
  * Zero elevation: Base surface (background/page level)
14
14
  * Positive elevations: Raised elements (floating above surface)
15
- *
15
+ *
16
16
  * All colors are calculated dynamically from a base surface color,
17
17
  * allowing for custom surfaces and consistent color relationships.
18
- *
18
+ *
19
19
  * Lighting Model: Light source from upper-right corner
20
20
  * - Raised elements are always lighter than base (consistent across themes)
21
21
  * - Base colors must allow sufficient lightening in both themes
@@ -49,7 +49,7 @@ export const elevationSystem: Record<ElevationLevel, ElevationConfig> = {
49
49
  shadowStyle: 'pressed',
50
50
  description: 'Deeply recessed elements (e.g., input fields, deeply pressed buttons)',
51
51
  },
52
-
52
+
53
53
  // Shallow inset (pressed buttons, sunken panels)
54
54
  [-1]: {
55
55
  name: 'inset',
@@ -58,7 +58,7 @@ export const elevationSystem: Record<ElevationLevel, ElevationConfig> = {
58
58
  shadowStyle: 'pressed',
59
59
  description: 'Shallow inset elements (e.g., pressed buttons, sunken panels within cards)',
60
60
  },
61
-
61
+
62
62
  // Base level (page background, default surface)
63
63
  [0]: {
64
64
  name: 'base',
@@ -67,7 +67,7 @@ export const elevationSystem: Record<ElevationLevel, ElevationConfig> = {
67
67
  shadowStyle: 'raised',
68
68
  description: 'Base surface level (page background, default containers)',
69
69
  },
70
-
70
+
71
71
  // Level 1: Subtle elevation (outline cards, subtle containers)
72
72
  [1]: {
73
73
  name: 'subtle',
@@ -76,7 +76,7 @@ export const elevationSystem: Record<ElevationLevel, ElevationConfig> = {
76
76
  shadowStyle: 'raised',
77
77
  description: 'Subtle elevation (outline cards, subtle containers)',
78
78
  },
79
-
79
+
80
80
  // Level 2: Standard elevation (default cards, buttons)
81
81
  [2]: {
82
82
  name: 'standard',
@@ -85,7 +85,7 @@ export const elevationSystem: Record<ElevationLevel, ElevationConfig> = {
85
85
  shadowStyle: 'raised',
86
86
  description: 'Standard elevation (default cards, raised buttons)',
87
87
  },
88
-
88
+
89
89
  // Level 3: Prominent elevation (important cards, modals)
90
90
  [3]: {
91
91
  name: 'prominent',
@@ -94,7 +94,7 @@ export const elevationSystem: Record<ElevationLevel, ElevationConfig> = {
94
94
  shadowStyle: 'raised',
95
95
  description: 'Prominent elevation (important cards, modals, dropdowns)',
96
96
  },
97
-
97
+
98
98
  // Level 4: High elevation (overlays, popovers)
99
99
  [4]: {
100
100
  name: 'high',
@@ -103,7 +103,7 @@ export const elevationSystem: Record<ElevationLevel, ElevationConfig> = {
103
103
  shadowStyle: 'raised',
104
104
  description: 'High elevation (overlays, popovers, tooltips)',
105
105
  },
106
-
106
+
107
107
  // Level 5: Floating (toasts, floating action buttons)
108
108
  [5]: {
109
109
  name: 'floating',
@@ -117,14 +117,14 @@ export const elevationSystem: Record<ElevationLevel, ElevationConfig> = {
117
117
  /**
118
118
  * Calculate rim light color from the BASE color.
119
119
  * Creates a consistent bright highlight with opacity that scales with elevation.
120
- *
121
- * The rim light represents light hitting the edge of raised elements from a
120
+ *
121
+ * The rim light represents light hitting the edge of raised elements from a
122
122
  * consistent light source. Uses ABSOLUTE HSV value (not relative lightening)
123
123
  * for consistent intensity across all surface colors.
124
- *
124
+ *
125
125
  * For colored surfaces: preserves hue/saturation, sets value to fixed bright level
126
126
  * For gray surfaces: uses white with appropriate opacity
127
- *
127
+ *
128
128
  * @param baseColor - The base surface color (hex)
129
129
  * @param level - Elevation level (used to scale opacity)
130
130
  */
@@ -133,22 +133,22 @@ function calculateRimLightColor(baseColor: string, level: ElevationLevel): strin
133
133
  if (!rgb) {
134
134
  return 'rgba(255, 255, 255, 0.5)'
135
135
  }
136
-
136
+
137
137
  const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b)
138
138
  const isLight = hsv.v > 0.6
139
- const isSaturated = hsv.s > 0.3 // Colored (not gray)
140
-
139
+ const isSaturated = hsv.s > 0.3 // Colored (not gray)
140
+
141
141
  // Opacity scales with elevation to compensate for lighter surfaces at higher levels
142
142
  // Range: +0% at level 1 to +20% at level 5
143
143
  const absLevel = Math.abs(level)
144
- const opacityBoost = Math.max(0, (absLevel - 1) * 0.05) // +5% per level above 1 (0% to 20%)
145
-
144
+ const opacityBoost = Math.max(0, (absLevel - 1) * 0.05) // +5% per level above 1 (0% to 20%)
145
+
146
146
  if (isSaturated) {
147
147
  // For colored surfaces, preserve hue and saturation but set value to fixed bright level
148
148
  // This ensures consistent rim light intensity across all colored surfaces
149
- const rimLightValue = 0.95 // Fixed bright value for rim light
150
- const rimLightRgb = hsvToRgb(hsv.h, hsv.s * 0.3, rimLightValue) // Reduce saturation for brighter appearance
151
-
149
+ const rimLightValue = 0.95 // Fixed bright value for rim light
150
+ const rimLightRgb = hsvToRgb(hsv.h, hsv.s * 0.3, rimLightValue) // Reduce saturation for brighter appearance
151
+
152
152
  // Base opacity + elevation boost, capped at 1.0
153
153
  const baseOpacity = isLight ? 0.9 : 0.5
154
154
  const finalOpacity = Math.min(1, baseOpacity + opacityBoost)
@@ -164,10 +164,10 @@ function calculateRimLightColor(baseColor: string, level: ElevationLevel): strin
164
164
  /**
165
165
  * Calculate dark shadow colors from the surface color.
166
166
  * The dark shadow is cast BY the element, so it should match the surface color.
167
- *
167
+ *
168
168
  * Uses ABSOLUTE HSV value (not relative darkening) for consistent shadow intensity
169
169
  * across all surface colors - simulating a consistent light source.
170
- *
170
+ *
171
171
  * For colored surfaces: preserves hue/saturation, sets value to fixed dark level
172
172
  * For gray surfaces: uses black with appropriate opacity
173
173
  */
@@ -182,17 +182,17 @@ function calculateDarkShadowColors(surfaceColor: string): {
182
182
  darker: 'rgba(0, 0, 0, 0.4)',
183
183
  }
184
184
  }
185
-
185
+
186
186
  const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b)
187
187
  const isLight = hsv.v > 0.6
188
188
  const isSaturated = hsv.s > 0.3
189
-
189
+
190
190
  if (isSaturated) {
191
191
  // For colored surfaces, preserve hue and saturation but set value to fixed dark level
192
192
  // This ensures consistent shadow intensity across all colored surfaces
193
- const darkShadowValue = 0.15 // Fixed dark value for shadows
193
+ const darkShadowValue = 0.15 // Fixed dark value for shadows
194
194
  const darkShadowRgb = hsvToRgb(hsv.h, hsv.s, darkShadowValue)
195
-
195
+
196
196
  if (isLight) {
197
197
  return {
198
198
  dark: `rgba(${darkShadowRgb.r}, ${darkShadowRgb.g}, ${darkShadowRgb.b}, 0.3)`,
@@ -227,8 +227,8 @@ function calculateDarkShadowColors(surfaceColor: string): {
227
227
  */
228
228
  function getRimLightConfig(): { offset: number; blur: number } {
229
229
  return {
230
- offset: 1, // Fixed 1px for crisp rim
231
- blur: 1, // Tight blur for crisp edge
230
+ offset: 1, // Fixed 1px for crisp rim
231
+ blur: 1, // Tight blur for crisp edge
232
232
  }
233
233
  }
234
234
 
@@ -253,18 +253,18 @@ function calculateElevationSurface(
253
253
  theme: 'light' | 'dark'
254
254
  ): string {
255
255
  const config = elevationSystem[level]
256
-
256
+
257
257
  if (config.colorAdjustment === 0) {
258
258
  return baseColor
259
259
  }
260
-
260
+
261
261
  // For raised elevations (positive), always lighten (consistent lighting model)
262
262
  // Light source is from upper-right, so raised elements are always lighter
263
263
  // For inset elevations (negative), we keep base color (shadow creates depth)
264
264
  if (level < 0) {
265
- return baseColor // Inset elements use base color
265
+ return baseColor // Inset elements use base color
266
266
  }
267
-
267
+
268
268
  // Always lighten for raised elements (consistent across themes)
269
269
  // Base colors must be chosen to allow sufficient lightening in dark mode
270
270
  return lighten(baseColor, config.colorAdjustment)
@@ -273,7 +273,7 @@ function calculateElevationSurface(
273
273
  /**
274
274
  * Calculate surface color for an elevation level from a base color.
275
275
  * Results are cached to avoid redundant calculations.
276
- *
276
+ *
277
277
  * @param baseColor - Base surface color (hex)
278
278
  * @param level - Elevation level
279
279
  * @param theme - Current theme ('light' or 'dark')
@@ -285,15 +285,15 @@ export function getElevationSurface(
285
285
  theme: 'light' | 'dark'
286
286
  ): string {
287
287
  const cacheKey = getCacheKey(baseColor, level, theme)
288
-
288
+
289
289
  // Check cache
290
290
  if (elevationSurfaceCache.has(cacheKey)) {
291
291
  return elevationSurfaceCache.get(cacheKey)!
292
292
  }
293
-
293
+
294
294
  // Calculate
295
295
  const result = calculateElevationSurface(baseColor, level, theme)
296
-
296
+
297
297
  // Cache result (with size limit to prevent memory leaks)
298
298
  if (elevationSurfaceCache.size >= MAX_CACHE_SIZE) {
299
299
  // Remove oldest entry (simple FIFO)
@@ -301,7 +301,7 @@ export function getElevationSurface(
301
301
  elevationSurfaceCache.delete(firstKey)
302
302
  }
303
303
  elevationSurfaceCache.set(cacheKey, result)
304
-
304
+
305
305
  return result
306
306
  }
307
307
 
@@ -309,7 +309,12 @@ export function getElevationSurface(
309
309
  const elevationShadowCache = new Map<string, ViewStyle>()
310
310
  const MAX_SHADOW_CACHE_SIZE = 50
311
311
 
312
- function getShadowCacheKey(baseColor: string, level: ElevationLevel, theme: 'light' | 'dark', isHovered: boolean): string {
312
+ function getShadowCacheKey(
313
+ baseColor: string,
314
+ level: ElevationLevel,
315
+ theme: 'light' | 'dark',
316
+ isHovered: boolean
317
+ ): string {
313
318
  return `${baseColor}-${level}-${theme}-${isHovered}`
314
319
  }
315
320
 
@@ -320,35 +325,35 @@ function calculateElevationShadow(
320
325
  isHovered: boolean = false
321
326
  ): ViewStyle {
322
327
  const config = elevationSystem[level]
323
-
328
+
324
329
  // Base level has no shadow
325
330
  if (level === 0) {
326
331
  return {}
327
332
  }
328
-
333
+
329
334
  // Calculate the actual elevated surface color
330
335
  const elevatedSurfaceColor = calculateElevationSurface(baseColor, level, theme)
331
-
336
+
332
337
  // Rim light: calculated from BASE color (consistent across all elevations)
333
338
  // This represents a consistent light source - only size changes with elevation
334
339
  const rimLightColor = calculateRimLightColor(baseColor, level)
335
-
340
+
336
341
  // Dark shadow: calculated from ELEVATED surface color
337
342
  // The shadow is cast by the element, so it should match the surface
338
343
  const darkShadowColors = calculateDarkShadowColors(elevatedSurfaceColor)
339
-
344
+
340
345
  // Get shadow intensity config (for dark shadow)
341
346
  const intensityConfig = {
342
347
  subtle: { offset: 1, blur: 2 },
343
348
  medium: { offset: 2, blur: 4 },
344
349
  strong: { offset: 2, blur: 5 },
345
350
  }
346
-
351
+
347
352
  const { offset, blur } = intensityConfig[config.shadowIntensity]
348
-
353
+
349
354
  // Get rim light config (scales with elevation level)
350
355
  const rimConfig = getRimLightConfig()
351
-
356
+
352
357
  if (config.shadowStyle === 'pressed') {
353
358
  // Inset shadow (pressed elements)
354
359
  if (isHovered) {
@@ -365,7 +370,7 @@ function calculateElevationShadow(
365
370
  },
366
371
  }) as ViewStyle
367
372
  }
368
-
373
+
369
374
  return Platform.select({
370
375
  web: {
371
376
  boxShadow: `inset ${offset}px ${offset}px ${blur}px ${darkShadowColors.darker}, inset -${rimConfig.offset}px -${rimConfig.offset}px ${rimConfig.blur}px ${rimLightColor}`,
@@ -394,7 +399,7 @@ function calculateElevationShadow(
394
399
  },
395
400
  }) as ViewStyle
396
401
  }
397
-
402
+
398
403
  return Platform.select({
399
404
  web: {
400
405
  boxShadow: `${offset}px ${offset}px ${blur}px ${darkShadowColors.dark}, -${rimConfig.offset}px -${rimConfig.offset}px ${rimConfig.blur}px ${rimLightColor}`,
@@ -413,7 +418,7 @@ function calculateElevationShadow(
413
418
  /**
414
419
  * Get shadow style for an elevation level, calculated dynamically from surface color.
415
420
  * Results are cached to avoid redundant calculations.
416
- *
421
+ *
417
422
  * @param baseColor - Base surface color (hex)
418
423
  * @param level - Elevation level
419
424
  * @param theme - Current theme ('light' or 'dark')
@@ -427,44 +432,65 @@ export function getElevationShadow(
427
432
  isHovered: boolean = false
428
433
  ): ViewStyle {
429
434
  const cacheKey = getShadowCacheKey(baseColor, level, theme, isHovered)
430
-
435
+
431
436
  // Check cache
432
437
  if (elevationShadowCache.has(cacheKey)) {
433
438
  return elevationShadowCache.get(cacheKey)!
434
439
  }
435
-
440
+
436
441
  // Calculate shadow
437
442
  const result = calculateElevationShadow(baseColor, level, theme, isHovered)
438
-
443
+
439
444
  // Cache result
440
445
  if (elevationShadowCache.size >= MAX_SHADOW_CACHE_SIZE) {
441
446
  const firstKey = elevationShadowCache.keys().next().value!
442
447
  elevationShadowCache.delete(firstKey)
443
448
  }
444
449
  elevationShadowCache.set(cacheKey, result)
445
-
450
+
446
451
  return result
447
452
  }
448
453
 
454
+ /**
455
+ * Elevation level backing a `<Surface pressed>` recess: the shallow `inset`
456
+ * (-1) step. Pressed exposes ONE level of depth — pressed-2 is intentionally
457
+ * not surfaced (the deeper `-2` deep-inset stays available to `elevation`).
458
+ */
459
+ export const PRESSED_ELEVATION_LEVEL: ElevationLevel = -1
460
+
461
+ /**
462
+ * Inner-shadow recess for a pressed (sunken) surface, tuned to the well's own
463
+ * fill colour so the rim/shadow tints track the darker fill. Composes WITH the
464
+ * darker pressed fill: on web it adds an inset boxShadow; on the native path
465
+ * (no inset-shadow support) it's a no-op and the recess reads via the fill
466
+ * alone — so a pressed surface stays legibly sunken with or without the shadow.
467
+ *
468
+ * @param fillColor - The pressed surface's own fill (one ramp step down).
469
+ * @param theme - Current theme ('light' or 'dark').
470
+ */
471
+ export function getPressedRecessShadow(fillColor: string, theme: 'light' | 'dark'): ViewStyle {
472
+ return getElevationShadow(fillColor, PRESSED_ELEVATION_LEVEL, theme)
473
+ }
474
+
449
475
  /**
450
476
  * Get base surface color from theme.
451
477
  * This is the foundation color that all elevations are calculated from.
452
- *
478
+ *
453
479
  * IMPORTANT: Base colors must be chosen to allow sufficient lightening.
454
- *
480
+ *
455
481
  * **Light Mode**: Cannot use `#FFFFFF` (white) - cannot lighten white!
456
482
  * - Use `#F3F4F6` (neutral[100]) - can lighten to `#FAFAFA` → `#FFFFFF`
457
483
  * - This allows raised elevations to be lighter (closer to white)
458
- *
484
+ *
459
485
  * **Dark Mode**: Need lighter base to allow sufficient lightening
460
- * - Use `#191919` (charcoal[600]) - allows lightening to `#2A2A2A` at max elevation
461
- *
486
+ * - Use `surface-elevated` (#2A2827, TD-surface-tokens ramp) - allows further lightening
487
+ *
462
488
  * Can be overridden to use custom base colors for special cases.
463
489
  */
464
490
  export function getBaseSurfaceColor(theme: 'light' | 'dark'): string {
465
491
  // Use semantic token values so elevation colors stay in sync with the token pipeline.
466
492
  // Light mode: surface-raised (#F3F4F6 / neutral[100]) - NOT white, so we can lighten it
467
- // Dark mode: surface-elevated (#191919 / charcoal[600]) - allows sufficient lightening
493
+ // Dark mode: surface-elevated (#2A2827, TD-surface-tokens ramp) - allows sufficient lightening
468
494
 
469
495
  return theme === 'light'
470
496
  ? semanticColorsLight['surface-raised']
@@ -544,10 +570,7 @@ const glowConfig: Record<GlowIntensity, { blur: number; spread: number; opacity:
544
570
  * @param intensity - Glow intensity level
545
571
  * @returns ViewStyle with platform-specific shadow properties
546
572
  */
547
- export function getGlowShadow(
548
- color: string,
549
- intensity: GlowIntensity = 'medium'
550
- ): ViewStyle {
573
+ export function getGlowShadow(color: string, intensity: GlowIntensity = 'medium'): ViewStyle {
551
574
  const config = glowConfig[intensity]
552
575
  const rgb = hexToRgb(color)
553
576
  if (!rgb) return {}
@@ -94,15 +94,25 @@
94
94
  --color-text-link: #828DF8;
95
95
  --color-text-link-hover: #60A5FA;
96
96
 
97
- --color-surface-base: #161616;
98
- --color-surface-elevated: #191919;
99
- --color-surface-raised: #1C1C1C;
100
- --color-surface-overlay: #191919;
101
- --color-surface-input: #191919;
102
-
103
- --color-background-base: #101010;
104
- --color-background-default: #161616;
105
- --color-background-subtle: #1C1C1C;
97
+ /* Surface ramp — warm-tapered DERIVED, shipped verbatim from deriveSurfaceRamp()
98
+ (TD-surface-tokens, S-1, re-spaced S-3). surface-overlay and surface-elevated
99
+ are distinct (were both #191919 pre-S-1); see surfaceRampDark in
100
+ tokens/primitives.ts. surface-inset is the ramp's floor, promoted to a real
101
+ token at S-3 (was unwired). */
102
+ --color-surface-base: #252321;
103
+ --color-surface-elevated: #2C2A28;
104
+ --color-surface-raised: #31302F;
105
+ --color-surface-overlay: #373635;
106
+ --color-surface-input: #2C2A28;
107
+ --color-surface-inset: #13100D;
108
+
109
+ --color-background-base: #1C1916;
110
+ --color-background-default: #252321;
111
+ --color-background-subtle: #2C2A28;
112
+ /* Frame/bezel chrome (S-3) — top bar + side nav shell, one step below
113
+ background-base, darker even than surface-inset. See backgroundFrameDark
114
+ in tokens/primitives.ts. */
115
+ --color-background-frame: #100D0A;
106
116
 
107
117
  --color-border-default: #1F1F1F;
108
118
  --color-border-subtle: #1C1C1C;
@@ -113,6 +123,11 @@
113
123
  --color-border-input-focus: #828DF8;
114
124
  --color-border-input-error: #EF4444;
115
125
 
126
+ /* Alpha hairline separators — self-normalizing on any plane (§4/S-2) */
127
+ --color-hairline-subtle: rgba(255, 255, 255, 0.06);
128
+ --color-hairline-default: rgba(255, 255, 255, 0.09);
129
+ --color-hairline-strong: rgba(255, 255, 255, 0.14);
130
+
116
131
  --color-interactive-hover: rgba(255, 255, 255, 0.04);
117
132
  --color-interactive-focus: rgba(255, 255, 255, 0.12);
118
133
  --color-interactive-active: rgba(255, 255, 255, 0.16);
@@ -236,10 +251,15 @@
236
251
  --color-surface-raised: #F3F4F6;
237
252
  --color-surface-overlay: #FFFFFF;
238
253
  --color-surface-input: #FAFAFA;
254
+ /* Placeholder pairing (S-3) — light-mode counterpart of the dark ramp's
255
+ inset plane; not part of the dark-ramp investigation. */
256
+ --color-surface-inset: #D1D5DB;
239
257
 
240
258
  --color-background-base: #EBEBEB;
241
259
  --color-background-default: #FFFFFF;
242
260
  --color-background-subtle: #FAFAFA;
261
+ /* Placeholder pairing (S-3) — see --color-surface-inset note above. */
262
+ --color-background-frame: #9CA3AF;
243
263
 
244
264
  --color-border-default: #E8E9EB;
245
265
  --color-border-subtle: #F3F4F6;
@@ -250,6 +270,12 @@
250
270
  --color-border-input-focus: #5048E5;
251
271
  --color-border-input-error: #D14343;
252
272
 
273
+ /* Alpha hairline separators — mirrors the dark-mode white-alpha family,
274
+ composited toward black on light surfaces (§4/S-2). */
275
+ --color-hairline-subtle: rgba(0, 0, 0, 0.06);
276
+ --color-hairline-default: rgba(0, 0, 0, 0.09);
277
+ --color-hairline-strong: rgba(0, 0, 0, 0.14);
278
+
253
279
  --color-interactive-hover: rgba(55, 65, 81, 0.04);
254
280
  --color-interactive-focus: rgba(55, 65, 81, 0.12);
255
281
  --color-interactive-active: rgba(55, 65, 81, 0.16);
@@ -84,6 +84,46 @@ export const primitiveColors = {
84
84
  transparent: 'transparent',
85
85
  } as const
86
86
 
87
+ /**
88
+ * Surface ramp — dark mode (TD-surface-tokens, S-1, re-spaced S-3)
89
+ *
90
+ * Warm-tapered, DERIVED ramp — NOT hand-picked. Shipped verbatim as the output of
91
+ * `deriveSurfaceRamp(shellL=9, steps=[4.5,3.5,3,2.5], rbShadow=6, rbHilite=1.5)`
92
+ * (see `packages/ui/src/components/ui/surface/surface-lab-shared.tsx` on branch
93
+ * `feat/TD-unified-lab`, story `Lab/Surface Exploration -> Surface Ramp System`
94
+ * — that lab default is `[4.5,2.5,2,1.5]` as of this writing and should be
95
+ * updated to match on that branch, follow-up, not done here).
96
+ * Lightness is CIELAB L* off the shell, with DIMINISHING steps (4.5/3.5/3/2.5) —
97
+ * the frame->content jump is biggest, each plane above adds less, but the top
98
+ * three planes (elevated/raised/overlay) were re-spaced WIDER (S-3) than the
99
+ * original 2.5/2/1.5 taper so they read as distinct planes without leaning on
100
+ * the hairline alone. `background`/`base`/`inset` are unchanged by the re-space.
101
+ * Warmth (R-B) tapers 6 -> 1.5 from frame to hero so bright content planes stay
102
+ * near-neutral.
103
+ * Kept separate from the `charcoal` scale above (which several existing
104
+ * components reference directly for unrelated shadow/track tints) so this
105
+ * change is additive and doesn't ripple into those consumers.
106
+ * See coordination/design-explorations/surface-system-north-star.md.
107
+ */
108
+ export const surfaceRampDark = {
109
+ inset: '#13100D', // L*4.5 — sub-shell well / pressed
110
+ background: '#1C1916', // L*9 — shell / frame
111
+ base: '#252321', // L*13.5 — main content plane
112
+ elevated: '#2C2A28', // L*17 — nav / rail
113
+ raised: '#31302F', // L*20 — cards
114
+ overlay: '#373635', // L*22.5 — hero / popover
115
+ } as const
116
+
117
+ /**
118
+ * Frame/bezel primitive — dark mode (TD-surface-tokens, S-3)
119
+ *
120
+ * The chrome that "exits the content ramp" entirely: the top bar + side nav
121
+ * shell. Sits ONE STEP BELOW `surfaceRampDark.background` (L*9), darker even
122
+ * than `surfaceRampDark.inset` (L*4.5) — it isn't a plane IN the ramp, it's
123
+ * the bezel the ramp sits inside. Literal, not `deriveSurfaceRamp()` output.
124
+ */
125
+ export const backgroundFrameDark = '#100D0A' // L*3.79
126
+
87
127
  /**
88
128
  * Derived tonal ramps (TD-05.09 color foundations)
89
129
  *