@rific/scroll-view 0.6.0 → 0.6.2

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.d.mts CHANGED
@@ -6,7 +6,7 @@ import { ChipProps as ChipProps$1 } from '@rific/auto-paper';
6
6
  import * as react_native_reanimated from 'react-native-reanimated';
7
7
  import { SharedValue } from 'react-native-reanimated';
8
8
 
9
- type ChipProps = Omit<ChipProps$1, 'compact' | 'icon' | 'onPress'> & {
9
+ type ChipProps = Omit<ChipProps$1, 'compact' | 'icon' | 'onPress' | 'children'> & {
10
10
  label?: ReactNode;
11
11
  };
12
12
 
@@ -122,7 +122,7 @@ declare const ScrollView: typeof ScrollViewInner;
122
122
  type ScrollViewContextType = {
123
123
  blur: boolean;
124
124
  footerAboveKeyboard: boolean;
125
- footerHeight: number;
125
+ footerHeight: number | null;
126
126
  footerHeightShared: SharedValue<number>;
127
127
  footerFixed: boolean;
128
128
  footerOffset: SharedValue<number>;
@@ -141,7 +141,7 @@ type ScrollViewContextType = {
141
141
  progressing: boolean;
142
142
  scrollHeight: number;
143
143
  scrollPosition: SharedValue<number>;
144
- setFooterHeight: (h: number) => void;
144
+ setFooterHeight: (h: number | null) => void;
145
145
  setHeaderHeight: (h: number | null) => void;
146
146
  setProgress: (p: number | null) => void;
147
147
  setProgressing: (b: boolean) => void;
@@ -158,7 +158,7 @@ declare const ScrollViewFooter: ({ children, style }: ScrollViewFooterProps) =>
158
158
  type ScrollViewHeaderProps = {
159
159
  actionSize?: number;
160
160
  actionStyle?: ViewStyle;
161
- backAction?: () => void;
161
+ backAction?: (() => void) | ReactNode;
162
162
  backActionAccessibilityLabel?: string;
163
163
  backActionFixed?: boolean;
164
164
  caption?: string;
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ import { ChipProps as ChipProps$1 } from '@rific/auto-paper';
6
6
  import * as react_native_reanimated from 'react-native-reanimated';
7
7
  import { SharedValue } from 'react-native-reanimated';
8
8
 
9
- type ChipProps = Omit<ChipProps$1, 'compact' | 'icon' | 'onPress'> & {
9
+ type ChipProps = Omit<ChipProps$1, 'compact' | 'icon' | 'onPress' | 'children'> & {
10
10
  label?: ReactNode;
11
11
  };
12
12
 
@@ -122,7 +122,7 @@ declare const ScrollView: typeof ScrollViewInner;
122
122
  type ScrollViewContextType = {
123
123
  blur: boolean;
124
124
  footerAboveKeyboard: boolean;
125
- footerHeight: number;
125
+ footerHeight: number | null;
126
126
  footerHeightShared: SharedValue<number>;
127
127
  footerFixed: boolean;
128
128
  footerOffset: SharedValue<number>;
@@ -141,7 +141,7 @@ type ScrollViewContextType = {
141
141
  progressing: boolean;
142
142
  scrollHeight: number;
143
143
  scrollPosition: SharedValue<number>;
144
- setFooterHeight: (h: number) => void;
144
+ setFooterHeight: (h: number | null) => void;
145
145
  setHeaderHeight: (h: number | null) => void;
146
146
  setProgress: (p: number | null) => void;
147
147
  setProgressing: (b: boolean) => void;
@@ -158,7 +158,7 @@ declare const ScrollViewFooter: ({ children, style }: ScrollViewFooterProps) =>
158
158
  type ScrollViewHeaderProps = {
159
159
  actionSize?: number;
160
160
  actionStyle?: ViewStyle;
161
- backAction?: () => void;
161
+ backAction?: (() => void) | ReactNode;
162
162
  backActionAccessibilityLabel?: string;
163
163
  backActionFixed?: boolean;
164
164
  caption?: string;
package/dist/index.js CHANGED
@@ -64,15 +64,18 @@ var RefreshControl = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(impo
64
64
  // src/internal/ScrollViewChip.tsx
65
65
  var import_auto_paper = require("@rific/auto-paper");
66
66
  var import_react_native2 = require("react-native");
67
+ var import_react_native_paper = require("react-native-paper");
67
68
  var import_react_native_reanimated = __toESM(require("react-native-reanimated"));
68
69
  var import_jsx_runtime2 = require("react/jsx-runtime");
69
70
  var ScrollViewChip = ({ chipProps, isHorizontal, onChipPress, onPress, style }) => {
70
- const { label, style: chipStyle, ...restChipProps } = chipProps ?? {};
71
+ const { colors } = (0, import_react_native_paper.useTheme)();
72
+ const { label, selectedColor, style: chipStyle, ...restChipProps } = chipProps ?? {};
71
73
  const handlePress = onChipPress ? () => {
72
74
  onPress();
73
75
  onChipPress();
74
76
  } : onPress;
75
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_reanimated.default.View, { style: [styles.chip, style], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_auto_paper.Chip, { compact: true, ...restChipProps, icon: isHorizontal ? "chevron-left" : "chevron-up", onPress: handlePress, style: [styles.chipInner, chipStyle], children: label ?? (isHorizontal ? "Start" : "Top") }) });
77
+ const iconColor = selectedColor ?? (restChipProps.mode === "outlined" ? colors.onSurfaceVariant : colors.onSecondaryContainer);
78
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_reanimated.default.View, { style: [styles.chip, style], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_auto_paper.Chip, { compact: true, selectedColor, ...restChipProps, avatar: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper.Icon, { source: isHorizontal ? "chevron-left" : "chevron-up", size: 18, color: iconColor }), onPress: handlePress, style: [styles.chipInner, chipStyle], children: label ?? (isHorizontal ? "Start" : "Top") }) });
76
79
  };
77
80
  var styles = import_react_native2.StyleSheet.create({
78
81
  chip: { alignItems: "center", left: 0, position: "absolute", right: 0, zIndex: 3 },
@@ -88,7 +91,7 @@ var import_react = require("react");
88
91
  var ScrollViewContext = (0, import_react.createContext)({
89
92
  blur: true,
90
93
  footerAboveKeyboard: false,
91
- footerHeight: 0,
94
+ footerHeight: null,
92
95
  footerHeightShared: null,
93
96
  footerFixed: false,
94
97
  footerOffset: null,
@@ -428,7 +431,7 @@ var CHIP_SLIDE = 48;
428
431
  function useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedProp, hideUntilMeasured, isHorizontal, keyboardAware, pullSearchHeight, style } = {}) {
429
432
  const insets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
430
433
  const keyboardHeight = useKeyboardInset();
431
- const { footerAboveKeyboard, footerHeight, footerFixed: contextFooterFixed, headerHeight, headerFixed: contextHeaderFixed, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared, tabBarHeight } = (0, import_react5.useContext)(ScrollViewContext);
434
+ const { footerAboveKeyboard, footerHeight, footerFixed: contextFooterFixed, headerHeight, headerHeightShared, headerFixed: contextHeaderFixed, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared, tabBarHeight } = (0, import_react5.useContext)(ScrollViewContext);
432
435
  const headerFixed = isHorizontal ? true : headerFixedProp ?? contextHeaderFixed;
433
436
  const footerFixed = isHorizontal ? true : footerFixedProp ?? contextFooterFixed;
434
437
  (0, import_react5.useEffect)(() => {
@@ -452,11 +455,11 @@ function useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedP
452
455
  return {
453
456
  opacity: chipHidden.value ? (0, import_react_native_reanimated5.withTiming)(0) : (0, import_react_native_reanimated5.withTiming)(1),
454
457
  pointerEvents,
455
- top: Math.max(headerHeight ?? 0, insets.top) + 4,
458
+ top: Math.max(headerHeightShared.value, insets.top) + 4,
456
459
  transform: [{ translateX: chipHidden.value ? (0, import_react_native_reanimated5.withTiming)(-CHIP_SLIDE) : (0, import_react_native_reanimated5.withTiming)(0) }]
457
460
  };
458
461
  }
459
- const h = headerHeight ?? 0;
462
+ const h = headerHeightShared.value;
460
463
  const slide = headerFixed ? 0 : snapBackHeaderShared.value ? -headerOffset.value : Math.max(0, scrollPosition.value + h - pullSearchHeightShared.value);
461
464
  const top = Math.max(h - slide, insets.top) + 4;
462
465
  return {
@@ -465,7 +468,7 @@ function useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedP
465
468
  top,
466
469
  transform: [{ translateY: chipHidden.value ? (0, import_react_native_reanimated5.withTiming)(-CHIP_SLIDE) : (0, import_react_native_reanimated5.withTiming)(0) }]
467
470
  };
468
- }, [isHorizontal, headerFixed, headerHeight, insets.top, chipHidden, snapBackHeaderShared, headerOffset, scrollPosition, pullSearchHeightShared]);
471
+ }, [isHorizontal, headerFixed, headerHeightShared, insets.top, chipHidden, snapBackHeaderShared, headerOffset, scrollPosition, pullSearchHeightShared]);
469
472
  return { chipHidden, chipStyle, containerStyle, contentInset, contentOffset, contentPadding, footerFixed, headerFixed, headerHeight, insets, isHorizontal: isHorizontal ?? false };
470
473
  }
471
474
 
@@ -556,7 +559,7 @@ var import_react_native_gesture_handler2 = require("react-native-gesture-handler
556
559
  var import_auto_paper2 = require("@rific/auto-paper");
557
560
  var import_react7 = require("react");
558
561
  var import_react_native7 = require("react-native");
559
- var import_react_native_paper = require("react-native-paper");
562
+ var import_react_native_paper2 = require("react-native-paper");
560
563
  var import_react_native_reanimated7 = __toESM(require("react-native-reanimated"));
561
564
  var import_react_native_safe_area_context2 = require("react-native-safe-area-context");
562
565
  var import_jsx_runtime4 = require("react/jsx-runtime");
@@ -586,7 +589,7 @@ var HorizontalDots = ({ total }) => {
586
589
  const { blur, footerHeightShared, scrollPosition } = (0, import_react7.useContext)(ScrollViewContext);
587
590
  const { width } = (0, import_react_native7.useWindowDimensions)();
588
591
  const insets = (0, import_react_native_safe_area_context2.useSafeAreaInsets)();
589
- const theme = (0, import_react_native_paper.useTheme)();
592
+ const theme = (0, import_react_native_paper2.useTheme)();
590
593
  const insetsBottom = insets.bottom;
591
594
  const activeColor = theme.colors.primary;
592
595
  const inactiveColor = theme.colors.outlineVariant;
@@ -880,12 +883,12 @@ var FlatList = (0, import_react9.memo)(FlatListInner);
880
883
  // src/PullSearch.tsx
881
884
  var import_react10 = require("react");
882
885
  var import_react_native9 = require("react-native");
883
- var import_react_native_paper2 = require("react-native-paper");
886
+ var import_react_native_paper3 = require("react-native-paper");
884
887
  var import_react_native_reanimated9 = __toESM(require("react-native-reanimated"));
885
888
  var import_jsx_runtime6 = require("react/jsx-runtime");
886
889
  var PullSearch = (0, import_react10.forwardRef)(function PullSearch2({ debounce, onChangeText, onHeightChange, placeholder, value: propValue }, ref) {
887
890
  const { headerHeight, scrollPosition } = (0, import_react10.useContext)(ScrollViewContext);
888
- const theme = (0, import_react_native_paper2.useTheme)();
891
+ const theme = (0, import_react_native_paper3.useTheme)();
889
892
  const inputRef = (0, import_react10.useRef)(null);
890
893
  const [barHeight, setBarHeight] = (0, import_react10.useState)(0);
891
894
  const [value, setValue] = (0, import_react10.useState)(propValue ?? "");
@@ -943,7 +946,7 @@ var PullSearch = (0, import_react10.forwardRef)(function PullSearch2({ debounce,
943
946
  const hideY = -h + barHeight;
944
947
  return { opacity: (0, import_react_native_reanimated9.interpolate)(scrollPosition.value, [showY, hideY], [1, 0], import_react_native_reanimated9.Extrapolation.CLAMP) };
945
948
  }, [barHeight, headerHeight, scrollPosition]);
946
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_reanimated9.default.View, { onLayout: handleLayout, style: [styles5.container, animatedStyle], children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_paper2.Searchbar, { autoCorrect: false, onChangeText: (text) => setValue((text || "").trimStart()), onClearIconPress: handleClear, placeholder, placeholderTextColor: theme.colors.onSurfaceVariant, ref: inputRef, spellCheck: false, style: styles5.input, value }) });
949
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_reanimated9.default.View, { onLayout: handleLayout, style: [styles5.container, animatedStyle], children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_paper3.Searchbar, { autoCorrect: false, onChangeText: (text) => setValue((text || "").trimStart()), onClearIconPress: handleClear, placeholder, placeholderTextColor: theme.colors.onSurfaceVariant, ref: inputRef, spellCheck: false, style: styles5.input, value }) });
947
950
  });
948
951
  var styles5 = import_react_native9.StyleSheet.create({
949
952
  container: { paddingVertical: 8 },
@@ -1058,14 +1061,20 @@ var import_react13 = require("react");
1058
1061
  var import_react_native11 = require("react-native");
1059
1062
  var import_react_native_reanimated11 = __toESM(require("react-native-reanimated"));
1060
1063
  var import_react_native_safe_area_context3 = require("react-native-safe-area-context");
1061
- var import_jsx_runtime8 = require("react/jsx-runtime");
1064
+ var import_jsx_runtime8 = (
1065
+ // bottom: tabBarHeight (not the styles.footer default of 0) - a consuming app's own persistent
1066
+ // tab bar (a fixture outside this package's header/footer concepts entirely, same as
1067
+ // useScrollList's content-inset reservation) sits below this at the true screen bottom, so this
1068
+ // bar has to clear it rather than rendering flush against 0 and overlapping/hiding behind it.
1069
+ require("react/jsx-runtime")
1070
+ );
1062
1071
  var ScrollViewFooter = ({ children, style }) => {
1063
- const { blur, footerAboveKeyboard, footerHeight, footerFixed, footerOffset, headerHeightShared, pullSearchHeightShared, scrollPosition, setFooterHeight, snapBackFooterShared } = (0, import_react13.useContext)(ScrollViewContext);
1072
+ const { blur, footerAboveKeyboard, footerHeight, footerFixed, footerOffset, headerHeightShared, pullSearchHeightShared, scrollPosition, setFooterHeight, snapBackFooterShared, tabBarHeight } = (0, import_react13.useContext)(ScrollViewContext);
1064
1073
  const insets = (0, import_react_native_safe_area_context3.useSafeAreaInsets)();
1065
1074
  const keyboardHeight = useKeyboardInset();
1066
1075
  (0, import_react13.useEffect)(
1067
1076
  () => () => {
1068
- setFooterHeight(0);
1077
+ setFooterHeight(null);
1069
1078
  },
1070
1079
  [setFooterHeight]
1071
1080
  );
@@ -1081,17 +1090,17 @@ var ScrollViewFooter = ({ children, style }) => {
1081
1090
  if (snapBackFooterShared.value) return { transform: [{ translateY: footerOffset.value }] };
1082
1091
  const effective = scrollPosition.value + headerHeightShared.value - pullSearchHeightShared.value;
1083
1092
  if (effective <= 0) return { transform: [{ translateY: 0 }] };
1084
- return { transform: [{ translateY: Math.min(effective, footerHeight) }] };
1093
+ return { transform: [{ translateY: Math.min(effective, footerHeight ?? 0) }] };
1085
1094
  }, [footerHeight, footerFixed, headerHeightShared, pullSearchHeightShared, scrollPosition, snapBackFooterShared, footerOffset]);
1086
1095
  const containerPaddingBottom = footerFixed && footerAboveKeyboard ? keyboardHeight : 0;
1087
1096
  const rowPaddingBottom = footerFixed && footerAboveKeyboard ? Math.max(insets.bottom - keyboardHeight, 0) : insets.bottom;
1088
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native_reanimated11.default.View, { onLayout: handleLayout, pointerEvents: "box-none", style: [styles6.footer, { paddingBottom: containerPaddingBottom }, footerStyle], children: [
1089
- footerHeight > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_auto_paper3.BlurView, { blur, style: import_react_native11.StyleSheet.absoluteFill }),
1097
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native_reanimated11.default.View, { onLayout: handleLayout, pointerEvents: "box-none", style: [styles6.footer, { bottom: tabBarHeight, paddingBottom: containerPaddingBottom }, footerStyle], children: [
1098
+ (footerHeight ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_auto_paper3.BlurView, { blur, style: import_react_native11.StyleSheet.absoluteFill }),
1090
1099
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native11.View, { style: [styles6.row, { paddingBottom: rowPaddingBottom }, style], children })
1091
1100
  ] });
1092
1101
  };
1093
1102
  var styles6 = import_react_native11.StyleSheet.create({
1094
- footer: { bottom: 0, left: 0, position: "absolute", right: 0, zIndex: 2 },
1103
+ footer: { left: 0, position: "absolute", right: 0, zIndex: 2 },
1095
1104
  row: { alignItems: "center", flexDirection: "row", zIndex: 1 }
1096
1105
  });
1097
1106
 
@@ -1099,23 +1108,25 @@ var styles6 = import_react_native11.StyleSheet.create({
1099
1108
  var import_auto_paper4 = require("@rific/auto-paper");
1100
1109
  var import_react14 = require("react");
1101
1110
  var import_react_native12 = require("react-native");
1102
- var import_react_native_paper3 = require("react-native-paper");
1111
+ var import_react_native_paper4 = require("react-native-paper");
1103
1112
  var import_react_native_reanimated12 = __toESM(require("react-native-reanimated"));
1104
1113
  var import_react_native_safe_area_context4 = require("react-native-safe-area-context");
1105
1114
  var import_jsx_runtime9 = require("react/jsx-runtime");
1106
- var ActionBg = ({ blur, style }) => blur ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native12.View, { style: [import_react_native12.StyleSheet.absoluteFill, styles7.actionBg, style], children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_auto_paper4.BlurView, { blur: true, style: import_react_native12.StyleSheet.absoluteFill }) }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native12.View, { style: [import_react_native12.StyleSheet.absoluteFill, styles7.actionBg, style], pointerEvents: "none", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper3.Surface, { style: import_react_native12.StyleSheet.absoluteFill, elevation: 1, children: null }) });
1115
+ var ActionBg = ({ blur, style }) => blur ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native12.View, { style: [import_react_native12.StyleSheet.absoluteFill, styles7.actionBg, style], children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_auto_paper4.BlurView, { blur: true, style: import_react_native12.StyleSheet.absoluteFill }) }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native12.View, { style: [import_react_native12.StyleSheet.absoluteFill, styles7.actionBg, style], pointerEvents: "none", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper4.Surface, { style: import_react_native12.StyleSheet.absoluteFill, elevation: 1, children: null }) });
1107
1116
  var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionAccessibilityLabel, backActionFixed, caption, centerContent, children, iconSize, style, title, topInset = true, trailingAction, trailingActionFixed = true }) => {
1108
- const { blur, headerHeight, headerFixed, headerOffset, progress, progressing, pullSearchHeightShared, scrollPosition, setHeaderHeight, snapBackHeaderShared } = (0, import_react14.useContext)(ScrollViewContext);
1117
+ const { blur, headerHeight, headerFixed, headerHeightShared, headerOffset, progress, progressing, pullSearchHeightShared, scrollPosition, setHeaderHeight, snapBackHeaderShared } = (0, import_react14.useContext)(ScrollViewContext);
1109
1118
  const { settings } = (0, import_react14.useContext)(ScrollViewSettingsContext);
1110
1119
  const effectiveBackActionFixed = backActionFixed ?? settings.backActionFixed;
1111
1120
  const insets = (0, import_react_native_safe_area_context4.useSafeAreaInsets)();
1112
- const theme = (0, import_react_native_paper3.useTheme)();
1121
+ const theme = (0, import_react_native_paper4.useTheme)();
1113
1122
  const top = topInset ? insets.top : 0;
1123
+ const measuredShared = (0, import_react_native_reanimated12.useSharedValue)(false);
1114
1124
  const handleLayout = ({
1115
1125
  nativeEvent: {
1116
1126
  layout: { height }
1117
1127
  }
1118
1128
  }) => {
1129
+ measuredShared.value = true;
1119
1130
  if (headerHeight !== height) setHeaderHeight(height);
1120
1131
  };
1121
1132
  const actionMargin = 4;
@@ -1124,23 +1135,23 @@ var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionAc
1124
1135
  const translateStyle = (0, import_react_native_reanimated12.useAnimatedStyle)(() => {
1125
1136
  if (headerFixed) return { transform: [{ translateY: 0 }] };
1126
1137
  if (snapBackHeaderShared.value) return { transform: [{ translateY: headerOffset.value }] };
1127
- const effective = scrollPosition.value + (headerHeight ?? 0) - pullSearchHeightShared.value;
1138
+ const effective = scrollPosition.value + headerHeightShared.value - pullSearchHeightShared.value;
1128
1139
  if (effective <= 0) return { transform: [{ translateY: 0 }] };
1129
1140
  return { transform: [{ translateY: -effective }] };
1130
- }, [headerFixed, headerHeight, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1141
+ }, [headerFixed, headerHeightShared, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1131
1142
  const blurStyle = (0, import_react_native_reanimated12.useAnimatedStyle)(() => {
1132
- if (headerHeight === null) return { height: unmeasuredHeight };
1133
- if (!headerHeight) return { height: 0 };
1134
- if (headerFixed) return { height: headerHeight };
1135
- const translateY = snapBackHeaderShared.value ? headerOffset.value : -Math.max(0, scrollPosition.value + headerHeight - pullSearchHeightShared.value);
1136
- return { height: Math.max(headerHeight + translateY, top) };
1137
- }, [headerFixed, headerHeight, top, unmeasuredHeight, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1143
+ if (!measuredShared.value) return { height: unmeasuredHeight };
1144
+ if (!headerHeightShared.value) return { height: 0 };
1145
+ if (headerFixed) return { height: headerHeightShared.value };
1146
+ const translateY = snapBackHeaderShared.value ? headerOffset.value : -Math.max(0, scrollPosition.value + headerHeightShared.value - pullSearchHeightShared.value);
1147
+ return { height: Math.max(headerHeightShared.value + translateY, top) };
1148
+ }, [headerFixed, top, unmeasuredHeight, measuredShared, headerHeightShared, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1138
1149
  const progressStyle = (0, import_react_native_reanimated12.useAnimatedStyle)(() => {
1139
- const h = headerHeight ?? 0;
1150
+ const h = headerHeightShared.value;
1140
1151
  if (headerFixed) return { top: h };
1141
1152
  const slide = snapBackHeaderShared.value ? -headerOffset.value : Math.max(0, scrollPosition.value + h - pullSearchHeightShared.value);
1142
1153
  return { top: Math.max(h - slide, top) };
1143
- }, [headerFixed, headerHeight, top, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1154
+ }, [headerFixed, headerHeightShared, top, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1144
1155
  const actionTop = top + actionMargin;
1145
1156
  const buttonStyle = { borderRadius: actionSize, height: actionSize, margin: 0, width: actionSize };
1146
1157
  const actionShadow = { borderRadius: actionSize / 2, elevation: 2, shadowColor: "#000", shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.15, shadowRadius: 2 };
@@ -1159,13 +1170,13 @@ var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionAc
1159
1170
  ] }) }),
1160
1171
  !blur && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native12.View, { style: [styles7.divider, { backgroundColor: theme.colors.outlineVariant }] })
1161
1172
  ] }),
