@rific/auto-paper 0.4.0 → 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 +51 -39
- package/dist/index.d.ts +51 -39
- package/dist/index.js +80 -1580
- package/dist/index.mjs +83 -1580
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rific/auto-paper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Adaptive react-native-paper theming: triadic palette from a seed color + system/light/dark appearance",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"type": "commonjs",
|
|
32
32
|
"exports": {
|
|
33
33
|
".": {
|
|
34
|
+
"react-native": "./src/index.ts",
|
|
34
35
|
"types": "./dist/index.d.ts",
|
|
35
36
|
"import": "./dist/index.mjs",
|
|
36
37
|
"require": "./dist/index.js"
|
|
@@ -40,7 +41,10 @@
|
|
|
40
41
|
"module": "dist/index.mjs",
|
|
41
42
|
"types": "dist/index.d.ts",
|
|
42
43
|
"files": [
|
|
43
|
-
"dist"
|
|
44
|
+
"dist",
|
|
45
|
+
"src",
|
|
46
|
+
"!src/__tests__",
|
|
47
|
+
"!src/__mocks__"
|
|
44
48
|
],
|
|
45
49
|
"scripts": {
|
|
46
50
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
@@ -90,8 +94,9 @@
|
|
|
90
94
|
},
|
|
91
95
|
"peerDependencies": {
|
|
92
96
|
"expo-blur": ">=14.0.0",
|
|
97
|
+
"expo-navigation-bar": ">=56.0.0",
|
|
93
98
|
"react": ">=18.0.0",
|
|
94
|
-
"react-native": ">=0.
|
|
99
|
+
"react-native": ">=0.76.0",
|
|
95
100
|
"react-native-paper": ">=5.0.0"
|
|
96
101
|
},
|
|
97
102
|
"optionalDependencies": {
|
|
@@ -99,5 +104,13 @@
|
|
|
99
104
|
},
|
|
100
105
|
"publishConfig": {
|
|
101
106
|
"access": "public"
|
|
107
|
+
},
|
|
108
|
+
"peerDependenciesMeta": {
|
|
109
|
+
"expo-blur": {
|
|
110
|
+
"optional": true
|
|
111
|
+
},
|
|
112
|
+
"expo-navigation-bar": {
|
|
113
|
+
"optional": true
|
|
114
|
+
}
|
|
102
115
|
}
|
|
103
116
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
import type { AppbarHeaderProps, BottomNavigationProps, BottomNavigationRoute, ButtonProps, ChipProps, FABProps, IconButtonProps, TextInputProps } from 'react-native-paper'
|
|
3
|
+
|
|
4
|
+
export type PaperDefaults = {
|
|
5
|
+
AppbarHeader?: Partial<AppbarHeaderProps>
|
|
6
|
+
BottomNavigation?: Partial<BottomNavigationProps<BottomNavigationRoute>>
|
|
7
|
+
Button?: Partial<ButtonProps>
|
|
8
|
+
Chip?: Partial<ChipProps>
|
|
9
|
+
FAB?: Partial<FABProps>
|
|
10
|
+
IconButton?: Partial<IconButtonProps>
|
|
11
|
+
TextInput?: Partial<TextInputProps>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const PaperDefaultsContext = createContext<PaperDefaults>({})
|
|
15
|
+
|
|
16
|
+
export function usePaperDefaults(): PaperDefaults {
|
|
17
|
+
return useContext(PaperDefaultsContext)
|
|
18
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createContext, type ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { StatusBar, type StatusBarProps, type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'
|
|
3
|
+
import { Provider as PaperProvider } from 'react-native-paper'
|
|
4
|
+
|
|
5
|
+
import { type PaperDefaults, PaperDefaultsContext } from './PaperDefaultsContext'
|
|
6
|
+
import { defaultThemeSettings, type ThemeSettings, ThemeSettingsContext } from './ThemeSettingsContext'
|
|
7
|
+
import { useComputedTheme } from './useComputedTheme'
|
|
8
|
+
|
|
9
|
+
type NavBarContextType = { onNavBarChange?: (color: string, dark: boolean) => void }
|
|
10
|
+
const NavBarContext = createContext<NavBarContextType>({})
|
|
11
|
+
export const useNavBarContext = () => useContext(NavBarContext)
|
|
12
|
+
|
|
13
|
+
export type ProviderProps = {
|
|
14
|
+
initialValue?: Partial<ThemeSettings>
|
|
15
|
+
onChange?: (settings: ThemeSettings) => void
|
|
16
|
+
children: ReactNode
|
|
17
|
+
defaults?: PaperDefaults
|
|
18
|
+
onNavBarChange?: (color: string, dark: boolean) => void
|
|
19
|
+
onReady?: () => void
|
|
20
|
+
statusBarProps?: StatusBarProps
|
|
21
|
+
style?: StyleProp<ViewStyle>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Provider({ initialValue, onChange, children, defaults, onNavBarChange, onReady, statusBarProps, style }: ProviderProps) {
|
|
25
|
+
const [settings, setSettings] = useState<ThemeSettings>(() => ({ ...defaultThemeSettings, ...initialValue }))
|
|
26
|
+
|
|
27
|
+
const set = useCallback((patch: Partial<ThemeSettings>) => {
|
|
28
|
+
setSettings((prev) => ({ ...prev, ...patch }))
|
|
29
|
+
}, [])
|
|
30
|
+
|
|
31
|
+
const onChangeRef = useRef(onChange)
|
|
32
|
+
onChangeRef.current = onChange
|
|
33
|
+
const prevSettings = useRef(settings)
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (prevSettings.current !== settings) {
|
|
36
|
+
prevSettings.current = settings
|
|
37
|
+
onChangeRef.current?.(settings)
|
|
38
|
+
}
|
|
39
|
+
}, [settings])
|
|
40
|
+
|
|
41
|
+
const theme = useComputedTheme(settings.appearance, settings.color, settings.harmony)
|
|
42
|
+
const called = useRef(false)
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (theme && !called.current) {
|
|
46
|
+
called.current = true
|
|
47
|
+
onReady?.()
|
|
48
|
+
}
|
|
49
|
+
}, [theme, onReady])
|
|
50
|
+
|
|
51
|
+
if (!theme) return null
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<ThemeSettingsContext.Provider value={{ settings, set }}>
|
|
55
|
+
<PaperProvider theme={theme}>
|
|
56
|
+
<StatusBar backgroundColor={theme.colors.background} barStyle={theme.dark ? 'light-content' : 'dark-content'} {...statusBarProps} />
|
|
57
|
+
<PaperDefaultsContext.Provider value={defaults ?? {}}>
|
|
58
|
+
<NavBarContext.Provider value={{ onNavBarChange }}>
|
|
59
|
+
<View style={[styles.flex, { backgroundColor: theme.colors.background }, style]}>{children}</View>
|
|
60
|
+
</NavBarContext.Provider>
|
|
61
|
+
</PaperDefaultsContext.Provider>
|
|
62
|
+
</PaperProvider>
|
|
63
|
+
</ThemeSettingsContext.Provider>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const styles = StyleSheet.create({
|
|
68
|
+
flex: { flex: 1 }
|
|
69
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createContext } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { ThemeAppearance } from './useComputedTheme'
|
|
4
|
+
import type { ColorHarmony } from './utils/getTriadicPalette'
|
|
5
|
+
|
|
6
|
+
export type ThemeSettings = {
|
|
7
|
+
appearance: ThemeAppearance
|
|
8
|
+
blur: boolean
|
|
9
|
+
blurTint: number
|
|
10
|
+
color: string
|
|
11
|
+
harmony: ColorHarmony
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const defaultThemeSettings: ThemeSettings = {
|
|
15
|
+
appearance: 'system',
|
|
16
|
+
blur: true,
|
|
17
|
+
blurTint: 0.2,
|
|
18
|
+
color: '#6750a4',
|
|
19
|
+
harmony: 'split-complementary'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ThemeSettingsContextType = {
|
|
23
|
+
settings: ThemeSettings
|
|
24
|
+
set: (patch: Partial<ThemeSettings>) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ThemeSettingsContext = createContext<ThemeSettingsContextType>({
|
|
28
|
+
settings: defaultThemeSettings,
|
|
29
|
+
set: () => {}
|
|
30
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { StatusBar } from 'react-native'
|
|
2
|
+
import { Appbar as PaperAppbar, type AppbarHeaderProps, useTheme } from 'react-native-paper'
|
|
3
|
+
|
|
4
|
+
import { usePaperDefaults } from '../PaperDefaultsContext'
|
|
5
|
+
|
|
6
|
+
function Header({ style, ...props }: AppbarHeaderProps) {
|
|
7
|
+
const defaults = usePaperDefaults()
|
|
8
|
+
const theme = useTheme()
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<StatusBar backgroundColor={theme.colors.surface} barStyle={theme.dark ? 'light-content' : 'dark-content'} />
|
|
12
|
+
<PaperAppbar.Header {...defaults.AppbarHeader} {...props} style={style} />
|
|
13
|
+
</>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Appbar = {
|
|
18
|
+
Header,
|
|
19
|
+
Content: PaperAppbar.Content,
|
|
20
|
+
Action: PaperAppbar.Action,
|
|
21
|
+
BackAction: PaperAppbar.BackAction
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SegmentedButtons } from 'react-native-paper'
|
|
2
|
+
|
|
3
|
+
import type { ThemeAppearance } from '../useComputedTheme'
|
|
4
|
+
|
|
5
|
+
const APPEARANCES: { label: string; value: ThemeAppearance; icon: string }[] = [
|
|
6
|
+
{ label: 'System', value: 'system', icon: 'brightness-auto' },
|
|
7
|
+
{ label: 'Light', value: 'light', icon: 'white-balance-sunny' },
|
|
8
|
+
{ label: 'Dark', value: 'dark', icon: 'weather-night' }
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
value: ThemeAppearance
|
|
13
|
+
onChange: (appearance: ThemeAppearance) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const AppearancePicker = ({ value, onChange }: Props) => <SegmentedButtons value={value} onValueChange={(v) => onChange(v as ThemeAppearance)} buttons={APPEARANCES.map((a) => ({ value: a.value, label: a.label, icon: a.icon }))} />
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { BlurViewProps as ExpoBlurViewProps } from 'expo-blur'
|
|
2
|
+
import { type ReactNode, useContext } from 'react'
|
|
3
|
+
import { type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'
|
|
4
|
+
import { useTheme } from 'react-native-paper'
|
|
5
|
+
|
|
6
|
+
import { ThemeSettingsContext } from '../ThemeSettingsContext'
|
|
7
|
+
|
|
8
|
+
// expo-blur is an optional peer — when absent, BlurView renders its solid (non-blur) fallback
|
|
9
|
+
const ExpoBlurView = (() => {
|
|
10
|
+
try {
|
|
11
|
+
return (require('expo-blur') as typeof import('expo-blur')).BlurView
|
|
12
|
+
} catch {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
})()
|
|
16
|
+
|
|
17
|
+
const ELEVATION_OPACITY: Record<number, number> = { 0: 0, 1: 0.05, 2: 0.08, 3: 0.11, 4: 0.12, 5: 0.14 }
|
|
18
|
+
const VARIANT_TINT_OPACITY = { light: 0.12, dark: 0.18 }
|
|
19
|
+
|
|
20
|
+
export type BlurViewProps = ExpoBlurViewProps & {
|
|
21
|
+
blur?: boolean
|
|
22
|
+
children?: ReactNode
|
|
23
|
+
elevation?: 0 | 1 | 2 | 3 | 4 | 5
|
|
24
|
+
style?: StyleProp<ViewStyle>
|
|
25
|
+
tintColor?: string
|
|
26
|
+
tintOpacity?: number
|
|
27
|
+
variant?: 'primary' | 'secondary' | 'tertiary' | 'error' | 'surface' | 'surfaceVariant'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const BlurView = ({ blur = true, children, elevation, style, tintColor, tintOpacity, variant = 'surface', ...props }: BlurViewProps) => {
|
|
31
|
+
const { dark, colors } = useTheme()
|
|
32
|
+
const { settings } = useContext(ThemeSettingsContext)
|
|
33
|
+
const surfaceColor = variant === 'surfaceVariant' ? colors.surfaceVariant : colors.surface
|
|
34
|
+
const variantColor = variant === 'primary' ? colors.primary : variant === 'secondary' ? colors.secondary : variant === 'tertiary' ? colors.tertiary : variant === 'error' ? colors.error : undefined
|
|
35
|
+
const resolvedTintColor = tintColor ?? variantColor ?? (elevation !== undefined ? colors.primary : undefined)
|
|
36
|
+
const resolvedTintOpacity = tintOpacity ?? (tintColor ? (dark ? 0.24 : 0.16) : variantColor ? (dark ? VARIANT_TINT_OPACITY.dark : VARIANT_TINT_OPACITY.light) : elevation !== undefined ? ELEVATION_OPACITY[elevation] : 0)
|
|
37
|
+
const tint = resolvedTintColor && resolvedTintOpacity > 0 ? <View style={[StyleSheet.absoluteFill, { backgroundColor: resolvedTintColor, opacity: resolvedTintOpacity }]} /> : null
|
|
38
|
+
if (blur && ExpoBlurView)
|
|
39
|
+
return (
|
|
40
|
+
<ExpoBlurView {...props} tint={dark ? 'dark' : 'light'} style={style}>
|
|
41
|
+
<View style={[StyleSheet.absoluteFill, { backgroundColor: surfaceColor, opacity: settings.blurTint }]} />
|
|
42
|
+
{tint}
|
|
43
|
+
{children}
|
|
44
|
+
</ExpoBlurView>
|
|
45
|
+
)
|
|
46
|
+
return (
|
|
47
|
+
<View style={[{ backgroundColor: surfaceColor }, style]}>
|
|
48
|
+
{tint}
|
|
49
|
+
{children}
|
|
50
|
+
</View>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React, { useEffect } from 'react'
|
|
2
|
+
import { Platform } from 'react-native'
|
|
3
|
+
import { BottomNavigation as PaperBottomNavigation, type BottomNavigationProps, type BottomNavigationRoute, useTheme } from 'react-native-paper'
|
|
4
|
+
|
|
5
|
+
import { navigationBar, setNavigationBarStyle } from '../navigation-bar'
|
|
6
|
+
import { usePaperDefaults } from '../PaperDefaultsContext'
|
|
7
|
+
import { useNavBarContext } from '../ThemeProvider'
|
|
8
|
+
|
|
9
|
+
function useNavigationBarSync() {
|
|
10
|
+
const theme = useTheme()
|
|
11
|
+
const { onNavBarChange } = useNavBarContext()
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (Platform.OS !== 'android') return
|
|
14
|
+
// An explicit onNavBarChange callback takes full control; otherwise sync the
|
|
15
|
+
// nav bar icon style automatically when expo-navigation-bar is installed.
|
|
16
|
+
if (onNavBarChange) onNavBarChange(theme.colors.surface, theme.dark)
|
|
17
|
+
else if (navigationBar) setNavigationBarStyle(theme.dark)
|
|
18
|
+
}, [theme, onNavBarChange])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function BottomNavigationComponent<Route extends BottomNavigationRoute>(props: BottomNavigationProps<Route>) {
|
|
22
|
+
const defaults = usePaperDefaults()
|
|
23
|
+
useNavigationBarSync()
|
|
24
|
+
return <PaperBottomNavigation {...(defaults.BottomNavigation as Partial<BottomNavigationProps<Route>>)} {...props} />
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type BarProps = React.ComponentProps<typeof PaperBottomNavigation.Bar>
|
|
28
|
+
|
|
29
|
+
function Bar(props: BarProps) {
|
|
30
|
+
useNavigationBarSync()
|
|
31
|
+
return <PaperBottomNavigation.Bar {...props} />
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
BottomNavigationComponent.Bar = Bar
|
|
35
|
+
// Guard module-scope access: under some CJS interop (e.g. jest) PaperBottomNavigation may be undefined at import time.
|
|
36
|
+
BottomNavigationComponent.SceneMap = PaperBottomNavigation?.SceneMap
|
|
37
|
+
|
|
38
|
+
export const BottomNavigation = BottomNavigationComponent
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Button as PaperButton, type ButtonProps } from 'react-native-paper'
|
|
2
|
+
|
|
3
|
+
import { usePaperDefaults } from '../PaperDefaultsContext'
|
|
4
|
+
|
|
5
|
+
export function Button(props: ButtonProps) {
|
|
6
|
+
const defaults = usePaperDefaults()
|
|
7
|
+
return <PaperButton {...defaults.Button} {...props} />
|
|
8
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Chip as PaperChip, type ChipProps as PaperChipProps, useTheme } from 'react-native-paper'
|
|
2
|
+
|
|
3
|
+
import { usePaperDefaults } from '../PaperDefaultsContext'
|
|
4
|
+
|
|
5
|
+
export type ChipProps = PaperChipProps & {
|
|
6
|
+
variant?: 'primary' | 'secondary' | 'tertiary' | 'surface'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Chip({ variant, style, selectedColor, ...props }: ChipProps) {
|
|
10
|
+
const defaults = usePaperDefaults()
|
|
11
|
+
const { colors } = useTheme()
|
|
12
|
+
const variantStyle = variant === 'primary' ? { backgroundColor: colors.primaryContainer } : variant === 'secondary' ? { backgroundColor: colors.secondaryContainer } : variant === 'tertiary' ? { backgroundColor: colors.tertiaryContainer } : variant === 'surface' ? { backgroundColor: colors.surface } : undefined
|
|
13
|
+
const variantSelectedColor = variant === 'primary' ? colors.onPrimaryContainer : variant === 'secondary' ? colors.onSecondaryContainer : variant === 'tertiary' ? colors.onTertiaryContainer : variant === 'surface' ? colors.onSurface : undefined
|
|
14
|
+
return <PaperChip {...defaults.Chip} selectedColor={selectedColor ?? variantSelectedColor} {...props} style={[variantStyle, style]} />
|
|
15
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { StyleSheet, TouchableOpacity, View } from 'react-native'
|
|
3
|
+
import { Icon, TouchableRipple } from 'react-native-paper'
|
|
4
|
+
|
|
5
|
+
import { useBlur } from '../BlurContext'
|
|
6
|
+
import { Dialog } from './Dialog'
|
|
7
|
+
|
|
8
|
+
export type SeedColor = { label: string; value: string }
|
|
9
|
+
|
|
10
|
+
export const defaultColors: SeedColor[] = [
|
|
11
|
+
{ label: 'Red', value: '#f44336' },
|
|
12
|
+
{ label: 'Coral', value: '#ff5722' },
|
|
13
|
+
{ label: 'Orange', value: '#ff6d00' },
|
|
14
|
+
{ label: 'Amber', value: '#ff9800' },
|
|
15
|
+
{ label: 'Yellow', value: '#fdd835' },
|
|
16
|
+
{ label: 'Lime', value: '#8bc34a' },
|
|
17
|
+
{ label: 'Emerald', value: '#4caf50' },
|
|
18
|
+
{ label: 'Teal', value: '#009688' },
|
|
19
|
+
{ label: 'Cyan', value: '#00bcd4' },
|
|
20
|
+
{ label: 'Blue', value: '#2196f3' },
|
|
21
|
+
{ label: 'Indigo', value: '#3f51b5' },
|
|
22
|
+
{ label: 'Deep Purple', value: '#673ab7' },
|
|
23
|
+
{ label: 'Violet', value: '#9c27b0' },
|
|
24
|
+
{ label: 'Magenta', value: '#d500f9' },
|
|
25
|
+
{ label: 'Pink', value: '#ec407a' },
|
|
26
|
+
{ label: 'Rose', value: '#e91e63' },
|
|
27
|
+
{ label: 'Brown', value: '#795548' },
|
|
28
|
+
{ label: 'Blue Grey', value: '#607d8b' }
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
type Props = {
|
|
32
|
+
value: string
|
|
33
|
+
onChange: (color: string) => void
|
|
34
|
+
colors?: SeedColor[]
|
|
35
|
+
blur?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const ColorPicker = ({ value, onChange, colors = defaultColors, blur: blurProp }: Props) => {
|
|
39
|
+
const blur = useBlur(blurProp)
|
|
40
|
+
const [open, setOpen] = useState(false)
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<TouchableRipple onPress={() => setOpen(true)} style={[styles.trigger, { backgroundColor: value }]} borderless>
|
|
45
|
+
<Icon source='palette' size={20} color='white' />
|
|
46
|
+
</TouchableRipple>
|
|
47
|
+
|
|
48
|
+
<Dialog blur={blur} visible={open} onDismiss={() => setOpen(false)}>
|
|
49
|
+
<Dialog.Content>
|
|
50
|
+
<View style={styles.swatches}>
|
|
51
|
+
{colors.map(({ value: hex }) => (
|
|
52
|
+
<TouchableOpacity
|
|
53
|
+
key={hex}
|
|
54
|
+
onPress={() => {
|
|
55
|
+
onChange(hex)
|
|
56
|
+
setOpen(false)
|
|
57
|
+
}}
|
|
58
|
+
style={[styles.swatch, { backgroundColor: hex }, value === hex && styles.selected]}
|
|
59
|
+
>
|
|
60
|
+
{value === hex && <Icon source='check' size={20} color='white' />}
|
|
61
|
+
</TouchableOpacity>
|
|
62
|
+
))}
|
|
63
|
+
</View>
|
|
64
|
+
</Dialog.Content>
|
|
65
|
+
</Dialog>
|
|
66
|
+
</>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const styles = StyleSheet.create({
|
|
71
|
+
selected: { borderColor: 'white', borderWidth: 3 },
|
|
72
|
+
swatch: { alignItems: 'center', borderRadius: 24, height: 48, justifyContent: 'center', width: 48 },
|
|
73
|
+
swatches: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, paddingTop: 12 },
|
|
74
|
+
trigger: { alignItems: 'center', alignSelf: 'flex-start', borderRadius: 24, height: 48, justifyContent: 'center', width: 48 }
|
|
75
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { Animated, StyleSheet, TouchableWithoutFeedback, View } from 'react-native'
|
|
3
|
+
import { Dialog as PaperDialog, type DialogActionsProps, type DialogContentProps, type DialogProps as PaperDialogProps, type DialogScrollAreaProps, type DialogTitleProps, Portal, useTheme } from 'react-native-paper'
|
|
4
|
+
|
|
5
|
+
import { useBlur } from '../BlurContext'
|
|
6
|
+
import { BlurView } from './BlurView'
|
|
7
|
+
|
|
8
|
+
export type DialogProps = PaperDialogProps & {
|
|
9
|
+
blur?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function DialogComponent({ blur: blurProp, visible, onDismiss, children, style, ...rest }: DialogProps) {
|
|
13
|
+
const blur = useBlur(blurProp)
|
|
14
|
+
const { roundness, colors } = useTheme()
|
|
15
|
+
const opacity = useRef(new Animated.Value(0)).current
|
|
16
|
+
const [mounted, setMounted] = useState(visible)
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (visible) {
|
|
20
|
+
setMounted(true)
|
|
21
|
+
Animated.timing(opacity, { toValue: 1, duration: 200, useNativeDriver: true }).start()
|
|
22
|
+
} else {
|
|
23
|
+
Animated.timing(opacity, { toValue: 0, duration: 150, useNativeDriver: true }).start(() => setMounted(false))
|
|
24
|
+
}
|
|
25
|
+
}, [visible])
|
|
26
|
+
|
|
27
|
+
if (!blur) {
|
|
28
|
+
return (
|
|
29
|
+
<PaperDialog visible={visible} onDismiss={onDismiss} style={style} {...rest}>
|
|
30
|
+
{children}
|
|
31
|
+
</PaperDialog>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!mounted) return null
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Portal>
|
|
39
|
+
<Animated.View style={[StyleSheet.absoluteFill, { opacity }]} pointerEvents={visible ? 'box-none' : 'none'}>
|
|
40
|
+
<TouchableWithoutFeedback onPress={onDismiss}>
|
|
41
|
+
<View style={[StyleSheet.absoluteFill, { backgroundColor: colors.backdrop }]} />
|
|
42
|
+
</TouchableWithoutFeedback>
|
|
43
|
+
<View style={styles.wrapper} pointerEvents='box-none'>
|
|
44
|
+
<BlurView style={[styles.card, { borderRadius: roundness * 7 }]}>{children}</BlurView>
|
|
45
|
+
</View>
|
|
46
|
+
</Animated.View>
|
|
47
|
+
</Portal>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
DialogComponent.Title = PaperDialog.Title as React.FC<DialogTitleProps>
|
|
52
|
+
DialogComponent.Content = PaperDialog.Content as React.FC<DialogContentProps>
|
|
53
|
+
DialogComponent.Actions = PaperDialog.Actions as React.FC<DialogActionsProps>
|
|
54
|
+
DialogComponent.ScrollArea = PaperDialog.ScrollArea as React.FC<DialogScrollAreaProps>
|
|
55
|
+
|
|
56
|
+
export const Dialog = DialogComponent
|
|
57
|
+
|
|
58
|
+
const styles = StyleSheet.create({
|
|
59
|
+
card: { overflow: 'hidden' },
|
|
60
|
+
wrapper: { flex: 1, justifyContent: 'center', paddingHorizontal: 26 }
|
|
61
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { FAB as PaperFAB, type FABProps } from 'react-native-paper'
|
|
2
|
+
|
|
3
|
+
import { usePaperDefaults } from '../PaperDefaultsContext'
|
|
4
|
+
|
|
5
|
+
export function FAB(props: FABProps) {
|
|
6
|
+
const defaults = usePaperDefaults()
|
|
7
|
+
return <PaperFAB {...defaults.FAB} {...props} />
|
|
8
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Button } from 'react-native-paper'
|
|
3
|
+
|
|
4
|
+
import { useBlur } from '../BlurContext'
|
|
5
|
+
import type { ColorHarmony } from '../utils/getTriadicPalette'
|
|
6
|
+
import { Menu } from './Menu'
|
|
7
|
+
|
|
8
|
+
const HARMONIES: { label: string; value: ColorHarmony; icon: string }[] = [
|
|
9
|
+
{ label: 'Triadic', value: 'triadic', icon: 'triangle-outline' },
|
|
10
|
+
{ label: 'Split', value: 'split-complementary', icon: 'arrow-split-vertical' },
|
|
11
|
+
{ label: 'Analogous', value: 'analogous', icon: 'ray-end-arrow' },
|
|
12
|
+
{ label: 'Square', value: 'square', icon: 'square-outline' },
|
|
13
|
+
{ label: 'Complement', value: 'complementary', icon: 'circle-half-full' },
|
|
14
|
+
{ label: 'Dbl Split', value: 'double-split', icon: 'arrow-expand-horizontal' }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
type Props = {
|
|
18
|
+
value: ColorHarmony
|
|
19
|
+
onChange: (harmony: ColorHarmony) => void
|
|
20
|
+
blur?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const HarmonyPicker = ({ value, onChange, blur: blurProp }: Props) => {
|
|
24
|
+
const blur = useBlur(blurProp)
|
|
25
|
+
const [open, setOpen] = useState(false)
|
|
26
|
+
const selected = HARMONIES.find((h) => h.value === value)!
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Menu
|
|
30
|
+
blur={blur}
|
|
31
|
+
visible={open}
|
|
32
|
+
onDismiss={() => setOpen(false)}
|
|
33
|
+
anchor={
|
|
34
|
+
<Button mode='outlined' icon={selected.icon} onPress={() => setOpen(true)}>
|
|
35
|
+
{selected.label}
|
|
36
|
+
</Button>
|
|
37
|
+
}
|
|
38
|
+
>
|
|
39
|
+
{HARMONIES.map((h) => (
|
|
40
|
+
<Menu.Item
|
|
41
|
+
key={h.value}
|
|
42
|
+
leadingIcon={h.value === value ? 'check' : h.icon}
|
|
43
|
+
title={h.label}
|
|
44
|
+
onPress={() => {
|
|
45
|
+
onChange(h.value)
|
|
46
|
+
setOpen(false)
|
|
47
|
+
}}
|
|
48
|
+
/>
|
|
49
|
+
))}
|
|
50
|
+
</Menu>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { IconButton as PaperIconButton, type IconButtonProps as PaperIconButtonProps, useTheme } from 'react-native-paper'
|
|
2
|
+
|
|
3
|
+
import { usePaperDefaults } from '../PaperDefaultsContext'
|
|
4
|
+
|
|
5
|
+
export type IconButtonProps = PaperIconButtonProps & {
|
|
6
|
+
variant?: 'primary' | 'secondary' | 'tertiary' | 'surface'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function IconButton({ variant, iconColor, containerColor, ...props }: IconButtonProps) {
|
|
10
|
+
const defaults = usePaperDefaults()
|
|
11
|
+
const { colors } = useTheme()
|
|
12
|
+
const variantContainerColor = variant === 'primary' ? colors.primaryContainer : variant === 'secondary' ? colors.secondaryContainer : variant === 'tertiary' ? colors.tertiaryContainer : variant === 'surface' ? colors.surface : undefined
|
|
13
|
+
const variantIconColor = variant === 'primary' ? colors.onPrimaryContainer : variant === 'secondary' ? colors.onSecondaryContainer : variant === 'tertiary' ? colors.onTertiaryContainer : variant === 'surface' ? colors.onSurface : undefined
|
|
14
|
+
return <PaperIconButton {...defaults.IconButton} containerColor={containerColor ?? variantContainerColor} iconColor={iconColor ?? variantIconColor} {...props} />
|
|
15
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { StyleSheet } from 'react-native'
|
|
3
|
+
import { Menu as PaperMenu, type MenuItemProps, type MenuProps as PaperMenuProps } from 'react-native-paper'
|
|
4
|
+
|
|
5
|
+
import { useBlur } from '../BlurContext'
|
|
6
|
+
import { BlurView } from './BlurView'
|
|
7
|
+
|
|
8
|
+
export type MenuProps = PaperMenuProps & {
|
|
9
|
+
blur?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const MenuComponent = ({ blur: blurProp, children, ...props }: MenuProps) => {
|
|
13
|
+
const blur = useBlur(blurProp)
|
|
14
|
+
return (
|
|
15
|
+
<PaperMenu {...props} contentStyle={[styles.content, props.contentStyle]}>
|
|
16
|
+
<BlurView blur={blur}>{children}</BlurView>
|
|
17
|
+
</PaperMenu>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Menu = Object.assign(MenuComponent, {
|
|
22
|
+
Item: PaperMenu.Item as React.FC<MenuItemProps>
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const styles = StyleSheet.create({
|
|
26
|
+
content: { backgroundColor: 'transparent', paddingVertical: 0 }
|
|
27
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { forwardRef } from 'react'
|
|
2
|
+
import { TextInput as RNTextInput } from 'react-native'
|
|
3
|
+
import { TextInput as PaperTextInput, type TextInputProps } from 'react-native-paper'
|
|
4
|
+
|
|
5
|
+
import { usePaperDefaults } from '../PaperDefaultsContext'
|
|
6
|
+
|
|
7
|
+
export const TextInput = forwardRef<RNTextInput, TextInputProps>(function TextInput(props, ref) {
|
|
8
|
+
const defaults = usePaperDefaults()
|
|
9
|
+
return <PaperTextInput ref={ref} {...defaults.TextInput} {...props} />
|
|
10
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export { useBlur } from './BlurContext'
|
|
2
|
+
export { Appbar } from './components/Appbar'
|
|
3
|
+
export { AppearancePicker } from './components/AppearancePicker'
|
|
4
|
+
export type { BlurViewProps } from './components/BlurView'
|
|
5
|
+
export { BlurView } from './components/BlurView'
|
|
6
|
+
export { BottomNavigation } from './components/BottomNavigation'
|
|
7
|
+
export { Button } from './components/Button'
|
|
8
|
+
export type { ChipProps } from './components/Chip'
|
|
9
|
+
export { Chip } from './components/Chip'
|
|
10
|
+
export type { SeedColor } from './components/ColorPicker'
|
|
11
|
+
export { ColorPicker, defaultColors } from './components/ColorPicker'
|
|
12
|
+
export type { DialogProps } from './components/Dialog'
|
|
13
|
+
export { Dialog } from './components/Dialog'
|
|
14
|
+
export { FAB } from './components/FAB'
|
|
15
|
+
export { HarmonyPicker } from './components/HarmonyPicker'
|
|
16
|
+
export type { IconButtonProps } from './components/IconButton'
|
|
17
|
+
export { IconButton } from './components/IconButton'
|
|
18
|
+
export type { MenuProps } from './components/Menu'
|
|
19
|
+
export { Menu } from './components/Menu'
|
|
20
|
+
export { TextInput } from './components/TextInput'
|
|
21
|
+
export type { PaperDefaults } from './PaperDefaultsContext'
|
|
22
|
+
export { usePaperDefaults } from './PaperDefaultsContext'
|
|
23
|
+
export type { ThemeState } from './redux/themeSlice'
|
|
24
|
+
export { createThemeReducer, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer } from './redux/themeSlice'
|
|
25
|
+
export type { ProviderProps } from './ThemeProvider'
|
|
26
|
+
export { Provider } from './ThemeProvider'
|
|
27
|
+
export type { ThemeSettings, ThemeSettingsContextType } from './ThemeSettingsContext'
|
|
28
|
+
export { defaultThemeSettings, ThemeSettingsContext } from './ThemeSettingsContext'
|
|
29
|
+
export type { ThemeAppearance } from './useComputedTheme'
|
|
30
|
+
export { useComputedTheme } from './useComputedTheme'
|
|
31
|
+
export { useThemeSettings } from './useThemeSettings'
|
|
32
|
+
export { getBlendedColor } from './utils/getBlendedColor'
|
|
33
|
+
export { getHex } from './utils/getHex'
|
|
34
|
+
export type { RGB } from './utils/getRgb'
|
|
35
|
+
export { getRgb } from './utils/getRgb'
|
|
36
|
+
export { getTintTextColor } from './utils/getTintTextColor'
|
|
37
|
+
export { getTonalColor } from './utils/getTonalColor'
|
|
38
|
+
export type { ColorHarmony, TriadicPalette } from './utils/getTriadicPalette'
|
|
39
|
+
export { getTriadicPalette } from './utils/getTriadicPalette'
|
|
40
|
+
export { isDarkColor } from './utils/isDarkColor'
|
|
41
|
+
export type { AppbarHeaderProps } from 'react-native-paper'
|
|
42
|
+
export type { BottomNavigationProps } from 'react-native-paper'
|
|
43
|
+
export type { BottomNavigationRoute } from 'react-native-paper'
|
|
44
|
+
export type { ButtonProps } from 'react-native-paper'
|
|
45
|
+
export type { ChipProps as PaperChipProps } from 'react-native-paper'
|
|
46
|
+
export type { DialogProps as PaperDialogProps } from 'react-native-paper'
|
|
47
|
+
export type { FABProps } from 'react-native-paper'
|
|
48
|
+
export type { IconButtonProps as PaperIconButtonProps } from 'react-native-paper'
|
|
49
|
+
export type { MenuProps as PaperMenuProps } from 'react-native-paper'
|
|
50
|
+
export type { TextInputProps } from 'react-native-paper'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const navigationBar = (() => {
|
|
2
|
+
try {
|
|
3
|
+
return require('expo-navigation-bar') as typeof import('expo-navigation-bar')
|
|
4
|
+
} catch {
|
|
5
|
+
return null
|
|
6
|
+
}
|
|
7
|
+
})()
|
|
8
|
+
|
|
9
|
+
/** Sets the Android navigation bar icon style (expo-navigation-bar >= 56, edge-to-edge API). */
|
|
10
|
+
export const setNavigationBarStyle = (dark: boolean) => {
|
|
11
|
+
if (!navigationBar) return
|
|
12
|
+
navigationBar.NavigationBar.setStyle(dark ? 'light' : 'dark')
|
|
13
|
+
}
|