@retray-dev/ui-kit 4.0.0 → 5.2.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/COMPONENTS.md +1806 -663
- package/README.md +14 -10
- package/dist/index.d.mts +274 -85
- package/dist/index.d.ts +274 -85
- package/dist/index.js +1048 -321
- package/dist/index.mjs +1046 -324
- package/package.json +3 -2
- package/src/components/Accordion/Accordion.tsx +1 -1
- package/src/components/AlertBanner/AlertBanner.tsx +50 -45
- package/src/components/Avatar/Avatar.tsx +61 -17
- package/src/components/Badge/Badge.tsx +17 -15
- package/src/components/Button/Button.tsx +31 -42
- package/src/components/Card/Card.tsx +4 -4
- package/src/components/CategoryStrip/CategoryStrip.tsx +185 -0
- package/src/components/CategoryStrip/index.ts +2 -0
- package/src/components/Checkbox/Checkbox.tsx +44 -16
- package/src/components/Chip/Chip.tsx +1 -1
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +9 -9
- package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +1 -0
- package/src/components/CurrencyInput/CurrencyInput.tsx +6 -4
- package/src/components/EmptyState/EmptyState.tsx +9 -9
- package/src/components/IconButton/IconButton.tsx +74 -34
- package/src/components/Input/Input.tsx +15 -13
- package/src/components/LabelValue/LabelValue.tsx +1 -1
- package/src/components/ListItem/ListItem.tsx +5 -5
- package/src/components/MediaCard/MediaCard.tsx +249 -0
- package/src/components/MediaCard/index.ts +2 -0
- package/src/components/Pressable/Pressable.tsx +100 -0
- package/src/components/Pressable/index.ts +1 -0
- package/src/components/Progress/Progress.tsx +14 -7
- package/src/components/RadioGroup/RadioGroup.tsx +1 -1
- package/src/components/Select/Select.tsx +5 -5
- package/src/components/Sheet/Sheet.tsx +35 -15
- package/src/components/Skeleton/Skeleton.tsx +34 -7
- package/src/components/Slider/Slider.tsx +2 -2
- package/src/components/Spinner/Spinner.tsx +1 -1
- package/src/components/Switch/Switch.tsx +31 -4
- package/src/components/Tabs/Tabs.tsx +63 -45
- package/src/components/Text/Text.tsx +59 -10
- package/src/components/Textarea/Textarea.tsx +4 -3
- package/src/components/Toast/Toast.tsx +77 -36
- package/src/components/Toggle/Toggle.tsx +3 -3
- package/src/index.ts +8 -2
- package/src/theme/ThemeProvider.tsx +11 -10
- package/src/theme/colorUtils.ts +80 -0
- package/src/theme/colors.ts +76 -35
- package/src/theme/index.ts +2 -2
- package/src/theme/types.ts +27 -13
- package/src/tokens.ts +150 -13
- package/src/utils/hover.ts +25 -0
|
@@ -16,7 +16,12 @@ import { useTheme } from '../../theme'
|
|
|
16
16
|
import { s, vs, ms } from '../../utils/scaling'
|
|
17
17
|
import { renderIcon } from '../../utils/icons'
|
|
18
18
|
|
|
19
|
-
export type ToastVariant = 'default' | 'destructive' | 'success'
|
|
19
|
+
export type ToastVariant = 'default' | 'destructive' | 'success' | 'warning'
|
|
20
|
+
|
|
21
|
+
export interface ToastAction {
|
|
22
|
+
label: string
|
|
23
|
+
onPress: () => void
|
|
24
|
+
}
|
|
20
25
|
|
|
21
26
|
export interface ToastItem {
|
|
22
27
|
id: string
|
|
@@ -24,15 +29,12 @@ export interface ToastItem {
|
|
|
24
29
|
description?: string
|
|
25
30
|
variant?: ToastVariant
|
|
26
31
|
icon?: React.ReactNode
|
|
27
|
-
/**
|
|
28
|
-
* Icon name from `@expo/vector-icons`. See https://icons.expo.fyi.
|
|
29
|
-
* Takes precedence over `icon`. When neither is set, a default variant icon is shown.
|
|
30
|
-
*/
|
|
31
32
|
iconName?: string
|
|
32
|
-
/** Override the resolved icon color. Defaults to the variant text color. */
|
|
33
33
|
iconColor?: string
|
|
34
34
|
/** Auto-dismiss delay in milliseconds. Defaults to `3000`. */
|
|
35
35
|
duration?: number
|
|
36
|
+
/** Optional inline action button rendered at the end of the toast. */
|
|
37
|
+
action?: ToastAction
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
interface ToastContextValue {
|
|
@@ -99,30 +101,42 @@ function ToastNotification({ item, onDismiss }: { item: ToastItem; onDismiss: ()
|
|
|
99
101
|
const variant = item.variant ?? 'default'
|
|
100
102
|
|
|
101
103
|
const bgColor = {
|
|
102
|
-
default:
|
|
104
|
+
default: colors.card,
|
|
105
|
+
destructive: colors.destructiveTint,
|
|
106
|
+
success: colors.successTint,
|
|
107
|
+
warning: colors.warningTint,
|
|
108
|
+
}[variant]
|
|
109
|
+
|
|
110
|
+
const borderColor = {
|
|
111
|
+
default: colors.border,
|
|
103
112
|
destructive: colors.destructiveBorder,
|
|
104
|
-
success:
|
|
113
|
+
success: colors.successBorder,
|
|
114
|
+
warning: colors.warningBorder,
|
|
105
115
|
}[variant]
|
|
106
116
|
|
|
107
|
-
const
|
|
108
|
-
default:
|
|
109
|
-
destructive:
|
|
110
|
-
success:
|
|
117
|
+
const accentColor = {
|
|
118
|
+
default: colors.primary,
|
|
119
|
+
destructive: colors.destructive,
|
|
120
|
+
success: colors.success,
|
|
121
|
+
warning: colors.warning,
|
|
111
122
|
}[variant]
|
|
112
123
|
|
|
113
|
-
const
|
|
124
|
+
const titleColor = variant === 'default' ? colors.foreground : accentColor
|
|
125
|
+
const descColor = variant === 'default' ? colors.foregroundMuted : accentColor
|
|
114
126
|
|
|
115
127
|
const defaultIcon =
|
|
116
128
|
variant === 'success' ? (
|
|
117
|
-
<FontAwesome5 name="check-circle" size={
|
|
129
|
+
<FontAwesome5 name="check-circle" size={16} color={accentColor} />
|
|
118
130
|
) : variant === 'destructive' ? (
|
|
119
|
-
<AntDesign name="exclamation-circle" size={
|
|
131
|
+
<AntDesign name="exclamation-circle" size={16} color={accentColor} />
|
|
132
|
+
) : variant === 'warning' ? (
|
|
133
|
+
<MaterialIcons name="warning-amber" size={17} color={accentColor} />
|
|
120
134
|
) : (
|
|
121
|
-
<Entypo name="info-with-circle" size={
|
|
135
|
+
<Entypo name="info-with-circle" size={16} color={accentColor} />
|
|
122
136
|
)
|
|
123
137
|
|
|
124
138
|
const leftIcon: React.ReactNode = item.iconName
|
|
125
|
-
? renderIcon(item.iconName,
|
|
139
|
+
? renderIcon(item.iconName, 16, item.iconColor ?? accentColor)
|
|
126
140
|
: item.icon ?? defaultIcon
|
|
127
141
|
|
|
128
142
|
return (
|
|
@@ -131,16 +145,27 @@ function ToastNotification({ item, onDismiss }: { item: ToastItem; onDismiss: ()
|
|
|
131
145
|
<View style={styles.leftIconContainer}>{leftIcon}</View>
|
|
132
146
|
<View style={styles.toastContent}>
|
|
133
147
|
{item.title ? (
|
|
134
|
-
<Text style={[styles.toastTitle, { color:
|
|
148
|
+
<Text style={[styles.toastTitle, { color: titleColor }]} allowFontScaling={true}>{item.title}</Text>
|
|
135
149
|
) : null}
|
|
136
150
|
{item.description ? (
|
|
137
|
-
<Text style={[styles.toastDescription, { color:
|
|
151
|
+
<Text style={[styles.toastDescription, { color: descColor }]} allowFontScaling={true}>
|
|
138
152
|
{item.description}
|
|
139
153
|
</Text>
|
|
140
154
|
) : null}
|
|
141
155
|
</View>
|
|
156
|
+
{item.action && (
|
|
157
|
+
<TouchableOpacity
|
|
158
|
+
onPress={() => { item.action!.onPress(); onDismiss() }}
|
|
159
|
+
style={styles.actionButton}
|
|
160
|
+
touchSoundDisabled={true}
|
|
161
|
+
>
|
|
162
|
+
<Text style={[styles.actionLabel, { color: accentColor }]} allowFontScaling={true}>
|
|
163
|
+
{item.action.label}
|
|
164
|
+
</Text>
|
|
165
|
+
</TouchableOpacity>
|
|
166
|
+
)}
|
|
142
167
|
<TouchableOpacity onPress={onDismiss} style={styles.dismissButton} touchSoundDisabled={true}>
|
|
143
|
-
<AntDesign name="close-circle" size={
|
|
168
|
+
<AntDesign name="close-circle" size={16} color={descColor} />
|
|
144
169
|
</TouchableOpacity>
|
|
145
170
|
</Animated.View>
|
|
146
171
|
</GestureDetector>
|
|
@@ -166,6 +191,8 @@ export function ToastProvider({ children }: ToastProviderProps) {
|
|
|
166
191
|
notificationSuccess()
|
|
167
192
|
} else if (item.variant === 'destructive') {
|
|
168
193
|
notificationError()
|
|
194
|
+
} else if (item.variant === 'warning') {
|
|
195
|
+
notificationError()
|
|
169
196
|
} else {
|
|
170
197
|
impactLight()
|
|
171
198
|
}
|
|
@@ -204,37 +231,51 @@ const styles = StyleSheet.create({
|
|
|
204
231
|
},
|
|
205
232
|
toast: {
|
|
206
233
|
flexDirection: 'row',
|
|
207
|
-
alignItems: '
|
|
208
|
-
borderRadius: ms(
|
|
209
|
-
borderWidth:
|
|
210
|
-
paddingHorizontal: s(
|
|
211
|
-
paddingVertical: vs(
|
|
234
|
+
alignItems: 'flex-start',
|
|
235
|
+
borderRadius: ms(10),
|
|
236
|
+
borderWidth: 0.5,
|
|
237
|
+
paddingHorizontal: s(12),
|
|
238
|
+
paddingVertical: vs(10),
|
|
212
239
|
shadowColor: '#000',
|
|
213
|
-
shadowOffset: { width: 0, height:
|
|
214
|
-
shadowOpacity: 0.
|
|
215
|
-
shadowRadius:
|
|
216
|
-
elevation:
|
|
240
|
+
shadowOffset: { width: 0, height: 2 },
|
|
241
|
+
shadowOpacity: 0.06,
|
|
242
|
+
shadowRadius: 4,
|
|
243
|
+
elevation: 3,
|
|
217
244
|
},
|
|
218
245
|
toastContent: {
|
|
219
246
|
flex: 1,
|
|
220
|
-
gap: vs(
|
|
247
|
+
gap: vs(2),
|
|
221
248
|
},
|
|
222
249
|
leftIconContainer: {
|
|
223
|
-
|
|
250
|
+
marginTop: vs(1),
|
|
224
251
|
alignItems: 'center',
|
|
225
252
|
justifyContent: 'center',
|
|
226
253
|
marginRight: s(10),
|
|
227
254
|
},
|
|
228
255
|
toastTitle: {
|
|
229
|
-
fontFamily: 'Poppins-
|
|
230
|
-
fontSize: ms(
|
|
256
|
+
fontFamily: 'Poppins-Medium',
|
|
257
|
+
fontSize: ms(13),
|
|
258
|
+
lineHeight: ms(18),
|
|
231
259
|
},
|
|
232
260
|
toastDescription: {
|
|
233
261
|
fontFamily: 'Poppins-Regular',
|
|
234
|
-
fontSize: ms(
|
|
262
|
+
fontSize: ms(12),
|
|
263
|
+
lineHeight: ms(17),
|
|
264
|
+
opacity: 0.85,
|
|
235
265
|
},
|
|
236
|
-
|
|
237
|
-
|
|
266
|
+
actionButton: {
|
|
267
|
+
paddingHorizontal: s(8),
|
|
268
|
+
paddingVertical: vs(4),
|
|
238
269
|
marginLeft: s(4),
|
|
239
270
|
},
|
|
271
|
+
actionLabel: {
|
|
272
|
+
fontFamily: 'Poppins-Medium',
|
|
273
|
+
fontSize: ms(12),
|
|
274
|
+
textDecorationLine: 'underline',
|
|
275
|
+
},
|
|
276
|
+
dismissButton: {
|
|
277
|
+
padding: s(6),
|
|
278
|
+
marginLeft: s(2),
|
|
279
|
+
marginTop: vs(0),
|
|
280
|
+
},
|
|
240
281
|
})
|
|
@@ -93,7 +93,7 @@ export function Toggle({
|
|
|
93
93
|
|
|
94
94
|
const backgroundColor = pressAnim.interpolate({
|
|
95
95
|
inputRange: [0, 1],
|
|
96
|
-
outputRange: ['transparent', colors.
|
|
96
|
+
outputRange: ['transparent', colors.surfaceStrong],
|
|
97
97
|
})
|
|
98
98
|
|
|
99
99
|
const textColor = pressAnim.interpolate({
|
|
@@ -117,12 +117,12 @@ export function Toggle({
|
|
|
117
117
|
return <FontAwesome5 name="check-circle" size={iconSize} color={colors.primary} />
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
if (iconName) return <>{renderIcon(iconName, iconSize, iconColor ?? colors.
|
|
120
|
+
if (iconName) return <>{renderIcon(iconName, iconSize, iconColor ?? colors.foregroundMuted)}</>
|
|
121
121
|
const custom = renderProp(icon)
|
|
122
122
|
if (custom) return <>{custom}</>
|
|
123
123
|
|
|
124
124
|
// Default: empty circle to signal an action is available
|
|
125
|
-
return <FontAwesome5 name="circle" size={iconSize} color={colors.
|
|
125
|
+
return <FontAwesome5 name="circle" size={iconSize} color={colors.foregroundMuted} />
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
return (
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Theme
|
|
2
2
|
export { ThemeProvider, useTheme } from './theme'
|
|
3
|
-
export type { ThemeProviderProps, ThemeColors, Theme, ColorScheme } from './theme'
|
|
4
|
-
export { defaultLight, defaultDark } from './theme'
|
|
3
|
+
export type { ThemeProviderProps, ThemeColors, ResolvedColors, Theme, ColorScheme } from './theme'
|
|
4
|
+
export { defaultLight, defaultDark, deriveColors } from './theme'
|
|
5
5
|
|
|
6
6
|
// Components
|
|
7
7
|
export * from './components/Button'
|
|
@@ -37,6 +37,9 @@ export * from './components/Chip'
|
|
|
37
37
|
export * from './components/ConfirmDialog'
|
|
38
38
|
export * from './components/LabelValue'
|
|
39
39
|
export * from './components/MonthPicker'
|
|
40
|
+
export * from './components/MediaCard'
|
|
41
|
+
export * from './components/CategoryStrip'
|
|
42
|
+
export * from './components/Pressable'
|
|
40
43
|
|
|
41
44
|
// Icon utility
|
|
42
45
|
export { Icon, renderIcon } from './utils/icons'
|
|
@@ -49,6 +52,7 @@ export {
|
|
|
49
52
|
RADIUS,
|
|
50
53
|
SHADOWS,
|
|
51
54
|
BREAKPOINTS,
|
|
55
|
+
TYPOGRAPHY,
|
|
52
56
|
} from './tokens'
|
|
53
57
|
export type {
|
|
54
58
|
Spacing,
|
|
@@ -57,4 +61,6 @@ export type {
|
|
|
57
61
|
IconSizeKey,
|
|
58
62
|
Radius,
|
|
59
63
|
RadiusKey,
|
|
64
|
+
Typography,
|
|
65
|
+
TypographyKey,
|
|
60
66
|
} from './tokens'
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import React, { createContext, useContext, useMemo } from 'react'
|
|
2
2
|
import { useColorScheme } from 'react-native'
|
|
3
3
|
import { ThemeColors, Theme, ColorScheme, ThemeContextValue } from './types'
|
|
4
|
-
import { defaultLight, defaultDark } from './colors'
|
|
4
|
+
import { defaultLight, defaultDark, deriveColors } from './colors'
|
|
5
5
|
|
|
6
6
|
const ThemeContext = createContext<ThemeContextValue>({
|
|
7
|
-
colors: defaultLight,
|
|
7
|
+
colors: deriveColors(defaultLight, 'light'),
|
|
8
8
|
colorScheme: 'light',
|
|
9
9
|
})
|
|
10
10
|
|
|
11
11
|
export interface ThemeProviderProps {
|
|
12
12
|
children: React.ReactNode
|
|
13
13
|
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
* Optional token overrides per scheme. Supply any of the 12 public ThemeColors
|
|
15
|
+
* tokens for `light` and/or `dark`. All derived colors are recomputed automatically.
|
|
16
|
+
* @example
|
|
17
|
+
* { light: { primary: '#ff385c' }, dark: { primary: '#ff385c' } }
|
|
18
18
|
*/
|
|
19
19
|
theme?: Theme
|
|
20
20
|
/**
|
|
21
|
-
* - `'system'` (default): auto-detects device setting
|
|
22
|
-
* - `'light'` / `'dark'`: forces a specific scheme
|
|
21
|
+
* - `'system'` (default): auto-detects device setting.
|
|
22
|
+
* - `'light'` / `'dark'`: forces a specific scheme.
|
|
23
23
|
*/
|
|
24
24
|
colorScheme?: ColorScheme
|
|
25
25
|
}
|
|
@@ -28,10 +28,11 @@ export function ThemeProvider({ children, theme, colorScheme = 'system' }: Theme
|
|
|
28
28
|
const systemScheme = useColorScheme() ?? 'light'
|
|
29
29
|
const resolvedScheme: 'light' | 'dark' = colorScheme === 'system' ? systemScheme : colorScheme
|
|
30
30
|
|
|
31
|
-
const colors = useMemo
|
|
31
|
+
const colors = useMemo(() => {
|
|
32
32
|
const base = resolvedScheme === 'dark' ? defaultDark : defaultLight
|
|
33
33
|
const override = resolvedScheme === 'dark' ? theme?.dark : theme?.light
|
|
34
|
-
|
|
34
|
+
const merged: ThemeColors = override ? { ...base, ...override } : base
|
|
35
|
+
return deriveColors(merged, resolvedScheme)
|
|
35
36
|
}, [resolvedScheme, theme])
|
|
36
37
|
|
|
37
38
|
return (
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Hex color manipulation utilities for internal theme derivation.
|
|
2
|
+
// All functions are pure — no side effects, no React dependencies.
|
|
3
|
+
|
|
4
|
+
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
|
5
|
+
const clean = hex.replace('#', '')
|
|
6
|
+
const full = clean.length === 3
|
|
7
|
+
? clean.split('').map(c => c + c).join('')
|
|
8
|
+
: clean
|
|
9
|
+
if (full.length !== 6) return null
|
|
10
|
+
return {
|
|
11
|
+
r: parseInt(full.slice(0, 2), 16),
|
|
12
|
+
g: parseInt(full.slice(2, 4), 16),
|
|
13
|
+
b: parseInt(full.slice(4, 6), 16),
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function componentToHex(c: number): string {
|
|
18
|
+
return Math.round(Math.max(0, Math.min(255, c))).toString(16).padStart(2, '0')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function rgbToHex(r: number, g: number, b: number): string {
|
|
22
|
+
return `#${componentToHex(r)}${componentToHex(g)}${componentToHex(b)}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Returns hex color with alpha blended onto a white background (for tint derivation)
|
|
26
|
+
export function withAlphaOnWhite(hex: string, alpha: number): string {
|
|
27
|
+
const rgb = hexToRgb(hex)
|
|
28
|
+
if (!rgb) return hex
|
|
29
|
+
const r = rgb.r * alpha + 255 * (1 - alpha)
|
|
30
|
+
const g = rgb.g * alpha + 255 * (1 - alpha)
|
|
31
|
+
const b = rgb.b * alpha + 255 * (1 - alpha)
|
|
32
|
+
return rgbToHex(r, g, b)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Returns hex color with alpha blended onto a dark background (for dark mode tints)
|
|
36
|
+
export function withAlphaOnDark(hex: string, alpha: number, bgHex = '#0f0f0f'): string {
|
|
37
|
+
const rgb = hexToRgb(hex)
|
|
38
|
+
const bg = hexToRgb(bgHex)
|
|
39
|
+
if (!rgb || !bg) return hex
|
|
40
|
+
const r = rgb.r * alpha + bg.r * (1 - alpha)
|
|
41
|
+
const g = rgb.g * alpha + bg.g * (1 - alpha)
|
|
42
|
+
const b = rgb.b * alpha + bg.b * (1 - alpha)
|
|
43
|
+
return rgbToHex(r, g, b)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Mix foreground color with background at given opacity (for text hierarchy)
|
|
47
|
+
export function mixWithBackground(fgHex: string, bgHex: string, opacity: number): string {
|
|
48
|
+
const fg = hexToRgb(fgHex)
|
|
49
|
+
const bg = hexToRgb(bgHex)
|
|
50
|
+
if (!fg || !bg) return fgHex
|
|
51
|
+
const r = fg.r * opacity + bg.r * (1 - opacity)
|
|
52
|
+
const g = fg.g * opacity + bg.g * (1 - opacity)
|
|
53
|
+
const b = fg.b * opacity + bg.b * (1 - opacity)
|
|
54
|
+
return rgbToHex(r, g, b)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Lighten a hex color by mixing with white
|
|
58
|
+
export function lighten(hex: string, amount: number): string {
|
|
59
|
+
return withAlphaOnWhite(hex, 1 - amount)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Darken a hex color by mixing with black
|
|
63
|
+
export function darken(hex: string, amount: number): string {
|
|
64
|
+
const rgb = hexToRgb(hex)
|
|
65
|
+
if (!rgb) return hex
|
|
66
|
+
return rgbToHex(rgb.r * (1 - amount), rgb.g * (1 - amount), rgb.b * (1 - amount))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Detect if a hex color is "dark" (luminance < 0.5)
|
|
70
|
+
export function isDark(hex: string): boolean {
|
|
71
|
+
const rgb = hexToRgb(hex)
|
|
72
|
+
if (!rgb) return false
|
|
73
|
+
// Relative luminance (WCAG formula)
|
|
74
|
+
const toLinear = (c: number) => {
|
|
75
|
+
const s = c / 255
|
|
76
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4)
|
|
77
|
+
}
|
|
78
|
+
const L = 0.2126 * toLinear(rgb.r) + 0.7152 * toLinear(rgb.g) + 0.0722 * toLinear(rgb.b)
|
|
79
|
+
return L < 0.5
|
|
80
|
+
}
|
package/src/theme/colors.ts
CHANGED
|
@@ -1,54 +1,95 @@
|
|
|
1
|
-
import { ThemeColors } from './types'
|
|
1
|
+
import { ThemeColors, ResolvedColors } from './types'
|
|
2
|
+
import { mixWithBackground, withAlphaOnWhite, withAlphaOnDark, lighten, darken, isDark } from './colorUtils'
|
|
3
|
+
|
|
4
|
+
// ─── Default palettes (12 tokens each) ───────────────────────────────────────
|
|
5
|
+
// These are the only values consumers need to override.
|
|
6
|
+
// Designed to look great out of the box with no customization.
|
|
2
7
|
|
|
3
|
-
// Full, explicit theme palettes. No derivation — palettes are direct and fully customizable.
|
|
4
8
|
export const defaultLight: ThemeColors = {
|
|
5
9
|
background: '#ffffff',
|
|
6
|
-
foreground: '#
|
|
10
|
+
foreground: '#222222', // Airbnb ink — deep near-black, never pure black
|
|
7
11
|
card: '#ffffff',
|
|
8
|
-
|
|
9
|
-
primary: '#1a1a1a',
|
|
12
|
+
primary: '#1a1a1a', // Near-black primary — clean, premium default
|
|
10
13
|
primaryForeground: '#ffffff',
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
muted: '#f1f1f1',
|
|
14
|
-
mutedForeground: '#a2a2a2',
|
|
15
|
-
accent: '#e4e4e4',
|
|
16
|
-
accentForeground: '#171717',
|
|
17
|
-
destructive: '#ef4444',
|
|
14
|
+
border: '#dddddd', // Airbnb hairline — light, airy
|
|
15
|
+
destructive: '#e53935',
|
|
18
16
|
destructiveForeground: '#ffffff',
|
|
19
|
-
border: '#e5e5e5',
|
|
20
|
-
input: '#e5e5e5',
|
|
21
|
-
ring: '#1a1a1a',
|
|
22
17
|
success: '#1a7a45',
|
|
23
18
|
successForeground: '#ffffff',
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
successTint: '#f0fdf4',
|
|
27
|
-
successBorder: '#bbf7d0',
|
|
19
|
+
warning: '#e67e00',
|
|
20
|
+
warningForeground: '#ffffff',
|
|
28
21
|
}
|
|
29
22
|
|
|
30
23
|
export const defaultDark: ThemeColors = {
|
|
31
24
|
background: '#0f0f0f',
|
|
32
25
|
foreground: '#fafafa',
|
|
33
26
|
card: '#1c1c1c',
|
|
34
|
-
cardForeground: '#fafafa',
|
|
35
27
|
primary: '#fafafa',
|
|
36
28
|
primaryForeground: '#0f0f0f',
|
|
37
|
-
secondary: '#272727',
|
|
38
|
-
secondaryForeground: '#fafafa',
|
|
39
|
-
muted: '#272727',
|
|
40
|
-
mutedForeground: '#9a9a9a',
|
|
41
|
-
accent: '#2e2e2e',
|
|
42
|
-
accentForeground: '#fafafa',
|
|
43
|
-
destructive: '#dc2626',
|
|
44
|
-
destructiveForeground: '#ffffff',
|
|
45
29
|
border: '#303030',
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
success: '#
|
|
30
|
+
destructive: '#ef5350',
|
|
31
|
+
destructiveForeground: '#ffffff',
|
|
32
|
+
success: '#2e7d52',
|
|
49
33
|
successForeground: '#ffffff',
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
34
|
+
warning: '#f57c00',
|
|
35
|
+
warningForeground: '#ffffff',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Color derivation ─────────────────────────────────────────────────────────
|
|
39
|
+
// Takes 12 public tokens → produces full ResolvedColors for component consumption.
|
|
40
|
+
// Dark mode uses bg-blended tints instead of white-blended to stay on-palette.
|
|
41
|
+
|
|
42
|
+
export function deriveColors(t: ThemeColors, scheme: 'light' | 'dark'): ResolvedColors {
|
|
43
|
+
const dark = scheme === 'dark'
|
|
44
|
+
const bg = t.background
|
|
45
|
+
|
|
46
|
+
// Text hierarchy: foreground mixed with background at different opacities
|
|
47
|
+
const foregroundSubtle = mixWithBackground(t.foreground, bg, 0.55)
|
|
48
|
+
const foregroundMuted = mixWithBackground(t.foreground, bg, 0.38)
|
|
49
|
+
|
|
50
|
+
// Surface fills: slight offset from background
|
|
51
|
+
const surface = dark
|
|
52
|
+
? lighten(bg, -0.06) // slightly lighter than dark bg
|
|
53
|
+
: darken(bg, 0.04) // slightly darker than white → #f7f7f7 equivalent
|
|
54
|
+
const surfaceStrong = dark
|
|
55
|
+
? lighten(bg, -0.12)
|
|
56
|
+
: darken(bg, 0.08) // #ebebeb equivalent
|
|
57
|
+
|
|
58
|
+
// Semantic tints: color blended toward background
|
|
59
|
+
const destructiveTint = dark
|
|
60
|
+
? withAlphaOnDark(t.destructive, 0.15, bg)
|
|
61
|
+
: withAlphaOnWhite(t.destructive, 0.08)
|
|
62
|
+
const destructiveBorder = dark
|
|
63
|
+
? withAlphaOnDark(t.destructive, 0.45, bg)
|
|
64
|
+
: withAlphaOnWhite(t.destructive, 0.30)
|
|
65
|
+
|
|
66
|
+
const successTint = dark
|
|
67
|
+
? withAlphaOnDark(t.success, 0.15, bg)
|
|
68
|
+
: withAlphaOnWhite(t.success, 0.08)
|
|
69
|
+
const successBorder = dark
|
|
70
|
+
? withAlphaOnDark(t.success, 0.45, bg)
|
|
71
|
+
: withAlphaOnWhite(t.success, 0.30)
|
|
72
|
+
|
|
73
|
+
const warningTint = dark
|
|
74
|
+
? withAlphaOnDark(t.warning, 0.15, bg)
|
|
75
|
+
: withAlphaOnWhite(t.warning, 0.08)
|
|
76
|
+
const warningBorder = dark
|
|
77
|
+
? withAlphaOnDark(t.warning, 0.45, bg)
|
|
78
|
+
: withAlphaOnWhite(t.warning, 0.30)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
...t,
|
|
82
|
+
foregroundSubtle,
|
|
83
|
+
foregroundMuted,
|
|
84
|
+
surface,
|
|
85
|
+
surfaceStrong,
|
|
86
|
+
destructiveTint,
|
|
87
|
+
destructiveBorder,
|
|
88
|
+
successTint,
|
|
89
|
+
successBorder,
|
|
90
|
+
warningTint,
|
|
91
|
+
warningBorder,
|
|
92
|
+
ring: t.primary, // focus ring always = primary
|
|
93
|
+
input: t.border, // input border always = border
|
|
94
|
+
}
|
|
54
95
|
}
|
package/src/theme/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { ThemeProvider, useTheme } from './ThemeProvider'
|
|
2
2
|
export type { ThemeProviderProps } from './ThemeProvider'
|
|
3
|
-
export type { ThemeColors, Theme, ColorScheme } from './types'
|
|
4
|
-
export { defaultLight, defaultDark } from './colors'
|
|
3
|
+
export type { ThemeColors, ResolvedColors, Theme, ColorScheme } from './types'
|
|
4
|
+
export { defaultLight, defaultDark, deriveColors } from './colors'
|
package/src/theme/types.ts
CHANGED
|
@@ -1,31 +1,45 @@
|
|
|
1
|
+
// 12 public tokens — the only values consumers need to supply.
|
|
2
|
+
// All derived colors (tints, borders, surface, text hierarchy) are computed
|
|
3
|
+
// internally by deriveColors() and exposed via useTheme().colors as ResolvedColors.
|
|
1
4
|
export type ThemeColors = {
|
|
2
5
|
background: string
|
|
3
6
|
foreground: string
|
|
4
7
|
card: string
|
|
5
|
-
cardForeground: string
|
|
6
8
|
primary: string
|
|
7
9
|
primaryForeground: string
|
|
8
|
-
|
|
9
|
-
secondaryForeground: string
|
|
10
|
-
muted: string
|
|
11
|
-
mutedForeground: string
|
|
12
|
-
accent: string
|
|
13
|
-
accentForeground: string
|
|
10
|
+
border: string
|
|
14
11
|
destructive: string
|
|
15
12
|
destructiveForeground: string
|
|
16
|
-
border: string
|
|
17
|
-
input: string
|
|
18
|
-
ring: string
|
|
19
13
|
success: string
|
|
20
14
|
successForeground: string
|
|
15
|
+
warning: string
|
|
16
|
+
warningForeground: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Full resolved palette — what components actually consume via useTheme().
|
|
20
|
+
// Derived from ThemeColors. Never supplied by consumers directly.
|
|
21
|
+
export type ResolvedColors = ThemeColors & {
|
|
22
|
+
// Text hierarchy
|
|
23
|
+
foregroundSubtle: string // ~55% — body text, subtitles
|
|
24
|
+
foregroundMuted: string // ~35% — captions, timestamps, placeholders
|
|
25
|
+
|
|
26
|
+
// Surface fills (chips unselected, input bg, tag bg, skeleton)
|
|
27
|
+
surface: string // background slightly off-canvas
|
|
28
|
+
surfaceStrong: string // slightly stronger fill for pressed/hover states
|
|
29
|
+
|
|
30
|
+
// Semantic tints (light bg for alert banners, toast backgrounds)
|
|
21
31
|
destructiveTint: string
|
|
22
32
|
destructiveBorder: string
|
|
23
33
|
successTint: string
|
|
24
34
|
successBorder: string
|
|
35
|
+
warningTint: string
|
|
36
|
+
warningBorder: string
|
|
37
|
+
|
|
38
|
+
// Aliases (ring + input always equal primary + border for coherence)
|
|
39
|
+
ring: string // = primary
|
|
40
|
+
input: string // = border
|
|
25
41
|
}
|
|
26
42
|
|
|
27
|
-
// Theme overrides: consumers may supply partial or full `ThemeColors` objects
|
|
28
|
-
// for `theme.light` / `theme.dark` to override only the tokens they want.
|
|
29
43
|
export type Theme = {
|
|
30
44
|
light?: Partial<ThemeColors>
|
|
31
45
|
dark?: Partial<ThemeColors>
|
|
@@ -34,6 +48,6 @@ export type Theme = {
|
|
|
34
48
|
export type ColorScheme = 'light' | 'dark' | 'system'
|
|
35
49
|
|
|
36
50
|
export type ThemeContextValue = {
|
|
37
|
-
colors:
|
|
51
|
+
colors: ResolvedColors
|
|
38
52
|
colorScheme: 'light' | 'dark'
|
|
39
53
|
}
|