@titan-design/react-ui 0.1.0 → 0.1.1
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/package.json +10 -5
- package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
- package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
- package/src/components/custom/DateTime/DateTime.tsx +235 -0
- package/src/components/custom/DateTime/index.ts +2 -0
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
- package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
- package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
- package/src/components/custom/EmptyState/index.ts +2 -0
- package/src/components/custom/Metric/Metric.stories.tsx +133 -0
- package/src/components/custom/Metric/Metric.test.tsx +120 -0
- package/src/components/custom/Metric/Metric.tsx +92 -0
- package/src/components/custom/Metric/index.ts +2 -0
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
- package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
- package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
- package/src/components/custom/Sidebar/index.ts +18 -0
- package/src/components/custom/Table/Table.stories.tsx +481 -0
- package/src/components/custom/Table/Table.test.tsx +531 -0
- package/src/components/custom/Table/Table.tsx +696 -0
- package/src/components/custom/Table/index.ts +22 -0
- package/src/components/custom/Typography/Typography.stories.tsx +153 -0
- package/src/components/custom/Typography/Typography.test.tsx +107 -0
- package/src/components/custom/Typography/Typography.tsx +218 -0
- package/src/components/custom/Typography/index.ts +19 -0
- package/src/components/custom/index.ts +8 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
- package/src/components/custom/stepper/Stepper.test.tsx +230 -0
- package/src/components/custom/stepper/Stepper.tsx +250 -0
- package/src/components/custom/stepper/index.ts +10 -0
- package/src/components/index.ts +5 -0
- package/src/components/ui/alert/Alert.stories.tsx +154 -0
- package/src/components/ui/alert/Alert.test.tsx +202 -0
- package/src/components/ui/alert/Alert.tsx +170 -0
- package/src/components/ui/alert/index.ts +8 -0
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
- package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
- package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
- package/src/components/ui/autocomplete/index.ts +2 -0
- package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
- package/src/components/ui/avatar/Avatar.test.tsx +135 -0
- package/src/components/ui/avatar/Avatar.tsx +168 -0
- package/src/components/ui/avatar/index.ts +2 -0
- package/src/components/ui/badge/Badge.stories.tsx +100 -0
- package/src/components/ui/badge/Badge.test.tsx +86 -0
- package/src/components/ui/badge/Badge.tsx +105 -0
- package/src/components/ui/badge/index.ts +2 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
- package/src/components/ui/breadcrumbs/index.ts +2 -0
- package/src/components/ui/button/Button.stories.tsx +216 -0
- package/src/components/ui/button/Button.test.tsx +132 -0
- package/src/components/ui/button/Button.tsx +242 -0
- package/src/components/ui/button/index.ts +10 -0
- package/src/components/ui/card/Card.stories.tsx +443 -0
- package/src/components/ui/card/Card.test.tsx +265 -0
- package/src/components/ui/card/Card.tsx +380 -0
- package/src/components/ui/card/index.ts +20 -0
- package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
- package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
- package/src/components/ui/checkbox/Checkbox.tsx +163 -0
- package/src/components/ui/checkbox/index.ts +2 -0
- package/src/components/ui/chip/Chip.stories.tsx +112 -0
- package/src/components/ui/chip/Chip.test.tsx +119 -0
- package/src/components/ui/chip/Chip.tsx +151 -0
- package/src/components/ui/chip/index.ts +2 -0
- package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
- package/src/components/ui/collapse/Collapse.test.tsx +298 -0
- package/src/components/ui/collapse/Collapse.tsx +275 -0
- package/src/components/ui/collapse/index.ts +18 -0
- package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
- package/src/components/ui/data-row/DataRow.test.tsx +78 -0
- package/src/components/ui/data-row/DataRow.tsx +29 -0
- package/src/components/ui/data-row/index.ts +2 -0
- package/src/components/ui/divider/Divider.stories.tsx +71 -0
- package/src/components/ui/divider/Divider.test.tsx +55 -0
- package/src/components/ui/divider/Divider.tsx +37 -0
- package/src/components/ui/divider/index.ts +2 -0
- package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
- package/src/components/ui/drawer/Drawer.test.tsx +142 -0
- package/src/components/ui/drawer/Drawer.tsx +228 -0
- package/src/components/ui/drawer/index.ts +9 -0
- package/src/components/ui/form-field/FormField.stories.tsx +260 -0
- package/src/components/ui/form-field/FormField.test.tsx +280 -0
- package/src/components/ui/form-field/FormField.tsx +308 -0
- package/src/components/ui/form-field/index.ts +2 -0
- package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
- package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
- package/src/components/ui/help-tip/HelpTip.tsx +229 -0
- package/src/components/ui/help-tip/index.ts +2 -0
- package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
- package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
- package/src/components/ui/icon-box/IconBox.tsx +57 -0
- package/src/components/ui/icon-box/index.ts +1 -0
- package/src/components/ui/index.ts +36 -0
- package/src/components/ui/input/Input.stories.tsx +192 -0
- package/src/components/ui/input/Input.test.tsx +86 -0
- package/src/components/ui/input/Input.tsx +273 -0
- package/src/components/ui/input/index.ts +2 -0
- package/src/components/ui/link/Link.stories.tsx +78 -0
- package/src/components/ui/link/Link.test.tsx +94 -0
- package/src/components/ui/link/Link.tsx +86 -0
- package/src/components/ui/link/index.ts +2 -0
- package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
- package/src/components/ui/list-item/ListItem.test.tsx +128 -0
- package/src/components/ui/list-item/ListItem.tsx +89 -0
- package/src/components/ui/list-item/index.ts +14 -0
- package/src/components/ui/menu/Menu.stories.tsx +180 -0
- package/src/components/ui/menu/Menu.test.tsx +353 -0
- package/src/components/ui/menu/Menu.tsx +222 -0
- package/src/components/ui/menu/index.ts +2 -0
- package/src/components/ui/modal/Modal.stories.tsx +350 -0
- package/src/components/ui/modal/Modal.test.tsx +159 -0
- package/src/components/ui/modal/Modal.tsx +254 -0
- package/src/components/ui/modal/index.ts +19 -0
- package/src/components/ui/popover/Popover.stories.tsx +190 -0
- package/src/components/ui/popover/Popover.test.tsx +277 -0
- package/src/components/ui/popover/Popover.tsx +162 -0
- package/src/components/ui/popover/index.ts +2 -0
- package/src/components/ui/progress/Progress.stories.tsx +190 -0
- package/src/components/ui/progress/Progress.test.tsx +181 -0
- package/src/components/ui/progress/Progress.tsx +320 -0
- package/src/components/ui/progress/index.ts +9 -0
- package/src/components/ui/radio/Radio.stories.tsx +199 -0
- package/src/components/ui/radio/Radio.test.tsx +166 -0
- package/src/components/ui/radio/Radio.tsx +200 -0
- package/src/components/ui/radio/index.ts +2 -0
- package/src/components/ui/section/Section.stories.tsx +111 -0
- package/src/components/ui/section/Section.test.tsx +121 -0
- package/src/components/ui/section/Section.tsx +52 -0
- package/src/components/ui/section/index.ts +6 -0
- package/src/components/ui/select/Select.stories.tsx +286 -0
- package/src/components/ui/select/Select.test.tsx +180 -0
- package/src/components/ui/select/Select.tsx +236 -0
- package/src/components/ui/select/index.ts +2 -0
- package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
- package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
- package/src/components/ui/skeleton/Skeleton.tsx +229 -0
- package/src/components/ui/skeleton/index.ts +16 -0
- package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
- package/src/components/ui/spinner/Spinner.test.tsx +67 -0
- package/src/components/ui/spinner/Spinner.tsx +72 -0
- package/src/components/ui/spinner/index.ts +2 -0
- package/src/components/ui/stack/Stack.stories.tsx +127 -0
- package/src/components/ui/stack/Stack.test.tsx +184 -0
- package/src/components/ui/stack/Stack.tsx +76 -0
- package/src/components/ui/stack/index.ts +2 -0
- package/src/components/ui/surface/Surface.stories.tsx +91 -0
- package/src/components/ui/surface/Surface.test.tsx +63 -0
- package/src/components/ui/surface/Surface.tsx +51 -0
- package/src/components/ui/surface/index.ts +1 -0
- package/src/components/ui/switch/Switch.stories.tsx +111 -0
- package/src/components/ui/switch/Switch.test.tsx +112 -0
- package/src/components/ui/switch/Switch.tsx +103 -0
- package/src/components/ui/switch/index.ts +2 -0
- package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
- package/src/components/ui/tabs/Tabs.test.tsx +192 -0
- package/src/components/ui/tabs/Tabs.tsx +253 -0
- package/src/components/ui/tabs/index.ts +16 -0
- package/src/components/ui/toast/Toast.stories.tsx +255 -0
- package/src/components/ui/toast/Toast.test.tsx +207 -0
- package/src/components/ui/toast/Toast.tsx +327 -0
- package/src/components/ui/toast/index.ts +8 -0
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
- package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
- package/src/components/ui/toolbar-button/index.ts +7 -0
- package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
- package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
- package/src/components/ui/tooltip/Tooltip.tsx +143 -0
- package/src/components/ui/tooltip/index.ts +2 -0
- package/src/index.ts +10 -0
- package/src/test/setup.ts +19 -0
- package/src/theme/Colors.stories.tsx +258 -0
- package/src/theme/config.ts +193 -0
- package/src/theme/elevation.stories.tsx +346 -0
- package/src/theme/elevation.test.ts +27 -0
- package/src/theme/elevation.ts +567 -0
- package/src/theme/index.ts +39 -0
- package/src/theme/shadows.stories.tsx +523 -0
- package/src/theme/shadows.ts +439 -0
- package/src/theme/tokens/index.ts +2 -0
- package/src/theme/tokens/primitives.ts +354 -0
- package/src/theme/tokens/semantic.ts +381 -0
- package/src/types/jest-axe.d.ts +16 -0
- package/src/types/nativewind.d.ts +54 -0
- package/src/utils/cn.ts +14 -0
- package/src/utils/colors.ts +140 -0
- package/src/utils/form.ts +188 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/useTheme.ts +72 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Elevation System
|
|
3
|
+
*
|
|
4
|
+
* A unified depth system where each elevation level corresponds to:
|
|
5
|
+
* - Calculated surface color (derived from base using lighten)
|
|
6
|
+
* - Shadow intensity and style (raised/pressed)
|
|
7
|
+
* - Dynamically calculated shadow colors (derived from surface color)
|
|
8
|
+
* - Semantic name for reference
|
|
9
|
+
*
|
|
10
|
+
* Elevation range: -2 (deep inset) to +5 (floating overlay)
|
|
11
|
+
*
|
|
12
|
+
* Negative elevations: Inset/pressed elements (sunken into surface)
|
|
13
|
+
* Zero elevation: Base surface (background/page level)
|
|
14
|
+
* Positive elevations: Raised elements (floating above surface)
|
|
15
|
+
*
|
|
16
|
+
* All colors are calculated dynamically from a base surface color,
|
|
17
|
+
* allowing for custom surfaces and consistent color relationships.
|
|
18
|
+
*
|
|
19
|
+
* Lighting Model: Light source from upper-right corner
|
|
20
|
+
* - Raised elements are always lighter than base (consistent across themes)
|
|
21
|
+
* - Base colors must allow sufficient lightening in both themes
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { Platform, type ViewStyle } from 'react-native'
|
|
25
|
+
import { lighten, darken, hexToRgb, rgbToHsv, hsvToRgb, rgbToHex } from './shadows'
|
|
26
|
+
import { semanticColorsLight, semanticColorsDark } from './tokens/semantic'
|
|
27
|
+
|
|
28
|
+
export type ElevationLevel = -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5
|
|
29
|
+
|
|
30
|
+
export interface ElevationConfig {
|
|
31
|
+
/** Semantic name for this elevation level */
|
|
32
|
+
name: string
|
|
33
|
+
/** Amount to lighten base color (0-1) */
|
|
34
|
+
colorAdjustment: number
|
|
35
|
+
/** Shadow intensity */
|
|
36
|
+
shadowIntensity: 'subtle' | 'medium' | 'strong'
|
|
37
|
+
/** Shadow style: raised (floating) or pressed (inset) */
|
|
38
|
+
shadowStyle: 'raised' | 'pressed'
|
|
39
|
+
/** Description of use case */
|
|
40
|
+
description: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const elevationSystem: Record<ElevationLevel, ElevationConfig> = {
|
|
44
|
+
// Deep inset (recessed input fields, deeply pressed buttons)
|
|
45
|
+
[-2]: {
|
|
46
|
+
name: 'deep-inset',
|
|
47
|
+
colorAdjustment: 0,
|
|
48
|
+
shadowIntensity: 'strong',
|
|
49
|
+
shadowStyle: 'pressed',
|
|
50
|
+
description: 'Deeply recessed elements (e.g., input fields, deeply pressed buttons)',
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Shallow inset (pressed buttons, sunken panels)
|
|
54
|
+
[-1]: {
|
|
55
|
+
name: 'inset',
|
|
56
|
+
colorAdjustment: 0,
|
|
57
|
+
shadowIntensity: 'medium',
|
|
58
|
+
shadowStyle: 'pressed',
|
|
59
|
+
description: 'Shallow inset elements (e.g., pressed buttons, sunken panels within cards)',
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Base level (page background, default surface)
|
|
63
|
+
[0]: {
|
|
64
|
+
name: 'base',
|
|
65
|
+
colorAdjustment: 0,
|
|
66
|
+
shadowIntensity: 'subtle',
|
|
67
|
+
shadowStyle: 'raised',
|
|
68
|
+
description: 'Base surface level (page background, default containers)',
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// Level 1: Subtle elevation (outline cards, subtle containers)
|
|
72
|
+
[1]: {
|
|
73
|
+
name: 'subtle',
|
|
74
|
+
colorAdjustment: 0.01,
|
|
75
|
+
shadowIntensity: 'subtle',
|
|
76
|
+
shadowStyle: 'raised',
|
|
77
|
+
description: 'Subtle elevation (outline cards, subtle containers)',
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Level 2: Standard elevation (default cards, buttons)
|
|
81
|
+
[2]: {
|
|
82
|
+
name: 'standard',
|
|
83
|
+
colorAdjustment: 0.02,
|
|
84
|
+
shadowIntensity: 'medium',
|
|
85
|
+
shadowStyle: 'raised',
|
|
86
|
+
description: 'Standard elevation (default cards, raised buttons)',
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
// Level 3: Prominent elevation (important cards, modals)
|
|
90
|
+
[3]: {
|
|
91
|
+
name: 'prominent',
|
|
92
|
+
colorAdjustment: 0.04,
|
|
93
|
+
shadowIntensity: 'strong',
|
|
94
|
+
shadowStyle: 'raised',
|
|
95
|
+
description: 'Prominent elevation (important cards, modals, dropdowns)',
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// Level 4: High elevation (overlays, popovers)
|
|
99
|
+
[4]: {
|
|
100
|
+
name: 'high',
|
|
101
|
+
colorAdjustment: 0.06,
|
|
102
|
+
shadowIntensity: 'strong',
|
|
103
|
+
shadowStyle: 'raised',
|
|
104
|
+
description: 'High elevation (overlays, popovers, tooltips)',
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// Level 5: Floating (toasts, floating action buttons)
|
|
108
|
+
[5]: {
|
|
109
|
+
name: 'floating',
|
|
110
|
+
colorAdjustment: 0.08,
|
|
111
|
+
shadowIntensity: 'strong',
|
|
112
|
+
shadowStyle: 'raised',
|
|
113
|
+
description: 'Floating elevation (toasts, floating action buttons, highest z-index)',
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Calculate rim light color from the BASE color.
|
|
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
|
|
122
|
+
* consistent light source. Uses ABSOLUTE HSV value (not relative lightening)
|
|
123
|
+
* for consistent intensity across all surface colors.
|
|
124
|
+
*
|
|
125
|
+
* For colored surfaces: preserves hue/saturation, sets value to fixed bright level
|
|
126
|
+
* For gray surfaces: uses white with appropriate opacity
|
|
127
|
+
*
|
|
128
|
+
* @param baseColor - The base surface color (hex)
|
|
129
|
+
* @param level - Elevation level (used to scale opacity)
|
|
130
|
+
*/
|
|
131
|
+
function calculateRimLightColor(baseColor: string, level: ElevationLevel): string {
|
|
132
|
+
const rgb = hexToRgb(baseColor)
|
|
133
|
+
if (!rgb) {
|
|
134
|
+
return 'rgba(255, 255, 255, 0.5)'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b)
|
|
138
|
+
const isLight = hsv.v > 0.6
|
|
139
|
+
const isSaturated = hsv.s > 0.3 // Colored (not gray)
|
|
140
|
+
|
|
141
|
+
// Opacity scales with elevation to compensate for lighter surfaces at higher levels
|
|
142
|
+
// Range: +0% at level 1 to +20% at level 5
|
|
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
|
+
|
|
146
|
+
if (isSaturated) {
|
|
147
|
+
// For colored surfaces, preserve hue and saturation but set value to fixed bright level
|
|
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
|
+
|
|
152
|
+
// Base opacity + elevation boost, capped at 1.0
|
|
153
|
+
const baseOpacity = isLight ? 0.9 : 0.5
|
|
154
|
+
const finalOpacity = Math.min(1, baseOpacity + opacityBoost)
|
|
155
|
+
return `rgba(${rimLightRgb.r}, ${rimLightRgb.g}, ${rimLightRgb.b}, ${finalOpacity.toFixed(2)})`
|
|
156
|
+
} else {
|
|
157
|
+
// For gray/neutral surfaces, use white with appropriate opacity
|
|
158
|
+
const baseOpacity = isLight ? 0.9 : 0.12
|
|
159
|
+
const finalOpacity = Math.min(1, baseOpacity + opacityBoost)
|
|
160
|
+
return `rgba(255, 255, 255, ${finalOpacity.toFixed(2)})`
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Calculate dark shadow colors from the surface color.
|
|
166
|
+
* The dark shadow is cast BY the element, so it should match the surface color.
|
|
167
|
+
*
|
|
168
|
+
* Uses ABSOLUTE HSV value (not relative darkening) for consistent shadow intensity
|
|
169
|
+
* across all surface colors - simulating a consistent light source.
|
|
170
|
+
*
|
|
171
|
+
* For colored surfaces: preserves hue/saturation, sets value to fixed dark level
|
|
172
|
+
* For gray surfaces: uses black with appropriate opacity
|
|
173
|
+
*/
|
|
174
|
+
function calculateDarkShadowColors(surfaceColor: string): {
|
|
175
|
+
dark: string
|
|
176
|
+
darker: string
|
|
177
|
+
} {
|
|
178
|
+
const rgb = hexToRgb(surfaceColor)
|
|
179
|
+
if (!rgb) {
|
|
180
|
+
return {
|
|
181
|
+
dark: 'rgba(0, 0, 0, 0.3)',
|
|
182
|
+
darker: 'rgba(0, 0, 0, 0.4)',
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b)
|
|
187
|
+
const isLight = hsv.v > 0.6
|
|
188
|
+
const isSaturated = hsv.s > 0.3
|
|
189
|
+
|
|
190
|
+
if (isSaturated) {
|
|
191
|
+
// For colored surfaces, preserve hue and saturation but set value to fixed dark level
|
|
192
|
+
// This ensures consistent shadow intensity across all colored surfaces
|
|
193
|
+
const darkShadowValue = 0.15 // Fixed dark value for shadows
|
|
194
|
+
const darkShadowRgb = hsvToRgb(hsv.h, hsv.s, darkShadowValue)
|
|
195
|
+
|
|
196
|
+
if (isLight) {
|
|
197
|
+
return {
|
|
198
|
+
dark: `rgba(${darkShadowRgb.r}, ${darkShadowRgb.g}, ${darkShadowRgb.b}, 0.3)`,
|
|
199
|
+
darker: `rgba(${darkShadowRgb.r}, ${darkShadowRgb.g}, ${darkShadowRgb.b}, 0.4)`,
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
return {
|
|
203
|
+
dark: `rgba(${darkShadowRgb.r}, ${darkShadowRgb.g}, ${darkShadowRgb.b}, 0.5)`,
|
|
204
|
+
darker: `rgba(${darkShadowRgb.r}, ${darkShadowRgb.g}, ${darkShadowRgb.b}, 0.6)`,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
// For gray surfaces, use standard black shadows
|
|
209
|
+
if (isLight) {
|
|
210
|
+
return {
|
|
211
|
+
dark: 'rgba(0, 0, 0, 0.2)',
|
|
212
|
+
darker: 'rgba(0, 0, 0, 0.3)',
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
return {
|
|
216
|
+
dark: 'rgba(0, 0, 0, 0.5)',
|
|
217
|
+
darker: 'rgba(0, 0, 0, 0.6)',
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get rim light size config.
|
|
225
|
+
* Fixed 1px offset and blur for a consistent, crisp rim highlight.
|
|
226
|
+
* Opacity scaling (in calculateRimLightColor) handles elevation differences.
|
|
227
|
+
*/
|
|
228
|
+
function getRimLightConfig(): { offset: number; blur: number } {
|
|
229
|
+
return {
|
|
230
|
+
offset: 1, // Fixed 1px for crisp rim
|
|
231
|
+
blur: 1, // Tight blur for crisp edge
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get elevation configuration for a given level
|
|
237
|
+
*/
|
|
238
|
+
export function getElevationConfig(level: ElevationLevel): ElevationConfig {
|
|
239
|
+
return elevationSystem[level]
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Simple cache for elevation surface colors
|
|
243
|
+
const elevationSurfaceCache = new Map<string, string>()
|
|
244
|
+
const MAX_CACHE_SIZE = 100
|
|
245
|
+
|
|
246
|
+
function getCacheKey(baseColor: string, level: ElevationLevel, theme: 'light' | 'dark'): string {
|
|
247
|
+
return `${baseColor}-${level}-${theme}`
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function calculateElevationSurface(
|
|
251
|
+
baseColor: string,
|
|
252
|
+
level: ElevationLevel,
|
|
253
|
+
theme: 'light' | 'dark'
|
|
254
|
+
): string {
|
|
255
|
+
const config = elevationSystem[level]
|
|
256
|
+
|
|
257
|
+
if (config.colorAdjustment === 0) {
|
|
258
|
+
return baseColor
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// For raised elevations (positive), always lighten (consistent lighting model)
|
|
262
|
+
// Light source is from upper-right, so raised elements are always lighter
|
|
263
|
+
// For inset elevations (negative), we keep base color (shadow creates depth)
|
|
264
|
+
if (level < 0) {
|
|
265
|
+
return baseColor // Inset elements use base color
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Always lighten for raised elements (consistent across themes)
|
|
269
|
+
// Base colors must be chosen to allow sufficient lightening in dark mode
|
|
270
|
+
return lighten(baseColor, config.colorAdjustment)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Calculate surface color for an elevation level from a base color.
|
|
275
|
+
* Results are cached to avoid redundant calculations.
|
|
276
|
+
*
|
|
277
|
+
* @param baseColor - Base surface color (hex)
|
|
278
|
+
* @param level - Elevation level
|
|
279
|
+
* @param theme - Current theme ('light' or 'dark')
|
|
280
|
+
* @returns Calculated surface color (hex)
|
|
281
|
+
*/
|
|
282
|
+
export function getElevationSurface(
|
|
283
|
+
baseColor: string,
|
|
284
|
+
level: ElevationLevel,
|
|
285
|
+
theme: 'light' | 'dark'
|
|
286
|
+
): string {
|
|
287
|
+
const cacheKey = getCacheKey(baseColor, level, theme)
|
|
288
|
+
|
|
289
|
+
// Check cache
|
|
290
|
+
if (elevationSurfaceCache.has(cacheKey)) {
|
|
291
|
+
return elevationSurfaceCache.get(cacheKey)!
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Calculate
|
|
295
|
+
const result = calculateElevationSurface(baseColor, level, theme)
|
|
296
|
+
|
|
297
|
+
// Cache result (with size limit to prevent memory leaks)
|
|
298
|
+
if (elevationSurfaceCache.size >= MAX_CACHE_SIZE) {
|
|
299
|
+
// Remove oldest entry (simple FIFO)
|
|
300
|
+
const firstKey = elevationSurfaceCache.keys().next().value!
|
|
301
|
+
elevationSurfaceCache.delete(firstKey)
|
|
302
|
+
}
|
|
303
|
+
elevationSurfaceCache.set(cacheKey, result)
|
|
304
|
+
|
|
305
|
+
return result
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Cache for shadow styles (less frequently used, smaller cache)
|
|
309
|
+
const elevationShadowCache = new Map<string, ViewStyle>()
|
|
310
|
+
const MAX_SHADOW_CACHE_SIZE = 50
|
|
311
|
+
|
|
312
|
+
function getShadowCacheKey(baseColor: string, level: ElevationLevel, theme: 'light' | 'dark', isHovered: boolean): string {
|
|
313
|
+
return `${baseColor}-${level}-${theme}-${isHovered}`
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function calculateElevationShadow(
|
|
317
|
+
baseColor: string,
|
|
318
|
+
level: ElevationLevel,
|
|
319
|
+
theme: 'light' | 'dark',
|
|
320
|
+
isHovered: boolean = false
|
|
321
|
+
): ViewStyle {
|
|
322
|
+
const config = elevationSystem[level]
|
|
323
|
+
|
|
324
|
+
// Base level has no shadow
|
|
325
|
+
if (level === 0) {
|
|
326
|
+
return {}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Calculate the actual elevated surface color
|
|
330
|
+
const elevatedSurfaceColor = calculateElevationSurface(baseColor, level, theme)
|
|
331
|
+
|
|
332
|
+
// Rim light: calculated from BASE color (consistent across all elevations)
|
|
333
|
+
// This represents a consistent light source - only size changes with elevation
|
|
334
|
+
const rimLightColor = calculateRimLightColor(baseColor, level)
|
|
335
|
+
|
|
336
|
+
// Dark shadow: calculated from ELEVATED surface color
|
|
337
|
+
// The shadow is cast by the element, so it should match the surface
|
|
338
|
+
const darkShadowColors = calculateDarkShadowColors(elevatedSurfaceColor)
|
|
339
|
+
|
|
340
|
+
// Get shadow intensity config (for dark shadow)
|
|
341
|
+
const intensityConfig = {
|
|
342
|
+
subtle: { offset: 1, blur: 2 },
|
|
343
|
+
medium: { offset: 2, blur: 4 },
|
|
344
|
+
strong: { offset: 2, blur: 5 },
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const { offset, blur } = intensityConfig[config.shadowIntensity]
|
|
348
|
+
|
|
349
|
+
// Get rim light config (scales with elevation level)
|
|
350
|
+
const rimConfig = getRimLightConfig()
|
|
351
|
+
|
|
352
|
+
if (config.shadowStyle === 'pressed') {
|
|
353
|
+
// Inset shadow (pressed elements)
|
|
354
|
+
if (isHovered) {
|
|
355
|
+
// Reduce shadow depth for hover
|
|
356
|
+
const hoverOffset = Math.max(1, Math.round(offset / 2))
|
|
357
|
+
const hoverBlur = Math.max(1, Math.round(blur / 2))
|
|
358
|
+
return Platform.select({
|
|
359
|
+
web: {
|
|
360
|
+
boxShadow: `inset ${hoverOffset}px ${hoverOffset}px ${hoverBlur}px ${darkShadowColors.darker}, inset -${rimConfig.offset}px -${rimConfig.offset}px ${rimConfig.blur}px ${rimLightColor}`,
|
|
361
|
+
} as any,
|
|
362
|
+
default: {
|
|
363
|
+
shadowOpacity: 0,
|
|
364
|
+
elevation: 0,
|
|
365
|
+
},
|
|
366
|
+
}) as ViewStyle
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return Platform.select({
|
|
370
|
+
web: {
|
|
371
|
+
boxShadow: `inset ${offset}px ${offset}px ${blur}px ${darkShadowColors.darker}, inset -${rimConfig.offset}px -${rimConfig.offset}px ${rimConfig.blur}px ${rimLightColor}`,
|
|
372
|
+
} as any,
|
|
373
|
+
default: {
|
|
374
|
+
shadowOpacity: 0,
|
|
375
|
+
elevation: 0,
|
|
376
|
+
},
|
|
377
|
+
}) as ViewStyle
|
|
378
|
+
} else {
|
|
379
|
+
// Raised shadow (floating elements)
|
|
380
|
+
if (isHovered) {
|
|
381
|
+
// Reduce shadow depth for hover
|
|
382
|
+
const hoverOffset = Math.max(1, Math.round(offset / 2))
|
|
383
|
+
const hoverBlur = Math.max(1, Math.round(blur / 2))
|
|
384
|
+
return Platform.select({
|
|
385
|
+
web: {
|
|
386
|
+
boxShadow: `${hoverOffset}px ${hoverOffset}px ${hoverBlur}px ${darkShadowColors.dark}, -${rimConfig.offset}px -${rimConfig.offset}px ${rimConfig.blur}px ${rimLightColor}`,
|
|
387
|
+
} as any,
|
|
388
|
+
default: {
|
|
389
|
+
shadowColor: '#000',
|
|
390
|
+
shadowOffset: { width: hoverOffset, height: hoverOffset },
|
|
391
|
+
shadowOpacity: 0.2,
|
|
392
|
+
shadowRadius: hoverBlur / 2,
|
|
393
|
+
elevation: hoverOffset,
|
|
394
|
+
},
|
|
395
|
+
}) as ViewStyle
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return Platform.select({
|
|
399
|
+
web: {
|
|
400
|
+
boxShadow: `${offset}px ${offset}px ${blur}px ${darkShadowColors.dark}, -${rimConfig.offset}px -${rimConfig.offset}px ${rimConfig.blur}px ${rimLightColor}`,
|
|
401
|
+
} as any,
|
|
402
|
+
default: {
|
|
403
|
+
shadowColor: '#000',
|
|
404
|
+
shadowOffset: { width: offset, height: offset },
|
|
405
|
+
shadowOpacity: 0.3,
|
|
406
|
+
shadowRadius: blur / 2,
|
|
407
|
+
elevation: offset,
|
|
408
|
+
},
|
|
409
|
+
}) as ViewStyle
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Get shadow style for an elevation level, calculated dynamically from surface color.
|
|
415
|
+
* Results are cached to avoid redundant calculations.
|
|
416
|
+
*
|
|
417
|
+
* @param baseColor - Base surface color (hex)
|
|
418
|
+
* @param level - Elevation level
|
|
419
|
+
* @param theme - Current theme ('light' or 'dark')
|
|
420
|
+
* @param isHovered - Whether element is hovered
|
|
421
|
+
* @returns ViewStyle with shadow properties
|
|
422
|
+
*/
|
|
423
|
+
export function getElevationShadow(
|
|
424
|
+
baseColor: string,
|
|
425
|
+
level: ElevationLevel,
|
|
426
|
+
theme: 'light' | 'dark',
|
|
427
|
+
isHovered: boolean = false
|
|
428
|
+
): ViewStyle {
|
|
429
|
+
const cacheKey = getShadowCacheKey(baseColor, level, theme, isHovered)
|
|
430
|
+
|
|
431
|
+
// Check cache
|
|
432
|
+
if (elevationShadowCache.has(cacheKey)) {
|
|
433
|
+
return elevationShadowCache.get(cacheKey)!
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Calculate shadow
|
|
437
|
+
const result = calculateElevationShadow(baseColor, level, theme, isHovered)
|
|
438
|
+
|
|
439
|
+
// Cache result
|
|
440
|
+
if (elevationShadowCache.size >= MAX_SHADOW_CACHE_SIZE) {
|
|
441
|
+
const firstKey = elevationShadowCache.keys().next().value!
|
|
442
|
+
elevationShadowCache.delete(firstKey)
|
|
443
|
+
}
|
|
444
|
+
elevationShadowCache.set(cacheKey, result)
|
|
445
|
+
|
|
446
|
+
return result
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Get base surface color from theme.
|
|
451
|
+
* This is the foundation color that all elevations are calculated from.
|
|
452
|
+
*
|
|
453
|
+
* IMPORTANT: Base colors must be chosen to allow sufficient lightening.
|
|
454
|
+
*
|
|
455
|
+
* **Light Mode**: Cannot use `#FFFFFF` (white) - cannot lighten white!
|
|
456
|
+
* - Use `#F3F4F6` (neutral[100]) - can lighten to `#FAFAFA` → `#FFFFFF`
|
|
457
|
+
* - This allows raised elevations to be lighter (closer to white)
|
|
458
|
+
*
|
|
459
|
+
* **Dark Mode**: Need lighter base to allow sufficient lightening
|
|
460
|
+
* - Use `#191919` (charcoal[600]) - allows lightening to `#2A2A2A` at max elevation
|
|
461
|
+
*
|
|
462
|
+
* Can be overridden to use custom base colors for special cases.
|
|
463
|
+
*/
|
|
464
|
+
export function getBaseSurfaceColor(theme: 'light' | 'dark'): string {
|
|
465
|
+
// Use semantic token values so elevation colors stay in sync with the token pipeline.
|
|
466
|
+
// 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
|
|
468
|
+
|
|
469
|
+
return theme === 'light'
|
|
470
|
+
? semanticColorsLight['surface-raised']
|
|
471
|
+
: semanticColorsDark['surface-elevated']
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Component-specific elevation ranges
|
|
476
|
+
* Defines which elevation levels each component type can use
|
|
477
|
+
*/
|
|
478
|
+
export const componentElevationRanges = {
|
|
479
|
+
button: {
|
|
480
|
+
allowed: [2] as const,
|
|
481
|
+
default: 2 as const,
|
|
482
|
+
hover: 3 as const,
|
|
483
|
+
},
|
|
484
|
+
card: {
|
|
485
|
+
allowed: [1, 2, 3] as const,
|
|
486
|
+
default: 2 as const,
|
|
487
|
+
hover: 3 as const,
|
|
488
|
+
},
|
|
489
|
+
input: {
|
|
490
|
+
allowed: [-2, -1, 0] as const,
|
|
491
|
+
default: -1 as const,
|
|
492
|
+
},
|
|
493
|
+
overlay: {
|
|
494
|
+
allowed: [4, 5] as const,
|
|
495
|
+
default: 4 as const,
|
|
496
|
+
},
|
|
497
|
+
} as const
|
|
498
|
+
|
|
499
|
+
export type ComponentType = keyof typeof componentElevationRanges
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Validate and clamp elevation to component's allowed range
|
|
503
|
+
*/
|
|
504
|
+
export function getValidatedElevation(
|
|
505
|
+
component: ComponentType,
|
|
506
|
+
requested: ElevationLevel
|
|
507
|
+
): ElevationLevel {
|
|
508
|
+
const range = componentElevationRanges[component]
|
|
509
|
+
if ((range.allowed as readonly ElevationLevel[]).includes(requested)) {
|
|
510
|
+
return requested
|
|
511
|
+
}
|
|
512
|
+
// Clamp to nearest allowed elevation
|
|
513
|
+
const sorted = [...range.allowed].sort((a, b) => a - b)
|
|
514
|
+
if (requested < sorted[0]) return sorted[0]
|
|
515
|
+
if (requested > sorted[sorted.length - 1]) return sorted[sorted.length - 1]
|
|
516
|
+
// Find closest
|
|
517
|
+
return sorted.reduce((prev, curr) =>
|
|
518
|
+
Math.abs(curr - requested) < Math.abs(prev - requested) ? curr : prev
|
|
519
|
+
) as ElevationLevel
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// =============================================================================
|
|
523
|
+
// Glow Shadow Support
|
|
524
|
+
// =============================================================================
|
|
525
|
+
|
|
526
|
+
export type GlowIntensity = 'subtle' | 'medium' | 'strong'
|
|
527
|
+
|
|
528
|
+
const glowConfig: Record<GlowIntensity, { blur: number; spread: number; opacity: number }> = {
|
|
529
|
+
subtle: { blur: 12, spread: 0, opacity: 0.25 },
|
|
530
|
+
medium: { blur: 20, spread: 2, opacity: 0.4 },
|
|
531
|
+
strong: { blur: 30, spread: 4, opacity: 0.55 },
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Get a glow shadow style for emphasis/active states.
|
|
536
|
+
*
|
|
537
|
+
* Creates a colored radial glow around an element, useful for:
|
|
538
|
+
* - Active recording indicators (orange glow)
|
|
539
|
+
* - Success states (green glow)
|
|
540
|
+
* - Error/danger indicators (red glow)
|
|
541
|
+
* - Focus rings and attention-drawing elements
|
|
542
|
+
*
|
|
543
|
+
* @param color - Glow color (hex string, e.g. '#FF7900')
|
|
544
|
+
* @param intensity - Glow intensity level
|
|
545
|
+
* @returns ViewStyle with platform-specific shadow properties
|
|
546
|
+
*/
|
|
547
|
+
export function getGlowShadow(
|
|
548
|
+
color: string,
|
|
549
|
+
intensity: GlowIntensity = 'medium'
|
|
550
|
+
): ViewStyle {
|
|
551
|
+
const config = glowConfig[intensity]
|
|
552
|
+
const rgb = hexToRgb(color)
|
|
553
|
+
if (!rgb) return {}
|
|
554
|
+
|
|
555
|
+
return Platform.select({
|
|
556
|
+
web: {
|
|
557
|
+
boxShadow: `0 0 ${config.blur}px ${config.spread}px rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${config.opacity})`,
|
|
558
|
+
} as any,
|
|
559
|
+
default: {
|
|
560
|
+
shadowColor: color,
|
|
561
|
+
shadowOffset: { width: 0, height: 0 },
|
|
562
|
+
shadowOpacity: config.opacity,
|
|
563
|
+
shadowRadius: config.blur / 2,
|
|
564
|
+
elevation: 0,
|
|
565
|
+
},
|
|
566
|
+
}) as ViewStyle
|
|
567
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Theme exports
|
|
2
|
+
export * from './tokens'
|
|
3
|
+
export * from './config'
|
|
4
|
+
export * from './shadows'
|
|
5
|
+
export * from './elevation'
|
|
6
|
+
|
|
7
|
+
// Re-export commonly used items
|
|
8
|
+
export { getSemanticColors, type ThemeMode } from './tokens/semantic'
|
|
9
|
+
export { getThemeCSSVars, gluestackConfig } from './config'
|
|
10
|
+
export {
|
|
11
|
+
neumorphicShadows,
|
|
12
|
+
// Color math utilities (HSV-based)
|
|
13
|
+
lighten,
|
|
14
|
+
darken,
|
|
15
|
+
getHoverColors,
|
|
16
|
+
isDark,
|
|
17
|
+
hexToRgb,
|
|
18
|
+
rgbToHsv,
|
|
19
|
+
// Shadow creation functions
|
|
20
|
+
createRaisedShadow,
|
|
21
|
+
createRaisedHoverShadow,
|
|
22
|
+
createPressedShadow,
|
|
23
|
+
createPressedHoverShadow,
|
|
24
|
+
createFlatShadow,
|
|
25
|
+
shadowColors,
|
|
26
|
+
type ShadowIntensity,
|
|
27
|
+
type ShadowSurface,
|
|
28
|
+
} from './shadows'
|
|
29
|
+
export {
|
|
30
|
+
getElevationConfig,
|
|
31
|
+
getElevationSurface,
|
|
32
|
+
getElevationShadow,
|
|
33
|
+
getBaseSurfaceColor,
|
|
34
|
+
getValidatedElevation,
|
|
35
|
+
componentElevationRanges,
|
|
36
|
+
type ElevationLevel,
|
|
37
|
+
type ElevationConfig,
|
|
38
|
+
type ComponentType,
|
|
39
|
+
} from './elevation'
|