@titan-design/react-ui 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bodymap.js +78 -24
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +78 -24
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +215 -123
- package/dist/index.d.ts +215 -123
- package/dist/index.js +408 -259
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +258 -121
- package/dist/index.mjs.map +1 -1
- package/dist/pages.js +214 -157
- package/dist/pages.js.map +1 -1
- package/dist/pages.mjs +214 -157
- package/dist/pages.mjs.map +1 -1
- package/dist/theme/index.d.mts +895 -795
- package/dist/theme/index.d.ts +895 -795
- package/dist/theme/index.js +101 -20
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +99 -22
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.js +88 -24
- package/dist/theme/tokens-css.js.map +1 -1
- package/dist/theme/tokens-css.mjs +88 -24
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/custom/Workout/BaseBadge.tsx +5 -5
- package/src/components/custom/Workout/RowInventory.stories.tsx +5 -2
- package/src/components/custom/Workout/S3FullRail.stories.tsx +13 -1
- package/src/components/custom/Workout/SessionHeader.tsx +11 -13
- package/src/components/custom/Workout/SessionRail.stories.tsx +4 -1
- package/src/components/custom/Workout/SessionRail.tsx +16 -14
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +16 -25
- package/src/components/custom/Workout/SupersetWrapper.tsx +1 -1
- package/src/components/custom/Workout/TempoDisplay.tsx +3 -7
- package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +4 -1
- package/src/components/custom/Workout/WorkoutPill.tsx +2 -2
- package/src/components/shell/DashboardShell.tsx +6 -3
- package/src/components/ui/surface/Surface.stories.tsx +244 -1
- package/src/components/ui/surface/Surface.test.tsx +244 -1
- package/src/components/ui/surface/Surface.tsx +98 -20
- package/src/components/ui/surface/SurfaceContext.ts +126 -0
- package/src/components/ui/surface/index.ts +13 -0
- package/src/components/ui/surface/surface.contract.test.ts +283 -0
- package/src/test/nativewind-stub.ts +13 -0
- package/src/theme/ColorSystem.stories.tsx +232 -47
- package/src/theme/ThemeProvider.test.tsx +24 -0
- package/src/theme/ThemeProvider.tsx +23 -6
- package/src/theme/config.ts +12 -0
- package/src/theme/elevation.ts +90 -67
- package/src/theme/global.css +35 -9
- package/src/theme/tokens/primitives.ts +40 -0
- package/src/theme/tokens/semantic.ts +63 -14
- package/tailwind.config.js +8 -0
package/src/theme/elevation.ts
CHANGED
|
@@ -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
|
|
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)
|
|
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
|
|
150
|
-
const rimLightRgb = hsvToRgb(hsv.h, hsv.s * 0.3, rimLightValue)
|
|
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
|
|
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,
|
|
231
|
-
blur: 1,
|
|
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
|
|
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(
|
|
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
|
|
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 (#
|
|
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 {}
|
package/src/theme/global.css
CHANGED
|
@@ -94,15 +94,25 @@
|
|
|
94
94
|
--color-text-link: #828DF8;
|
|
95
95
|
--color-text-link-hover: #60A5FA;
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
--color-
|
|
104
|
-
--color-
|
|
105
|
-
--color-
|
|
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
|
*
|
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
* - interactive-* : Hover/focus/active/disabled states
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
primitiveColors as p,
|
|
22
|
+
primitiveRamps as ramp,
|
|
23
|
+
discreteRainbow,
|
|
24
|
+
surfaceRampDark as surf,
|
|
25
|
+
backgroundFrameDark,
|
|
26
|
+
} from './primitives'
|
|
21
27
|
|
|
22
28
|
// Light mode semantic colors (default)
|
|
23
29
|
export const semanticColorsLight = {
|
|
@@ -120,11 +126,20 @@ export const semanticColorsLight = {
|
|
|
120
126
|
'surface-raised': p.neutral[100], // light gray for raised cards
|
|
121
127
|
'surface-overlay': p.white,
|
|
122
128
|
'surface-input': p.neutral[50], // Input field background (filled variant)
|
|
129
|
+
// Deepest pressed pit (TD-surface-tokens, S-3) — light-mode counterpart of
|
|
130
|
+
// the dark ramp's `inset` plane; a placeholder pairing, not part of the
|
|
131
|
+
// dark-ramp investigation (surface.contract.test.ts is dark-mode only).
|
|
132
|
+
'surface-inset': p.neutral[300],
|
|
123
133
|
|
|
124
134
|
// Background colors (background-*)
|
|
125
135
|
'background-base': '#EBEBEB',
|
|
126
136
|
'background-default': p.white,
|
|
127
137
|
'background-subtle': p.neutral[50],
|
|
138
|
+
// Frame/bezel chrome (TD-surface-tokens, S-3) — top bar + side nav shell,
|
|
139
|
+
// one step below `background-base` (darker still than `surface-inset`
|
|
140
|
+
// above, mirroring the dark-mode ordering). Placeholder pairing for light
|
|
141
|
+
// mode (see dark-mode note on `surface-inset` above).
|
|
142
|
+
'background-frame': p.neutral[400],
|
|
128
143
|
|
|
129
144
|
// Border colors (border-*)
|
|
130
145
|
'border-default': '#E8E9EB',
|
|
@@ -137,6 +152,12 @@ export const semanticColorsLight = {
|
|
|
137
152
|
'border-input-focus': p.blue[600], // Input field border on focus
|
|
138
153
|
'border-input-error': p.red[600], // Input field border on error
|
|
139
154
|
|
|
155
|
+
// Alpha hairline separators (surface-independent — composite toward black on
|
|
156
|
+
// light surfaces, mirroring the dark-mode white-alpha family). See §4/S-2.
|
|
157
|
+
'hairline-subtle': 'rgba(0, 0, 0, 0.06)',
|
|
158
|
+
'hairline-default': 'rgba(0, 0, 0, 0.09)',
|
|
159
|
+
'hairline-strong': 'rgba(0, 0, 0, 0.14)',
|
|
160
|
+
|
|
140
161
|
// Interactive states (interactive-*)
|
|
141
162
|
'interactive-hover': 'rgba(55, 65, 81, 0.04)',
|
|
142
163
|
'interactive-focus': 'rgba(55, 65, 81, 0.12)',
|
|
@@ -247,19 +268,38 @@ export const semanticColorsDark = {
|
|
|
247
268
|
'text-link': '#828DF8',
|
|
248
269
|
'text-link-hover': p.blue[400],
|
|
249
270
|
|
|
250
|
-
// Surface colors - dark backgrounds
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
'
|
|
261
|
-
|
|
262
|
-
//
|
|
271
|
+
// Surface colors - dark backgrounds — warm-tapered DERIVED ramp (TD-surface-tokens,
|
|
272
|
+
// S-1, re-spaced S-3). Shipped verbatim from `deriveSurfaceRamp()` — see
|
|
273
|
+
// `surfaceRampDark` in primitives.ts for the full derivation note. `surface-overlay`
|
|
274
|
+
// and `surface-elevated` are distinct (were both #191919 pre-S-1); `surface-input`
|
|
275
|
+
// tracks one plane above `surface-base`, same relative position as before the remap.
|
|
276
|
+
// `background-base` backs `Surface level="background"` (SurfaceContext.SURFACE_LEVEL_TOKEN),
|
|
277
|
+
// so it takes the ramp's `background` (shell/frame) role, NOT `inset`. The ramp's
|
|
278
|
+
// deepest `inset` plane is now promoted to `surface-inset` below (S-3) — still no
|
|
279
|
+
// `SurfaceLevel` member for it (follow-up, alongside the elevation.ts -1/-2 pressed
|
|
280
|
+
// levels — parallel workstream, not this branch).
|
|
281
|
+
'surface-base': surf.base, // main surface (#252321, L*13.5)
|
|
282
|
+
'surface-elevated': surf.elevated, // elevated surface (#2C2A28, L*17 — nav/rail)
|
|
283
|
+
'surface-raised': surf.raised, // raised surface (#31302F, L*20 — cards)
|
|
284
|
+
'surface-overlay': surf.overlay, // overlay surface (#373635, L*22.5 — hero/popover)
|
|
285
|
+
'surface-input': surf.elevated, // input surface (#2C2A28 — one plane above base)
|
|
286
|
+
'surface-inset': surf.inset, // deepest pressed pit (#13100D, L*4.5 — sub-shell well)
|
|
287
|
+
|
|
288
|
+
// Background colors — same ramp, the frame/shell end of it.
|
|
289
|
+
'background-base': surf.background, // shell / frame (#1C1916, L*9 — Surface level="background")
|
|
290
|
+
'background-default': surf.base, // main background (#252321, L*13.5 — matches surface-base)
|
|
291
|
+
'background-subtle': surf.elevated, // subtle background (#2C2A28, L*17 — matches surface-elevated)
|
|
292
|
+
// Frame/bezel chrome (S-3) — the top bar + side nav shell; one step BELOW
|
|
293
|
+
// `background-base`, darker even than `surface-inset` — it exits the content
|
|
294
|
+
// ramp entirely rather than sitting in it. See `backgroundFrameDark` in
|
|
295
|
+
// primitives.ts.
|
|
296
|
+
'background-frame': backgroundFrameDark, // frame / bezel (#100D0A, L*3.79)
|
|
297
|
+
|
|
298
|
+
// Border colors — `border-subtle` (#1C1C1C) previously collided with
|
|
299
|
+
// `surface-raised` (also #1C1C1C, an invisible border on raised cards). The
|
|
300
|
+
// re-spaced ramp above moved `surface-raised` to #2D2C2B, so this value is now
|
|
301
|
+
// distinct from every surface hex without needing to change it — see
|
|
302
|
+
// `surface.contract.test.ts` R2/R4.
|
|
263
303
|
'border-default': p.charcoal[400], // default border
|
|
264
304
|
'border-subtle': p.charcoal[500], // subtle border
|
|
265
305
|
'border-strong': p.charcoal[300], // strong border
|
|
@@ -270,6 +310,15 @@ export const semanticColorsDark = {
|
|
|
270
310
|
'border-input-focus': '#828DF8',
|
|
271
311
|
'border-input-error': p.red[500],
|
|
272
312
|
|
|
313
|
+
// Alpha hairline separators — the primary separation cue (§4/S-2). Self-
|
|
314
|
+
// normalizing: composites toward white by ~the same amount on ANY plane, so
|
|
315
|
+
// one family works at every elevation instead of per-surface border tokens.
|
|
316
|
+
// Shadows are demoted to floating-overlay use only (not shipped as a fill
|
|
317
|
+
// separator here — see elevation.ts, follow-up S-4).
|
|
318
|
+
'hairline-subtle': 'rgba(255, 255, 255, 0.06)',
|
|
319
|
+
'hairline-default': 'rgba(255, 255, 255, 0.09)',
|
|
320
|
+
'hairline-strong': 'rgba(255, 255, 255, 0.14)',
|
|
321
|
+
|
|
273
322
|
// Interactive states
|
|
274
323
|
'interactive-hover': 'rgba(255, 255, 255, 0.04)',
|
|
275
324
|
'interactive-focus': 'rgba(255, 255, 255, 0.12)',
|