@rific/auto-paper 0.12.3 → 0.13.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 +53 -9
- package/dist/index.d.mts +21 -6
- package/dist/index.d.ts +21 -6
- package/dist/index.js +128 -33
- package/dist/index.mjs +117 -25
- package/package.json +6 -1
- package/src/ThemeProvider.tsx +19 -11
- package/src/components/AutoAppearancePicker.tsx +1 -1
- package/src/components/AutoPalettePicker.tsx +2 -2
- package/src/components/BlurView.tsx +24 -10
- package/src/components/Dialog.tsx +3 -3
- package/src/components/FadingScrollView.tsx +127 -0
- package/src/components/HarmonyPicker.tsx +5 -5
- package/src/components/PalettePicker.tsx +1 -1
- package/src/index.ts +3 -1
- package/src/useComputedTheme.ts +1 -1
- package/src/utils/getTonalColor.ts +1 -1
- package/src/utils/resolveSeedColor.ts +3 -3
package/dist/index.mjs
CHANGED
|
@@ -601,11 +601,13 @@ var BlurModuleContext = createContext2(void 0);
|
|
|
601
601
|
var useBlurModule = () => useContext3(BlurModuleContext);
|
|
602
602
|
var ReanimatedModuleContext = createContext2(void 0);
|
|
603
603
|
var useReanimatedModule = () => useContext3(ReanimatedModuleContext);
|
|
604
|
-
|
|
605
|
-
|
|
604
|
+
var MaskedViewModuleContext = createContext2(void 0);
|
|
605
|
+
var useMaskedViewModule = () => useContext3(MaskedViewModuleContext);
|
|
606
|
+
function Provider({ children, defaults, expoBlur, fontFamily, initialValue, maskedView, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }) {
|
|
607
|
+
return /* @__PURE__ */ jsx4(ThemeSettingsProvider, { initialValue, onChange, children: /* @__PURE__ */ jsx4(ThemeProviderBody, { defaults, expoBlur, fontFamily, maskedView, navigationBar, onNavBarChange, onReady, reanimated, statusBarProps, style, children }) });
|
|
606
608
|
}
|
|
607
609
|
var ThemeSettingsProvider = themeSettingsProvider;
|
|
608
|
-
function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, navigationBar, onNavBarChange, onReady, reanimated, statusBarProps, style }) {
|
|
610
|
+
function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, maskedView, navigationBar, onNavBarChange, onReady, reanimated, statusBarProps, style }) {
|
|
609
611
|
const { settings } = useThemeSettingsInternal();
|
|
610
612
|
const computedTheme = useComputedTheme(settings.appearance, settings.color, settings.harmony);
|
|
611
613
|
const theme = useMemo(() => {
|
|
@@ -623,7 +625,7 @@ function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, navigatio
|
|
|
623
625
|
if (!theme) return null;
|
|
624
626
|
return /* @__PURE__ */ jsxs2(PaperProvider, { theme, children: [
|
|
625
627
|
/* @__PURE__ */ jsx4(StatusBar2, { backgroundColor: theme.colors.background, barStyle: theme.dark ? "light-content" : "dark-content", ...statusBarProps }),
|
|
626
|
-
/* @__PURE__ */ jsx4(PaperDefaultsContext.Provider, { value: defaults ?? {}, children: /* @__PURE__ */ jsx4(NavBarContext.Provider, { value: { navigationBar, onNavBarChange }, children: /* @__PURE__ */ jsx4(BlurModuleContext.Provider, { value: expoBlur, children: /* @__PURE__ */ jsx4(ReanimatedModuleContext.Provider, { value: reanimated, children: /* @__PURE__ */ jsx4(Portal.Host, { children: /* @__PURE__ */ jsx4(View, { style: [styles.flex, { backgroundColor: theme.colors.background }, style], children }) }) }) }) }) })
|
|
628
|
+
/* @__PURE__ */ jsx4(PaperDefaultsContext.Provider, { value: defaults ?? {}, children: /* @__PURE__ */ jsx4(NavBarContext.Provider, { value: { navigationBar, onNavBarChange }, children: /* @__PURE__ */ jsx4(BlurModuleContext.Provider, { value: expoBlur, children: /* @__PURE__ */ jsx4(ReanimatedModuleContext.Provider, { value: reanimated, children: /* @__PURE__ */ jsx4(MaskedViewModuleContext.Provider, { value: maskedView, children: /* @__PURE__ */ jsx4(Portal.Host, { children: /* @__PURE__ */ jsx4(View, { style: [styles.flex, { backgroundColor: theme.colors.background }, style], children }) }) }) }) }) }) })
|
|
627
629
|
] });
|
|
628
630
|
}
|
|
629
631
|
var styles = StyleSheet.create({
|
|
@@ -642,7 +644,12 @@ var blursOnAndroid = ({ blurMethod, blurTarget, experimentalBlurMethod }) => {
|
|
|
642
644
|
};
|
|
643
645
|
var ELEVATION_OPACITY = { 0: 0, 1: 0.05, 2: 0.08, 3: 0.11, 4: 0.12, 5: 0.14 };
|
|
644
646
|
var VARIANT_TINT_OPACITY = { light: 0.12, dark: 0.18 };
|
|
645
|
-
var
|
|
647
|
+
var getWebBlurStyle = (intensity, dark) => {
|
|
648
|
+
const amount = Math.min(intensity, 100);
|
|
649
|
+
const filter = `saturate(180%) blur(${amount * 0.2}px)`;
|
|
650
|
+
return { backdropFilter: filter, backgroundColor: `rgba(${dark ? "25,25,25" : "249,249,249"},${amount / 100 * 0.78})`, WebkitBackdropFilter: filter };
|
|
651
|
+
};
|
|
652
|
+
var BlurView = ({ blur = true, blurMethod, blurReductionFactor, blurTarget, children, elevation, experimentalBlurMethod, intensity, style, tint: _tint, tintColor, tintOpacity, variant = "surface", ...props }) => {
|
|
646
653
|
const { dark, colors } = useTheme2();
|
|
647
654
|
const { settings } = useContext4(ThemeSettingsContext);
|
|
648
655
|
const expoBlur = useBlurModule();
|
|
@@ -651,13 +658,13 @@ var BlurView = ({ blur = true, children, elevation, style, tintColor, tintOpacit
|
|
|
651
658
|
const resolvedTintColor = tintColor ?? variantColor ?? (elevation !== void 0 ? colors.primary : void 0);
|
|
652
659
|
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);
|
|
653
660
|
const tint = resolvedTintColor && resolvedTintOpacity > 0 ? /* @__PURE__ */ jsx5(View2, { style: [StyleSheet2.absoluteFill, { backgroundColor: resolvedTintColor, opacity: resolvedTintOpacity }] }) : null;
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
+
const blurProps = { blurMethod, blurReductionFactor, blurTarget, experimentalBlurMethod, intensity };
|
|
662
|
+
const blurs = blur && expoBlur !== void 0 && (Platform.OS !== "android" || blursOnAndroid(blurProps));
|
|
663
|
+
const web = Platform.OS === "web";
|
|
664
|
+
const containerStyle = !blurs ? [{ backgroundColor: surfaceColor }, style] : web ? [style, getWebBlurStyle(intensity ?? 50, dark)] : style;
|
|
665
|
+
return /* @__PURE__ */ jsxs3(View2, { ...props, style: containerStyle, children: [
|
|
666
|
+
blurs && !web ? /* @__PURE__ */ jsx5(expoBlur.BlurView, { ...blurProps, style: StyleSheet2.absoluteFill, tint: dark ? "dark" : "light" }) : null,
|
|
667
|
+
blurs ? /* @__PURE__ */ jsx5(View2, { style: [StyleSheet2.absoluteFill, { backgroundColor: surfaceColor, opacity: settings.blurTint }] }) : null,
|
|
661
668
|
tint,
|
|
662
669
|
children
|
|
663
670
|
] });
|
|
@@ -950,22 +957,104 @@ function FAB(props) {
|
|
|
950
957
|
return /* @__PURE__ */ jsx14(PaperFAB, { ...defaults.FAB, ...props });
|
|
951
958
|
}
|
|
952
959
|
|
|
960
|
+
// src/components/FadingScrollView.tsx
|
|
961
|
+
import { useCallback as useCallback2, useRef as useRef2, useState as useState6 } from "react";
|
|
962
|
+
import { Platform as Platform4, ScrollView, StyleSheet as StyleSheet6, View as View6 } from "react-native";
|
|
963
|
+
import { jsx as jsx15, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
964
|
+
var SCROLL_FADE_HEIGHT = 24;
|
|
965
|
+
function FadingScrollView({ style, fadeHeight = SCROLL_FADE_HEIGHT, onScroll, onLayout, onContentSizeChange, scrollEventThrottle = 16, children, ...rest }) {
|
|
966
|
+
const MaskedView = useMaskedViewModule();
|
|
967
|
+
const sizes = useRef2({ viewport: 0, content: 0, offset: 0 });
|
|
968
|
+
const [edges, setEdges] = useState6(NO_EDGES);
|
|
969
|
+
const update = useCallback2(() => {
|
|
970
|
+
const { viewport, content, offset } = sizes.current;
|
|
971
|
+
const above = offset > 1;
|
|
972
|
+
const below = content - viewport - offset > 1;
|
|
973
|
+
setEdges((prev) => prev.above === above && prev.below === below ? prev : { above, below });
|
|
974
|
+
}, []);
|
|
975
|
+
const scroll = /* @__PURE__ */ jsx15(
|
|
976
|
+
ScrollView,
|
|
977
|
+
{
|
|
978
|
+
...rest,
|
|
979
|
+
style: styles5.fill,
|
|
980
|
+
scrollEventThrottle,
|
|
981
|
+
onScroll: (e) => {
|
|
982
|
+
sizes.current.offset = e.nativeEvent.contentOffset.y;
|
|
983
|
+
update();
|
|
984
|
+
onScroll?.(e);
|
|
985
|
+
},
|
|
986
|
+
onLayout: (e) => {
|
|
987
|
+
sizes.current.viewport = e.nativeEvent.layout.height;
|
|
988
|
+
update();
|
|
989
|
+
onLayout?.(e);
|
|
990
|
+
},
|
|
991
|
+
onContentSizeChange: (width, height) => {
|
|
992
|
+
sizes.current.content = height;
|
|
993
|
+
update();
|
|
994
|
+
onContentSizeChange?.(width, height);
|
|
995
|
+
},
|
|
996
|
+
children
|
|
997
|
+
}
|
|
998
|
+
);
|
|
999
|
+
if (Platform4.OS === "web") return /* @__PURE__ */ jsx15(View6, { style: [styles5.box, style, (edges.above || edges.below) && webFadeStyle(fadeHeight, edges)], children: scroll });
|
|
1000
|
+
if (!MaskedView) return /* @__PURE__ */ jsx15(View6, { style: [styles5.box, style], children: scroll });
|
|
1001
|
+
return /* @__PURE__ */ jsx15(
|
|
1002
|
+
MaskedView,
|
|
1003
|
+
{
|
|
1004
|
+
style: [styles5.box, style],
|
|
1005
|
+
maskElement: /* @__PURE__ */ jsxs7(View6, { style: styles5.fill, children: [
|
|
1006
|
+
/* @__PURE__ */ jsx15(View6, { style: [{ height: fadeHeight }, edges.above ? FADE_IN_BAND : styles5.opaque] }),
|
|
1007
|
+
/* @__PURE__ */ jsx15(View6, { style: styles5.fillOpaque }),
|
|
1008
|
+
/* @__PURE__ */ jsx15(View6, { style: [{ height: fadeHeight }, edges.below ? FADE_OUT_BAND : styles5.opaque] })
|
|
1009
|
+
] }),
|
|
1010
|
+
children: scroll
|
|
1011
|
+
}
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
var FADE_IN_BAND = { experimental_backgroundImage: "linear-gradient(transparent, #000)" };
|
|
1015
|
+
var FADE_OUT_BAND = { experimental_backgroundImage: "linear-gradient(#000, transparent)" };
|
|
1016
|
+
var NO_EDGES = { above: false, below: false };
|
|
1017
|
+
function webFadeStyle(fadeHeight, edges) {
|
|
1018
|
+
const top = edges.above ? `transparent 0, #000 ${fadeHeight}px` : "#000 0";
|
|
1019
|
+
const bottom = edges.below ? `#000 calc(100% - ${fadeHeight}px), transparent 100%` : "#000 100%";
|
|
1020
|
+
const mask = `linear-gradient(to bottom, ${top}, ${bottom})`;
|
|
1021
|
+
return { maskImage: mask, WebkitMaskImage: mask };
|
|
1022
|
+
}
|
|
1023
|
+
var styles5 = StyleSheet6.create({
|
|
1024
|
+
// A plain ScrollView's own flex defaults, so it sizes the same in its parent.
|
|
1025
|
+
box: {
|
|
1026
|
+
flexGrow: 1,
|
|
1027
|
+
flexShrink: 1
|
|
1028
|
+
},
|
|
1029
|
+
fill: {
|
|
1030
|
+
flexGrow: 1,
|
|
1031
|
+
flexShrink: 1
|
|
1032
|
+
},
|
|
1033
|
+
fillOpaque: {
|
|
1034
|
+
backgroundColor: "#000",
|
|
1035
|
+
flexGrow: 1
|
|
1036
|
+
},
|
|
1037
|
+
opaque: {
|
|
1038
|
+
backgroundColor: "#000"
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
|
|
953
1042
|
// src/components/IconButton.tsx
|
|
954
1043
|
import { IconButton as PaperIconButton, useTheme as useTheme8 } from "react-native-paper";
|
|
955
|
-
import { jsx as
|
|
1044
|
+
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
956
1045
|
function IconButton({ variant, iconColor, containerColor, ...props }) {
|
|
957
1046
|
const defaults = usePaperDefaults();
|
|
958
1047
|
const { colors } = useTheme8();
|
|
959
1048
|
const variantContainerColor = variant === "primary" ? colors.primaryContainer : variant === "secondary" ? colors.secondaryContainer : variant === "tertiary" ? colors.tertiaryContainer : variant === "surface" ? colors.surface : void 0;
|
|
960
1049
|
const variantIconColor = variant === "primary" ? colors.onPrimaryContainer : variant === "secondary" ? colors.onSecondaryContainer : variant === "tertiary" ? colors.onTertiaryContainer : variant === "surface" ? colors.onSurface : void 0;
|
|
961
|
-
return /* @__PURE__ */
|
|
1050
|
+
return /* @__PURE__ */ jsx16(PaperIconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
|
|
962
1051
|
}
|
|
963
1052
|
|
|
964
1053
|
// src/components/Menu.tsx
|
|
965
|
-
import { useEffect as useEffect5, useState as
|
|
966
|
-
import { Animated as Animated2, Easing as Easing2, Platform as
|
|
1054
|
+
import { useEffect as useEffect5, useState as useState7 } from "react";
|
|
1055
|
+
import { Animated as Animated2, Easing as Easing2, Platform as Platform5, StyleSheet as StyleSheet7 } from "react-native";
|
|
967
1056
|
import { Menu as PaperMenu, useTheme as useTheme9 } from "react-native-paper";
|
|
968
|
-
import { jsx as
|
|
1057
|
+
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
969
1058
|
var ANIMATION_DURATION2 = 250;
|
|
970
1059
|
var EASING = Easing2.bezier(0.4, 0, 0.2, 1);
|
|
971
1060
|
var DEFAULT_ELEVATION = 2;
|
|
@@ -974,24 +1063,24 @@ var SHADOW_RADIUS = [0, 3, 6, 8, 10, 12];
|
|
|
974
1063
|
var AnimatedBlurView2 = Animated2.createAnimatedComponent(BlurView);
|
|
975
1064
|
var FadingBlurView = ({ blur, children, elevation, theme: themeOverrides, visible }) => {
|
|
976
1065
|
const { animation, roundness } = useTheme9(themeOverrides);
|
|
977
|
-
const [opacity] =
|
|
978
|
-
const [closing, setClosing] =
|
|
1066
|
+
const [opacity] = useState7(() => new Animated2.Value(0));
|
|
1067
|
+
const [closing, setClosing] = useState7(false);
|
|
979
1068
|
if (!visible && !closing) setClosing(true);
|
|
980
1069
|
useEffect5(() => {
|
|
981
1070
|
Animated2.timing(opacity, { duration: animation.scale * ANIMATION_DURATION2, easing: EASING, toValue: closing ? 0 : 1, useNativeDriver: false }).start();
|
|
982
1071
|
}, [animation.scale, closing, opacity]);
|
|
983
1072
|
const shadow = elevation > 0 ? { shadowColor: "#000000", shadowOffset: { height: SHADOW_HEIGHT[elevation], width: 0 }, shadowOpacity: 0.3, shadowRadius: SHADOW_RADIUS[elevation] } : null;
|
|
984
|
-
return /* @__PURE__ */
|
|
1073
|
+
return /* @__PURE__ */ jsx17(AnimatedBlurView2, { blur, style: [{ borderRadius: roundness, opacity }, shadow], children });
|
|
985
1074
|
};
|
|
986
1075
|
var MenuComponent = ({ blur: blurProp, children, ...props }) => {
|
|
987
1076
|
const blur = useBlur(blurProp);
|
|
988
|
-
const web =
|
|
989
|
-
return /* @__PURE__ */
|
|
1077
|
+
const web = Platform5.OS === "web";
|
|
1078
|
+
return /* @__PURE__ */ jsx17(PaperMenu, { ...props, contentStyle: [styles6.content, web && styles6.webSurface, props.contentStyle], children: web ? /* @__PURE__ */ jsx17(FadingBlurView, { blur, elevation: props.mode === "flat" ? 0 : props.elevation ?? DEFAULT_ELEVATION, theme: props.theme, visible: props.visible, children }) : /* @__PURE__ */ jsx17(BlurView, { blur, children }) });
|
|
990
1079
|
};
|
|
991
1080
|
var Menu = Object.assign(MenuComponent, {
|
|
992
1081
|
Item: PaperMenu.Item
|
|
993
1082
|
});
|
|
994
|
-
var
|
|
1083
|
+
var styles6 = StyleSheet7.create({
|
|
995
1084
|
content: { backgroundColor: "transparent", paddingVertical: 0 },
|
|
996
1085
|
webSurface: { opacity: 1, shadowOpacity: 0 }
|
|
997
1086
|
});
|
|
@@ -999,10 +1088,10 @@ var styles5 = StyleSheet6.create({
|
|
|
999
1088
|
// src/components/TextInput.tsx
|
|
1000
1089
|
import { forwardRef } from "react";
|
|
1001
1090
|
import { TextInput as PaperTextInput } from "react-native-paper";
|
|
1002
|
-
import { jsx as
|
|
1091
|
+
import { jsx as jsx18 } from "react/jsx-runtime";
|
|
1003
1092
|
var TextInput = forwardRef(function TextInput2(props, ref) {
|
|
1004
1093
|
const defaults = usePaperDefaults();
|
|
1005
|
-
return /* @__PURE__ */
|
|
1094
|
+
return /* @__PURE__ */ jsx18(PaperTextInput, { ref, ...defaults.TextInput, ...props });
|
|
1006
1095
|
});
|
|
1007
1096
|
|
|
1008
1097
|
// src/redux/themeSlice.ts
|
|
@@ -1130,11 +1219,13 @@ export {
|
|
|
1130
1219
|
ColorPicker,
|
|
1131
1220
|
Dialog,
|
|
1132
1221
|
FAB,
|
|
1222
|
+
FadingScrollView,
|
|
1133
1223
|
HarmonyPicker,
|
|
1134
1224
|
IconButton,
|
|
1135
1225
|
Menu,
|
|
1136
1226
|
PalettePicker,
|
|
1137
1227
|
Provider,
|
|
1228
|
+
SCROLL_FADE_HEIGHT,
|
|
1138
1229
|
SEMANTIC_BASE_COLORS,
|
|
1139
1230
|
TextInput,
|
|
1140
1231
|
ThemeSettingsContext,
|
|
@@ -1162,6 +1253,7 @@ export {
|
|
|
1162
1253
|
useBlur,
|
|
1163
1254
|
useBlurModule,
|
|
1164
1255
|
useComputedTheme,
|
|
1256
|
+
useMaskedViewModule,
|
|
1165
1257
|
useNavBarContext,
|
|
1166
1258
|
usePaperDefaults,
|
|
1167
1259
|
useReanimatedModule,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rific/auto-paper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"@infinitetoken/eslint-config": "0.2.0",
|
|
72
72
|
"@infinitetoken/jest-config": "^0.4.0",
|
|
73
73
|
"@infinitetoken/tsconfig": "^0.5.0",
|
|
74
|
+
"@react-native-masked-view/masked-view": "^0.3.2",
|
|
74
75
|
"@reduxjs/toolkit": "^2.0.0",
|
|
75
76
|
"@rific/core": "^0.2.0",
|
|
76
77
|
"@testing-library/dom": "^10.4.1",
|
|
@@ -94,6 +95,7 @@
|
|
|
94
95
|
"typescript": "^6.0.3"
|
|
95
96
|
},
|
|
96
97
|
"peerDependencies": {
|
|
98
|
+
"@react-native-masked-view/masked-view": ">=0.3.0",
|
|
97
99
|
"@rific/core": ">=0.1.1",
|
|
98
100
|
"expo-blur": ">=56.0.0",
|
|
99
101
|
"expo-navigation-bar": ">=56.0.0",
|
|
@@ -103,6 +105,9 @@
|
|
|
103
105
|
"react-native-safe-area-context": "*"
|
|
104
106
|
},
|
|
105
107
|
"peerDependenciesMeta": {
|
|
108
|
+
"@react-native-masked-view/masked-view": {
|
|
109
|
+
"optional": true
|
|
110
|
+
},
|
|
106
111
|
"expo-blur": {
|
|
107
112
|
"optional": true
|
|
108
113
|
},
|
package/src/ThemeProvider.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { configureFonts, Portal, Provider as PaperProvider } from 'react-native-
|
|
|
4
4
|
|
|
5
5
|
import type { ExpoBlurModule } from './components/BlurView'
|
|
6
6
|
import type { ReanimatedModule } from './components/Dialog'
|
|
7
|
+
import type { MaskedViewModule } from './components/FadingScrollView'
|
|
7
8
|
import type { ExpoNavigationBarModule } from './navigation-bar'
|
|
8
9
|
import { type PaperDefaults, PaperDefaultsContext } from './PaperDefaultsContext'
|
|
9
10
|
import { type ThemeSettings, themeSettingsProvider, useThemeSettingsInternal } from './ThemeSettingsContext'
|
|
@@ -24,18 +25,23 @@ export const useBlurModule = () => useContext(BlurModuleContext)
|
|
|
24
25
|
const ReanimatedModuleContext = createContext<ReanimatedModule | undefined>(undefined)
|
|
25
26
|
export const useReanimatedModule = () => useContext(ReanimatedModuleContext)
|
|
26
27
|
|
|
28
|
+
const MaskedViewModuleContext = createContext<MaskedViewModule | undefined>(undefined)
|
|
29
|
+
export const useMaskedViewModule = () => useContext(MaskedViewModuleContext)
|
|
30
|
+
|
|
27
31
|
export type ProviderProps = {
|
|
28
32
|
children: ReactNode
|
|
29
33
|
defaults?: PaperDefaults
|
|
30
34
|
/** Injects expo-blur for `<BlurView>`. Pass `import * as ExpoBlur from 'expo-blur'`; omit to always render BlurView's solid fallback. */
|
|
31
35
|
expoBlur?: ExpoBlurModule
|
|
32
36
|
/** Applied to every Paper typography variant (bodyLarge, headlineMedium, labelSmall, ...) via
|
|
33
|
-
* `configureFonts`'s flat-config mode
|
|
37
|
+
* `configureFonts`'s flat-config mode - each variant keeps its own MD3 fontSize/lineHeight/weight,
|
|
34
38
|
* only fontFamily changes. The one-line way to reskin an app's whole text to a custom typeface,
|
|
35
39
|
* instead of passing `fontFamily` to every individual Text/TextInput/Button across the app. Omit
|
|
36
40
|
* to keep each platform's system font (MD3's own default). */
|
|
37
41
|
fontFamily?: string
|
|
38
42
|
initialValue?: Partial<ThemeSettings>
|
|
43
|
+
/** Injects @react-native-masked-view/masked-view so `<FadingScrollView>` can fade its content out above a pinned footer on iOS and Android. Pass `import MaskedView from '@react-native-masked-view/masked-view'`; omit to have it scroll without the fade there (web never needs it). */
|
|
44
|
+
maskedView?: MaskedViewModule
|
|
39
45
|
/** 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. */
|
|
40
46
|
navigationBar?: ExpoNavigationBarModule
|
|
41
47
|
onChange?: (settings: ThemeSettings) => void
|
|
@@ -48,15 +54,15 @@ export type ProviderProps = {
|
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
// createSettingsContext's generated Provider only exposes `settings` to descendants (via its
|
|
51
|
-
// generated useSettings())
|
|
57
|
+
// generated useSettings()) - it can't itself also compute the theme/gate mounting on it being
|
|
52
58
|
// ready, since that logic needs `settings` in scope. Provider (below) is just this thin outer
|
|
53
59
|
// wrapper for the settings half; ThemeProviderBody (an actual descendant, reached via
|
|
54
60
|
// useThemeSettingsInternal()) does all the theme-computation/gating work the single combined
|
|
55
61
|
// component used to do directly.
|
|
56
|
-
export function Provider({ children, defaults, expoBlur, fontFamily, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps) {
|
|
62
|
+
export function Provider({ children, defaults, expoBlur, fontFamily, initialValue, maskedView, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps) {
|
|
57
63
|
return (
|
|
58
64
|
<ThemeSettingsProvider initialValue={initialValue} onChange={onChange}>
|
|
59
|
-
<ThemeProviderBody defaults={defaults} expoBlur={expoBlur} fontFamily={fontFamily} navigationBar={navigationBar} onNavBarChange={onNavBarChange} onReady={onReady} reanimated={reanimated} statusBarProps={statusBarProps} style={style}>
|
|
65
|
+
<ThemeProviderBody defaults={defaults} expoBlur={expoBlur} fontFamily={fontFamily} maskedView={maskedView} navigationBar={navigationBar} onNavBarChange={onNavBarChange} onReady={onReady} reanimated={reanimated} statusBarProps={statusBarProps} style={style}>
|
|
60
66
|
{children}
|
|
61
67
|
</ThemeProviderBody>
|
|
62
68
|
</ThemeSettingsProvider>
|
|
@@ -67,11 +73,11 @@ const ThemeSettingsProvider = themeSettingsProvider
|
|
|
67
73
|
|
|
68
74
|
type ThemeProviderBodyProps = Omit<ProviderProps, 'initialValue' | 'onChange'>
|
|
69
75
|
|
|
70
|
-
function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, navigationBar, onNavBarChange, onReady, reanimated, statusBarProps, style }: ThemeProviderBodyProps) {
|
|
76
|
+
function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, maskedView, navigationBar, onNavBarChange, onReady, reanimated, statusBarProps, style }: ThemeProviderBodyProps) {
|
|
71
77
|
const { settings } = useThemeSettingsInternal()
|
|
72
78
|
|
|
73
79
|
const computedTheme = useComputedTheme(settings.appearance, settings.color, settings.harmony)
|
|
74
|
-
// Layered on top of useComputedTheme's own output rather than folded into it
|
|
80
|
+
// Layered on top of useComputedTheme's own output rather than folded into it - that hook only
|
|
75
81
|
// ever recomputes for a color/appearance/harmony change, and fontFamily is a completely
|
|
76
82
|
// independent axis; merging it there would mean either re-running the whole color computation on
|
|
77
83
|
// every fontFamily change (wasteful) or threading fontFamily through as a fourth computeTheme
|
|
@@ -99,17 +105,19 @@ function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, navigatio
|
|
|
99
105
|
<NavBarContext.Provider value={{ navigationBar, onNavBarChange }}>
|
|
100
106
|
<BlurModuleContext.Provider value={expoBlur}>
|
|
101
107
|
<ReanimatedModuleContext.Provider value={reanimated}>
|
|
102
|
-
{
|
|
108
|
+
<MaskedViewModuleContext.Provider value={maskedView}>
|
|
109
|
+
{/* PaperProvider's own PortalHost sits above every context provided here, and Paper's
|
|
103
110
|
Portal only carries its theme and settings across, so anything rendered through a
|
|
104
111
|
Portal (this package's Dialog, Paper's Menu and Tooltip, or anything an app wraps
|
|
105
|
-
in <Portal> itself) would otherwise lose expoBlur, reanimated, defaults and the nav
|
|
112
|
+
in <Portal> itself) would otherwise lose expoBlur, reanimated, maskedView, defaults and the nav
|
|
106
113
|
bar hooks: Dialog/Menu's blur silently fell back to solid. This nested host is the
|
|
107
114
|
nearest one for every Portal under Provider, so their content mounts inside all of
|
|
108
115
|
these contexts instead. It wraps the styled View, not the other way round, so
|
|
109
116
|
portals still cover the full area regardless of `style`. */}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
<Portal.Host>
|
|
118
|
+
<View style={[styles.flex, { backgroundColor: theme.colors.background }, style]}>{children}</View>
|
|
119
|
+
</Portal.Host>
|
|
120
|
+
</MaskedViewModuleContext.Provider>
|
|
113
121
|
</ReanimatedModuleContext.Provider>
|
|
114
122
|
</BlurModuleContext.Provider>
|
|
115
123
|
</NavBarContext.Provider>
|
|
@@ -8,7 +8,7 @@ type Props = {
|
|
|
8
8
|
|
|
9
9
|
// AppearancePicker wired straight to the Provider's own ThemeSettings, for the common case where
|
|
10
10
|
// the appearance being edited IS the whole app's own theme. Takes neither `value` nor `onChange`
|
|
11
|
-
//
|
|
11
|
+
// - reads/writes useThemeSettings() itself, so there's nothing for a consumer to wire up. Reach
|
|
12
12
|
// for the plain AppearancePicker instead when it needs to be controlled by something other than
|
|
13
13
|
// the global theme (a local preview before committing, one-off UI unrelated to Provider).
|
|
14
14
|
export const AutoAppearancePicker = (props: Props) => {
|
|
@@ -9,9 +9,9 @@ type Props = {
|
|
|
9
9
|
blur?: boolean
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
// PalettePicker wired straight to the Provider's own ThemeSettings
|
|
12
|
+
// PalettePicker wired straight to the Provider's own ThemeSettings - both color AND harmony -
|
|
13
13
|
// for the common case where the palette being edited IS the whole app's own theme. Takes none of
|
|
14
|
-
// `value`/`onChange`/`harmony`/`onHarmonyChange`
|
|
14
|
+
// `value`/`onChange`/`harmony`/`onHarmonyChange` - reads/writes useThemeSettings() itself
|
|
15
15
|
// (resolveSeedColor narrows ThemeSettings.color's `string | TriadicPalette` down to the plain
|
|
16
16
|
// string PalettePicker's own `value` wants). Reach for the plain PalettePicker instead when it
|
|
17
17
|
// needs to be controlled by something other than the global theme.
|
|
@@ -58,7 +58,16 @@ export type BlurViewProps = ExpoBlurViewProps & {
|
|
|
58
58
|
variant?: 'primary' | 'secondary' | 'tertiary' | 'error' | 'surface' | 'surfaceVariant'
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
// The style expo-blur's web BlurView puts on its View (getBlurStyle in its src/BlurView.web.tsx, with
|
|
62
|
+
// getBackgroundColor's 'dark' and 'light' tints, the only two this component uses): a CSS
|
|
63
|
+
// backdrop-filter plus a translucent tint. On web BlurView applies it to its own View instead.
|
|
64
|
+
const getWebBlurStyle = (intensity: number, dark: boolean) => {
|
|
65
|
+
const amount = Math.min(intensity, 100)
|
|
66
|
+
const filter = `saturate(180%) blur(${amount * 0.2}px)`
|
|
67
|
+
return { backdropFilter: filter, backgroundColor: `rgba(${dark ? '25,25,25' : '249,249,249'},${(amount / 100) * 0.78})`, WebkitBackdropFilter: filter } as ViewStyle
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const BlurView = ({ blur = true, blurMethod, blurReductionFactor, blurTarget, children, elevation, experimentalBlurMethod, intensity, style, tint: _tint, tintColor, tintOpacity, variant = 'surface', ...props }: BlurViewProps) => {
|
|
62
71
|
const { dark, colors } = useTheme()
|
|
63
72
|
const { settings } = useContext(ThemeSettingsContext)
|
|
64
73
|
const expoBlur = useBlurModule()
|
|
@@ -67,16 +76,21 @@ export const BlurView = ({ blur = true, children, elevation, style, tintColor, t
|
|
|
67
76
|
const resolvedTintColor = tintColor ?? variantColor ?? (elevation !== undefined ? colors.primary : undefined)
|
|
68
77
|
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)
|
|
69
78
|
const tint = resolvedTintColor && resolvedTintOpacity > 0 ? <View style={[StyleSheet.absoluteFill, { backgroundColor: resolvedTintColor, opacity: resolvedTintOpacity }]} /> : null
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
const blurProps = { blurMethod, blurReductionFactor, blurTarget, experimentalBlurMethod, intensity }
|
|
80
|
+
const blurs = blur && expoBlur !== undefined && (Platform.OS !== 'android' || blursOnAndroid(blurProps))
|
|
81
|
+
const web = Platform.OS === 'web'
|
|
82
|
+
// The View holding the children is the same element whether or not blur applies, so flipping
|
|
83
|
+
// `blur` (a Dialog or Menu following the player's live settings.blur) never remounts them: their
|
|
84
|
+
// state, scroll offset, focus and ripples all survive. On iOS and Android the blur is an absoluteFill
|
|
85
|
+
// layer behind everything else, the same structure as expo-blur's own native BlurView. On web it
|
|
86
|
+
// can't be: Dialog and Menu fade this View's own opacity there, and an ancestor below opacity 1 cuts
|
|
87
|
+
// a CSS backdrop-filter off from the page behind it, so on web this View itself is the
|
|
88
|
+
// backdrop-filter element, styled the way expo-blur's web BlurView styles its own.
|
|
89
|
+
const containerStyle = !blurs ? [{ backgroundColor: surfaceColor }, style] : web ? [style, getWebBlurStyle(intensity ?? 50, dark)] : style
|
|
78
90
|
return (
|
|
79
|
-
<View {...props} style={
|
|
91
|
+
<View {...props} style={containerStyle}>
|
|
92
|
+
{blurs && !web ? <expoBlur.BlurView {...blurProps} style={StyleSheet.absoluteFill} tint={dark ? 'dark' : 'light'} /> : null}
|
|
93
|
+
{blurs ? <View style={[StyleSheet.absoluteFill, { backgroundColor: surfaceColor, opacity: settings.blurTint }]} /> : null}
|
|
80
94
|
{tint}
|
|
81
95
|
{children}
|
|
82
96
|
</View>
|
|
@@ -7,11 +7,11 @@ import { useBlur } from '../BlurContext'
|
|
|
7
7
|
import { useReanimatedModule } from '../ThemeProvider'
|
|
8
8
|
import { BlurView } from './BlurView'
|
|
9
9
|
|
|
10
|
-
// Minimal local mirror of react-native-reanimated's Animated.View
|
|
10
|
+
// Minimal local mirror of react-native-reanimated's Animated.View - same reasoning as BlurView's
|
|
11
11
|
// ExpoBlurModule: Metro doesn't rewrite a require()-in-try/catch call into its module graph inside
|
|
12
12
|
// an ESM build, so the already-imported peer module is injected via <Provider reanimated={...}>
|
|
13
13
|
// (see ThemeProvider.tsx) instead of required directly. Only `View` is needed here. `style: any`,
|
|
14
|
-
// not a real style type
|
|
14
|
+
// not a real style type - reanimated's actual AnimatedView only accepts its own branded
|
|
15
15
|
// AnimatedStyle shape, which this package never imports (no reanimated dependency at all), so `any`
|
|
16
16
|
// is the only type that both sides (this interface and the real component passed in) can agree on.
|
|
17
17
|
export interface ReanimatedModule {
|
|
@@ -23,7 +23,7 @@ export type DialogProps = Omit<PaperDialogProps, 'style'> & {
|
|
|
23
23
|
blur?: boolean
|
|
24
24
|
style?: StyleProp<ViewStyle>
|
|
25
25
|
// Applied to the card's outer wrapper instead of the card itself, and only takes effect once a
|
|
26
|
-
// reanimated module has been injected via <Provider reanimated={...}>
|
|
26
|
+
// reanimated module has been injected via <Provider reanimated={...}> - otherwise ignored. `style`
|
|
27
27
|
// above is a plain object read once per render, so a fast-changing value (e.g. tracking the
|
|
28
28
|
// keyboard height every frame) driven through it needs a full JS re-render per update to paint at
|
|
29
29
|
// all; wiring the same value through `animatedStyle` (e.g. the result of Reanimated's
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { type ComponentType, type ReactElement, type ReactNode, useCallback, useRef, useState } from 'react'
|
|
2
|
+
import { type LayoutChangeEvent, type NativeScrollEvent, type NativeSyntheticEvent, Platform, ScrollView, type ScrollViewProps, type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'
|
|
3
|
+
|
|
4
|
+
import { useMaskedViewModule } from '../ThemeProvider'
|
|
5
|
+
|
|
6
|
+
// Minimal local shape of @react-native-masked-view/masked-view's default export, the same way
|
|
7
|
+
// ExpoBlurModule stands in for expo-blur: the optional peer's real types are never imported, so a
|
|
8
|
+
// consumer that never installed it doesn't need them. <Provider maskedView={MaskedView}> hands it in.
|
|
9
|
+
export type MaskedViewModule = ComponentType<{ children?: ReactNode; maskElement: ReactElement; style?: StyleProp<ViewStyle> }>
|
|
10
|
+
|
|
11
|
+
// How far in from each edge a FadingScrollView's content fades out while there is more past that edge.
|
|
12
|
+
export const SCROLL_FADE_HEIGHT = 24
|
|
13
|
+
|
|
14
|
+
export type FadingScrollViewProps = Omit<ScrollViewProps, 'style'> & {
|
|
15
|
+
// Placed on the outer box (the flex item), so a caller's flexGrow/flexShrink/alignSelf work as they
|
|
16
|
+
// did on a plain ScrollView.
|
|
17
|
+
style?: StyleProp<ViewStyle>
|
|
18
|
+
fadeHeight?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// A vertical ScrollView whose content fades to transparent along an edge while there is more to scroll
|
|
22
|
+
// past it: at the bottom, the cue that rows continue below a pinned footer, like a dialog's Done
|
|
23
|
+
// button; at the top, the same once rows have scrolled up past the edge (Jay, 2026-10-15: one treatment
|
|
24
|
+
// at both ends instead of a hard cut at the top). It masks
|
|
25
|
+
// the content itself rather than painting anything over it, so whatever is behind (a blurred
|
|
26
|
+
// Dialog/BlurView surface, a plain one, light or dark) shows through unchanged: no band, no seam. (A
|
|
27
|
+
// second BlurView as a frosted footer was tried in @tastic/hud first and left a seam on iOS and web: a
|
|
28
|
+
// blur nested in a blurred card can't frost the card's own content.) Once the end is reached the fade
|
|
29
|
+
// goes away, and the top fade only shows once scrolled down, so neither the last row nor the first
|
|
30
|
+
// (a dialog's header, say) is ever dimmed at rest.
|
|
31
|
+
//
|
|
32
|
+
// iOS/Android: a MaskedView (from <Provider maskedView={...}>) whose top and bottom bands are React
|
|
33
|
+
// Native's native linear gradient (experimental_backgroundImage). Without one passed in, content just scrolls
|
|
34
|
+
// with no fade. Web: react-native-masked-view's web build renders only the mask element (it doesn't
|
|
35
|
+
// mask), so the outer box gets a CSS mask-image instead, which react-native-web passes through; no
|
|
36
|
+
// module needed there.
|
|
37
|
+
export function FadingScrollView({ style, fadeHeight = SCROLL_FADE_HEIGHT, onScroll, onLayout, onContentSizeChange, scrollEventThrottle = 16, children, ...rest }: FadingScrollViewProps) {
|
|
38
|
+
const MaskedView = useMaskedViewModule()
|
|
39
|
+
// Sizes live in a ref and only the yes/no answers are state, so scrolling re-renders nothing until a
|
|
40
|
+
// fade has to appear or go.
|
|
41
|
+
const sizes = useRef({ viewport: 0, content: 0, offset: 0 })
|
|
42
|
+
const [edges, setEdges] = useState(NO_EDGES)
|
|
43
|
+
const update = useCallback(() => {
|
|
44
|
+
const { viewport, content, offset } = sizes.current
|
|
45
|
+
const above = offset > 1
|
|
46
|
+
const below = content - viewport - offset > 1
|
|
47
|
+
setEdges((prev) => (prev.above === above && prev.below === below ? prev : { above, below }))
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
50
|
+
const scroll = (
|
|
51
|
+
<ScrollView
|
|
52
|
+
{...rest}
|
|
53
|
+
style={styles.fill}
|
|
54
|
+
scrollEventThrottle={scrollEventThrottle}
|
|
55
|
+
onScroll={(e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
56
|
+
sizes.current.offset = e.nativeEvent.contentOffset.y
|
|
57
|
+
update()
|
|
58
|
+
onScroll?.(e)
|
|
59
|
+
}}
|
|
60
|
+
onLayout={(e: LayoutChangeEvent) => {
|
|
61
|
+
sizes.current.viewport = e.nativeEvent.layout.height
|
|
62
|
+
update()
|
|
63
|
+
onLayout?.(e)
|
|
64
|
+
}}
|
|
65
|
+
onContentSizeChange={(width: number, height: number) => {
|
|
66
|
+
sizes.current.content = height
|
|
67
|
+
update()
|
|
68
|
+
onContentSizeChange?.(width, height)
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
</ScrollView>
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if (Platform.OS === 'web') return <View style={[styles.box, style, (edges.above || edges.below) && webFadeStyle(fadeHeight, edges)]}>{scroll}</View>
|
|
76
|
+
if (!MaskedView) return <View style={[styles.box, style]}>{scroll}</View>
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<MaskedView
|
|
80
|
+
style={[styles.box, style]}
|
|
81
|
+
maskElement={
|
|
82
|
+
<View style={styles.fill}>
|
|
83
|
+
<View style={[{ height: fadeHeight }, edges.above ? FADE_IN_BAND : styles.opaque]} />
|
|
84
|
+
<View style={styles.fillOpaque} />
|
|
85
|
+
<View style={[{ height: fadeHeight }, edges.below ? FADE_OUT_BAND : styles.opaque]} />
|
|
86
|
+
</View>
|
|
87
|
+
}
|
|
88
|
+
>
|
|
89
|
+
{scroll}
|
|
90
|
+
</MaskedView>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// React Native's native linear gradient (0.77+, the New Architecture; the fleet's apps are on 0.86), not
|
|
95
|
+
// in this package's own 0.76 dev types, hence the cast. A mask's alpha is what shows: opaque keeps the
|
|
96
|
+
// content, transparent hides it.
|
|
97
|
+
const FADE_IN_BAND = { experimental_backgroundImage: 'linear-gradient(transparent, #000)' } as unknown as ViewStyle
|
|
98
|
+
const FADE_OUT_BAND = { experimental_backgroundImage: 'linear-gradient(#000, transparent)' } as unknown as ViewStyle
|
|
99
|
+
|
|
100
|
+
const NO_EDGES = { above: false, below: false }
|
|
101
|
+
|
|
102
|
+
// mask-image isn't in React Native's style types, but react-native-web passes it to the DOM as is.
|
|
103
|
+
function webFadeStyle(fadeHeight: number, edges: { above: boolean; below: boolean }): ViewStyle {
|
|
104
|
+
const top = edges.above ? `transparent 0, #000 ${fadeHeight}px` : '#000 0'
|
|
105
|
+
const bottom = edges.below ? `#000 calc(100% - ${fadeHeight}px), transparent 100%` : '#000 100%'
|
|
106
|
+
const mask = `linear-gradient(to bottom, ${top}, ${bottom})`
|
|
107
|
+
return { maskImage: mask, WebkitMaskImage: mask } as unknown as ViewStyle
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const styles = StyleSheet.create({
|
|
111
|
+
// A plain ScrollView's own flex defaults, so it sizes the same in its parent.
|
|
112
|
+
box: {
|
|
113
|
+
flexGrow: 1,
|
|
114
|
+
flexShrink: 1
|
|
115
|
+
},
|
|
116
|
+
fill: {
|
|
117
|
+
flexGrow: 1,
|
|
118
|
+
flexShrink: 1
|
|
119
|
+
},
|
|
120
|
+
fillOpaque: {
|
|
121
|
+
backgroundColor: '#000',
|
|
122
|
+
flexGrow: 1
|
|
123
|
+
},
|
|
124
|
+
opaque: {
|
|
125
|
+
backgroundColor: '#000'
|
|
126
|
+
}
|
|
127
|
+
})
|
|
@@ -13,12 +13,12 @@ const HARMONIES: { label: string; value: ColorHarmony; icon: string }[] = [
|
|
|
13
13
|
|
|
14
14
|
// Paper's own SegmentedButtonItem hardcodes `minWidth: 76` per segment (sized for a labeled text
|
|
15
15
|
// button), which 6 of never fit inside PalettePicker's own dialog card (capped at 400, and
|
|
16
|
-
// narrower still once its padding and safe-area margins are subtracted)
|
|
16
|
+
// narrower still once its padding and safe-area margins are subtracted) - they just overflow past
|
|
17
17
|
// its `overflow: hidden` edge instead of shrinking. A fixed smaller guess (say 44) only moves the
|
|
18
|
-
// breakpoint rather than removing it
|
|
18
|
+
// breakpoint rather than removing it - 6x44 still exceeds the card's content width on an ordinary
|
|
19
19
|
// ~360-375px phone. Zeroing the floor instead lets each segment's own `flex: 1` divide whatever
|
|
20
20
|
// width IS available with nothing left to enforce a minimum, so 6 of them mathematically can never
|
|
21
|
-
// exceed their container at any screen size
|
|
21
|
+
// exceed their container at any screen size - same "shrink to fit, never overflow" contract
|
|
22
22
|
// useSwatchGrid.ts's own SWATCH_GRID_COLUMNS already uses for the swatches below this row. Left
|
|
23
23
|
// untouched in labeled mode, where the wider floor is still doing its job of fitting the text.
|
|
24
24
|
const COMPACT_MIN_WIDTH = 0
|
|
@@ -26,13 +26,13 @@ const COMPACT_MIN_WIDTH = 0
|
|
|
26
26
|
type Props = {
|
|
27
27
|
value: ColorHarmony
|
|
28
28
|
onChange: (harmony: ColorHarmony) => void
|
|
29
|
-
// Hides text labels, leaving just the icon per segment
|
|
29
|
+
// Hides text labels, leaving just the icon per segment - mirrors AppearancePicker's own
|
|
30
30
|
// showLabels, useful here since there are 6 segments to fit instead of 3.
|
|
31
31
|
// accessibilityLabel is always set regardless, so screen readers still announce the full
|
|
32
32
|
// harmony name either way.
|
|
33
33
|
showLabels?: boolean
|
|
34
34
|
// Per-segment fill, e.g. theme.colors.secondary/onSecondary for the checked segment and
|
|
35
|
-
// secondaryContainer/onSecondaryContainer for the rest
|
|
35
|
+
// secondaryContainer/onSecondaryContainer for the rest - matches Paper's own checked/
|
|
36
36
|
// unchecked + container color vocabulary. Omit to fall back to Paper's default segmented-
|
|
37
37
|
// button styling.
|
|
38
38
|
checkedColor?: string
|
|
@@ -58,7 +58,7 @@ type Props = {
|
|
|
58
58
|
harmony?: ColorHarmony
|
|
59
59
|
// Opt-in: passing this turns on a compact, icon-only HarmonyPicker row inside the dialog, above
|
|
60
60
|
// the swatch grid, so every swatch's own Pie preview reshapes live as harmony changes. Omit it
|
|
61
|
-
// and this renders exactly as a plain PalettePicker always has
|
|
61
|
+
// and this renders exactly as a plain PalettePicker always has - same trigger, same dialog,
|
|
62
62
|
// harmony fixed at whatever the `harmony` prop says. Gated on the callback itself rather than a
|
|
63
63
|
// separate boolean so there's no way to pass one without the other.
|
|
64
64
|
onHarmonyChange?: (harmony: ColorHarmony) => void
|