@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/README.md +76 -33
- package/dist/index.d.mts +37 -11
- package/dist/index.d.ts +37 -11
- package/dist/index.js +162 -82
- package/dist/index.mjs +142 -72
- package/package.json +7 -6
- package/src/ThemeProvider.tsx +22 -6
- package/src/ThemeSettingsContext.ts +2 -2
- package/src/components/AppearancePicker.tsx +3 -3
- package/src/components/BlurView.tsx +14 -17
- package/src/components/BottomNavigation.tsx +5 -5
- package/src/components/PalettePicker.tsx +2 -2
- package/src/index.ts +4 -2
- package/src/navigation-bar.ts +3 -11
- package/src/redux/themeSlice.ts +4 -4
- package/src/useComputedTheme.ts +26 -12
- package/src/useSwatchGrid.ts +1 -1
- package/src/useTheme.ts +1 -1
- package/src/utils/getColorRoles.ts +1 -1
- package/src/utils/getSwatchContrast.ts +2 -2
- package/src/utils/getThirdColor.ts +103 -0
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
|
|
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
|
|
132
|
-
import { StatusBar as StatusBar2, StyleSheet
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
|
554
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
602
555
|
var NavBarContext = createContext3({});
|
|
603
|
-
var useNavBarContext = () =>
|
|
604
|
-
|
|
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__ */
|
|
628
|
-
/* @__PURE__ */
|
|
629
|
-
/* @__PURE__ */
|
|
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 =
|
|
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.
|
|
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": "./
|
|
34
|
+
"react-native": "./src/index.ts",
|
|
35
35
|
"types": "./dist/index.d.ts",
|
|
36
|
-
"import": "./dist/index.
|
|
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.
|
|
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": ">=
|
|
98
|
+
"expo-blur": ">=56.0.0",
|
|
98
99
|
"expo-navigation-bar": ">=56.0.0",
|
|
99
|
-
"react": ">=
|
|
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": "*"
|
package/src/ThemeProvider.tsx
CHANGED
|
@@ -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
|
-
|
|
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({
|
|
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
|
-
<
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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 &&
|
|
58
|
+
if (blur && expoBlur)
|
|
62
59
|
return (
|
|
63
|
-
<
|
|
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
|
-
</
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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'
|
package/src/navigation-bar.ts
CHANGED
|
@@ -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
|
}
|