@rific/auto-paper 0.8.0 → 0.9.1

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/dist/index.mjs CHANGED
@@ -1,10 +1,3 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
7
-
8
1
  // src/BlurContext.tsx
9
2
  import { useContext } from "react";
10
3
 
@@ -76,60 +69,13 @@ var AppearancePicker = ({ value, onChange, showLabels = true, icons }) => {
76
69
  };
77
70
 
78
71
  // src/components/BlurView.tsx
79
- import { useContext as useContext3 } from "react";
80
- import { StyleSheet, View } from "react-native";
72
+ import { useContext as useContext4 } from "react";
73
+ import { StyleSheet as StyleSheet2, View as View2 } from "react-native";
81
74
  import { useTheme as useTheme2 } from "react-native-paper";
82
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
83
- var ExpoBlurView = (() => {
84
- try {
85
- return __require("expo-blur").BlurView;
86
- } catch {
87
- return null;
88
- }
89
- })();
90
- var ELEVATION_OPACITY = { 0: 0, 1: 0.05, 2: 0.08, 3: 0.11, 4: 0.12, 5: 0.14 };
91
- var VARIANT_TINT_OPACITY = { light: 0.12, dark: 0.18 };
92
- var BlurView = ({ blur = true, children, elevation, style, tintColor, tintOpacity, variant = "surface", ...props }) => {
93
- const { dark, colors } = useTheme2();
94
- const { settings } = useContext3(ThemeSettingsContext);
95
- const surfaceColor = variant === "surfaceVariant" ? colors.surfaceVariant : colors.surface;
96
- const variantColor = variant === "primary" ? colors.primary : variant === "secondary" ? colors.secondary : variant === "tertiary" ? colors.tertiary : variant === "error" ? colors.error : void 0;
97
- const resolvedTintColor = tintColor ?? variantColor ?? (elevation !== void 0 ? colors.primary : void 0);
98
- const resolvedTintOpacity = tintOpacity ?? (tintColor ? dark ? 0.24 : 0.16 : variantColor ? dark ? VARIANT_TINT_OPACITY.dark : VARIANT_TINT_OPACITY.light : elevation !== void 0 ? ELEVATION_OPACITY[elevation] : 0);
99
- const tint = resolvedTintColor && resolvedTintOpacity > 0 ? /* @__PURE__ */ jsx3(View, { style: [StyleSheet.absoluteFill, { backgroundColor: resolvedTintColor, opacity: resolvedTintOpacity }] }) : null;
100
- if (blur && ExpoBlurView)
101
- return /* @__PURE__ */ jsxs2(ExpoBlurView, { ...props, tint: dark ? "dark" : "light", style, children: [
102
- /* @__PURE__ */ jsx3(View, { style: [StyleSheet.absoluteFill, { backgroundColor: surfaceColor, opacity: settings.blurTint }] }),
103
- tint,
104
- children
105
- ] });
106
- return /* @__PURE__ */ jsxs2(View, { ...props, style: [{ backgroundColor: surfaceColor }, style], children: [
107
- tint,
108
- children
109
- ] });
110
- };
111
-
112
- // src/components/BottomNavigation.tsx
113
- import { useEffect as useEffect3 } from "react";
114
- import { Platform } from "react-native";
115
- import { BottomNavigation as PaperBottomNavigation, useTheme as useTheme3 } from "react-native-paper";
116
-
117
- // src/navigation-bar.ts
118
- var navigationBar = (() => {
119
- try {
120
- return __require("expo-navigation-bar");
121
- } catch {
122
- return null;
123
- }
124
- })();
125
- var setNavigationBarStyle = (dark) => {
126
- if (!navigationBar) return;
127
- navigationBar.NavigationBar.setStyle(dark ? "light" : "dark");
128
- };
129
75
 
130
76
  // src/ThemeProvider.tsx
131
- import { createContext as createContext3, useCallback as useCallback2, useContext as useContext4, useEffect as useEffect2, useRef, useState as useState2 } from "react";
132
- import { StatusBar as StatusBar2, StyleSheet as StyleSheet2, View as View2 } from "react-native";
77
+ import { createContext as createContext3, useCallback as useCallback2, useContext as useContext3, useEffect as useEffect2, useRef, useState as useState2 } from "react";
78
+ import { StatusBar as StatusBar2, StyleSheet, View } from "react-native";
133
79
  import { Provider as PaperProvider } from "react-native-paper";
134
80
 
135
81
  // src/useComputedTheme.ts
@@ -518,6 +464,10 @@ function hslToHex(h, s, l) {
518
464
  }
519
465
 
520
466
  // src/useComputedTheme.ts
