@rific/auto-paper 0.5.1 → 0.6.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/README.md +12 -6
- package/dist/index.d.mts +42 -37
- package/dist/index.d.ts +42 -37
- package/dist/index.js +49 -1565
- package/dist/index.mjs +54 -1565
- package/package.json +16 -3
- package/src/BlurContext.tsx +8 -0
- package/src/PaperDefaultsContext.tsx +18 -0
- package/src/ThemeProvider.tsx +69 -0
- package/src/ThemeSettingsContext.ts +30 -0
- package/src/components/Appbar.tsx +22 -0
- package/src/components/AppearancePicker.tsx +16 -0
- package/src/components/BlurView.tsx +52 -0
- package/src/components/BottomNavigation.tsx +38 -0
- package/src/components/Button.tsx +8 -0
- package/src/components/Chip.tsx +15 -0
- package/src/components/ColorPicker.tsx +75 -0
- package/src/components/Dialog.tsx +61 -0
- package/src/components/FAB.tsx +8 -0
- package/src/components/HarmonyPicker.tsx +52 -0
- package/src/components/IconButton.tsx +15 -0
- package/src/components/Menu.tsx +27 -0
- package/src/components/TextInput.tsx +10 -0
- package/src/index.ts +50 -0
- package/src/navigation-bar.ts +13 -0
- package/src/redux/themeSlice.ts +59 -0
- package/src/useComputedTheme.ts +79 -0
- package/src/useThemeSettings.ts +5 -0
- package/src/utils/colorNames.ts +151 -0
- package/src/utils/getBlendedColor.ts +15 -0
- package/src/utils/getHex.ts +21 -0
- package/src/utils/getRgb.ts +65 -0
- package/src/utils/getTintTextColor.ts +13 -0
- package/src/utils/getTonalColor.ts +67 -0
- package/src/utils/getTriadicPalette.ts +103 -0
- package/src/utils/isDarkColor.ts +14 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ThemeAppearance } from '../useComputedTheme'
|
|
2
|
+
import type { ColorHarmony } from '../utils/getTriadicPalette'
|
|
3
|
+
|
|
4
|
+
export type { ThemeAppearance }
|
|
5
|
+
|
|
6
|
+
export type ThemeState = {
|
|
7
|
+
appearance: ThemeAppearance
|
|
8
|
+
blur: boolean
|
|
9
|
+
color: string
|
|
10
|
+
harmony: ColorHarmony
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const defaultInitialState: ThemeState = {
|
|
14
|
+
appearance: 'system',
|
|
15
|
+
blur: true,
|
|
16
|
+
color: '#6750a4',
|
|
17
|
+
harmony: 'split-complementary'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Hand-rolled slice — no @reduxjs/toolkit dependency. Action types and creator
|
|
21
|
+
// behavior match the previous createSlice implementation exactly, so this works
|
|
22
|
+
// with RTK stores, vanilla Redux, or any reducer-based state container.
|
|
23
|
+
type PayloadAction<P> = { payload: P; type: string }
|
|
24
|
+
|
|
25
|
+
const createAction = <P>(type: string) => {
|
|
26
|
+
const actionCreator = (payload: P): PayloadAction<P> => ({ payload, type })
|
|
27
|
+
actionCreator.type = type
|
|
28
|
+
actionCreator.match = (action: { type: string }): action is PayloadAction<P> => action.type === type
|
|
29
|
+
return actionCreator
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const initialize = createAction<Partial<ThemeState>>('theme/initialize')
|
|
33
|
+
const setAppearance = createAction<ThemeAppearance>('theme/setAppearance')
|
|
34
|
+
const setBlur = createAction<boolean>('theme/setBlur')
|
|
35
|
+
const setColor = createAction<string>('theme/setColor')
|
|
36
|
+
const setHarmony = createAction<ColorHarmony>('theme/setHarmony')
|
|
37
|
+
|
|
38
|
+
export const themeActions = { initialize, setAppearance, setBlur, setColor, setHarmony }
|
|
39
|
+
|
|
40
|
+
const reduce = (state: ThemeState, action: { type: string }): ThemeState => {
|
|
41
|
+
if (initialize.match(action)) return { ...state, ...action.payload }
|
|
42
|
+
if (setAppearance.match(action)) return { ...state, appearance: action.payload }
|
|
43
|
+
if (setBlur.match(action)) return { ...state, blur: action.payload }
|
|
44
|
+
if (setColor.match(action)) return { ...state, color: action.payload }
|
|
45
|
+
if (setHarmony.match(action)) return { ...state, harmony: action.payload }
|
|
46
|
+
return state
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createThemeReducer(initialState?: Partial<ThemeState>) {
|
|
50
|
+
const initial = { ...defaultInitialState, ...initialState }
|
|
51
|
+
return (state: ThemeState = initial, action: { type: string }): ThemeState => reduce(state, action)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const themeReducer = (state: ThemeState = defaultInitialState, action: { type: string }): ThemeState => reduce(state, action)
|
|
55
|
+
|
|
56
|
+
export const selectThemeAppearance = (state: ThemeState) => state.appearance
|
|
57
|
+
export const selectThemeBlur = (state: ThemeState) => state.blur
|
|
58
|
+
export const selectThemeColor = (state: ThemeState) => state.color
|
|
59
|
+
export const selectThemeHarmony = (state: ThemeState) => state.harmony
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
+
import { Appearance } from 'react-native'
|
|
3
|
+
import { MD3DarkTheme, MD3LightTheme, MD3Theme } from 'react-native-paper'
|
|
4
|
+
|
|
5
|
+
import { getBlendedColor } from './utils/getBlendedColor'
|
|
6
|
+
import { getTonalColor } from './utils/getTonalColor'
|
|
7
|
+
import { type ColorHarmony, getTriadicPalette } from './utils/getTriadicPalette'
|
|
8
|
+
import { isDarkColor } from './utils/isDarkColor'
|
|
9
|
+
|
|
10
|
+
export type ThemeAppearance = 'system' | 'light' | 'dark'
|
|
11
|
+
|
|
12
|
+
export function useComputedTheme(appearance: ThemeAppearance, color: string, harmony: ColorHarmony = 'split-complementary'): MD3Theme | null {
|
|
13
|
+
const [theme, setTheme] = useState<MD3Theme | null>(null)
|
|
14
|
+
|
|
15
|
+
const computeTheme = useCallback(() => {
|
|
16
|
+
let resolved: string | null | undefined = appearance
|
|
17
|
+
if (resolved === 'system') resolved = Appearance.getColorScheme()
|
|
18
|
+
if (resolved === 'no-preference' || !resolved) resolved = 'light'
|
|
19
|
+
const base = resolved === 'dark' ? MD3DarkTheme : MD3LightTheme
|
|
20
|
+
|
|
21
|
+
const _theme: MD3Theme = {
|
|
22
|
+
...base,
|
|
23
|
+
colors: {
|
|
24
|
+
...base.colors,
|
|
25
|
+
elevation: { ...base.colors.elevation }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const palette = getTriadicPalette(color, harmony)
|
|
30
|
+
const surface = base.colors.surface
|
|
31
|
+
|
|
32
|
+
_theme.colors.primary = palette.primary
|
|
33
|
+
_theme.colors.onPrimary = getTonalColor(palette.primary, isDarkColor(palette.primary) ? 0.95 : 0.08)
|
|
34
|
+
_theme.colors.primaryContainer = getBlendedColor(palette.primary, surface, 0.15)
|
|
35
|
+
_theme.colors.onPrimaryContainer = getTonalColor(palette.primary, isDarkColor(_theme.colors.primaryContainer) ? 0.92 : 0.12)
|
|
36
|
+
|
|
37
|
+
_theme.colors.secondary = palette.secondary
|
|
38
|
+
_theme.colors.onSecondary = getTonalColor(palette.secondary, isDarkColor(palette.secondary) ? 0.95 : 0.08)
|
|
39
|
+
_theme.colors.secondaryContainer = getBlendedColor(palette.secondary, surface, 0.15)
|
|
40
|
+
_theme.colors.onSecondaryContainer = getTonalColor(palette.secondary, isDarkColor(_theme.colors.secondaryContainer) ? 0.92 : 0.12)
|
|
41
|
+
|
|
42
|
+
_theme.colors.tertiary = palette.tertiary
|
|
43
|
+
_theme.colors.onTertiary = getTonalColor(palette.tertiary, isDarkColor(palette.tertiary) ? 0.95 : 0.08)
|
|
44
|
+
_theme.colors.tertiaryContainer = getBlendedColor(palette.tertiary, surface, 0.15)
|
|
45
|
+
_theme.colors.onTertiaryContainer = getTonalColor(palette.tertiary, isDarkColor(_theme.colors.tertiaryContainer) ? 0.92 : 0.12)
|
|
46
|
+
|
|
47
|
+
const blended = getBlendedColor(color, palette.secondary, 0.5)
|
|
48
|
+
const tintSurface = (alpha: number) => getBlendedColor(blended, surface, alpha)
|
|
49
|
+
|
|
50
|
+
_theme.colors.background = base.dark ? '#0F0F0F' : '#F0F0F0'
|
|
51
|
+
_theme.colors.surface = surface
|
|
52
|
+
_theme.colors.surfaceVariant = tintSurface(0.2)
|
|
53
|
+
_theme.colors.outline = getBlendedColor(color, base.colors.outline, 0.45)
|
|
54
|
+
|
|
55
|
+
const grey = base.dark ? '#424242' : '#c2c2c2'
|
|
56
|
+
const blendedGrey = getBlendedColor(blended, grey, 0.05)
|
|
57
|
+
_theme.colors.elevation = {
|
|
58
|
+
..._theme.colors.elevation,
|
|
59
|
+
level1: _theme.colors.surface,
|
|
60
|
+
level2: getBlendedColor(blendedGrey, _theme.colors.surface, 0.25),
|
|
61
|
+
level3: getBlendedColor(blendedGrey, _theme.colors.surface, 0.4),
|
|
62
|
+
level4: getBlendedColor(blendedGrey, _theme.colors.surface, 0.55),
|
|
63
|
+
level5: getBlendedColor(blendedGrey, _theme.colors.surface, 0.7)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setTheme(_theme)
|
|
67
|
+
}, [appearance, color, harmony])
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
computeTheme()
|
|
71
|
+
}, [computeTheme])
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const subscription = Appearance.addChangeListener(computeTheme)
|
|
75
|
+
return subscription.remove
|
|
76
|
+
}, [computeTheme])
|
|
77
|
+
|
|
78
|
+
return theme
|
|
79
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
export const colorNames: Record<string, string> = {
|
|
2
|
+
aliceblue: '#f0f8ff',
|
|
3
|
+
antiquewhite: '#faebd7',
|
|
4
|
+
aqua: '#00ffff',
|
|
5
|
+
aquamarine: '#7fffd4',
|
|
6
|
+
azure: '#f0ffff',
|
|
7
|
+
beige: '#f5f5dc',
|
|
8
|
+
bisque: '#ffe4c4',
|
|
9
|
+
black: '#000000',
|
|
10
|
+
blanchedalmond: '#ffebcd',
|
|
11
|
+
blue: '#0000ff',
|
|
12
|
+
blueviolet: '#8a2be2',
|
|
13
|
+
brown: '#a52a2a',
|
|
14
|
+
burlywood: '#deb887',
|
|
15
|
+
cadetblue: '#5f9ea0',
|
|
16
|
+
chartreuse: '#7fff00',
|
|
17
|
+
chocolate: '#d2691e',
|
|
18
|
+
coral: '#ff7f50',
|
|
19
|
+
cornflowerblue: '#6495ed',
|
|
20
|
+
cornsilk: '#fff8dc',
|
|
21
|
+
crimson: '#dc143c',
|
|
22
|
+
cyan: '#00ffff',
|
|
23
|
+
darkblue: '#00008b',
|
|
24
|
+
darkcyan: '#008b8b',
|
|
25
|
+
darkgoldenrod: '#b8860b',
|
|
26
|
+
darkgray: '#a9a9a9',
|
|
27
|
+
darkgrey: '#a9a9a9',
|
|
28
|
+
darkgreen: '#006400',
|
|
29
|
+
darkkhaki: '#bdb76b',
|
|
30
|
+
darkmagenta: '#8b008b',
|
|
31
|
+
darkolivegreen: '#556b2f',
|
|
32
|
+
darkorange: '#ff8c00',
|
|
33
|
+
darkorchid: '#9932cc',
|
|
34
|
+
darkred: '#8b0000',
|
|
35
|
+
darksalmon: '#e9967a',
|
|
36
|
+
darkseagreen: '#8fbc8f',
|
|
37
|
+
darkslateblue: '#483d8b',
|
|
38
|
+
darkslategray: '#2f4f4f',
|
|
39
|
+
darkslategrey: '#2f4f4f',
|
|
40
|
+
darkturquoise: '#00ced1',
|
|
41
|
+
darkviolet: '#9400d3',
|
|
42
|
+
deeppink: '#ff1493',
|
|
43
|
+
deepskyblue: '#00bfff',
|
|
44
|
+
dimgray: '#696969',
|
|
45
|
+
dimgrey: '#696969',
|
|
46
|
+
dodgerblue: '#1e90ff',
|
|
47
|
+
firebrick: '#b22222',
|
|
48
|
+
floralwhite: '#fffaf0',
|
|
49
|
+
forestgreen: '#228b22',
|
|
50
|
+
fuchsia: '#ff00ff',
|
|
51
|
+
gainsboro: '#dcdcdc',
|
|
52
|
+
ghostwhite: '#f8f8ff',
|
|
53
|
+
gold: '#ffd700',
|
|
54
|
+
goldenrod: '#daa520',
|
|
55
|
+
gray: '#808080',
|
|
56
|
+
grey: '#808080',
|
|
57
|
+
green: '#008000',
|
|
58
|
+
greenyellow: '#adff2f',
|
|
59
|
+
honeydew: '#f0fff0',
|
|
60
|
+
hotpink: '#ff69b4',
|
|
61
|
+
indianred: '#cd5c5c',
|
|
62
|
+
indigo: '#4b0082',
|
|
63
|
+
ivory: '#fffff0',
|
|
64
|
+
khaki: '#f0e68c',
|
|
65
|
+
lavender: '#e6e6fa',
|
|
66
|
+
lavenderblush: '#fff0f5',
|
|
67
|
+
lawngreen: '#7cfc00',
|
|
68
|
+
lemonchiffon: '#fffacd',
|
|
69
|
+
lightblue: '#add8e6',
|
|
70
|
+
lightcoral: '#f08080',
|
|
71
|
+
lightcyan: '#e0ffff',
|
|
72
|
+
lightgoldenrodyellow: '#fafad2',
|
|
73
|
+
lightgray: '#d3d3d3',
|
|
74
|
+
lightgrey: '#d3d3d3',
|
|
75
|
+
lightgreen: '#90ee90',
|
|
76
|
+
lightpink: '#ffb6c1',
|
|
77
|
+
lightsalmon: '#ffa07a',
|
|
78
|
+
lightseagreen: '#20b2aa',
|
|
79
|
+
lightskyblue: '#87cefa',
|
|
80
|
+
lightslategray: '#778899',
|
|
81
|
+
lightslategrey: '#778899',
|
|
82
|
+
lightsteelblue: '#b0c4de',
|
|
83
|
+
lightyellow: '#ffffe0',
|
|
84
|
+
lime: '#00ff00',
|
|
85
|
+
limegreen: '#32cd32',
|
|
86
|
+
linen: '#faf0e6',
|
|
87
|
+
magenta: '#ff00ff',
|
|
88
|
+
maroon: '#800000',
|
|
89
|
+
mediumaquamarine: '#66cdaa',
|
|
90
|
+
mediumblue: '#0000cd',
|
|
91
|
+
mediumorchid: '#ba55d3',
|
|
92
|
+
mediumpurple: '#9370db',
|
|
93
|
+
mediumseagreen: '#3cb371',
|
|
94
|
+
mediumslateblue: '#7b68ee',
|
|
95
|
+
mediumspringgreen: '#00fa9a',
|
|
96
|
+
mediumturquoise: '#48d1cc',
|
|
97
|
+
mediumvioletred: '#c71585',
|
|
98
|
+
midnightblue: '#191970',
|
|
99
|
+
mintcream: '#f5fffa',
|
|
100
|
+
mistyrose: '#ffe4e1',
|
|
101
|
+
moccasin: '#ffe4b5',
|
|
102
|
+
navajowhite: '#ffdead',
|
|
103
|
+
navy: '#000080',
|
|
104
|
+
oldlace: '#fdf5e6',
|
|
105
|
+
olive: '#808000',
|
|
106
|
+
olivedrab: '#6b8e23',
|
|
107
|
+
orange: '#ffa500',
|
|
108
|
+
orangered: '#ff4500',
|
|
109
|
+
orchid: '#da70d6',
|
|
110
|
+
palegoldenrod: '#eee8aa',
|
|
111
|
+
palegreen: '#98fb98',
|
|
112
|
+
paleturquoise: '#afeeee',
|
|
113
|
+
palevioletred: '#db7093',
|
|
114
|
+
papayawhip: '#ffefd5',
|
|
115
|
+
peachpuff: '#ffdab9',
|
|
116
|
+
peru: '#cd853f',
|
|
117
|
+
pink: '#ffc0cb',
|
|
118
|
+
plum: '#dda0dd',
|
|
119
|
+
powderblue: '#b0e0e6',
|
|
120
|
+
purple: '#800080',
|
|
121
|
+
rebeccapurple: '#663399',
|
|
122
|
+
red: '#ff0000',
|
|
123
|
+
rosybrown: '#bc8f8f',
|
|
124
|
+
royalblue: '#4169e1',
|
|
125
|
+
saddlebrown: '#8b4513',
|
|
126
|
+
salmon: '#fa8072',
|
|
127
|
+
sandybrown: '#f4a460',
|
|
128
|
+
seagreen: '#2e8b57',
|
|
129
|
+
seashell: '#fff5ee',
|
|
130
|
+
sienna: '#a0522d',
|
|
131
|
+
silver: '#c0c0c0',
|
|
132
|
+
skyblue: '#87ceeb',
|
|
133
|
+
slateblue: '#6a5acd',
|
|
134
|
+
slategray: '#708090',
|
|
135
|
+
slategrey: '#708090',
|
|
136
|
+
snow: '#fffafa',
|
|
137
|
+
springgreen: '#00ff7f',
|
|
138
|
+
steelblue: '#4682b4',
|
|
139
|
+
tan: '#d2b48c',
|
|
140
|
+
teal: '#008080',
|
|
141
|
+
thistle: '#d8bfd8',
|
|
142
|
+
tomato: '#ff6347',
|
|
143
|
+
transparent: 'transparent',
|
|
144
|
+
turquoise: '#40e0d0',
|
|
145
|
+
violet: '#ee82ee',
|
|
146
|
+
wheat: '#f5deb3',
|
|
147
|
+
white: '#ffffff',
|
|
148
|
+
whitesmoke: '#f5f5f5',
|
|
149
|
+
yellow: '#ffff00',
|
|
150
|
+
yellowgreen: '#9acd32'
|
|
151
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getHex } from './getHex'
|
|
2
|
+
import { getRgb } from './getRgb'
|
|
3
|
+
|
|
4
|
+
export const getBlendedColor = (foreground: string, background: string, alpha: number): string => {
|
|
5
|
+
const fg = getRgb(foreground)
|
|
6
|
+
const bg = getRgb(background)
|
|
7
|
+
if (!fg || !bg) return background
|
|
8
|
+
const safeAlpha = Math.min(1, Math.max(0, alpha))
|
|
9
|
+
const r = Math.round(fg.r * safeAlpha + bg.r * (1 - safeAlpha))
|
|
10
|
+
const g = Math.round(fg.g * safeAlpha + bg.g * (1 - safeAlpha))
|
|
11
|
+
const b = Math.round(fg.b * safeAlpha + bg.b * (1 - safeAlpha))
|
|
12
|
+
return getHex(`rgb(${r}, ${g}, ${b})`) ?? background
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default getBlendedColor
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getRgb } from './getRgb'
|
|
2
|
+
|
|
3
|
+
export const getHex = (value: string): string | null => {
|
|
4
|
+
const rgb = getRgb(value)
|
|
5
|
+
if (!rgb) return null
|
|
6
|
+
|
|
7
|
+
const toHex = (n: number) => {
|
|
8
|
+
const hex = Math.round(n).toString(16)
|
|
9
|
+
return hex.length === 1 ? '0' + hex : hex
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const hexString = `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`
|
|
13
|
+
|
|
14
|
+
if (rgb.a !== undefined) {
|
|
15
|
+
return `${hexString}${toHex(rgb.a * 255)}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return hexString
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default getHex
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { colorNames } from './colorNames'
|
|
2
|
+
|
|
3
|
+
export type RGB = {
|
|
4
|
+
r: number
|
|
5
|
+
g: number
|
|
6
|
+
b: number
|
|
7
|
+
a?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const getRgb = (value: string): RGB | null => {
|
|
11
|
+
if (!value) return null
|
|
12
|
+
|
|
13
|
+
let trimmed = value.trim().toLowerCase()
|
|
14
|
+
|
|
15
|
+
const namedColor = colorNames[trimmed]
|
|
16
|
+
if (namedColor) trimmed = namedColor
|
|
17
|
+
|
|
18
|
+
const hexMatch = trimmed.match(/^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i)
|
|
19
|
+
|
|
20
|
+
if (hexMatch) {
|
|
21
|
+
const hex = hexMatch[1]
|
|
22
|
+
let normalized = hex
|
|
23
|
+
|
|
24
|
+
if (hex.length === 3) {
|
|
25
|
+
normalized = hex
|
|
26
|
+
.split('')
|
|
27
|
+
.map((c) => c + c)
|
|
28
|
+
.join('')
|
|
29
|
+
} else if (hex.length === 8) {
|
|
30
|
+
const alpha = parseInt(hex.substring(6, 8), 16)
|
|
31
|
+
normalized = hex.substring(0, 6)
|
|
32
|
+
const intVal = parseInt(normalized, 16)
|
|
33
|
+
return {
|
|
34
|
+
r: (intVal >> 16) & 255,
|
|
35
|
+
g: (intVal >> 8) & 255,
|
|
36
|
+
b: intVal & 255,
|
|
37
|
+
a: alpha / 255
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const intVal = parseInt(normalized, 16)
|
|
42
|
+
return {
|
|
43
|
+
r: (intVal >> 16) & 255,
|
|
44
|
+
g: (intVal >> 8) & 255,
|
|
45
|
+
b: intVal & 255
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const rgbMatch = trimmed.match(/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([0-9.]+))?\s*\)$/i)
|
|
50
|
+
if (rgbMatch) {
|
|
51
|
+
const result: RGB = {
|
|
52
|
+
r: Number(rgbMatch[1]),
|
|
53
|
+
g: Number(rgbMatch[2]),
|
|
54
|
+
b: Number(rgbMatch[3])
|
|
55
|
+
}
|
|
56
|
+
if (rgbMatch[4] !== undefined) {
|
|
57
|
+
result.a = Number(rgbMatch[4])
|
|
58
|
+
}
|
|
59
|
+
return result
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default getRgb
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getBlendedColor } from './getBlendedColor'
|
|
2
|
+
import { getTonalColor } from './getTonalColor'
|
|
3
|
+
import { isDarkColor } from './isDarkColor'
|
|
4
|
+
|
|
5
|
+
// Computes a contrast-safe text color for content sitting on a BlurView tintColor,
|
|
6
|
+
// accounting for the tint being blended over the backdrop at the given opacity
|
|
7
|
+
// (same approach useComputedTheme uses to derive onPrimaryContainer from primaryContainer).
|
|
8
|
+
export const getTintTextColor = (tintColor: string, backdropColor: string, tintOpacity: number): string => {
|
|
9
|
+
const blended = getBlendedColor(tintColor, backdropColor, tintOpacity)
|
|
10
|
+
return getTonalColor(tintColor, isDarkColor(blended) ? 0.92 : 0.12)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default getTintTextColor
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getHex } from './getHex'
|
|
2
|
+
import { getRgb } from './getRgb'
|
|
3
|
+
|
|
4
|
+
// Returns the color with lightness clamped to the given target (0–1), preserving hue and saturation.
|
|
5
|
+
export const getTonalColor = (color: string, lightness: number): string => {
|
|
6
|
+
const rgb = getRgb(color)
|
|
7
|
+
if (!rgb) return color
|
|
8
|
+
|
|
9
|
+
const r = rgb.r / 255
|
|
10
|
+
const g = rgb.g / 255
|
|
11
|
+
const b = rgb.b / 255
|
|
12
|
+
|
|
13
|
+
const max = Math.max(r, g, b)
|
|
14
|
+
const min = Math.min(r, g, b)
|
|
15
|
+
const delta = max - min
|
|
16
|
+
|
|
17
|
+
let h = 0
|
|
18
|
+
if (delta !== 0) {
|
|
19
|
+
if (max === r) h = ((g - b) / delta) % 6
|
|
20
|
+
else if (max === g) h = (b - r) / delta + 2
|
|
21
|
+
else h = (r - g) / delta + 4
|
|
22
|
+
h = (h * 60 + 360) % 360
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const l = (max + min) / 2
|
|
26
|
+
const s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1))
|
|
27
|
+
|
|
28
|
+
// Rebuild with new lightness
|
|
29
|
+
const targetL = Math.min(1, Math.max(0, lightness))
|
|
30
|
+
const c = (1 - Math.abs(2 * targetL - 1)) * s
|
|
31
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
|
|
32
|
+
const m = targetL - c / 2
|
|
33
|
+
|
|
34
|
+
let r2 = 0,
|
|
35
|
+
g2 = 0,
|
|
36
|
+
b2 = 0
|
|
37
|
+
if (h < 60) {
|
|
38
|
+
r2 = c
|
|
39
|
+
g2 = x
|
|
40
|
+
b2 = 0
|
|
41
|
+
} else if (h < 120) {
|
|
42
|
+
r2 = x
|
|
43
|
+
g2 = c
|
|
44
|
+
b2 = 0
|
|
45
|
+
} else if (h < 180) {
|
|
46
|
+
r2 = 0
|
|
47
|
+
g2 = c
|
|
48
|
+
b2 = x
|
|
49
|
+
} else if (h < 240) {
|
|
50
|
+
r2 = 0
|
|
51
|
+
g2 = x
|
|
52
|
+
b2 = c
|
|
53
|
+
} else if (h < 300) {
|
|
54
|
+
r2 = x
|
|
55
|
+
g2 = 0
|
|
56
|
+
b2 = c
|
|
57
|
+
} else {
|
|
58
|
+
r2 = c
|
|
59
|
+
g2 = 0
|
|
60
|
+
b2 = x
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const toInt = (n: number) => Math.round((n + m) * 255)
|
|
64
|
+
return getHex(`rgb(${toInt(r2)}, ${toInt(g2)}, ${toInt(b2)})`) ?? color
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default getTonalColor
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { getHex } from './getHex'
|
|
2
|
+
import { getRgb } from './getRgb'
|
|
3
|
+
|
|
4
|
+
export type ColorHarmony = 'triadic' | 'split-complementary' | 'analogous' | 'square' | 'complementary' | 'double-split'
|
|
5
|
+
|
|
6
|
+
export type TriadicPalette = {
|
|
7
|
+
primary: string
|
|
8
|
+
secondary: string
|
|
9
|
+
tertiary: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const HARMONY_OFFSETS: Record<ColorHarmony, [number, number]> = {
|
|
13
|
+
analogous: [1 / 12, 2 / 12],
|
|
14
|
+
complementary: [1 / 4, 1 / 2],
|
|
15
|
+
'double-split': [1 / 12, 11 / 12],
|
|
16
|
+
'split-complementary': [5 / 12, 7 / 12],
|
|
17
|
+
square: [1 / 4, 3 / 4],
|
|
18
|
+
triadic: [1 / 3, 2 / 3]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getTriadicPalette(primaryColor: string, harmony: ColorHarmony = 'split-complementary'): TriadicPalette {
|
|
22
|
+
const rgb = getRgb(primaryColor)
|
|
23
|
+
if (!rgb) throw new Error(`Invalid color format: ${primaryColor}`)
|
|
24
|
+
|
|
25
|
+
const r = rgb.r / 255
|
|
26
|
+
const g = rgb.g / 255
|
|
27
|
+
const b = rgb.b / 255
|
|
28
|
+
|
|
29
|
+
const max = Math.max(r, g, b)
|
|
30
|
+
const min = Math.min(r, g, b)
|
|
31
|
+
const l = (max + min) / 2
|
|
32
|
+
|
|
33
|
+
let h = 0
|
|
34
|
+
let s = 0
|
|
35
|
+
|
|
36
|
+
if (max !== min) {
|
|
37
|
+
const d = max - min
|
|
38
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
|
39
|
+
|
|
40
|
+
switch (max) {
|
|
41
|
+
case r:
|
|
42
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6
|
|
43
|
+
break
|
|
44
|
+
case g:
|
|
45
|
+
h = ((b - r) / d + 2) / 6
|
|
46
|
+
break
|
|
47
|
+
case b:
|
|
48
|
+
h = ((r - g) / d + 4) / 6
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const [o1, o2] = HARMONY_OFFSETS[harmony]
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
primary: getHex(primaryColor) ?? primaryColor,
|
|
57
|
+
secondary: hslToHex((h + o1) % 1, s, l),
|
|
58
|
+
tertiary: hslToHex((h + o2) % 1, s, l)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hslToHex(h: number, s: number, l: number): string {
|
|
63
|
+
const c = (1 - Math.abs(2 * l - 1)) * s
|
|
64
|
+
const x = c * (1 - Math.abs(((h * 6) % 2) - 1))
|
|
65
|
+
const m = l - c / 2
|
|
66
|
+
|
|
67
|
+
let r = 0
|
|
68
|
+
let g = 0
|
|
69
|
+
let b = 0
|
|
70
|
+
|
|
71
|
+
if (h < 1 / 6) {
|
|
72
|
+
r = c
|
|
73
|
+
g = x
|
|
74
|
+
b = 0
|
|
75
|
+
} else if (h < 2 / 6) {
|
|
76
|
+
r = x
|
|
77
|
+
g = c
|
|
78
|
+
b = 0
|
|
79
|
+
} else if (h < 3 / 6) {
|
|
80
|
+
r = 0
|
|
81
|
+
g = c
|
|
82
|
+
b = x
|
|
83
|
+
} else if (h < 4 / 6) {
|
|
84
|
+
r = 0
|
|
85
|
+
g = x
|
|
86
|
+
b = c
|
|
87
|
+
} else if (h < 5 / 6) {
|
|
88
|
+
r = x
|
|
89
|
+
g = 0
|
|
90
|
+
b = c
|
|
91
|
+
} else {
|
|
92
|
+
r = c
|
|
93
|
+
g = 0
|
|
94
|
+
b = x
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const toHex = (value: number) => {
|
|
98
|
+
const hex = Math.round((value + m) * 255).toString(16)
|
|
99
|
+
return hex.length === 1 ? `0${hex}` : hex
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
|
103
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getRgb } from './getRgb'
|
|
2
|
+
|
|
3
|
+
export const isDarkColor = (value: string): boolean => {
|
|
4
|
+
const rgb = getRgb(value)
|
|
5
|
+
if (!rgb) return false
|
|
6
|
+
|
|
7
|
+
const [r, g, b] = [rgb.r / 255, rgb.g / 255, rgb.b / 255]
|
|
8
|
+
const [rs, gs, bs] = [r, g, b].map((c) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)))
|
|
9
|
+
const luminance = 0.2126 * rs + 0.7152 * gs + 0.0722 * bs
|
|
10
|
+
|
|
11
|
+
return luminance < 0.5
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default isDarkColor
|