@rific/auto-paper 0.9.4 → 0.10.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 CHANGED
@@ -10,6 +10,7 @@ Adaptive theming for [`react-native-paper`](https://callstack.github.io/react-na
10
10
  - System/light/dark appearance with live updates via `Appearance` API
11
11
  - Tinted surface, surfaceVariant, outline, and elevation levels derived from the seed
12
12
  - Fixed `success`/`warning`/`danger` semantic color roles (each with `on*`/`*Container` variants) alongside MD3's built-in `error`
13
+ - App-wide `fontFamily` override, applied across every Paper typography variant in one prop
13
14
  - Optional Redux slice for wiring appearance and color into your store
14
15
  - Wrapper components (`Appbar`, `AppearancePicker`, `AutoAppearancePicker`, `AutoPalettePicker`, `BlurView`, `BottomNavigation`, `Button`, `Chip`, `ColorPicker`, `Dialog`, `FAB`, `HarmonyPicker`, `IconButton`, `Menu`, `PalettePicker`, `TextInput`) with prop defaults via context
15
16
  - All color utilities exported for standalone use
@@ -61,6 +62,14 @@ export default function App() {
61
62
  </AutoPaperProvider>
62
63
  ```
63
64
 
65
+ Pass `fontFamily` to reskin every Paper component's text to a custom typeface app-wide, instead of passing `fontFamily` to each individual `Text`/`TextInput`/`Button` yourself:
66
+
67
+ ```tsx
68
+ <AutoPaperProvider initialValue={{ appearance: 'system', color: '#6750a4' }} fontFamily='Menlo'>
69
+ {/* your app */}
70
+ </AutoPaperProvider>
71
+ ```
72
+
64
73
  Update theme settings from anywhere inside the tree using `useThemeSettings`:
65
74
 
66
75
  ```tsx
@@ -235,6 +244,7 @@ export default function App() {
235
244
  | `children` | `ReactNode` | |
236
245
  | `defaults` | `PaperDefaults` | Prop defaults for wrapper components (see below) |
237
246
  | `expoBlur` | `ExpoBlurModule` | Injects `expo-blur` (`import * as ExpoBlur from 'expo-blur'`) so `<BlurView>` renders the real frosted-glass effect. Omit to always render its solid fallback. |
247
+ | `fontFamily` | `string` | Applied to every Paper typography variant (`bodyLarge`, `headlineMedium`, `labelSmall`, ...) via `configureFonts`'s flat-config mode — each variant keeps its own MD3 fontSize/lineHeight/weight, only fontFamily changes. Omit to keep each platform's system font. |
238
248
  | `navigationBar` | `ExpoNavigationBarModule` | Injects `expo-navigation-bar` (`import * as ExpoNavigationBar from 'expo-navigation-bar'`) so the Android nav bar icon style auto-syncs with the theme while a `BottomNavigation` is mounted. Omit (and don't pass `onNavBarChange`) to skip nav bar syncing entirely. |
239
249
  | `onNavBarChange` | `(color: string, dark: boolean) => void` | Overrides the built-in nav bar sync: called on Android when the theme changes while a `BottomNavigation` is mounted, instead of the automatic `navigationBar`-driven icon-style sync |
240
250
  | `onReady` | `() => void` | Called once when the theme first resolves |
package/dist/index.d.mts CHANGED
@@ -305,6 +305,12 @@ type ProviderProps = {
305
305
  defaults?: PaperDefaults;
306
306
  /** Injects expo-blur for `<BlurView>`. Pass `import * as ExpoBlur from 'expo-blur'`; omit to always render BlurView's solid fallback. */
307
307
  expoBlur?: ExpoBlurModule;
308
+ /** Applied to every Paper typography variant (bodyLarge, headlineMedium, labelSmall, ...) via
309
+ * `configureFonts`'s flat-config mode — each variant keeps its own MD3 fontSize/lineHeight/weight,
310
+ * only fontFamily changes. The one-line way to reskin an app's whole text to a custom typeface,
311
+ * instead of passing `fontFamily` to every individual Text/TextInput/Button across the app. Omit
312
+ * to keep each platform's system font (MD3's own default). */
313
+ fontFamily?: string;
308
314
  initialValue?: Partial<ThemeSettings>;
309
315
  /** Injects expo-navigation-bar so the Android nav bar icon style auto-syncs with the theme. Pass `import * as ExpoNavigationBar from 'expo-navigation-bar'`; omit (and don't pass onNavBarChange) to skip nav bar syncing entirely. */
310
316
  navigationBar?: ExpoNavigationBarModule;
@@ -316,7 +322,7 @@ type ProviderProps = {
316
322
  statusBarProps?: StatusBarProps;
317
323
  style?: StyleProp<ViewStyle>;
318
324
  };
319
- declare function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps): react.JSX.Element | null;
325
+ declare function Provider({ children, defaults, expoBlur, fontFamily, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps): react.JSX.Element | null;
320
326
 
321
327
  declare const useAutoPaperTheme: () => AutoPaperTheme;
322
328
 
package/dist/index.d.ts CHANGED
@@ -305,6 +305,12 @@ type ProviderProps = {
305
305
  defaults?: PaperDefaults;
306
306
  /** Injects expo-blur for `<BlurView>`. Pass `import * as ExpoBlur from 'expo-blur'`; omit to always render BlurView's solid fallback. */
307
307
  expoBlur?: ExpoBlurModule;
308
+ /** Applied to every Paper typography variant (bodyLarge, headlineMedium, labelSmall, ...) via
309
+ * `configureFonts`'s flat-config mode — each variant keeps its own MD3 fontSize/lineHeight/weight,
310
+ * only fontFamily changes. The one-line way to reskin an app's whole text to a custom typeface,
311
+ * instead of passing `fontFamily` to every individual Text/TextInput/Button across the app. Omit
312
+ * to keep each platform's system font (MD3's own default). */
313
+ fontFamily?: string;
308
314
  initialValue?: Partial<ThemeSettings>;
309
315
  /** Injects expo-navigation-bar so the Android nav bar icon style auto-syncs with the theme. Pass `import * as ExpoNavigationBar from 'expo-navigation-bar'`; omit (and don't pass onNavBarChange) to skip nav bar syncing entirely. */
310
316
  navigationBar?: ExpoNavigationBarModule;
@@ -316,7 +322,7 @@ type ProviderProps = {
316
322
  statusBarProps?: StatusBarProps;
317
323
  style?: StyleProp<ViewStyle>;
318
324
  };
319
- declare function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps): react.JSX.Element | null;
325
+ declare function Provider({ children, defaults, expoBlur, fontFamily, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps): react.JSX.Element | null;
320
326
 
321
327
  declare const useAutoPaperTheme: () => AutoPaperTheme;
322
328
 
package/dist/index.js CHANGED
@@ -685,13 +685,15 @@ var BlurModuleContext = (0, import_react7.createContext)(void 0);
685
685
  var useBlurModule = () => (0, import_react7.useContext)(BlurModuleContext);
686
686
  var ReanimatedModuleContext = (0, import_react7.createContext)(void 0);
687
687
  var useReanimatedModule = () => (0, import_react7.useContext)(ReanimatedModuleContext);
688
- function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }) {
688
+ function Provider({ children, defaults, expoBlur, fontFamily, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }) {
689
689
  const [settings, setSettings] = (0, import_react7.useState)(() => ({ ...defaultThemeSettings, ...initialValue }));
690
690
  const set = (0, import_react7.useCallback)((patch) => {
691
691
  setSettings((prev) => ({ ...prev, ...patch }));
692
692
  }, []);
693
693
  const onChangeRef = (0, import_react7.useRef)(onChange);
694
- onChangeRef.current = onChange;
694
+ (0, import_react7.useEffect)(() => {
695
+ onChangeRef.current = onChange;
696
+ });
695
697
  const prevSettings = (0, import_react7.useRef)(settings);
696
698
  (0, import_react7.useEffect)(() => {
697
699
  if (prevSettings.current !== settings) {
@@ -699,7 +701,12 @@ function Provider({ children, defaults, expoBlur, initialValue, navigationBar, o
699
701
  onChangeRef.current?.(settings);
700
702
  }
701
703
  }, [settings]);
702
- const theme = useComputedTheme(settings.appearance, settings.color, settings.harmony);
704
+ const computedTheme = useComputedTheme(settings.appearance, settings.color, settings.harmony);
705
+ const theme = (0, import_react7.useMemo)(() => {
706
+ if (!computedTheme) return null;
707
+ if (!fontFamily) return computedTheme;
708
+ return { ...computedTheme, fonts: (0, import_react_native_paper4.configureFonts)({ config: { fontFamily } }) };
709
+ }, [computedTheme, fontFamily]);
703
710
  const called = (0, import_react7.useRef)(false);
704
711
  (0, import_react7.useEffect)(() => {
705
712
  if (theme && !called.current) {
@@ -756,18 +763,20 @@ function DialogComponent({ animatedStyle, blur: blurProp, dismissable = true, di
756
763
  const { colors, isV3, roundness } = theme;
757
764
  const { scale } = theme.animation;
758
765
  const { bottom, left, right, top } = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
759
- const opacity = (0, import_react9.useRef)(new import_react_native5.Animated.Value(visible ? 1 : 0)).current;
766
+ const [opacity] = (0, import_react9.useState)(() => new import_react_native5.Animated.Value(visible ? 1 : 0));
760
767
  const [mounted, setMounted] = (0, import_react9.useState)(visible);
768
+ if (visible && !mounted) {
769
+ setMounted(true);
770
+ }
761
771
  (0, import_react9.useEffect)(() => {
762
772
  if (visible) {
763
- setMounted(true);
764
773
  import_react_native5.Animated.timing(opacity, { duration: scale * ANIMATION_DURATION, easing: import_react_native5.Easing.out(import_react_native5.Easing.cubic), toValue: 1, useNativeDriver: true }).start();
765
774
  } else {
766
775
  import_react_native5.Animated.timing(opacity, { duration: scale * ANIMATION_DURATION, easing: import_react_native5.Easing.out(import_react_native5.Easing.cubic), toValue: 0, useNativeDriver: true }).start(({ finished }) => {
767
776
  if (finished) setMounted(false);
768
777
  });
769
778
  }
770
- }, [visible]);
779
+ }, [visible, opacity, scale]);
771
780
  (0, import_react9.useEffect)(() => {
772
781
  if (!visible) return void 0;
773
782
  const onHardwareBackPress = () => {
package/dist/index.mjs CHANGED
@@ -417,15 +417,15 @@ import { StyleSheet as StyleSheet4, TouchableOpacity, View as View4 } from "reac
417
417
  import { Icon, TouchableRipple, useTheme as useTheme4 } from "react-native-paper";
418
418
 
419
419
  // src/components/Dialog.tsx
420
- import React, { useEffect as useEffect3, useRef as useRef2, useState as useState4 } from "react";
420
+ import React, { useEffect as useEffect3, useState as useState4 } from "react";
421
421
  import { Animated, BackHandler, Easing, Pressable, StyleSheet as StyleSheet3, View as View3 } from "react-native";
422
422
  import { Dialog as PaperDialog, Portal, useTheme as useTheme3 } from "react-native-paper";
423
423
  import { useSafeAreaInsets } from "react-native-safe-area-context";
424
424
 
425
425
  // src/ThemeProvider.tsx
426
- import { createContext as createContext3, useCallback as useCallback2, useContext as useContext4, useEffect as useEffect2, useRef, useState as useState3 } from "react";
426
+ import { createContext as createContext3, useCallback as useCallback2, useContext as useContext4, useEffect as useEffect2, useMemo, useRef, useState as useState3 } from "react";
427
427
  import { StatusBar as StatusBar2, StyleSheet, View } from "react-native";
428
- import { Provider as PaperProvider } from "react-native-paper";
428
+ import { configureFonts, Provider as PaperProvider } from "react-native-paper";
429
429
 
430
430
  // src/useComputedTheme.ts
431
431
  import { useCallback, useEffect, useState as useState2 } from "react";
@@ -603,13 +603,15 @@ var BlurModuleContext = createContext3(void 0);
603
603
  var useBlurModule = () => useContext4(BlurModuleContext);
604
604
  var ReanimatedModuleContext = createContext3(void 0);
605
605
  var useReanimatedModule = () => useContext4(ReanimatedModuleContext);
606
- function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }) {
606
+ function Provider({ children, defaults, expoBlur, fontFamily, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }) {
607
607
  const [settings, setSettings] = useState3(() => ({ ...defaultThemeSettings, ...initialValue }));
608
608
  const set = useCallback2((patch) => {
609
609
  setSettings((prev) => ({ ...prev, ...patch }));
610
610
  }, []);
611
611
  const onChangeRef = useRef(onChange);
612
- onChangeRef.current = onChange;
612
+ useEffect2(() => {
613
+ onChangeRef.current = onChange;
614
+ });
613
615
  const prevSettings = useRef(settings);
614
616
  useEffect2(() => {
615
617
  if (prevSettings.current !== settings) {
@@ -617,7 +619,12 @@ function Provider({ children, defaults, expoBlur, initialValue, navigationBar, o
617
619
  onChangeRef.current?.(settings);
618
620
  }
619
621
  }, [settings]);
620
- const theme = useComputedTheme(settings.appearance, settings.color, settings.harmony);
622
+ const computedTheme = useComputedTheme(settings.appearance, settings.color, settings.harmony);
623
+ const theme = useMemo(() => {
624
+ if (!computedTheme) return null;
625
+ if (!fontFamily) return computedTheme;
626
+ return { ...computedTheme, fonts: configureFonts({ config: { fontFamily } }) };
627
+ }, [computedTheme, fontFamily]);
621
628
  const called = useRef(false);
622
629
  useEffect2(() => {
623
630
  if (theme && !called.current) {
@@ -674,18 +681,20 @@ function DialogComponent({ animatedStyle, blur: blurProp, dismissable = true, di
674
681
  const { colors, isV3, roundness } = theme;
675
682
  const { scale } = theme.animation;
676
683
  const { bottom, left, right, top } = useSafeAreaInsets();
677
- const opacity = useRef2(new Animated.Value(visible ? 1 : 0)).current;
684
+ const [opacity] = useState4(() => new Animated.Value(visible ? 1 : 0));
678
685
  const [mounted, setMounted] = useState4(visible);
686
+ if (visible && !mounted) {
687
+ setMounted(true);
688
+ }
679
689
  useEffect3(() => {
680
690
  if (visible) {
681
- setMounted(true);
682
691
  Animated.timing(opacity, { duration: scale * ANIMATION_DURATION, easing: Easing.out(Easing.cubic), toValue: 1, useNativeDriver: true }).start();
683
692
  } else {
684
693
  Animated.timing(opacity, { duration: scale * ANIMATION_DURATION, easing: Easing.out(Easing.cubic), toValue: 0, useNativeDriver: true }).start(({ finished }) => {
685
694
  if (finished) setMounted(false);
686
695
  });
687
696
  }
688
- }, [visible]);
697
+ }, [visible, opacity, scale]);
689
698
  useEffect3(() => {
690
699
  if (!visible) return void 0;
691
700
  const onHardwareBackPress = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rific/auto-paper",
3
- "version": "0.9.4",
3
+ "version": "0.10.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",
@@ -76,6 +76,7 @@
76
76
  "eslint-config-prettier": "^10.1.8",
77
77
  "eslint-plugin-package-json": "^1.0.0",
78
78
  "eslint-plugin-prettier": "^5.5.5",
79
+ "eslint-plugin-react-hooks": "^7.1.1",
79
80
  "eslint-plugin-react-native": "^5.0.0",
80
81
  "eslint-plugin-simple-import-sort": "^13.0.0",
81
82
  "expo-blur": "^56.0.3",
@@ -1,6 +1,6 @@
1
- import { createContext, type ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react'
1
+ import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
2
2
  import { StatusBar, type StatusBarProps, type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'
3
- import { Provider as PaperProvider } from 'react-native-paper'
3
+ import { configureFonts, Provider as PaperProvider } from 'react-native-paper'
4
4
 
5
5
  import type { ExpoBlurModule } from './components/BlurView'
6
6
  import type { ReanimatedModule } from './components/Dialog'
@@ -29,6 +29,12 @@ export type ProviderProps = {
29
29
  defaults?: PaperDefaults
30
30
  /** Injects expo-blur for `<BlurView>`. Pass `import * as ExpoBlur from 'expo-blur'`; omit to always render BlurView's solid fallback. */
31
31
  expoBlur?: ExpoBlurModule
32
+ /** Applied to every Paper typography variant (bodyLarge, headlineMedium, labelSmall, ...) via
33
+ * `configureFonts`'s flat-config mode — each variant keeps its own MD3 fontSize/lineHeight/weight,
34
+ * only fontFamily changes. The one-line way to reskin an app's whole text to a custom typeface,
35
+ * instead of passing `fontFamily` to every individual Text/TextInput/Button across the app. Omit
36
+ * to keep each platform's system font (MD3's own default). */
37
+ fontFamily?: string
32
38
  initialValue?: Partial<ThemeSettings>
33
39
  /** Injects expo-navigation-bar so the Android nav bar icon style auto-syncs with the theme. Pass `import * as ExpoNavigationBar from 'expo-navigation-bar'`; omit (and don't pass onNavBarChange) to skip nav bar syncing entirely. */
34
40
  navigationBar?: ExpoNavigationBarModule
@@ -41,7 +47,7 @@ export type ProviderProps = {
41
47
  style?: StyleProp<ViewStyle>
42
48
  }
43
49
 
44
- export function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps) {
50
+ export function Provider({ children, defaults, expoBlur, fontFamily, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps) {
45
51
  const [settings, setSettings] = useState<ThemeSettings>(() => ({ ...defaultThemeSettings, ...initialValue }))
46
52
 
47
53
  const set = useCallback((patch: Partial<ThemeSettings>) => {
@@ -49,7 +55,9 @@ export function Provider({ children, defaults, expoBlur, initialValue, navigatio
49
55
  }, [])
50
56
 
51
57
  const onChangeRef = useRef(onChange)
52
- onChangeRef.current = onChange
58
+ useEffect(() => {
59
+ onChangeRef.current = onChange
60
+ })
53
61
  const prevSettings = useRef(settings)
54
62
  useEffect(() => {
55
63
  if (prevSettings.current !== settings) {
@@ -58,7 +66,17 @@ export function Provider({ children, defaults, expoBlur, initialValue, navigatio
58
66
  }
59
67
  }, [settings])
60
68
 
61
- const theme = useComputedTheme(settings.appearance, settings.color, settings.harmony)
69
+ const computedTheme = useComputedTheme(settings.appearance, settings.color, settings.harmony)
70
+ // Layered on top of useComputedTheme's own output rather than folded into it — that hook only
71
+ // ever recomputes for a color/appearance/harmony change, and fontFamily is a completely
72
+ // independent axis; merging it there would mean either re-running the whole color computation on
73
+ // every fontFamily change (wasteful) or threading fontFamily through as a fourth computeTheme
74
+ // dependency for a value it never actually uses.
75
+ const theme = useMemo(() => {
76
+ if (!computedTheme) return null
77
+ if (!fontFamily) return computedTheme
78
+ return { ...computedTheme, fonts: configureFonts({ config: { fontFamily } }) }
79
+ }, [computedTheme, fontFamily])
62
80
  const called = useRef(false)
63
81
 
64
82
  useEffect(() => {
@@ -1,4 +1,4 @@
1
- import React, { type ComponentType, type ReactNode, useEffect, useRef, useState } from 'react'
1
+ import React, { type ComponentType, type ReactNode, useEffect, useState } from 'react'
2
2
  import { Animated, BackHandler, Easing, Pressable, type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'
3
3
  import { Dialog as PaperDialog, type DialogActionsProps, type DialogContentProps, type DialogProps as PaperDialogProps, type DialogScrollAreaProps, type DialogTitleProps, Portal, useTheme } from 'react-native-paper'
4
4
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
@@ -46,19 +46,22 @@ function DialogComponent({ animatedStyle, blur: blurProp, dismissable = true, di
46
46
  const { colors, isV3, roundness } = theme
47
47
  const { scale } = theme.animation
48
48
  const { bottom, left, right, top } = useSafeAreaInsets()
49
- const opacity = useRef(new Animated.Value(visible ? 1 : 0)).current
49
+ const [opacity] = useState(() => new Animated.Value(visible ? 1 : 0))
50
50
  const [mounted, setMounted] = useState(visible)
51
51
 
52
+ if (visible && !mounted) {
53
+ setMounted(true)
54
+ }
55
+
52
56
  useEffect(() => {
53
57
  if (visible) {
54
- setMounted(true)
55
58
  Animated.timing(opacity, { duration: scale * ANIMATION_DURATION, easing: Easing.out(Easing.cubic), toValue: 1, useNativeDriver: true }).start()
56
59
  } else {
57
60
  Animated.timing(opacity, { duration: scale * ANIMATION_DURATION, easing: Easing.out(Easing.cubic), toValue: 0, useNativeDriver: true }).start(({ finished }) => {
58
61
  if (finished) setMounted(false)
59
62
  })
60
63
  }
61
- }, [visible])
64
+ }, [visible, opacity, scale])
62
65
 
63
66
  useEffect(() => {
64
67
  if (!visible) return undefined
@@ -147,6 +147,7 @@ export function useComputedTheme(appearance: ThemeAppearance, color: string | Tr
147
147
  }, [appearance, primaryInput, secondaryInput, tertiaryInput, harmony])
148
148
 
149
149
  useEffect(() => {
150
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: first render must stay theme-less (README's onReady/splash-screen contract) and Appearance must not be read during render (SSR/hydration risk)
150
151
  computeTheme()
151
152
  }, [computeTheme])
152
153