cogsbox-state 0.5.382 → 0.5.383

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-state",
3
- "version": "0.5.382",
3
+ "version": "0.5.383",
4
4
  "description": "React state management library with form controls and server sync",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/CogsState.tsx CHANGED
@@ -1828,8 +1828,14 @@ function createProxyHandler<T>(
1828
1828
  const prevTotalCountRef = useRef(0);
1829
1829
  const prevDepsRef = useRef(dependencies);
1830
1830
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
1831
-
1832
- // Subscribe to external state updates for item heights
1831
+ // CHANGE: Add a ref to store the scroll position BEFORE new items are added.
1832
+ // This is the key to preventing the jump when scrolled up.
1833
+ const scrollAnchorRef = useRef<{
1834
+ scrollTop: number;
1835
+ scrollHeight: number;
1836
+ } | null>(null);
1837
+
1838
+ // ... (Your existing useEffect for shadow state and useMemo for data are fine) ...
1833
1839
  useEffect(() => {
1834
1840
  const unsubscribe = getGlobalStore
1835
1841
  .getState()
@@ -1845,7 +1851,6 @@ function createProxyHandler<T>(
1845
1851
  ) as any[];
1846
1852
  const totalCount = sourceArray.length;
1847
1853
 
1848
- // Memoize positions and total height based on measured items
1849
1854
  const { totalHeight, positions } = useMemo(() => {
1850
1855
  const shadowArray =
1851
1856
  getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
@@ -1867,7 +1872,6 @@ function createProxyHandler<T>(
1867
1872
  shadowUpdateTrigger,
1868
1873
  ]);
1869
1874
 
1870
- // Memoize the virtualized data slice
1871
1875
  const virtualState = useMemo(() => {
1872
1876
  const start = Math.max(0, range.startIndex);
1873
1877
  const end = Math.min(totalCount, range.endIndex);
@@ -1882,23 +1886,28 @@ function createProxyHandler<T>(
1882
1886
  });
1883
1887
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1884
1888
 
1885
- // --- 1. STATE CONTROLLER ---
1886
- // Decides which state to transition TO based on data changes.
1889
+ // --- 1. STATE CONTROLLER (REVISED LOGIC) ---
1887
1890
  useLayoutEffect(() => {
1891
+ const container = containerRef.current;
1892
+ if (!container) return;
1893
+
1894
+ const hasNewItems = totalCount > prevTotalCountRef.current;
1888
1895
  const depsChanged = !isDeepEqual(
1889
1896
  dependencies,
1890
1897
  prevDepsRef.current
1891
1898
  );
1892
- const hasNewItems = totalCount > prevTotalCountRef.current;
1893
1899
 
1900
+ // Condition 1: Hard Reset.
1901
+ // This happens when you load a completely new list (e.g., switch chats).
1894
1902
  if (depsChanged) {
1895
- console.log("TRANSITION: Deps changed -> IDLE_AT_TOP");
1903
+ console.log(
1904
+ "TRANSITION (Hard Reset): Deps changed -> IDLE_AT_TOP"
1905
+ );
1896
1906
  setStatus("IDLE_AT_TOP");
1897
- return;
1907
+ container.scrollTo({ top: 0 }); // Reset scroll position
1898
1908
  }
1899
-
1900
- // THIS IS THE CRITICAL CHECK: It only scrolls if new items arrive AND we are already locked.
1901
- if (
1909
+ // Condition 2: New items arrive while we're locked to the bottom.
1910
+ else if (
1902
1911
  hasNewItems &&
1903
1912
  status === "LOCKED_AT_BOTTOM" &&
1904
1913
  stickToBottom
@@ -1908,14 +1917,29 @@ function createProxyHandler<T>(
1908
1917
  );
1909
1918
  setStatus("GETTING_HEIGHTS");
1910
1919
  }
1920
+ // CHANGE: Condition 3: New items arrive while we are scrolled up.
1921
+ // This is the "scroll anchoring" logic that prevents the jump.
1922
+ else if (hasNewItems && scrollAnchorRef.current) {
1923
+ console.log(
1924
+ "ACTION: Maintaining scroll position after new items added."
1925
+ );
1926
+ // We adjust the scroll position by the amount of height that was just added.
1927
+ container.scrollTop =
1928
+ scrollAnchorRef.current.scrollTop +
1929
+ (container.scrollHeight -
1930
+ scrollAnchorRef.current.scrollHeight);
1931
+ }
1911
1932
 
1933
+ // Finally, update the refs for the next render.
1912
1934
  prevTotalCountRef.current = totalCount;
1913
1935
  prevDepsRef.current = dependencies;
1914
- }, [totalCount, ...dependencies]);
1936
+ // Clear the anchor after using it. It will be re-set by the user scroll handler.
1937
+ scrollAnchorRef.current = null;
1938
+ }, [totalCount, ...dependencies]); // This dependency array is correct.
1915
1939
 
1916
- // --- 2. STATE ACTION HANDLER ---
1917
- // Performs the ACTION for the current state (e.g., scrolling).
1940
+ // --- 2. STATE ACTION HANDLER (Mostly Unchanged) ---
1918
1941
  useLayoutEffect(() => {
1942
+ // ... (This effect's logic for GETTING_HEIGHTS and SCROLLING_TO_BOTTOM is correct and can remain the same)
1919
1943
  const container = containerRef.current;
1920
1944
  if (!container) return;
1921
1945
 
@@ -1982,11 +2006,14 @@ function createProxyHandler<T>(
1982
2006
  };
1983
2007
  }, [status, totalCount, positions]);
1984
2008
 
1985
- // --- 3. USER INTERACTION & RANGE UPDATER (THE CORRECTED VERSION) ---
2009
+ // --- 3. USER INTERACTION & RANGE UPDATER (REVISED) ---
1986
2010
  useEffect(() => {
1987
2011
  const container = containerRef.current;
1988
2012
  if (!container) return;
1989
2013
 
2014
+ // CHANGE: Add a buffer to prevent jittering at the bottom.
2015
+ const bottomLockThreshold = 10; // 10px buffer
2016
+
1990
2017
  const handleUserScroll = () => {
1991
2018
  if (isProgrammaticScroll.current) {
1992
2019
  return;
@@ -1994,10 +2021,14 @@ function createProxyHandler<T>(
1994
2021
 
1995
2022
  const { scrollTop, clientHeight, scrollHeight } = container;
1996
2023
 
1997
- // Part 1: UPDATE THE STATE MACHINE STATUS (The critical fix)
1998
- // This tells the rest of the component whether we should auto-scroll on new messages.
2024
+ // CHANGE: Before the next state update, save the current scroll positions.
2025
+ // This 'anchors' our view, so the State Controller knows where we were.
2026
+ scrollAnchorRef.current = { scrollTop, scrollHeight };
2027
+
2028
+ // Part 1: Update the state machine with a tolerance
1999
2029
  const isAtBottom =
2000
- scrollHeight - scrollTop - clientHeight < 1;
2030
+ scrollHeight - scrollTop - clientHeight <
2031
+ bottomLockThreshold;
2001
2032
 
2002
2033
  if (isAtBottom) {
2003
2034
  if (status !== "LOCKED_AT_BOTTOM") {
@@ -2015,9 +2046,7 @@ function createProxyHandler<T>(
2015
2046
  }
2016
2047
  }
2017
2048
 
2018
- // Part 2: EFFICIENTLY UPDATE THE RENDERED RANGE
2019
- // This is the superior optimization from your first version. It only
2020
- // re-renders if the actual items in view have changed.
2049
+ // Part 2: Efficiently update the rendered range (this logic is good)
2021
2050
  let high = totalCount - 1;
2022
2051
  let low = 0;
2023
2052
  let potentialTopIndex = 0;
@@ -2036,12 +2065,10 @@ function createProxyHandler<T>(
2036
2065
  potentialTopIndex - overscan
2037
2066
  );
2038
2067
 
2039
- // Only update state if the visible start index has actually changed.
2040
2068
  if (potentialStartIndex === range.startIndex) {
2041
2069
  return;
2042
2070
  }
2043
2071
 
2044
- // If we must update, calculate the new end index.
2045
2072
  let endIndex = potentialStartIndex;
2046
2073
  const visibleEnd = scrollTop + clientHeight;
2047
2074
  while (
@@ -2065,14 +2092,13 @@ function createProxyHandler<T>(
2065
2092
  });
2066
2093
  return () =>
2067
2094
  container.removeEventListener("scroll", handleUserScroll);
2068
- }, [totalCount, positions, status, range.startIndex]); // Dependencies are correct
2095
+ }, [totalCount, positions, status, range.startIndex]);
2069
2096
 
2070
- // --- 4. EXPOSED ACTIONS ---
2097
+ // --- (The rest of your code: scrollToBottom, scrollToIndex, virtualizerProps is fine) ---
2071
2098
  const scrollToBottom = useCallback(() => {
2072
2099
  console.log(
2073
2100
  "USER ACTION: Clicked scroll button -> SCROLLING_TO_BOTTOM"
2074
2101
  );
2075
- // Don't scroll if there's nothing to scroll to.
2076
2102
  if (totalCount === 0) return;
2077
2103
  setStatus("SCROLLING_TO_BOTTOM");
2078
2104
  }, [totalCount]);
@@ -2080,7 +2106,6 @@ function createProxyHandler<T>(
2080
2106
  const scrollToIndex = useCallback(
2081
2107
  (index: number, behavior: ScrollBehavior = "smooth") => {
2082
2108
  if (containerRef.current && positions[index] !== undefined) {
2083
- // Manually scrolling to an index means we are no longer at the bottom.
2084
2109
  setStatus("IDLE_NOT_AT_BOTTOM");
2085
2110
  containerRef.current.scrollTo({
2086
2111
  top: positions[index],
@@ -2091,7 +2116,6 @@ function createProxyHandler<T>(
2091
2116
  [positions]
2092
2117
  );
2093
2118
 
2094
- // --- 5. RENDER PROPS ---
2095
2119
  const virtualizerProps = {
2096
2120
  outer: {
2097
2121
  ref: containerRef,