@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rific/auto-paper",
3
- "version": "0.5.1",
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.73.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,8 @@
1
+ import { useContext } from 'react'
2
+
3
+ import { ThemeSettingsContext } from './ThemeSettingsContext'
4
+
5
+ export function useBlur(override?: boolean): boolean {
6
+ const { settings } = useContext(ThemeSettingsContext)
7
+ return override ?? settings.blur
8
+ }
@@ -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
+ }