1162
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_reanimated12.default.View, { pointerEvents: "none", style: [styles7.progress, progressStyle], children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper3.ProgressBar, { indeterminate: progress === null, visible: progressing, progress: progress ?? void 0 }) }),
1173
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_reanimated12.default.View, { pointerEvents: "none", style: [styles7.progress, progressStyle], children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper4.ProgressBar, { indeterminate: progress === null, visible: progressing, progress: progress ?? void 0 }) }),
1163
1174
  backAction && (effectiveBackActionFixed ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native_reanimated12.default.View, { style: leadingStyle, children: [
1164
1175
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ActionBg, { blur, style: actionStyle }),
1165
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper3.Appbar.BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle })
1176
+ typeof backAction === "function" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper4.Appbar.BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle }) : backAction
1166
1177
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native_reanimated12.default.View, { style: [leadingStyle, translateStyle], children: [
1167
1178
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ActionBg, { blur, style: actionStyle }),
1168
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper3.Appbar.BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle })
1179
+ typeof backAction === "function" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper4.Appbar.BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle }) : backAction
1169
1180
  ] })),
1170
1181
  trailingAction && (trailingActionFixed ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native12.View, { style: trailingStyle, children: [
1171
1182
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ActionBg, { blur, style: actionStyle }),
@@ -1205,7 +1216,7 @@ var ScrollViewProvider = ({ blur, children, fixed = false, footerAboveKeyboard =
1205
1216
  const effectiveSnapBackHeader = snapBackHeader ?? settings.snapBackHeader;
1206
1217
  const effectiveSnapBackFooter = snapBackFooter ?? settings.snapBackFooter;
1207
1218
  const [headerHeight, setHeaderHeightState] = (0, import_react15.useState)(null);
1208
- const [footerHeight, setFooterHeightState] = (0, import_react15.useState)(0);
1219
+ const [footerHeight, setFooterHeightState] = (0, import_react15.useState)(null);
1209
1220
  const [progress, setProgress] = (0, import_react15.useState)(null);
1210
1221
  const [progressing, setProgressing] = (0, import_react15.useState)(false);
1211
1222
  const scrollPosition = (0, import_react_native_reanimated13.useSharedValue)(0);
@@ -1245,12 +1256,13 @@ var ScrollViewProvider = ({ blur, children, fixed = false, footerAboveKeyboard =
1245
1256
  }, [headerHeight]);
1246
1257
  const setFooterHeight = (0, import_react15.useCallback)(
1247
1258
  (h) => {
1259
+ if (h === 0 && footerHeight) return;
1248
1260
  setFooterHeightState(h);
1249
- footerHeightShared.value = h;
1261
+ footerHeightShared.value = h ?? 0;
1250
1262
  },
1251
- [footerHeightShared]
1263
+ [footerHeight, footerHeightShared]
1252
1264
  );
1253
- const scrollHeight = (0, import_react15.useMemo)(() => import_react_native13.Dimensions.get("window").height - (headerHeight ?? 0) - footerHeight, [headerHeight, footerHeight]);
1265
+ const scrollHeight = (0, import_react15.useMemo)(() => import_react_native13.Dimensions.get("window").height - (headerHeight ?? 0) - (footerHeight ?? 0), [headerHeight, footerHeight]);
1254
1266
  const effectiveHeaderFixed = fixed || (headerFixed ?? settings.headerFixed);
1255
1267
  const effectiveFooterFixed = fixed || (footerFixed ?? settings.footerFixed);
1256
1268
  const value = (0, import_react15.useMemo)(() => ({ blur: effectiveBlur, footerAboveKeyboard, footerHeight, footerHeightShared, footerFixed: effectiveFooterFixed, footerOffset, headerHeight, headerHeightShared, headerFixed: effectiveHeaderFixed, headerOffset, jsListGeneration, listGeneration, onJsListUnmount, onListUnmount, progress, pullSearchHeightShared, progressing, scrollHeight, scrollPosition, setFooterHeight, setHeaderHeight, setProgress, setProgressing, snapBackFooterShared, snapBackHeaderShared, tabBarHeight }), [effectiveBlur, footerAboveKeyboard, effectiveFooterFixed, effectiveHeaderFixed, footerHeight, footerHeightShared, footerOffset, headerHeight, headerHeightShared, headerOffset, jsListGeneration, listGeneration, onJsListUnmount, onListUnmount, progress, pullSearchHeightShared, progressing, scrollHeight, scrollPosition, setFooterHeight, setHeaderHeight, snapBackFooterShared, snapBackHeaderShared, tabBarHeight]);
package/dist/index.mjs CHANGED
@@ -12,15 +12,18 @@ var RefreshControl = (props) => /* @__PURE__ */ jsx(RNRefreshControl, { refreshi
12
12
  // src/internal/ScrollViewChip.tsx
13
13
  import { Chip } from "@rific/auto-paper";
14
14
  import { StyleSheet } from "react-native";
15
+ import { Icon, useTheme } from "react-native-paper";
15
16
  import Animated from "react-native-reanimated";
16
17
  import { jsx as jsx2 } from "react/jsx-runtime";
17
18
  var ScrollViewChip = ({ chipProps, isHorizontal, onChipPress, onPress, style }) => {
18
- const { label, style: chipStyle, ...restChipProps } = chipProps ?? {};
19
+ const { colors } = useTheme();
20
+ const { label, selectedColor, style: chipStyle, ...restChipProps } = chipProps ?? {};
19
21
  const handlePress = onChipPress ? () => {
20
22
  onPress();
21
23
  onChipPress();
22
24
  } : onPress;
23
- return /* @__PURE__ */ jsx2(Animated.View, { style: [styles.chip, style], children: /* @__PURE__ */ jsx2(Chip, { compact: true, ...restChipProps, icon: isHorizontal ? "chevron-left" : "chevron-up", onPress: handlePress, style: [styles.chipInner, chipStyle], children: label ?? (isHorizontal ? "Start" : "Top") }) });
25
+ const iconColor = selectedColor ?? (restChipProps.mode === "outlined" ? colors.onSurfaceVariant : colors.onSecondaryContainer);
26
+ return /* @__PURE__ */ jsx2(Animated.View, { style: [styles.chip, style], children: /* @__PURE__ */ jsx2(Chip, { compact: true, selectedColor, ...restChipProps, avatar: /* @__PURE__ */ jsx2(Icon, { source: isHorizontal ? "chevron-left" : "chevron-up", size: 18, color: iconColor }), onPress: handlePress, style: [styles.chipInner, chipStyle], children: label ?? (isHorizontal ? "Start" : "Top") }) });
24
27
  };
25
28
  var styles = StyleSheet.create({
26
29
  chip: { alignItems: "center", left: 0, position: "absolute", right: 0, zIndex: 3 },
@@ -36,7 +39,7 @@ import { createContext } from "react";
36
39
  var ScrollViewContext = createContext({
37
40
  blur: true,
38
41
  footerAboveKeyboard: false,
39
- footerHeight: 0,
42
+ footerHeight: null,
40
43
  footerHeightShared: null,
41
44
  footerFixed: false,
42
45
  footerOffset: null,
@@ -376,7 +379,7 @@ var CHIP_SLIDE = 48;
376
379
  function useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedProp, hideUntilMeasured, isHorizontal, keyboardAware, pullSearchHeight, style } = {}) {
377
380
  const insets = useSafeAreaInsets();
378
381
  const keyboardHeight = useKeyboardInset();
379
- const { footerAboveKeyboard, footerHeight, footerFixed: contextFooterFixed, headerHeight, headerFixed: contextHeaderFixed, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared, tabBarHeight } = useContext3(ScrollViewContext);
382
+ const { footerAboveKeyboard, footerHeight, footerFixed: contextFooterFixed, headerHeight, headerHeightShared, headerFixed: contextHeaderFixed, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared, tabBarHeight } = useContext3(ScrollViewContext);
380
383
  const headerFixed = isHorizontal ? true : headerFixedProp ?? contextHeaderFixed;
381
384
  const footerFixed = isHorizontal ? true : footerFixedProp ?? contextFooterFixed;
382
385
  useEffect2(() => {
@@ -400,11 +403,11 @@ function useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedP
400
403
  return {
401
404
  opacity: chipHidden.value ? withTiming2(0) : withTiming2(1),
402
405
  pointerEvents,
403
- top: Math.max(headerHeight ?? 0, insets.top) + 4,
406
+ top: Math.max(headerHeightShared.value, insets.top) + 4,
404
407
  transform: [{ translateX: chipHidden.value ? withTiming2(-CHIP_SLIDE) : withTiming2(0) }]
405
408
  };
406
409
  }
407
- const h = headerHeight ?? 0;
410
+ const h = headerHeightShared.value;
408
411
  const slide = headerFixed ? 0 : snapBackHeaderShared.value ? -headerOffset.value : Math.max(0, scrollPosition.value + h - pullSearchHeightShared.value);
409
412
  const top = Math.max(h - slide, insets.top) + 4;
410
413
  return {
@@ -413,7 +416,7 @@ function useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedP
413
416
  top,
414
417
  transform: [{ translateY: chipHidden.value ? withTiming2(-CHIP_SLIDE) : withTiming2(0) }]
415
418
  };
416
- }, [isHorizontal, headerFixed, headerHeight, insets.top, chipHidden, snapBackHeaderShared, headerOffset, scrollPosition, pullSearchHeightShared]);
419
+ }, [isHorizontal, headerFixed, headerHeightShared, insets.top, chipHidden, snapBackHeaderShared, headerOffset, scrollPosition, pullSearchHeightShared]);
417
420
  return { chipHidden, chipStyle, containerStyle, contentInset, contentOffset, contentPadding, footerFixed, headerFixed, headerHeight, insets, isHorizontal: isHorizontal ?? false };
418
421
  }
419
422
 
@@ -504,7 +507,7 @@ import { Gesture as Gesture2, GestureDetector as GestureDetector2 } from "react-
504
507
  import { BlurView } from "@rific/auto-paper";
505
508
  import { useContext as useContext5 } from "react";
506
509
  import { StyleSheet as StyleSheet3, useWindowDimensions, View as View2 } from "react-native";
507
- import { useTheme } from "react-native-paper";
510
+ import { useTheme as useTheme2 } from "react-native-paper";
508
511
  import Animated2, { interpolateColor, useAnimatedStyle as useAnimatedStyle2 } from "react-native-reanimated";
509
512
  import { useSafeAreaInsets as useSafeAreaInsets2 } from "react-native-safe-area-context";
510
513
  import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -534,7 +537,7 @@ var HorizontalDots = ({ total }) => {
534
537
  const { blur, footerHeightShared, scrollPosition } = useContext5(ScrollViewContext);
535
538
  const { width } = useWindowDimensions();
536
539
  const insets = useSafeAreaInsets2();
537
- const theme = useTheme();
540
+ const theme = useTheme2();
538
541
  const insetsBottom = insets.bottom;
539
542
  const activeColor = theme.colors.primary;
540
543
  const inactiveColor = theme.colors.outlineVariant;
@@ -828,12 +831,12 @@ var FlatList = memo2(FlatListInner);
828
831
  // src/PullSearch.tsx
829
832
  import { forwardRef, useCallback as useCallback5, useContext as useContext8, useEffect as useEffect3, useImperativeHandle, useRef as useRef5, useState as useState4 } from "react";
830
833
  import { StyleSheet as StyleSheet4 } from "react-native";
831
- import { Searchbar, useTheme as useTheme2 } from "react-native-paper";
834
+ import { Searchbar, useTheme as useTheme3 } from "react-native-paper";
832
835
  import Animated3, { Extrapolation, interpolate, runOnJS as runOnJS3, useAnimatedReaction, useAnimatedStyle as useAnimatedStyle3 } from "react-native-reanimated";
833
836
  import { jsx as jsx6 } from "react/jsx-runtime";
834
837
  var PullSearch = forwardRef(function PullSearch2({ debounce, onChangeText, onHeightChange, placeholder, value: propValue }, ref) {
835
838
  const { headerHeight, scrollPosition } = useContext8(ScrollViewContext);
836
- const theme = useTheme2();
839
+ const theme = useTheme3();
837
840
  const inputRef = useRef5(null);
838
841
  const [barHeight, setBarHeight] = useState4(0);
839
842
  const [value, setValue] = useState4(propValue ?? "");
@@ -1008,12 +1011,12 @@ import Animated5, { useAnimatedStyle as useAnimatedStyle4 } from "react-native-r
1008
1011
  import { useSafeAreaInsets as useSafeAreaInsets3 } from "react-native-safe-area-context";
1009
1012
  import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1010
1013
  var ScrollViewFooter = ({ children, style }) => {
1011
- const { blur, footerAboveKeyboard, footerHeight, footerFixed, footerOffset, headerHeightShared, pullSearchHeightShared, scrollPosition, setFooterHeight, snapBackFooterShared } = useContext10(ScrollViewContext);
1014
+ const { blur, footerAboveKeyboard, footerHeight, footerFixed, footerOffset, headerHeightShared, pullSearchHeightShared, scrollPosition, setFooterHeight, snapBackFooterShared, tabBarHeight } = useContext10(ScrollViewContext);
1012
1015
  const insets = useSafeAreaInsets3();
1013
1016
  const keyboardHeight = useKeyboardInset();
1014
1017
  useEffect4(
1015
1018
  () => () => {
1016
- setFooterHeight(0);
1019
+ setFooterHeight(null);
1017
1020
  },
1018
1021
  [setFooterHeight]
1019
1022
  );
@@ -1029,17 +1032,23 @@ var ScrollViewFooter = ({ children, style }) => {
1029
1032
  if (snapBackFooterShared.value) return { transform: [{ translateY: footerOffset.value }] };
1030
1033
  const effective = scrollPosition.value + headerHeightShared.value - pullSearchHeightShared.value;
1031
1034
  if (effective <= 0) return { transform: [{ translateY: 0 }] };
1032
- return { transform: [{ translateY: Math.min(effective, footerHeight) }] };
1035
+ return { transform: [{ translateY: Math.min(effective, footerHeight ?? 0) }] };
1033
1036
  }, [footerHeight, footerFixed, headerHeightShared, pullSearchHeightShared, scrollPosition, snapBackFooterShared, footerOffset]);
1034
1037
  const containerPaddingBottom = footerFixed && footerAboveKeyboard ? keyboardHeight : 0;
1035
1038
  const rowPaddingBottom = footerFixed && footerAboveKeyboard ? Math.max(insets.bottom - keyboardHeight, 0) : insets.bottom;
1036
- return /* @__PURE__ */ jsxs5(Animated5.View, { onLayout: handleLayout, pointerEvents: "box-none", style: [styles6.footer, { paddingBottom: containerPaddingBottom }, footerStyle], children: [
1037
- footerHeight > 0 && /* @__PURE__ */ jsx8(BlurView2, { blur, style: StyleSheet5.absoluteFill }),
1038
- /* @__PURE__ */ jsx8(View5, { style: [styles6.row, { paddingBottom: rowPaddingBottom }, style], children })
1039
- ] });
1039
+ return (
1040
+ // bottom: tabBarHeight (not the styles.footer default of 0) - a consuming app's own persistent
1041
+ // tab bar (a fixture outside this package's header/footer concepts entirely, same as
1042
+ // useScrollList's content-inset reservation) sits below this at the true screen bottom, so this
1043
+ // bar has to clear it rather than rendering flush against 0 and overlapping/hiding behind it.
1044
+ /* @__PURE__ */ jsxs5(Animated5.View, { onLayout: handleLayout, pointerEvents: "box-none", style: [styles6.footer, { bottom: tabBarHeight, paddingBottom: containerPaddingBottom }, footerStyle], children: [
1045
+ (footerHeight ?? 0) > 0 && /* @__PURE__ */ jsx8(BlurView2, { blur, style: StyleSheet5.absoluteFill }),
1046
+ /* @__PURE__ */ jsx8(View5, { style: [styles6.row, { paddingBottom: rowPaddingBottom }, style], children })
1047
+ ] })
1048
+ );
1040
1049
  };
1041
1050
  var styles6 = StyleSheet5.create({
1042
- footer: { bottom: 0, left: 0, position: "absolute", right: 0, zIndex: 2 },
1051
+ footer: { left: 0, position: "absolute", right: 0, zIndex: 2 },
1043
1052
  row: { alignItems: "center", flexDirection: "row", zIndex: 1 }
1044
1053
  });
1045
1054
 
@@ -1047,23 +1056,25 @@ var styles6 = StyleSheet5.create({
1047
1056
  import { BlurView as BlurView3 } from "@rific/auto-paper";
1048
1057
  import { useContext as useContext11 } from "react";
1049
1058
  import { StyleSheet as StyleSheet6, Text, View as View6 } from "react-native";
1050
- import { Appbar, ProgressBar, Surface, useTheme as useTheme3 } from "react-native-paper";
1051
- import Animated6, { useAnimatedStyle as useAnimatedStyle5 } from "react-native-reanimated";
1059
+ import { Appbar, ProgressBar, Surface, useTheme as useTheme4 } from "react-native-paper";
1060
+ import Animated6, { useAnimatedStyle as useAnimatedStyle5, useSharedValue as useSharedValue6 } from "react-native-reanimated";
1052
1061
  import { useSafeAreaInsets as useSafeAreaInsets4 } from "react-native-safe-area-context";
1053
1062
  import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1054
1063
  var ActionBg = ({ blur, style }) => blur ? /* @__PURE__ */ jsx9(View6, { style: [StyleSheet6.absoluteFill, styles7.actionBg, style], children: /* @__PURE__ */ jsx9(BlurView3, { blur: true, style: StyleSheet6.absoluteFill }) }) : /* @__PURE__ */ jsx9(View6, { style: [StyleSheet6.absoluteFill, styles7.actionBg, style], pointerEvents: "none", children: /* @__PURE__ */ jsx9(Surface, { style: StyleSheet6.absoluteFill, elevation: 1, children: null }) });
1055
1064
  var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionAccessibilityLabel, backActionFixed, caption, centerContent, children, iconSize, style, title, topInset = true, trailingAction, trailingActionFixed = true }) => {
1056
- const { blur, headerHeight, headerFixed, headerOffset, progress, progressing, pullSearchHeightShared, scrollPosition, setHeaderHeight, snapBackHeaderShared } = useContext11(ScrollViewContext);
1065
+ const { blur, headerHeight, headerFixed, headerHeightShared, headerOffset, progress, progressing, pullSearchHeightShared, scrollPosition, setHeaderHeight, snapBackHeaderShared } = useContext11(ScrollViewContext);
1057
1066
  const { settings } = useContext11(ScrollViewSettingsContext);
1058
1067
  const effectiveBackActionFixed = backActionFixed ?? settings.backActionFixed;
1059
1068
  const insets = useSafeAreaInsets4();
1060
- const theme = useTheme3();
1069
+ const theme = useTheme4();
1061
1070
  const top = topInset ? insets.top : 0;
1071
+ const measuredShared = useSharedValue6(false);
1062
1072
  const handleLayout = ({
1063
1073
  nativeEvent: {
1064
1074
  layout: { height }
1065
1075
  }
1066
1076
  }) => {
1077
+ measuredShared.value = true;
1067
1078
  if (headerHeight !== height) setHeaderHeight(height);
1068
1079
  };
1069
1080
  const actionMargin = 4;
@@ -1072,23 +1083,23 @@ var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionAc
1072
1083
  const translateStyle = useAnimatedStyle5(() => {
1073
1084
  if (headerFixed) return { transform: [{ translateY: 0 }] };
1074
1085
  if (snapBackHeaderShared.value) return { transform: [{ translateY: headerOffset.value }] };
1075
- const effective = scrollPosition.value + (headerHeight ?? 0) - pullSearchHeightShared.value;
1086
+ const effective = scrollPosition.value + headerHeightShared.value - pullSearchHeightShared.value;
1076
1087
  if (effective <= 0) return { transform: [{ translateY: 0 }] };
1077
1088
  return { transform: [{ translateY: -effective }] };
1078
- }, [headerFixed, headerHeight, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1089
+ }, [headerFixed, headerHeightShared, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1079
1090
  const blurStyle = useAnimatedStyle5(() => {
1080
- if (headerHeight === null) return { height: unmeasuredHeight };
1081
- if (!headerHeight) return { height: 0 };
1082
- if (headerFixed) return { height: headerHeight };
1083
- const translateY = snapBackHeaderShared.value ? headerOffset.value : -Math.max(0, scrollPosition.value + headerHeight - pullSearchHeightShared.value);
1084
- return { height: Math.max(headerHeight + translateY, top) };
1085
- }, [headerFixed, headerHeight, top, unmeasuredHeight, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1091
+ if (!measuredShared.value) return { height: unmeasuredHeight };
1092
+ if (!headerHeightShared.value) return { height: 0 };
1093
+ if (headerFixed) return { height: headerHeightShared.value };
1094
+ const translateY = snapBackHeaderShared.value ? headerOffset.value : -Math.max(0, scrollPosition.value + headerHeightShared.value - pullSearchHeightShared.value);
1095
+ return { height: Math.max(headerHeightShared.value + translateY, top) };
1096
+ }, [headerFixed, top, unmeasuredHeight, measuredShared, headerHeightShared, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1086
1097
  const progressStyle = useAnimatedStyle5(() => {
1087
- const h = headerHeight ?? 0;
1098
+ const h = headerHeightShared.value;
1088
1099
  if (headerFixed) return { top: h };
1089
1100
  const slide = snapBackHeaderShared.value ? -headerOffset.value : Math.max(0, scrollPosition.value + h - pullSearchHeightShared.value);
1090
1101
  return { top: Math.max(h - slide, top) };
1091
- }, [headerFixed, headerHeight, top, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1102
+ }, [headerFixed, headerHeightShared, top, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1092
1103
  const actionTop = top + actionMargin;
1093
1104
  const buttonStyle = { borderRadius: actionSize, height: actionSize, margin: 0, width: actionSize };
1094
1105
  const actionShadow = { borderRadius: actionSize / 2, elevation: 2, shadowColor: "#000", shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.15, shadowRadius: 2 };
@@ -1110,10 +1121,10 @@ var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionAc
1110
1121
  /* @__PURE__ */ jsx9(Animated6.View, { pointerEvents: "none", style: [styles7.progress, progressStyle], children: /* @__PURE__ */ jsx9(ProgressBar, { indeterminate: progress === null, visible: progressing, progress: progress ?? void 0 }) }),
1111
1122
  backAction && (effectiveBackActionFixed ? /* @__PURE__ */ jsxs6(Animated6.View, { style: leadingStyle, children: [
1112
1123
  /* @__PURE__ */ jsx9(ActionBg, { blur, style: actionStyle }),
1113
- /* @__PURE__ */ jsx9(Appbar.BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle })
1124
+ typeof backAction === "function" ? /* @__PURE__ */ jsx9(Appbar.BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle }) : backAction
1114
1125
  ] }) : /* @__PURE__ */ jsxs6(Animated6.View, { style: [leadingStyle, translateStyle], children: [
1115
1126
  /* @__PURE__ */ jsx9(ActionBg, { blur, style: actionStyle }),
1116
- /* @__PURE__ */ jsx9(Appbar.BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle })
1127
+ typeof backAction === "function" ? /* @__PURE__ */ jsx9(Appbar.BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle }) : backAction
1117
1128
  ] })),
1118
1129
  trailingAction && (trailingActionFixed ? /* @__PURE__ */ jsxs6(View6, { style: trailingStyle, children: [
1119
1130
  /* @__PURE__ */ jsx9(ActionBg, { blur, style: actionStyle }),
@@ -1144,7 +1155,7 @@ var styles7 = StyleSheet6.create({
1144
1155
  import { useBlur } from "@rific/auto-paper";
1145
1156
  import { useCallback as useCallback7, useContext as useContext12, useEffect as useEffect5, useMemo as useMemo5, useRef as useRef7, useState as useState5 } from "react";
1146
1157
  import { Dimensions as Dimensions3 } from "react-native";
1147
- import { useSharedValue as useSharedValue6 } from "react-native-reanimated";
1158
+ import { useSharedValue as useSharedValue7 } from "react-native-reanimated";
1148
1159
  import { jsx as jsx10 } from "react/jsx-runtime";
1149
1160
  var ScrollViewProvider = ({ blur, children, fixed = false, footerAboveKeyboard = false, footerFixed, headerFixed, snapBack, snapBackFooter, snapBackHeader, tabBarHeight = 0 }) => {
1150
1161
  const { settings } = useContext12(ScrollViewSettingsContext);
@@ -1153,19 +1164,19 @@ var ScrollViewProvider = ({ blur, children, fixed = false, footerAboveKeyboard =
1153
1164
  const effectiveSnapBackHeader = snapBackHeader ?? settings.snapBackHeader;
1154
1165
  const effectiveSnapBackFooter = snapBackFooter ?? settings.snapBackFooter;
1155
1166
  const [headerHeight, setHeaderHeightState] = useState5(null);
1156
- const [footerHeight, setFooterHeightState] = useState5(0);
1167
+ const [footerHeight, setFooterHeightState] = useState5(null);
1157
1168
  const [progress, setProgress] = useState5(null);
1158
1169
  const [progressing, setProgressing] = useState5(false);
1159
- const scrollPosition = useSharedValue6(0);
1160
- const listGeneration = useSharedValue6(0);
1170
+ const scrollPosition = useSharedValue7(0);
1171
+ const listGeneration = useSharedValue7(0);
1161
1172
  const jsListGeneration = useRef7(0);
1162
- const headerHeightShared = useSharedValue6(0);
1163
- const footerHeightShared = useSharedValue6(0);
1164
- const headerOffset = useSharedValue6(0);
1165
- const footerOffset = useSharedValue6(0);
1166
- const pullSearchHeightShared = useSharedValue6(0);
1167
- const snapBackHeaderShared = useSharedValue6(false);
1168
- const snapBackFooterShared = useSharedValue6(false);
1173
+ const headerHeightShared = useSharedValue7(0);
1174
+ const footerHeightShared = useSharedValue7(0);
1175
+ const headerOffset = useSharedValue7(0);
1176
+ const footerOffset = useSharedValue7(0);
1177
+ const pullSearchHeightShared = useSharedValue7(0);
1178
+ const snapBackHeaderShared = useSharedValue7(false);
1179
+ const snapBackFooterShared = useSharedValue7(false);
1169
1180
  useEffect5(() => {
1170
1181
  snapBackHeaderShared.value = effectiveSnapBackHeader ?? effectiveSnapBack;
1171
1182
  snapBackFooterShared.value = effectiveSnapBackFooter ?? effectiveSnapBack;
@@ -1193,12 +1204,13 @@ var ScrollViewProvider = ({ blur, children, fixed = false, footerAboveKeyboard =
1193
1204
  }, [headerHeight]);
1194
1205
  const setFooterHeight = useCallback7(
1195
1206
  (h) => {
1207
+ if (h === 0 && footerHeight) return;
1196
1208
  setFooterHeightState(h);
1197
- footerHeightShared.value = h;
1209
+ footerHeightShared.value = h ?? 0;
1198
1210
  },
1199
- [footerHeightShared]
1211
+ [footerHeight, footerHeightShared]
1200
1212
  );
1201
- const scrollHeight = useMemo5(() => Dimensions3.get("window").height - (headerHeight ?? 0) - footerHeight, [headerHeight, footerHeight]);
1213
+ const scrollHeight = useMemo5(() => Dimensions3.get("window").height - (headerHeight ?? 0) - (footerHeight ?? 0), [headerHeight, footerHeight]);
1202
1214
  const effectiveHeaderFixed = fixed || (headerFixed ?? settings.headerFixed);
1203
1215
  const effectiveFooterFixed = fixed || (footerFixed ?? settings.footerFixed);
1204
1216
  const value = useMemo5(() => ({ blur: effectiveBlur, footerAboveKeyboard, footerHeight, footerHeightShared, footerFixed: effectiveFooterFixed, footerOffset, headerHeight, headerHeightShared, headerFixed: effectiveHeaderFixed, headerOffset, jsListGeneration, listGeneration, onJsListUnmount, onListUnmount, progress, pullSearchHeightShared, progressing, scrollHeight, scrollPosition, setFooterHeight, setHeaderHeight, setProgress, setProgressing, snapBackFooterShared, snapBackHeaderShared, tabBarHeight }), [effectiveBlur, footerAboveKeyboard, effectiveFooterFixed, effectiveHeaderFixed, footerHeight, footerHeightShared, footerOffset, headerHeight, headerHeightShared, headerOffset, jsListGeneration, listGeneration, onJsListUnmount, onListUnmount, progress, pullSearchHeightShared, progressing, scrollHeight, scrollPosition, setFooterHeight, setHeaderHeight, snapBackFooterShared, snapBackHeaderShared, tabBarHeight]);
@@ -1231,19 +1243,19 @@ var ScrollViewSettingsProvider = ({ children, onChange, initialValue }) => {
1231
1243
  import { memo as memo4, useCallback as useCallback10, useContext as useContext14, useEffect as useEffect6, useLayoutEffect as useLayoutEffect6, useMemo as useMemo7, useRef as useRef9 } from "react";
1232
1244
  import { Dimensions as Dimensions4, Platform as Platform5, SectionList as RNSectionList, View as View7 } from "react-native";
1233
1245
  import { Gesture as Gesture4, GestureDetector as GestureDetector4 } from "react-native-gesture-handler";
1234
- import Animated7, { runOnUI as runOnUI3, useSharedValue as useSharedValue8 } from "react-native-reanimated";
1246
+ import Animated7, { runOnUI as runOnUI3, useSharedValue as useSharedValue9 } from "react-native-reanimated";
1235
1247
 
1236
1248
  // src/internal/useStickyHeaders.ts
1237
1249
  import { useCallback as useCallback9, useContext as useContext13, useLayoutEffect as useLayoutEffect5, useRef as useRef8, useState as useState7 } from "react";
1238
- import { runOnJS as runOnJS4, useAnimatedReaction as useAnimatedReaction2, useAnimatedStyle as useAnimatedStyle6, useSharedValue as useSharedValue7 } from "react-native-reanimated";
1250
+ import { runOnJS as runOnJS4, useAnimatedReaction as useAnimatedReaction2, useAnimatedStyle as useAnimatedStyle6, useSharedValue as useSharedValue8 } from "react-native-reanimated";
1239
1251
  import { useSafeAreaInsets as useSafeAreaInsets5 } from "react-native-safe-area-context";
1240
1252
  function useStickyHeaders(headerFixed, enabled) {
1241
1253
  const { headerHeightShared, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared } = useContext13(ScrollViewContext);
1242
1254
  const insetsTop = useSafeAreaInsets5().top;
1243
- const sectionYPositions = useSharedValue7([]);
1244
- const sectionHeights = useSharedValue7([]);
1255
+ const sectionYPositions = useSharedValue8([]);
1256
+ const sectionHeights = useSharedValue8([]);
1245
1257
  const [activeIndex, setActiveIndex] = useState7(-1);
1246
- const contentIndexShared = useSharedValue7(-1);
1258
+ const contentIndexShared = useSharedValue8(-1);
1247
1259
  useLayoutEffect5(() => {
1248
1260
  contentIndexShared.value = activeIndex;
1249
1261
  }, [activeIndex, contentIndexShared]);
@@ -1355,7 +1367,7 @@ var SectionListInner = ({ chipProps, chipThreshold, contentContainerStyle: exter
1355
1367
  const { stickySectionHeadersEnabled = true, renderSectionHeader, ...passThroughProps } = props;
1356
1368
  const { chipHidden, chipStyle, containerStyle, contentInset, contentOffset, contentPadding, footerFixed, headerFixed, headerHeight } = useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedProp, hideUntilMeasured: true, keyboardAware, pullSearchHeight, style });
1357
1369
  const { listGeneration, onListUnmount } = useContext14(ScrollViewContext);
1358
- const capturedGeneration = useSharedValue8(-1);
1370
+ const capturedGeneration = useSharedValue9(-1);
1359
1371
  useLayoutEffect6(() => {
1360
1372
  const generation = listGeneration.value;
1361
1373
  if (Platform5.OS === "web") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rific/scroll-view",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Blur styled scroll system for React Native. Floating headers/footers, keyboard awareness, and scroll-away chrome",
5
5
  "keywords": [
6
6
  "react-native",
@@ -27,7 +27,7 @@
27
27
  "type": "commonjs",
28
28
  "exports": {
29
29
  ".": {
30
- "react-native": "./dist/index.mjs",
30
+ "react-native": "./src/index.ts",
31
31
  "types": "./dist/index.d.ts",
32
32
  "import": "./dist/index.mjs",
33
33
  "require": "./dist/index.js"
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "main": "dist/index.js",
37
37
  "module": "dist/index.mjs",
38
- "react-native": "dist/index.mjs",
38
+ "react-native": "src/index.ts",
39
39
  "types": "dist/index.d.ts",
40
40
  "files": [
41
41
  "dist",
@@ -79,12 +79,13 @@
79
79
  "prettier": "^3.8.3",
80
80
  "react": "^19.0.0",
81
81
  "react-dom": "^19.0.0",
82
- "react-native": "^0.76.0",
83
- "react-native-gesture-handler": "*",
82
+ "react-native": "^0.83.0",
83
+ "react-native-gesture-handler": "^2.32.0",
84
84
  "react-native-keyboard-controller": "*",
85
85
  "react-native-paper": "^5.12.0",
86
- "react-native-reanimated": "*",
86
+ "react-native-reanimated": "^4.5.1",
87
87
  "react-native-safe-area-context": "*",
88
+ "react-native-worklets": "^0.10.3",
88
89
  "ts-jest": "^29.4.9",
89
90
  "ts-node": "^10.9.2",
90
91
  "tsup": "^8.0.0",
@@ -93,13 +94,14 @@
93
94
  },
94
95
  "peerDependencies": {
95
96
  "@rific/auto-paper": "^0.8.0",
96
- "react": ">=18.0.0",
97
- "react-native": ">=0.76.0",
98
- "react-native-gesture-handler": ">=2.0.0",
97
+ "react": ">=19.0.0",
98
+ "react-native": ">=0.83.0",
99
+ "react-native-gesture-handler": ">=2.0.0 <3.0.0",
99
100
  "react-native-keyboard-controller": ">=1.0.0",
100
101
  "react-native-paper": ">=5.0.0",
101
- "react-native-reanimated": ">=3.0.0",
102
- "react-native-safe-area-context": ">=5.0.0"
102
+ "react-native-reanimated": ">=4.0.0",
103
+ "react-native-safe-area-context": ">=5.0.0",
104
+ "react-native-worklets": "0.10.x"
103
105
  },
104
106
  "publishConfig": {
105
107
  "access": "public"
@@ -4,7 +4,7 @@ import { SharedValue } from 'react-native-reanimated'
4
4
  export type ScrollViewContextType = {
5
5
  blur: boolean
6
6
  footerAboveKeyboard: boolean
7
- footerHeight: number
7
+ footerHeight: number | null
8
8
  footerHeightShared: SharedValue<number>
9
9
  footerFixed: boolean
10
10
  footerOffset: SharedValue<number>
@@ -28,7 +28,7 @@ export type ScrollViewContextType = {
28
28
  progressing: boolean
29
29
  scrollHeight: number
30
30
  scrollPosition: SharedValue<number>
31
- setFooterHeight: (h: number) => void
31
+ setFooterHeight: (h: number | null) => void
32
32
  setHeaderHeight: (h: number | null) => void
33
33
  setProgress: (p: number | null) => void
34
34
  setProgressing: (b: boolean) => void
@@ -38,7 +38,7 @@ export type ScrollViewContextType = {
38
38
  export const ScrollViewContext = createContext<ScrollViewContextType>({
39
39
  blur: true,
40
40
  footerAboveKeyboard: false,
41
- footerHeight: 0,
41
+ footerHeight: null,
42
42
  footerHeightShared: null as unknown as SharedValue<number>,
43
43
  footerFixed: false,
44
44
  footerOffset: null as unknown as SharedValue<number>,
@@ -13,12 +13,12 @@ export type ScrollViewFooterProps = {
13
13
  }
14
14
 
15
15
  export const ScrollViewFooter = ({ children, style }: ScrollViewFooterProps) => {
16
- const { blur, footerAboveKeyboard, footerHeight, footerFixed, footerOffset, headerHeightShared, pullSearchHeightShared, scrollPosition, setFooterHeight, snapBackFooterShared } = useContext(ScrollViewContext)
16
+ const { blur, footerAboveKeyboard, footerHeight, footerFixed, footerOffset, headerHeightShared, pullSearchHeightShared, scrollPosition, setFooterHeight, snapBackFooterShared, tabBarHeight } = useContext(ScrollViewContext)
17
17
  const insets = useSafeAreaInsets()
18
18
  const keyboardHeight = useKeyboardInset()
19
19
  useEffect(
20
20
  () => () => {
21
- setFooterHeight(0)
21
+ setFooterHeight(null)
22
22
  },
23
23
  [setFooterHeight]
24
24
  )
@@ -38,7 +38,7 @@ export const ScrollViewFooter = ({ children, style }: ScrollViewFooterProps) =>
38
38
  if (snapBackFooterShared.value) return { transform: [{ translateY: footerOffset.value }] }
39
39
  const effective = scrollPosition.value + headerHeightShared.value - pullSearchHeightShared.value
40
40
  if (effective <= 0) return { transform: [{ translateY: 0 }] }
41
- return { transform: [{ translateY: Math.min(effective, footerHeight) }] }
41
+ return { transform: [{ translateY: Math.min(effective, footerHeight ?? 0) }] }
42
42
  }, [footerHeight, footerFixed, headerHeightShared, pullSearchHeightShared, scrollPosition, snapBackFooterShared, footerOffset])
43
43
  // Floating above the keyboard is done by GROWING this container (via paddingBottom) rather than
44
44
  // translating it. Translating the whole bar left its bottom edge — and the BlurView filling it —
@@ -66,14 +66,18 @@ export const ScrollViewFooter = ({ children, style }: ScrollViewFooterProps) =>
66
66
  // of dropping past it and correcting back up.
67
67
  const rowPaddingBottom = footerFixed && footerAboveKeyboard ? Math.max(insets.bottom - keyboardHeight, 0) : insets.bottom
68
68
  return (
69
- <Animated.View onLayout={handleLayout} pointerEvents='box-none' style={[styles.footer, { paddingBottom: containerPaddingBottom }, footerStyle]}>
70
- {footerHeight > 0 && <BlurView blur={blur} style={StyleSheet.absoluteFill} />}
69
+ // bottom: tabBarHeight (not the styles.footer default of 0) - a consuming app's own persistent
70
+ // tab bar (a fixture outside this package's header/footer concepts entirely, same as
71
+ // useScrollList's content-inset reservation) sits below this at the true screen bottom, so this
72
+ // bar has to clear it rather than rendering flush against 0 and overlapping/hiding behind it.
73
+ <Animated.View onLayout={handleLayout} pointerEvents='box-none' style={[styles.footer, { bottom: tabBarHeight, paddingBottom: containerPaddingBottom }, footerStyle]}>
74
+ {(footerHeight ?? 0) > 0 && <BlurView blur={blur} style={StyleSheet.absoluteFill} />}
71
75
  <View style={[styles.row, { paddingBottom: rowPaddingBottom }, style]}>{children}</View>
72
76
  </Animated.View>
73
77
  )
74
78
  }
75
79
 
76
80
  const styles = StyleSheet.create({
77
- footer: { bottom: 0, left: 0, position: 'absolute', right: 0, zIndex: 2 },
81
+ footer: { left: 0, position: 'absolute', right: 0, zIndex: 2 },
78
82
  row: { alignItems: 'center', flexDirection: 'row', zIndex: 1 }
79
83
  })
@@ -2,7 +2,7 @@ import { BlurView } from '@rific/auto-paper'
2
2
  import { type ReactNode, useContext } from 'react'
3
3
  import { type LayoutChangeEvent, StyleSheet, Text, View, type ViewStyle } from 'react-native'
4
4
  import { Appbar, ProgressBar, Surface, useTheme } from 'react-native-paper'
5
- import Animated, { useAnimatedStyle } from 'react-native-reanimated'
5
+ import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
6
6
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
7
7
 
8
8
  import { ScrollViewContext } from './ScrollViewContext'
@@ -11,7 +11,14 @@ import { ScrollViewSettingsContext } from './ScrollViewSettingsContext'
11
11
  export type ScrollViewHeaderProps = {
12
12
  actionSize?: number
13
13
  actionStyle?: ViewStyle
14
- backAction?: () => void
14
+ // A plain callback renders the default Appbar.BackAction (themed onSurface, sized/positioned by
15
+ // actionSize/iconSize below) — the common case, unchanged from before. Passing a ReactNode
16
+ // instead skips that default entirely and renders the given element in its place, same escape
17
+ // hatch trailingAction already gave the other corner: a caller that wants its own color, icon,
18
+ // or component altogether (a colored IconButton, say) isn't stuck fighting Appbar.BackAction's
19
+ // own fixed color/no-customization API to get it. backActionAccessibilityLabel/iconSize below
20
+ // only apply to the default-rendered case — a custom element carries its own.
21
+ backAction?: (() => void) | ReactNode
15
22
  backActionAccessibilityLabel?: string
16
23
  backActionFixed?: boolean
17
24
  caption?: string
@@ -41,17 +48,24 @@ const ActionBg = ({ blur, style }: ActionBgProps) =>
41
48
  )
42
49
 
43
50
  export const ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionAccessibilityLabel, backActionFixed, caption, centerContent, children, iconSize, style, title, topInset = true, trailingAction, trailingActionFixed = true }: ScrollViewHeaderProps) => {
44
- const { blur, headerHeight, headerFixed, headerOffset, progress, progressing, pullSearchHeightShared, scrollPosition, setHeaderHeight, snapBackHeaderShared } = useContext(ScrollViewContext)
51
+ const { blur, headerHeight, headerFixed, headerHeightShared, headerOffset, progress, progressing, pullSearchHeightShared, scrollPosition, setHeaderHeight, snapBackHeaderShared } = useContext(ScrollViewContext)
45
52
  const { settings } = useContext(ScrollViewSettingsContext)
46
53
  const effectiveBackActionFixed = backActionFixed ?? settings.backActionFixed
47
54
  const insets = useSafeAreaInsets()
48
55
  const theme = useTheme()
49
56
  const top = topInset ? insets.top : 0
57
+ // Tracks "has this header's own onLayout fired yet" as a SharedValue rather than deriving it from
58
+ // headerHeight (a plain, React-state-backed number) so the worklets below never have to list a
59
+ // value that flips from null to a real measured number in their dependency arrays — see the
60
+ // comment on translateStyle/blurStyle/progressStyle for why that recreation was a real crash, not
61
+ // just a perf concern.
62
+ const measuredShared = useSharedValue(false)
50
63
  const handleLayout = ({
51
64
  nativeEvent: {
52
65
  layout: { height }
53
66
  }
54
67
  }: LayoutChangeEvent) => {
68
+ measuredShared.value = true
55
69
  if (headerHeight !== height) setHeaderHeight(height)
56
70
  }
57
71
  const actionMargin = 4
@@ -61,33 +75,39 @@ export const ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, bac
61
75
  // pattern) — used as the blur backdrop's own height for that same pre-measurement window, so it
62
76
  // doesn't render as a flat, unblurred box for the ~60-80ms an onLayout round trip takes on web.
63
77
  const unmeasuredHeight = top + contentMinHeight
64
- // Every one of these worklets reads several SharedValues (scrollPosition, snapBackHeaderShared,
65
- // headerOffset, pullSearchHeightShared) whose *mutations* — not just headerFixed/headerHeight
66
- // changing — need to re-trigger the style recompute. Reanimated's automatic worklet dependency
67
- // capture covers this on native without needing them listed, but not on web, where a SharedValue
68
- // has to appear in the array itself (object identity, not .value) for its mutations to be
69
- // treated as reactive — omitting them here silently froze the header/blur/progress-bar's scroll
70
- // tracking on web at whatever scrollPosition happened to be when headerHeight last changed.
78
+ // Every one of these worklets reads SharedValues only (scrollPosition, snapBackHeaderShared,
79
+ // headerOffset, pullSearchHeightShared, headerHeightShared, measuredShared) — deliberately NOT the
80
+ // plain `headerHeight` context value, even though it holds the same number. `headerHeight` is a
81
+ // React-state-backed `number | null` that changes identity on every measurement (starting at null,
82
+ // then flipping to a real pixel value within a frame of mount); putting it in a useAnimatedStyle
83
+ // dependency array forces Reanimated to tear down and recreate the worklet the moment that happens
84
+ // — which crashed outright ("[Worklets] Tried to synchronously call a Remote Function") on
85
+ // react-native-worklets 0.10.1. SharedValues never change identity, only their `.value`, so they're
86
+ // safe to depend on and don't retrigger this. They still have to be LISTED here despite that: on
87
+ // web, without the Reanimated Babel plugin's automatic worklet dependency capture, a SharedValue's
88
+ // mutations aren't picked up as reactive unless its reference appears in this array — omitting one
89
+ // silently freezes that value's contribution to the header/blur/progress-bar's scroll tracking at
90
+ // whatever it was when the array last changed.
71
91
  const translateStyle = useAnimatedStyle(() => {
72
92
  if (headerFixed) return { transform: [{ translateY: 0 }] }
73
93
  if (snapBackHeaderShared.value) return { transform: [{ translateY: headerOffset.value }] }
74
- const effective = scrollPosition.value + (headerHeight ?? 0) - pullSearchHeightShared.value
94
+ const effective = scrollPosition.value + headerHeightShared.value - pullSearchHeightShared.value
75
95
  if (effective <= 0) return { transform: [{ translateY: 0 }] }
76
96
  return { transform: [{ translateY: -effective }] }
77
- }, [headerFixed, headerHeight, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared])
97
+ }, [headerFixed, headerHeightShared, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared])
78
98
  const blurStyle = useAnimatedStyle(() => {
79
- if (headerHeight === null) return { height: unmeasuredHeight }
80
- if (!headerHeight) return { height: 0 }
81
- if (headerFixed) return { height: headerHeight }
82
- const translateY = snapBackHeaderShared.value ? headerOffset.value : -Math.max(0, scrollPosition.value + headerHeight - pullSearchHeightShared.value)
83
- return { height: Math.max(headerHeight + translateY, top) }
84
- }, [headerFixed, headerHeight, top, unmeasuredHeight, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared])
99
+ if (!measuredShared.value) return { height: unmeasuredHeight }
100
+ if (!headerHeightShared.value) return { height: 0 }
101
+ if (headerFixed) return { height: headerHeightShared.value }
102
+ const translateY = snapBackHeaderShared.value ? headerOffset.value : -Math.max(0, scrollPosition.value + headerHeightShared.value - pullSearchHeightShared.value)
103
+ return { height: Math.max(headerHeightShared.value + translateY, top) }
104
+ }, [headerFixed, top, unmeasuredHeight, measuredShared, headerHeightShared, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared])
85
105
  const progressStyle = useAnimatedStyle(() => {
86
- const h = headerHeight ?? 0
106
+ const h = headerHeightShared.value
87
107
  if (headerFixed) return { top: h }
88
108
  const slide = snapBackHeaderShared.value ? -headerOffset.value : Math.max(0, scrollPosition.value + h - pullSearchHeightShared.value)
89
109
  return { top: Math.max(h - slide, top) }
90
- }, [headerFixed, headerHeight, top, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared])
110
+ }, [headerFixed, headerHeightShared, top, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared])
91
111
  const actionTop = top + actionMargin
92
112
  const buttonStyle = { borderRadius: actionSize, height: actionSize, margin: 0, width: actionSize }
93
113
  const actionShadow = { borderRadius: actionSize / 2, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.15, shadowRadius: 2 }
@@ -134,12 +154,12 @@ export const ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, bac
134
154
  (effectiveBackActionFixed ? (
135
155
  <Animated.View style={leadingStyle}>
136
156
  <ActionBg blur={blur} style={actionStyle} />
137
- <Appbar.BackAction accessibilityLabel={backActionAccessibilityLabel} onPress={backAction} size={iconSize ?? actionSize / 2} style={buttonStyle} />
157
+ {typeof backAction === 'function' ? <Appbar.BackAction accessibilityLabel={backActionAccessibilityLabel} onPress={backAction} size={iconSize ?? actionSize / 2} style={buttonStyle} /> : backAction}
138
158
  </Animated.View>
139
159
  ) : (
140
160
  <Animated.View style={[leadingStyle, translateStyle]}>
141
161
  <ActionBg blur={blur} style={actionStyle} />
142
- <Appbar.BackAction accessibilityLabel={backActionAccessibilityLabel} onPress={backAction} size={iconSize ?? actionSize / 2} style={buttonStyle} />
162
+ {typeof backAction === 'function' ? <Appbar.BackAction accessibilityLabel={backActionAccessibilityLabel} onPress={backAction} size={iconSize ?? actionSize / 2} style={buttonStyle} /> : backAction}
143
163
  </Animated.View>
144
164
  ))}
145
165
  {trailingAction &&
@@ -26,7 +26,7 @@ export const ScrollViewProvider = ({ blur, children, fixed = false, footerAboveK
26
26
  const effectiveSnapBackHeader = snapBackHeader ?? settings.snapBackHeader
27
27
  const effectiveSnapBackFooter = snapBackFooter ?? settings.snapBackFooter
28
28
  const [headerHeight, setHeaderHeightState] = useState<number | null>(null)
29
- const [footerHeight, setFooterHeightState] = useState(0)
29
+ const [footerHeight, setFooterHeightState] = useState<number | null>(null)
30
30
  const [progress, setProgress] = useState<number | null>(null)
31
31
  const [progressing, setProgressing] = useState(false)
32
32
  const scrollPosition = useSharedValue(0)
@@ -71,13 +71,20 @@ export const ScrollViewProvider = ({ blur, children, fixed = false, footerAboveK
71
71
  return () => clearTimeout(timeout)
72
72
  }, [headerHeight])
73
73
  const setFooterHeight = useCallback(
74
- (h: number) => {
74
+ (h: number | null) => {
75
+ // Same race setHeaderHeight guards against, on the footer side: react-native-screens' web
76
+ // shim (and a footer's own unmount cleanup below, which now passes null instead of 0 for
77
+ // exactly this reason) can report/force a spurious 0 while a real footer is effectively
78
+ // still on screen (e.g. a wizard cycling through several <ScrollViewFooter> instances).
79
+ // Never let that clobber a known height — only an explicit null (genuinely no footer
80
+ // mounted) may reset it.
81
+ if (h === 0 && footerHeight) return
75
82
  setFooterHeightState(h)
76
- footerHeightShared.value = h
83
+ footerHeightShared.value = h ?? 0
77
84
  },
78
- [footerHeightShared]
85
+ [footerHeight, footerHeightShared]
79
86
  )
80
- const scrollHeight = useMemo(() => Dimensions.get('window').height - (headerHeight ?? 0) - footerHeight, [headerHeight, footerHeight])
87
+ const scrollHeight = useMemo(() => Dimensions.get('window').height - (headerHeight ?? 0) - (footerHeight ?? 0), [headerHeight, footerHeight])
81
88
  const effectiveHeaderFixed = fixed || (headerFixed ?? settings.headerFixed)
82
89
  const effectiveFooterFixed = fixed || (footerFixed ?? settings.footerFixed)
83
90
  const value = useMemo(() => ({ blur: effectiveBlur, footerAboveKeyboard, footerHeight, footerHeightShared, footerFixed: effectiveFooterFixed, footerOffset, headerHeight, headerHeightShared, headerFixed: effectiveHeaderFixed, headerOffset, jsListGeneration, listGeneration, onJsListUnmount, onListUnmount, progress, pullSearchHeightShared, progressing, scrollHeight, scrollPosition, setFooterHeight, setHeaderHeight, setProgress, setProgressing, snapBackFooterShared, snapBackHeaderShared, tabBarHeight }), [effectiveBlur, footerAboveKeyboard, effectiveFooterFixed, effectiveHeaderFixed, footerHeight, footerHeightShared, footerOffset, headerHeight, headerHeightShared, headerOffset, jsListGeneration, listGeneration, onJsListUnmount, onListUnmount, progress, pullSearchHeightShared, progressing, scrollHeight, scrollPosition, setFooterHeight, setHeaderHeight, snapBackFooterShared, snapBackHeaderShared, tabBarHeight])
@@ -1,9 +1,16 @@
1
1
  import { Chip, type ChipProps as AutoPaperChipProps } from '@rific/auto-paper'
2
2
  import { type ReactNode } from 'react'
3
3
  import { StyleSheet, type ViewStyle } from 'react-native'
4
+ import { Icon, useTheme } from 'react-native-paper'
4
5
  import Animated, { type AnimatedStyle } from 'react-native-reanimated'
5
6
 
6
- export type ChipProps = Omit<AutoPaperChipProps, 'compact' | 'icon' | 'onPress'> & {
7
+ // 'children' omitted alongside the rest — this chip's own text always comes from `label` (or the
8
+ // isHorizontal-driven 'Start'/'Top' default below), passed as the real JSX children the underlying
9
+ // Chip renders with, which is what actually wins: React always takes the children an element is
10
+ // given between its tags over one merely passed as a "children" prop via spread. A `children` in
11
+ // chipProps would therefore type-check but silently do nothing at runtime — omitting it here means
12
+ // TypeScript catches that mismatch instead of a caller finding out the hard way.
13
+ export type ChipProps = Omit<AutoPaperChipProps, 'compact' | 'icon' | 'onPress' | 'children'> & {
7
14
  label?: ReactNode
8
15
  }
9
16
 
@@ -16,16 +23,27 @@ type Props = {
16
23
  }
17
24
 
18
25
  export const ScrollViewChip = ({ chipProps, isHorizontal, onChipPress, onPress, style }: Props) => {
19
- const { label, style: chipStyle, ...restChipProps } = (chipProps ?? {}) as Partial<ChipProps>
26
+ const { colors } = useTheme()
27
+ const { label, selectedColor, style: chipStyle, ...restChipProps } = (chipProps ?? {}) as Partial<ChipProps>
20
28
  const handlePress = onChipPress
21
29
  ? () => {
22
30
  onPress()
23
31
  onChipPress()
24
32
  }
25
33
  : onPress
34
+ // Rendered via `avatar`, not Chip's own `icon` prop: react-native-paper's MD3 Chip hardcodes
35
+ // `icon`'s color to theme.colors.primary and ignores `selectedColor` entirely (its own doc
36
+ // comment: "With theme version 3 selectedColor doesn't apply to the icon. If you want [a]
37
+ // custom color for the icon, render your own Icon component") — a chip whose own background
38
+ // IS theme.colors.primary (this library's callers all set that; see Hangman's own vivid-chip
39
+ // convention) renders that icon invisible against itself. `avatar` has no such override; it
40
+ // clones whatever element/color it's given. Falls back to the same default Paper itself would
41
+ // resolve for unselected/non-outlined text (onSecondaryContainer) or outlined text
42
+ // (onSurfaceVariant), so the icon still matches the label color when no selectedColor is set.
43
+ const iconColor = selectedColor ?? (restChipProps.mode === 'outlined' ? colors.onSurfaceVariant : colors.onSecondaryContainer)
26
44
  return (
27
45
  <Animated.View style={[styles.chip, style]}>
28
- <Chip compact {...restChipProps} icon={isHorizontal ? 'chevron-left' : 'chevron-up'} onPress={handlePress} style={[styles.chipInner, chipStyle]}>
46
+ <Chip compact selectedColor={selectedColor} {...restChipProps} avatar={<Icon source={isHorizontal ? 'chevron-left' : 'chevron-up'} size={18} color={iconColor} />} onPress={handlePress} style={[styles.chipInner, chipStyle]}>
29
47
  {label ?? (isHorizontal ? 'Start' : 'Top')}
30
48
  </Chip>
31
49
  </Animated.View>
@@ -25,7 +25,7 @@ export type UseScrollListOptions = {
25
25
  export function useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedProp, hideUntilMeasured, isHorizontal, keyboardAware, pullSearchHeight, style }: UseScrollListOptions = {}) {
26
26
  const insets = useSafeAreaInsets()
27
27
  const keyboardHeight = useKeyboardInset()
28
- const { footerAboveKeyboard, footerHeight, footerFixed: contextFooterFixed, headerHeight, headerFixed: contextHeaderFixed, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared, tabBarHeight } = useContext(ScrollViewContext)
28
+ const { footerAboveKeyboard, footerHeight, footerFixed: contextFooterFixed, headerHeight, headerHeightShared, headerFixed: contextHeaderFixed, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared, tabBarHeight } = useContext(ScrollViewContext)
29
29
 
30
30
  const headerFixed = isHorizontal ? true : (headerFixedProp ?? contextHeaderFixed)
31
31
  const footerFixed = isHorizontal ? true : (footerFixedProp ?? contextFooterFixed)
@@ -79,21 +79,25 @@ export function useScrollList({ footerFixed: footerFixedProp, headerFixed: heade
79
79
  const contentOffset = usesContentInset ? insetContentOffset : ZERO_OFFSET
80
80
 
81
81
  const chipHidden = useSharedValue(1)
82
- // chipHidden/snapBackHeaderShared/headerOffset/scrollPosition/pullSearchHeightShared all need to
83
- // be listed here too, not just read — see ScrollViewHeader's translateStyle for why (web-only
84
- // reanimated reactivity gap: a SharedValue's mutations don't retrigger useAnimatedStyle unless
85
- // the value itself is in this array).
82
+ // chipHidden/snapBackHeaderShared/headerOffset/scrollPosition/pullSearchHeightShared/
83
+ // headerHeightShared all need to be listed here too, not just read — see ScrollViewHeader's
84
+ // translateStyle for why (web-only reanimated reactivity gap: a SharedValue's mutations don't
85
+ // retrigger useAnimatedStyle unless the value itself is in this array). headerHeightShared is
86
+ // used here rather than the plain `headerHeight` context value specifically because the latter
87
+ // changes identity (null -> a real number) shortly after mount, which forces Reanimated to
88
+ // recreate this worklet — a crash on react-native-worklets 0.10.1, not just a perf concern. See
89
+ // ScrollViewHeader's translateStyle/blurStyle/progressStyle for the full explanation.
86
90
  const chipStyle = useAnimatedStyle(() => {
87
91
  const pointerEvents = chipHidden.value ? ('none' as const) : ('box-none' as const)
88
92
  if (isHorizontal) {
89
93
  return {
90
94
  opacity: chipHidden.value ? withTiming(0) : withTiming(1),
91
95
  pointerEvents,
92
- top: Math.max(headerHeight ?? 0, insets.top) + 4,
96
+ top: Math.max(headerHeightShared.value, insets.top) + 4,
93
97
  transform: [{ translateX: chipHidden.value ? withTiming(-CHIP_SLIDE) : withTiming(0) }]
94
98
  }
95
99
  }
96
- const h = headerHeight ?? 0
100
+ const h = headerHeightShared.value
97
101
  const slide = headerFixed ? 0 : snapBackHeaderShared.value ? -headerOffset.value : Math.max(0, scrollPosition.value + h - pullSearchHeightShared.value)
98
102
  const top = Math.max(h - slide, insets.top) + 4
99
103
  return {
@@ -102,7 +106,7 @@ export function useScrollList({ footerFixed: footerFixedProp, headerFixed: heade
102
106
  top,
103
107
  transform: [{ translateY: chipHidden.value ? withTiming(-CHIP_SLIDE) : withTiming(0) }]
104
108
  }
105
- }, [isHorizontal, headerFixed, headerHeight, insets.top, chipHidden, snapBackHeaderShared, headerOffset, scrollPosition, pullSearchHeightShared])
109
+ }, [isHorizontal, headerFixed, headerHeightShared, insets.top, chipHidden, snapBackHeaderShared, headerOffset, scrollPosition, pullSearchHeightShared])
106
110
 
107
111
  return { chipHidden, chipStyle, containerStyle, contentInset, contentOffset, contentPadding, footerFixed, headerFixed, headerHeight, insets, isHorizontal: isHorizontal ?? false }
108
112
  }