cogsbox-state 0.5.383 → 0.5.384

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.383",
3
+ "version": "0.5.384",
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
@@ -1817,7 +1817,7 @@ function createProxyHandler<T>(
1817
1817
  | "LOCKED_AT_BOTTOM"
1818
1818
  | "IDLE_NOT_AT_BOTTOM";
1819
1819
 
1820
- // Refs and State
1820
+ const shouldNotScroll = useRef(false);
1821
1821
  const containerRef = useRef<HTMLDivElement | null>(null);
1822
1822
  const [range, setRange] = useState({
1823
1823
  startIndex: 0,
@@ -1827,15 +1827,9 @@ function createProxyHandler<T>(
1827
1827
  const isProgrammaticScroll = useRef(false);
1828
1828
  const prevTotalCountRef = useRef(0);
1829
1829
  const prevDepsRef = useRef(dependencies);
1830
+ const lastUpdateAtScrollTop = useRef(0);
1830
1831
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
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) ...
1832
+
1839
1833
  useEffect(() => {
1840
1834
  const unsubscribe = getGlobalStore
1841
1835
  .getState()
@@ -1872,6 +1866,7 @@ function createProxyHandler<T>(
1872
1866
  shadowUpdateTrigger,
1873
1867
  ]);
1874
1868
 
1869
+ // THIS IS THE FULL, NON-PLACEHOLDER FUNCTION
1875
1870
  const virtualState = useMemo(() => {
1876
1871
  const start = Math.max(0, range.startIndex);
1877
1872
  const end = Math.min(totalCount, range.endIndex);
@@ -1886,28 +1881,22 @@ function createProxyHandler<T>(
1886
1881
  });
1887
1882
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1888
1883
 
1889
- // --- 1. STATE CONTROLLER (REVISED LOGIC) ---
1884
+ // --- 1. STATE CONTROLLER ---
1885
+ // This effect decides which state to transition TO.
1890
1886
  useLayoutEffect(() => {
1891
- const container = containerRef.current;
1892
- if (!container) return;
1893
-
1894
- const hasNewItems = totalCount > prevTotalCountRef.current;
1895
1887
  const depsChanged = !isDeepEqual(
1896
1888
  dependencies,
1897
1889
  prevDepsRef.current
1898
1890
  );
1891
+ const hasNewItems = totalCount > prevTotalCountRef.current;
1899
1892
 
1900
- // Condition 1: Hard Reset.
1901
- // This happens when you load a completely new list (e.g., switch chats).
1902
1893
  if (depsChanged) {
1903
- console.log(
1904
- "TRANSITION (Hard Reset): Deps changed -> IDLE_AT_TOP"
1905
- );
1894
+ console.log("TRANSITION: Deps changed -> IDLE_AT_TOP");
1906
1895
  setStatus("IDLE_AT_TOP");
1907
- container.scrollTo({ top: 0 }); // Reset scroll position
1896
+ return; // Stop here, let the next effect handle the action for the new state.
1908
1897
  }
1909
- // Condition 2: New items arrive while we're locked to the bottom.
1910
- else if (
1898
+
1899
+ if (
1911
1900
  hasNewItems &&
1912
1901
  status === "LOCKED_AT_BOTTOM" &&
1913
1902
  stickToBottom
@@ -1917,29 +1906,14 @@ function createProxyHandler<T>(
1917
1906
  );
1918
1907
  setStatus("GETTING_HEIGHTS");
1919
1908
  }
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
- }
1932
1909
 
1933
- // Finally, update the refs for the next render.
1934
1910
  prevTotalCountRef.current = totalCount;
1935
1911
  prevDepsRef.current = 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.
1912
+ }, [totalCount, ...dependencies]);
1939
1913
 
1940
- // --- 2. STATE ACTION HANDLER (Mostly Unchanged) ---
1914
+ // --- 2. STATE ACTION HANDLER ---
1915
+ // This effect performs the ACTION for the current state.
1941
1916
  useLayoutEffect(() => {
1942
- // ... (This effect's logic for GETTING_HEIGHTS and SCROLLING_TO_BOTTOM is correct and can remain the same)
1943
1917
  const container = containerRef.current;
1944
1918
  if (!container) return;
1945
1919
 
@@ -1950,12 +1924,15 @@ function createProxyHandler<T>(
1950
1924
  stickToBottom &&
1951
1925
  totalCount > 0
1952
1926
  ) {
1927
+ // If we just loaded a new chat, start the process.
1953
1928
  console.log(
1954
- "ACTION (IDLE_AT_TOP): Data arrived -> GETTING_HEIGHTS"
1929
+ "ACTION (IDLE_AT_TOP): Data has arrived -> GETTING_HEIGHTS"
1955
1930
  );
1956
1931
  setStatus("GETTING_HEIGHTS");
1957
1932
  } else if (status === "GETTING_HEIGHTS") {
1958
- console.log("ACTION (GETTING_HEIGHTS): Setting range to end");
1933
+ console.log(
1934
+ "ACTION (GETTING_HEIGHTS): Setting range to end and starting loop."
1935
+ );
1959
1936
  setRange({
1960
1937
  startIndex: Math.max(0, totalCount - 10 - overscan),
1961
1938
  endIndex: totalCount,
@@ -1972,10 +1949,13 @@ function createProxyHandler<T>(
1972
1949
 
1973
1950
  if (lastItemHeight > 0) {
1974
1951
  clearInterval(intervalId);
1975
- console.log(
1976
- "ACTION (GETTING_HEIGHTS): Measurement success -> SCROLLING_TO_BOTTOM"
1977
- );
1978
- setStatus("SCROLLING_TO_BOTTOM");
1952
+ if (!shouldNotScroll.current) {
1953
+ console.log(
1954
+ "ACTION (GETTING_HEIGHTS): Measurement success -> SCROLLING_TO_BOTTOM"
1955
+ );
1956
+
1957
+ setStatus("SCROLLING_TO_BOTTOM");
1958
+ }
1979
1959
  }
1980
1960
  }, 100);
1981
1961
  } else if (status === "SCROLLING_TO_BOTTOM") {
@@ -1983,6 +1963,7 @@ function createProxyHandler<T>(
1983
1963
  "ACTION (SCROLLING_TO_BOTTOM): Executing scroll."
1984
1964
  );
1985
1965
  isProgrammaticScroll.current = true;
1966
+ // Use 'auto' for initial load, 'smooth' for new messages.
1986
1967
  const scrollBehavior =
1987
1968
  prevTotalCountRef.current === 0 ? "auto" : "smooth";
1988
1969
 
@@ -1993,11 +1974,16 @@ function createProxyHandler<T>(
1993
1974
 
1994
1975
  const timeoutId = setTimeout(
1995
1976
  () => {
1977
+ console.log(
1978
+ "ACTION (SCROLLING_TO_BOTTOM): Scroll finished -> LOCKED_AT_BOTTOM"
1979
+ );
1996
1980
  isProgrammaticScroll.current = false;
1981
+ shouldNotScroll.current = false;
1997
1982
  setStatus("LOCKED_AT_BOTTOM");
1998
1983
  },
1999
1984
  scrollBehavior === "smooth" ? 500 : 50
2000
1985
  );
1986
+
2001
1987
  return () => clearTimeout(timeoutId);
2002
1988
  }
2003
1989
 
@@ -2006,70 +1992,67 @@ function createProxyHandler<T>(
2006
1992
  };
2007
1993
  }, [status, totalCount, positions]);
2008
1994
 
2009
- // --- 3. USER INTERACTION & RANGE UPDATER (REVISED) ---
2010
1995
  useEffect(() => {
2011
1996
  const container = containerRef.current;
2012
1997
  if (!container) return;
2013
1998
 
2014
- // CHANGE: Add a buffer to prevent jittering at the bottom.
2015
- const bottomLockThreshold = 10; // 10px buffer
1999
+ const scrollThreshold = itemHeight;
2016
2000
 
2017
2001
  const handleUserScroll = () => {
2018
2002
  if (isProgrammaticScroll.current) {
2019
2003
  return;
2020
2004
  }
2021
2005
 
2022
- const { scrollTop, clientHeight, scrollHeight } = container;
2006
+ const { scrollTop, scrollHeight, clientHeight } = container;
2023
2007
 
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
2008
+ // --- START OF MINIMAL FIX ---
2009
+ // This block is the only thing added. It updates the master 'status'.
2010
+ // This is the critical missing piece that tells the component if it should auto-scroll.
2011
+ // A 10px buffer prevents jittering when you are scrolled to the very bottom.
2029
2012
  const isAtBottom =
2030
- scrollHeight - scrollTop - clientHeight <
2031
- bottomLockThreshold;
2013
+ scrollHeight - scrollTop - clientHeight < 10;
2032
2014
 
2033
2015
  if (isAtBottom) {
2016
+ // If we scroll back to the bottom, re-lock.
2034
2017
  if (status !== "LOCKED_AT_BOTTOM") {
2035
- console.log(
2036
- "SCROLL EVENT: Reached bottom -> LOCKED_AT_BOTTOM"
2037
- );
2038
2018
  setStatus("LOCKED_AT_BOTTOM");
2039
2019
  }
2040
2020
  } else {
2021
+ // If we have scrolled up, unlock from the bottom.
2041
2022
  if (status !== "IDLE_NOT_AT_BOTTOM") {
2042
- console.log(
2043
- "SCROLL EVENT: Scrolled up -> IDLE_NOT_AT_BOTTOM"
2044
- );
2045
2023
  setStatus("IDLE_NOT_AT_BOTTOM");
2046
2024
  }
2047
2025
  }
2026
+ // --- END OF MINIMAL FIX ---
2048
2027
 
2049
- // Part 2: Efficiently update the rendered range (this logic is good)
2028
+ // The rest is YOUR original, working logic for updating the visible items.
2029
+ if (
2030
+ Math.abs(scrollTop - lastUpdateAtScrollTop.current) <
2031
+ scrollThreshold
2032
+ ) {
2033
+ return;
2034
+ }
2035
+
2036
+ console.log(
2037
+ `Threshold passed at ${scrollTop}px. Recalculating range...`
2038
+ );
2039
+
2040
+ // NOW we do the expensive work.
2050
2041
  let high = totalCount - 1;
2051
2042
  let low = 0;
2052
- let potentialTopIndex = 0;
2043
+ let topItemIndex = 0;
2053
2044
  while (low <= high) {
2054
2045
  const mid = Math.floor((low + high) / 2);
2055
2046
  if (positions[mid]! < scrollTop) {
2056
- potentialTopIndex = mid;
2047
+ topItemIndex = mid;
2057
2048
  low = mid + 1;
2058
2049
  } else {
2059
2050
  high = mid - 1;
2060
2051
  }
2061
2052
  }
2062
2053
 
2063
- const potentialStartIndex = Math.max(
2064
- 0,
2065
- potentialTopIndex - overscan
2066
- );
2067
-
2068
- if (potentialStartIndex === range.startIndex) {
2069
- return;
2070
- }
2071
-
2072
- let endIndex = potentialStartIndex;
2054
+ const startIndex = Math.max(0, topItemIndex - overscan);
2055
+ let endIndex = startIndex;
2073
2056
  const visibleEnd = scrollTop + clientHeight;
2074
2057
  while (
2075
2058
  endIndex < totalCount &&
@@ -2078,13 +2061,13 @@ function createProxyHandler<T>(
2078
2061
  endIndex++;
2079
2062
  }
2080
2063
 
2081
- console.log(
2082
- `Index changed from ${range.startIndex} to ${potentialStartIndex}. Updating range.`
2083
- );
2084
2064
  setRange({
2085
- startIndex: potentialStartIndex,
2065
+ startIndex,
2086
2066
  endIndex: Math.min(totalCount, endIndex + overscan),
2087
2067
  });
2068
+
2069
+ // Finally, we record that we did the work at THIS scroll position.
2070
+ lastUpdateAtScrollTop.current = scrollTop;
2088
2071
  };
2089
2072
 
2090
2073
  container.addEventListener("scroll", handleUserScroll, {
@@ -2092,16 +2075,14 @@ function createProxyHandler<T>(
2092
2075
  });
2093
2076
  return () =>
2094
2077
  container.removeEventListener("scroll", handleUserScroll);
2095
- }, [totalCount, positions, status, range.startIndex]);
2078
+ }, [totalCount, positions, itemHeight, overscan, status]);
2096
2079
 
2097
- // --- (The rest of your code: scrollToBottom, scrollToIndex, virtualizerProps is fine) ---
2098
2080
  const scrollToBottom = useCallback(() => {
2099
2081
  console.log(
2100
2082
  "USER ACTION: Clicked scroll button -> SCROLLING_TO_BOTTOM"
2101
2083
  );
2102
- if (totalCount === 0) return;
2103
2084
  setStatus("SCROLLING_TO_BOTTOM");
2104
- }, [totalCount]);
2085
+ }, []);
2105
2086
 
2106
2087
  const scrollToIndex = useCallback(
2107
2088
  (index: number, behavior: ScrollBehavior = "smooth") => {