467
+ function normalizeExplicitColor(value) {
468
+ if (!getRgb(value)) throw new Error(`Invalid color format: ${value}`);
469
+ return getHex(value) ?? value;
470
+ }
521
471
  var SEMANTIC_BASE_COLORS = {
522
472
  success: "#2E7D32",
523
473
  warning: "#A15C00",
@@ -525,6 +475,9 @@ var SEMANTIC_BASE_COLORS = {
525
475
  };
526
476
  function useComputedTheme(appearance, color, harmony = "split-complementary") {
527
477
  const [theme, setTheme] = useState(null);
478
+ const primaryInput = typeof color === "string" ? color : color.primary;
479
+ const secondaryInput = typeof color === "string" ? null : color.secondary;
480
+ const tertiaryInput = typeof color === "string" ? null : color.tertiary;
528
481
  const computeTheme = useCallback(() => {
529
482
  let resolved = appearance;
530
483
  if (resolved === "system") resolved = Appearance.getColorScheme();
@@ -553,7 +506,7 @@ function useComputedTheme(appearance, color, harmony = "split-complementary") {
553
506
  onDangerContainer: dangerRoles.onContainer
554
507
  }
555
508
  };
556
- const palette = getTriadicPalette(color, harmony);
509
+ const palette = secondaryInput !== null && tertiaryInput !== null ? { primary: normalizeExplicitColor(primaryInput), secondary: normalizeExplicitColor(secondaryInput), tertiary: normalizeExplicitColor(tertiaryInput) } : getTriadicPalette(primaryInput, harmony);
557
510
  const primaryRoles = getColorRoles(palette.primary, surface);
558
511
  _theme.colors.primary = primaryRoles.color;
559
512
  _theme.colors.onPrimary = primaryRoles.onColor;
@@ -569,12 +522,12 @@ function useComputedTheme(appearance, color, harmony = "split-complementary") {
569
522
  _theme.colors.onTertiary = tertiaryRoles.onColor;
570
523
  _theme.colors.tertiaryContainer = tertiaryRoles.container;
571
524
  _theme.colors.onTertiaryContainer = tertiaryRoles.onContainer;
572
- const blended = getBlendedColor(color, palette.secondary, 0.5);
525
+ const blended = getBlendedColor(palette.primary, palette.secondary, 0.5);
573
526
  const tintSurface = (alpha) => getBlendedColor(blended, surface, alpha);
574
527
  _theme.colors.background = base.dark ? "#0F0F0F" : "#F0F0F0";
575
528
  _theme.colors.surface = surface;
576
529
  _theme.colors.surfaceVariant = tintSurface(0.2);
577
- _theme.colors.outline = getBlendedColor(color, base.colors.outline, 0.45);
530
+ _theme.colors.outline = getBlendedColor(palette.primary, base.colors.outline, 0.45);
578
531
  const grey = base.dark ? "#424242" : "#c2c2c2";
579
532
  const blendedGrey = getBlendedColor(blended, grey, 0.05);
580
533
  _theme.colors.elevation = {
@@ -586,7 +539,7 @@ function useComputedTheme(appearance, color, harmony = "split-complementary") {
586
539
  level5: getBlendedColor(blendedGrey, _theme.colors.surface, 0.7)
587
540
  };
588
541
  setTheme(_theme);
589
- }, [appearance, color, harmony]);
542
+ }, [appearance, primaryInput, secondaryInput, tertiaryInput, harmony]);
590
543
  useEffect(() => {
591
544
  computeTheme();
592
545
  }, [computeTheme]);
@@ -598,10 +551,12 @@ function useComputedTheme(appearance, color, harmony = "split-complementary") {
598
551
  }
599
552
 
600
553
  // src/ThemeProvider.tsx
601
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
554
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
602
555
  var NavBarContext = createContext3({});
603
- var useNavBarContext = () => useContext4(NavBarContext);
604
- function Provider({ initialValue, onChange, children, defaults, onNavBarChange, onReady, statusBarProps, style }) {
556
+ var useNavBarContext = () => useContext3(NavBarContext);
557
+ var BlurModuleContext = createContext3(void 0);
558
+ var useBlurModule = () => useContext3(BlurModuleContext);
559
+ function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, statusBarProps, style }) {
605
560
  const [settings, setSettings] = useState2(() => ({ ...defaultThemeSettings, ...initialValue }));
606
561
  const set = useCallback2((patch) => {
607
562
  setSettings((prev) => ({ ...prev, ...patch }));
@@ -624,25 +579,61 @@ function Provider({ initialValue, onChange, children, defaults, onNavBarChange,
624
579
  }
625
580
  }, [theme, onReady]);
626
581
  if (!theme) return null;
627
- return /* @__PURE__ */ jsx4(ThemeSettingsContext.Provider, { value: { settings, set }, children: /* @__PURE__ */ jsxs3(PaperProvider, { theme, children: [
628
- /* @__PURE__ */ jsx4(StatusBar2, { backgroundColor: theme.colors.background, barStyle: theme.dark ? "light-content" : "dark-content", ...statusBarProps }),
629
- /* @__PURE__ */ jsx4(PaperDefaultsContext.Provider, { value: defaults ?? {}, children: /* @__PURE__ */ jsx4(NavBarContext.Provider, { value: { onNavBarChange }, children: /* @__PURE__ */ jsx4(View2, { style: [styles.flex, { backgroundColor: theme.colors.background }, style], children }) }) })
582
+ return /* @__PURE__ */ jsx3(ThemeSettingsContext.Provider, { value: { settings, set }, children: /* @__PURE__ */ jsxs2(PaperProvider, { theme, children: [
583
+ /* @__PURE__ */ jsx3(StatusBar2, { backgroundColor: theme.colors.background, barStyle: theme.dark ? "light-content" : "dark-content", ...statusBarProps }),
584
+ /* @__PURE__ */ jsx3(PaperDefaultsContext.Provider, { value: defaults ?? {}, children: /* @__PURE__ */ jsx3(NavBarContext.Provider, { value: { navigationBar, onNavBarChange }, children: /* @__PURE__ */ jsx3(BlurModuleContext.Provider, { value: expoBlur, children: /* @__PURE__ */ jsx3(View, { style: [styles.flex, { backgroundColor: theme.colors.background }, style], children }) }) }) })
630
585
  ] }) });
631
586
  }
632
- var styles = StyleSheet2.create({
587
+ var styles = StyleSheet.create({
633
588
  flex: { flex: 1 }
634
589
  });
635
590
 
591
+ // src/components/BlurView.tsx
592
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
593
+ var ELEVATION_OPACITY = { 0: 0, 1: 0.05, 2: 0.08, 3: 0.11, 4: 0.12, 5: 0.14 };
594
+ var VARIANT_TINT_OPACITY = { light: 0.12, dark: 0.18 };
595
+ var BlurView = ({ blur = true, children, elevation, style, tintColor, tintOpacity, variant = "surface", ...props }) => {
596
+ const { dark, colors } = useTheme2();
597
+ const { settings } = useContext4(ThemeSettingsContext);
598
+ const expoBlur = useBlurModule();
599
+ const surfaceColor = variant === "surfaceVariant" ? colors.surfaceVariant : colors.surface;
600
+ const variantColor = variant === "primary" ? colors.primary : variant === "secondary" ? colors.secondary : variant === "tertiary" ? colors.tertiary : variant === "error" ? colors.error : void 0;
601
+ const resolvedTintColor = tintColor ?? variantColor ?? (elevation !== void 0 ? colors.primary : void 0);
602
+ const resolvedTintOpacity = tintOpacity ?? (tintColor ? dark ? 0.24 : 0.16 : variantColor ? dark ? VARIANT_TINT_OPACITY.dark : VARIANT_TINT_OPACITY.light : elevation !== void 0 ? ELEVATION_OPACITY[elevation] : 0);
603
+ const tint = resolvedTintColor && resolvedTintOpacity > 0 ? /* @__PURE__ */ jsx4(View2, { style: [StyleSheet2.absoluteFill, { backgroundColor: resolvedTintColor, opacity: resolvedTintOpacity }] }) : null;
604
+ if (blur && expoBlur)
605
+ return /* @__PURE__ */ jsxs3(expoBlur.BlurView, { ...props, tint: dark ? "dark" : "light", style, children: [
606
+ /* @__PURE__ */ jsx4(View2, { style: [StyleSheet2.absoluteFill, { backgroundColor: surfaceColor, opacity: settings.blurTint }] }),
607
+ tint,
608
+ children
609
+ ] });
610
+ return /* @__PURE__ */ jsxs3(View2, { ...props, style: [{ backgroundColor: surfaceColor }, style], children: [
611
+ tint,
612
+ children
613
+ ] });
614
+ };
615
+
616
+ // src/components/BottomNavigation.tsx
617
+ import { useEffect as useEffect3 } from "react";
618
+ import { Platform } from "react-native";
619
+ import { BottomNavigation as PaperBottomNavigation, useTheme as useTheme3 } from "react-native-paper";
620
+
621
+ // src/navigation-bar.ts
622
+ var setNavigationBarStyle = (navigationBar, dark) => {
623
+ if (!navigationBar) return;
624
+ navigationBar.NavigationBar.setStyle(dark ? "light" : "dark");
625
+ };
626
+
636
627
  // src/components/BottomNavigation.tsx
637
628
  import { jsx as jsx5 } from "react/jsx-runtime";
638
629
  function useNavigationBarSync() {
639
630
  const theme = useTheme3();
640
- const { onNavBarChange } = useNavBarContext();
631
+ const { navigationBar, onNavBarChange } = useNavBarContext();
641
632
  useEffect3(() => {
642
633
  if (Platform.OS !== "android") return;
643
634
  if (onNavBarChange) onNavBarChange(theme.colors.surface, theme.dark);
644
- else if (navigationBar) setNavigationBarStyle(theme.dark);
645
- }, [theme, onNavBarChange]);
635
+ else if (navigationBar) setNavigationBarStyle(navigationBar, theme.dark);
636
+ }, [theme, onNavBarChange, navigationBar]);
646
637
  }
647
638
  function BottomNavigationComponent(props) {
648
639
  const defaults = usePaperDefaults();
@@ -1006,6 +997,82 @@ var useAutoPaperTheme = () => usePaperTheme();
1006
997
  import { useContext as useContext5 } from "react";
1007
998
  var useThemeSettings = () => useContext5(ThemeSettingsContext);
1008
999
 
1000
+ // src/utils/getThirdColor.ts
1001
+ function rgbToHsl(r, g, b) {
1002
+ r /= 255;
1003
+ g /= 255;
1004
+ b /= 255;
1005
+ const max = Math.max(r, g, b);
1006
+ const min = Math.min(r, g, b);
1007
+ const delta = max - min;
1008
+ let h = 0;
1009
+ if (delta !== 0) {
1010
+ if (max === r) h = (g - b) / delta % 6;
1011
+ else if (max === g) h = (b - r) / delta + 2;
1012
+ else h = (r - g) / delta + 4;
1013
+ h = (h * 60 + 360) % 360;
1014
+ }
1015
+ const l = (max + min) / 2;
1016
+ const s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
1017
+ return { h, s, l };
1018
+ }
1019
+ function hslToHex2(h, s, l) {
1020
+ const c = (1 - Math.abs(2 * l - 1)) * s;
1021
+ const x = c * (1 - Math.abs(h / 60 % 2 - 1));
1022
+ const m = l - c / 2;
1023
+ let r = 0;
1024
+ let g = 0;
1025
+ let b = 0;
1026
+ if (h < 60) {
1027
+ r = c;
1028
+ g = x;
1029
+ b = 0;
1030
+ } else if (h < 120) {
1031
+ r = x;
1032
+ g = c;
1033
+ b = 0;
1034
+ } else if (h < 180) {
1035
+ r = 0;
1036
+ g = c;
1037
+ b = x;
1038
+ } else if (h < 240) {
1039
+ r = 0;
1040
+ g = x;
1041
+ b = c;
1042
+ } else if (h < 300) {
1043
+ r = x;
1044
+ g = 0;
1045
+ b = c;
1046
+ } else {
1047
+ r = c;
1048
+ g = 0;
1049
+ b = x;
1050
+ }
1051
+ const toInt = (value) => Math.round((value + m) * 255);
1052
+ return getHex(`rgb(${toInt(r)}, ${toInt(g)}, ${toInt(b)})`) ?? "#000000";
1053
+ }
1054
+ var toRad = (deg) => deg * Math.PI / 180;
1055
+ var toDeg = (rad) => rad * 180 / Math.PI;
1056
+ function thirdHue(hA, hB) {
1057
+ const x = Math.cos(toRad(hA)) + Math.cos(toRad(hB));
1058
+ const y = Math.sin(toRad(hA)) + Math.sin(toRad(hB));
1059
+ if (Math.abs(x) < 1e-9 && Math.abs(y) < 1e-9) return (hA + 90) % 360;
1060
+ const meanHue = (toDeg(Math.atan2(y, x)) + 360) % 360;
1061
+ return (meanHue + 180) % 360;
1062
+ }
1063
+ function getThirdColor(colorA, colorB) {
1064
+ const rgbA = getRgb(colorA);
1065
+ const rgbB = getRgb(colorB);
1066
+ if (!rgbA) throw new Error(`Invalid color format: ${colorA}`);
1067
+ if (!rgbB) throw new Error(`Invalid color format: ${colorB}`);
1068
+ const hslA = rgbToHsl(rgbA.r, rgbA.g, rgbA.b);
1069
+ const hslB = rgbToHsl(rgbB.r, rgbB.g, rgbB.b);
1070
+ const h = thirdHue(hslA.h, hslB.h);
1071
+ const s = (hslA.s + hslB.s) / 2;
1072
+ const l = (hslA.l + hslB.l) / 2;
1073
+ return hslToHex2(h, s, l);
1074
+ }
1075
+
1009
1076
  // src/utils/getTintTextColor.ts
1010
1077
  var getTintTextColor = (tintColor, backdropColor, tintOpacity) => {
1011
1078
  const blended = getBlendedColor(tintColor, backdropColor, tintOpacity);
@@ -1037,6 +1104,7 @@ export {
1037
1104
  getContrastColor,
1038
1105
  getHex,
1039
1106
  getRgb,
1107
+ getThirdColor,
1040
1108
  getTintTextColor,
1041
1109
  getTonalColor,
1042
1110
  getTriadicPalette,
@@ -1049,7 +1117,9 @@ export {
1049
1117
  themeReducer,
1050
1118
  useAutoPaperTheme,
1051
1119
  useBlur,
1120
+ useBlurModule,
1052
1121
  useComputedTheme,
1122
+ useNavBarContext,
1053
1123
  usePaperDefaults,
1054
1124
  useThemeSettings
1055
1125
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rific/auto-paper",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
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,14 +31,15 @@
31
31
  "type": "commonjs",
32
32
  "exports": {
33
33
  ".": {
34
- "react-native": "./dist/index.js",
34
+ "react-native": "./src/index.ts",
35
35
  "types": "./dist/index.d.ts",
36
- "import": "./dist/index.js",
36
+ "import": "./dist/index.mjs",
37
37
  "require": "./dist/index.js"
38
38
  }
39
39
  },
40
40
  "main": "dist/index.js",
41
- "module": "dist/index.js",
41
+ "module": "dist/index.mjs",
42
+ "react-native": "src/index.ts",
42
43
  "types": "dist/index.d.ts",
43
44
  "files": [
44
45
  "dist",
@@ -94,9 +95,9 @@
94
95
  "typescript-eslint": "^8.59.3"
95
96
  },
96
97
  "peerDependencies": {
97
- "expo-blur": ">=14.0.0",
98
+ "expo-blur": ">=56.0.0",
98
99
  "expo-navigation-bar": ">=56.0.0",
99
- "react": ">=18.0.0",
100
+ "react": ">=19.0.0",
100
101
  "react-native": ">=0.76.0",
101
102
  "react-native-paper": ">=5.0.0",
102
103
  "react-native-safe-area-context": "*"
@@ -2,26 +2,40 @@ import { createContext, type ReactNode, useCallback, useContext, useEffect, useR
2
2
  import { StatusBar, type StatusBarProps, type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'
3
3
  import { Provider as PaperProvider } from 'react-native-paper'
4
4
 
5
+ import type { ExpoBlurModule } from './components/BlurView'
6
+ import type { ExpoNavigationBarModule } from './navigation-bar'
5
7
  import { type PaperDefaults, PaperDefaultsContext } from './PaperDefaultsContext'
6
8
  import { defaultThemeSettings, type ThemeSettings, ThemeSettingsContext } from './ThemeSettingsContext'
7
9
  import { useComputedTheme } from './useComputedTheme'
8
10
 
9
- type NavBarContextType = { onNavBarChange?: (color: string, dark: boolean) => void }
11
+ // expo-navigation-bar/expo-blur are no longer auto-detected via require() - Metro doesn't
12
+ // rewrite a require()-in-try/catch call into its module graph inside an ESM (.mjs) build, so
13
+ // the module-level detection this package used to do silently broke as soon as consumers'
14
+ // bundlers resolved this package's ESM entry point. <Provider navigationBar={...} expoBlur={...}>
15
+ // receives the already-imported modules directly instead.
16
+ type NavBarContextType = { navigationBar?: ExpoNavigationBarModule; onNavBarChange?: (color: string, dark: boolean) => void }
10
17
  const NavBarContext = createContext<NavBarContextType>({})
11
18
  export const useNavBarContext = () => useContext(NavBarContext)
12
19
 
20
+ const BlurModuleContext = createContext<ExpoBlurModule | undefined>(undefined)
21
+ export const useBlurModule = () => useContext(BlurModuleContext)
22
+
13
23
  export type ProviderProps = {
14
- initialValue?: Partial<ThemeSettings>
15
- onChange?: (settings: ThemeSettings) => void
16
24
  children: ReactNode
17
25
  defaults?: PaperDefaults
26
+ /** Injects expo-blur for `<BlurView>`. Pass `import * as ExpoBlur from 'expo-blur'`; omit to always render BlurView's solid fallback. */
27
+ expoBlur?: ExpoBlurModule
28
+ initialValue?: Partial<ThemeSettings>
29
+ /** 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. */
30
+ navigationBar?: ExpoNavigationBarModule
31
+ onChange?: (settings: ThemeSettings) => void
18
32
  onNavBarChange?: (color: string, dark: boolean) => void
19
33
  onReady?: () => void
20
34
  statusBarProps?: StatusBarProps
21
35
  style?: StyleProp<ViewStyle>
22
36
  }
23
37
 
24
- export function Provider({ initialValue, onChange, children, defaults, onNavBarChange, onReady, statusBarProps, style }: ProviderProps) {
38
+ export function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, statusBarProps, style }: ProviderProps) {
25
39
  const [settings, setSettings] = useState<ThemeSettings>(() => ({ ...defaultThemeSettings, ...initialValue }))
26
40
 
27
41
  const set = useCallback((patch: Partial<ThemeSettings>) => {
@@ -55,8 +69,10 @@ export function Provider({ initialValue, onChange, children, defaults, onNavBarC
55
69
  <PaperProvider theme={theme}>
56
70
  <StatusBar backgroundColor={theme.colors.background} barStyle={theme.dark ? 'light-content' : 'dark-content'} {...statusBarProps} />
57
71
  <PaperDefaultsContext.Provider value={defaults ?? {}}>
58
- <NavBarContext.Provider value={{ onNavBarChange }}>
59
- <View style={[styles.flex, { backgroundColor: theme.colors.background }, style]}>{children}</View>
72
+ <NavBarContext.Provider value={{ navigationBar, onNavBarChange }}>
73
+ <BlurModuleContext.Provider value={expoBlur}>
74
+ <View style={[styles.flex, { backgroundColor: theme.colors.background }, style]}>{children}</View>
75
+ </BlurModuleContext.Provider>
60
76
  </NavBarContext.Provider>
61
77
  </PaperDefaultsContext.Provider>
62
78
  </PaperProvider>
@@ -1,13 +1,13 @@
1
1
  import { createContext } from 'react'
2
2
 
3
3
  import type { ThemeAppearance } from './useComputedTheme'
4
- import type { ColorHarmony } from './utils/getTriadicPalette'
4
+ import type { ColorHarmony, TriadicPalette } from './utils/getTriadicPalette'
5
5
 
6
6
  export type ThemeSettings = {
7
7
  appearance: ThemeAppearance
8
8
  blur: boolean
9
9
  blurTint: number
10
- color: string
10
+ color: string | TriadicPalette
11
11
  harmony: ColorHarmony
12
12
  }
13
13
 
@@ -11,7 +11,7 @@ const APPEARANCES: { label: string; value: ThemeAppearance }[] = [
11
11
  export type AppearanceIcons = Partial<Record<ThemeAppearance, string>>
12
12
 
13
13
  // 'monitor' (a plain device/screen glyph) reads more clearly as "match my device" than
14
- // 'brightness-auto' (an exposure/brightness symbol) — swap via the `icons` prop for a different
14
+ // 'brightness-auto' (an exposure/brightness symbol). Swap via the `icons` prop for a different
15
15
  // convention, e.g. `theme-light-dark`'s half-sun/half-moon.
16
16
  const DEFAULT_ICONS: Record<ThemeAppearance, string> = {
17
17
  system: 'monitor',
@@ -22,11 +22,11 @@ const DEFAULT_ICONS: Record<ThemeAppearance, string> = {
22
22
  type Props = {
23
23
  value: ThemeAppearance
24
24
  onChange: (appearance: ThemeAppearance) => void
25
- // Hides the text label, leaving just the icon per segment — useful in tight layouts where
25
+ // Hides the text label, leaving just the icon per segment, useful in tight layouts where
26
26
  // "System" truncates. accessibilityLabel is always set regardless, so screen readers still
27
27
  // announce the full word either way.
28
28
  showLabels?: boolean
29
- // Per-value icon overrides, merged over DEFAULT_ICONS — pass just the ones you want to change.
29
+ // Per-value icon overrides, merged over DEFAULT_ICONS: pass just the ones you want to change.
30
30
  icons?: AppearanceIcons
31
31
  }
32
32
 
@@ -2,21 +2,26 @@ import { type ComponentType, type ReactNode, type RefObject, useContext } from '
2
2
  import { type StyleProp, StyleSheet, View, type ViewProps, type ViewStyle } from 'react-native'
3
3
  import { useTheme } from 'react-native-paper'
4
4
 
5
+ import { useBlurModule } from '../ThemeProvider'
5
6
  import { ThemeSettingsContext } from '../ThemeSettingsContext'
6
7
 
7
- // Minimal local shape of expo-blur covering only the member used below — avoids forcing
8
+ // Minimal local shape of expo-blur covering only the member used below: avoids forcing
8
9
  // TypeScript to resolve the optional peer's real types for consumers who never installed it.
9
- interface ExpoBlurModule {
10
+ // tint is deliberately loose (not expo-blur's real narrow BlurTint union) so the real module
11
+ // stays structurally assignable to `<Provider expoBlur={...}>` without importing its types -
12
+ // this component only ever passes it 'dark'/'light', both valid BlurTint members at runtime.
13
+ export interface ExpoBlurModule {
10
14
  BlurView: ComponentType<{
11
15
  children?: ReactNode
12
16
  style?: StyleProp<ViewStyle>
13
- tint?: string
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ tint?: any
14
19
  [key: string]: unknown
15
20
  }>
16
21
  }
17
22
 
18
- // Local mirror of expo-blur's public BlurViewProps (node_modules/expo-blur/build/BlurView.types.d.ts)
19
- // — same reason as ExpoBlurModule above: the peer's real type must never be imported unconditionally.
23
+ // Local mirror of expo-blur's public BlurViewProps (node_modules/expo-blur/build/BlurView.types.d.ts),
24
+ // same reason as ExpoBlurModule above: the peer's real type must never be imported unconditionally.
20
25
  type ExpoBlurTint = 'light' | 'dark' | 'default' | 'extraLight' | 'regular' | 'prominent' | 'systemUltraThinMaterial' | 'systemThinMaterial' | 'systemMaterial' | 'systemThickMaterial' | 'systemChromeMaterial' | 'systemUltraThinMaterialLight' | 'systemThinMaterialLight' | 'systemMaterialLight' | 'systemThickMaterialLight' | 'systemChromeMaterialLight' | 'systemUltraThinMaterialDark' | 'systemThinMaterialDark' | 'systemMaterialDark' | 'systemThickMaterialDark' | 'systemChromeMaterialDark'
21
26
  type ExpoBlurMethod = 'none' | 'dimezisBlurView' | 'dimezisBlurViewSdk31Plus'
22
27
  type ExpoBlurViewProps = {
@@ -28,15 +33,6 @@ type ExpoBlurViewProps = {
28
33
  tint?: ExpoBlurTint
29
34
  } & ViewProps
30
35
 
31
- // expo-blur is an optional peer — when absent, BlurView renders its solid (non-blur) fallback
32
- const ExpoBlurView = (() => {
33
- try {
34
- return (require('expo-blur') as ExpoBlurModule).BlurView
35
- } catch {
36
- return null
37
- }
38
- })()
39
-
40
36
  const ELEVATION_OPACITY: Record<number, number> = { 0: 0, 1: 0.05, 2: 0.08, 3: 0.11, 4: 0.12, 5: 0.14 }
41
37
  const VARIANT_TINT_OPACITY = { light: 0.12, dark: 0.18 }
42
38
 
@@ -53,18 +49,19 @@ export type BlurViewProps = ExpoBlurViewProps & {
53
49
  export const BlurView = ({ blur = true, children, elevation, style, tintColor, tintOpacity, variant = 'surface', ...props }: BlurViewProps) => {
54
50
  const { dark, colors } = useTheme()
55
51
  const { settings } = useContext(ThemeSettingsContext)
52
+ const expoBlur = useBlurModule()
56
53
  const surfaceColor = variant === 'surfaceVariant' ? colors.surfaceVariant : colors.surface
57
54
  const variantColor = variant === 'primary' ? colors.primary : variant === 'secondary' ? colors.secondary : variant === 'tertiary' ? colors.tertiary : variant === 'error' ? colors.error : undefined
58
55
  const resolvedTintColor = tintColor ?? variantColor ?? (elevation !== undefined ? colors.primary : undefined)
59
56
  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)
60
57
  const tint = resolvedTintColor && resolvedTintOpacity > 0 ? <View style={[StyleSheet.absoluteFill, { backgroundColor: resolvedTintColor, opacity: resolvedTintOpacity }]} /> : null
61
- if (blur && ExpoBlurView)
58
+ if (blur && expoBlur)
62
59
  return (
63
- <ExpoBlurView {...props} tint={dark ? 'dark' : 'light'} style={style}>
60
+ <expoBlur.BlurView {...props} tint={dark ? 'dark' : 'light'} style={style}>
64
61
  <View style={[StyleSheet.absoluteFill, { backgroundColor: surfaceColor, opacity: settings.blurTint }]} />
65
62
  {tint}
66
63
  {children}
67
- </ExpoBlurView>
64
+ </expoBlur.BlurView>
68
65
  )
69
66
  return (
70
67
  <View {...props} style={[{ backgroundColor: surfaceColor }, style]}>
@@ -2,20 +2,20 @@ import React, { useEffect } from 'react'
2
2
  import { Platform } from 'react-native'
3
3
  import { BottomNavigation as PaperBottomNavigation, type BottomNavigationProps, type BottomNavigationRoute, useTheme } from 'react-native-paper'
4
4
 
5
- import { navigationBar, setNavigationBarStyle } from '../navigation-bar'
5
+ import { setNavigationBarStyle } from '../navigation-bar'
6
6
  import { usePaperDefaults } from '../PaperDefaultsContext'
7
7
  import { useNavBarContext } from '../ThemeProvider'
8
8
 
9
9
  function useNavigationBarSync() {
10
10
  const theme = useTheme()
11
- const { onNavBarChange } = useNavBarContext()
11
+ const { navigationBar, onNavBarChange } = useNavBarContext()
12
12
  useEffect(() => {
13
13
  if (Platform.OS !== 'android') return
14
14
  // An explicit onNavBarChange callback takes full control; otherwise sync the
15
- // nav bar icon style automatically when expo-navigation-bar is installed.
15
+ // nav bar icon style automatically when expo-navigation-bar was injected via <Provider>.
16
16
  if (onNavBarChange) onNavBarChange(theme.colors.surface, theme.dark)
17
- else if (navigationBar) setNavigationBarStyle(theme.dark)
18
- }, [theme, onNavBarChange])
17
+ else if (navigationBar) setNavigationBarStyle(navigationBar, theme.dark)
18
+ }, [theme, onNavBarChange, navigationBar])
19
19
  }
20
20
 
21
21
  function BottomNavigationComponent<Route extends BottomNavigationRoute>(props: BottomNavigationProps<Route>) {
@@ -19,9 +19,9 @@ const defaultWeights: PaletteWeights = { primary: 2, secondary: 1, tertiary: 1 }
19
19
 
20
20
  // Each wedge = a static right-half mask, doubly rotated: an outer spin positions the slice's
21
21
  // leading edge around the circle, then an inner spin (its own share, minus 180deg) carves the
22
- // trailing edge — together covering exactly `share` of weights' total, for any share <= 50%.
22
+ // trailing edge, together covering exactly `share` of weights' total, for any share <= 50%.
23
23
  // Both rotators are sized to exactly overlap the full circle so React Native's default
24
- // (own-center) rotation pivot is already correct — transformOrigin is intentionally avoided
24
+ // (own-center) rotation pivot is already correct; transformOrigin is intentionally avoided
25
25
  // since it isn't reliably honored across RN renderers. Primary is centered at 12 o'clock,
26
26
  // tertiary fills clockwise from there (bottom-right), secondary fills the remainder (bottom-left).
27
27
  const buildWedges = (weights: PaletteWeights) => {
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ export { useBlur } from './BlurContext'
2
2
  export { Appbar } from './components/Appbar'
3
3
  export type { AppearanceIcons } from './components/AppearancePicker'
4
4
  export { AppearancePicker } from './components/AppearancePicker'
5
- export type { BlurViewProps } from './components/BlurView'
5
+ export type { BlurViewProps, ExpoBlurModule } from './components/BlurView'
6
6
  export { BlurView } from './components/BlurView'
7
7
  export { BottomNavigation } from './components/BottomNavigation'
8
8
  export { Button } from './components/Button'
@@ -21,12 +21,13 @@ export { Menu } from './components/Menu'
21
21
  export type { PaletteWeights } from './components/PalettePicker'
22
22
  export { PalettePicker } from './components/PalettePicker'
23
23
  export { TextInput } from './components/TextInput'
24
+ export type { ExpoNavigationBarModule } from './navigation-bar'
24
25
  export type { PaperDefaults } from './PaperDefaultsContext'
25
26
  export { usePaperDefaults } from './PaperDefaultsContext'
26
27
  export type { ThemeState } from './redux/themeSlice'
27
28
  export { createThemeReducer, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer } from './redux/themeSlice'
28
29
  export type { ProviderProps } from './ThemeProvider'
29
- export { Provider } from './ThemeProvider'
30
+ export { Provider, useBlurModule, useNavBarContext } from './ThemeProvider'
30
31
  export type { ThemeSettings, ThemeSettingsContextType } from './ThemeSettingsContext'
31
32
  export { defaultThemeSettings, ThemeSettingsContext } from './ThemeSettingsContext'
32
33
  export type { AutoPaperTheme, SemanticColorRoles, ThemeAppearance } from './useComputedTheme'
@@ -40,6 +41,7 @@ export { getHex } from './utils/getHex'
40
41
  export type { RGB } from './utils/getRgb'
41
42
  export { getRgb } from './utils/getRgb'
42
43
  export { getContrastColor } from './utils/getSwatchContrast'
44
+ export { getThirdColor } from './utils/getThirdColor'
43
45
  export { getTintTextColor } from './utils/getTintTextColor'
44
46
  export { getTonalColor } from './utils/getTonalColor'
45
47
  export type { ColorHarmony, TriadicPalette } from './utils/getTriadicPalette'
@@ -1,22 +1,14 @@
1
- // Minimal local shape of expo-navigation-bar covering only the members used below —
1
+ // Minimal local shape of expo-navigation-bar covering only the members used below:
2
2
  // avoids forcing TypeScript to resolve the optional peer's real types for consumers
3
3
  // who never installed it.
4
- interface ExpoNavigationBarModule {
4
+ export interface ExpoNavigationBarModule {
5
5
  NavigationBar: {
6
6
  setStyle: (style: 'light' | 'dark') => void
7
7
  }
8
8
  }
9
9
 
10
- export const navigationBar = (() => {
11
- try {
12
- return require('expo-navigation-bar') as ExpoNavigationBarModule
13
- } catch {
14
- return null
15
- }
16
- })()
17
-
18
10
  /** Sets the Android navigation bar icon style (expo-navigation-bar >= 56, edge-to-edge API). */
19
- export const setNavigationBarStyle = (dark: boolean) => {
11
+ export const setNavigationBarStyle = (navigationBar: ExpoNavigationBarModule | undefined, dark: boolean) => {
20
12
  if (!navigationBar) return
21
13
  navigationBar.NavigationBar.setStyle(dark ? 'light' : 'dark')
22
14
  }