cogsbox-state 0.5.339 → 0.5.342

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.339",
3
+ "version": "0.5.342",
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
@@ -1804,7 +1804,7 @@ function createProxyHandler<T>(
1804
1804
  ): VirtualStateObjectResult<any[]> => {
1805
1805
  const {
1806
1806
  itemHeight = 50,
1807
- overscan = 5,
1807
+ overscan = 6, // Keeping your working overscan value
1808
1808
  stickToBottom = false,
1809
1809
  } = options;
1810
1810
 
@@ -1813,11 +1813,11 @@ function createProxyHandler<T>(
1813
1813
  startIndex: 0,
1814
1814
  endIndex: 10,
1815
1815
  });
1816
-
1817
- // This ref tracks if the user is locked to the bottom.
1816
+ // The MASTER SWITCH for auto-scrolling. Starts true.
1818
1817
  const isLockedToBottomRef = useRef(stickToBottom);
1818
+ // Store the previous item count to detect when new items are added.
1819
+ const prevTotalCountRef = useRef(0);
1819
1820
 
1820
- // This state triggers a re-render when item heights change.
1821
1821
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
1822
1822
 
1823
1823
  useEffect(() => {
@@ -1835,7 +1835,6 @@ function createProxyHandler<T>(
1835
1835
  ) as any[];
1836
1836
  const totalCount = sourceArray.length;
1837
1837
 
1838
- // Calculate heights from shadow state. This runs when data or measurements change.
1839
1838
  const { totalHeight, positions } = useMemo(() => {
1840
1839
  const shadowArray =
1841
1840
  getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
@@ -1857,7 +1856,6 @@ function createProxyHandler<T>(
1857
1856
  shadowUpdateTrigger,
1858
1857
  ]);
1859
1858
 
1860
- // Memoize the virtualized slice of data.
1861
1859
  const virtualState = useMemo(() => {
1862
1860
  const start = Math.max(0, range.startIndex);
1863
1861
  const end = Math.min(totalCount, range.endIndex);
@@ -1871,48 +1869,18 @@ function createProxyHandler<T>(
1871
1869
  validIndices,
1872
1870
  });
1873
1871
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1874
- // useEffect(() => {
1875
- // if (stickToBottom && totalCount > 0 && containerRef.current) {
1876
- // // When count increases, immediately adjust range to show bottom
1877
- // const container = containerRef.current;
1878
- // const visibleCount = Math.ceil(
1879
- // container.clientHeight / itemHeight
1880
- // );
1881
-
1882
- // // Set range to show the last items including the new one
1883
- // setRange({
1884
- // startIndex: Math.max(
1885
- // 0,
1886
- // totalCount - visibleCount - overscan
1887
- // ),
1888
- // endIndex: totalCount,
1889
- // });
1890
-
1891
- // // Then scroll to bottom after a short delay
1892
- // setTimeout(() => {
1893
- // container.scrollTop = container.scrollHeight + 9999;
1894
- // }, 200);
1895
- // }
1896
- // }, [totalCount]);
1897
- // This is the main effect that handles all scrolling and updates.
1872
+
1873
+ // This layout effect is for SCROLLING and RANGE updates ONLY.
1898
1874
  useLayoutEffect(() => {
1899
1875
  const container = containerRef.current;
1900
1876
  if (!container) return;
1901
1877
 
1902
- // --- STEP 1: Remember if we were scrolled to the bottom BEFORE this render ---
1903
- // We check this now, before the new items might have pushed the scrollbar up.
1904
- const wasScrolledToBottom =
1905
- container.scrollHeight -
1906
- container.scrollTop -
1907
- container.clientHeight <
1908
- itemHeight;
1878
+ const hasNewItems = totalCount > prevTotalCountRef.current;
1909
1879
 
1910
1880
  // This function determines what's visible in the viewport.
1911
1881
  const updateVirtualRange = () => {
1912
1882
  if (!container) return;
1913
1883
  const { scrollTop, clientHeight } = container;
1914
-
1915
- // Find the first visible item
1916
1884
  let low = 0,
1917
1885
  high = totalCount - 1;
1918
1886
  while (low <= high) {
@@ -1921,8 +1889,6 @@ function createProxyHandler<T>(
1921
1889
  else high = mid - 1;
1922
1890
  }
1923
1891
  const startIndex = Math.max(0, high - overscan);
1924
-
1925
- // Find the last visible item
1926
1892
  let endIndex = startIndex;
1927
1893
  const visibleEnd = scrollTop + clientHeight;
1928
1894
  while (
@@ -1932,50 +1898,78 @@ function createProxyHandler<T>(
1932
1898
  endIndex++;
1933
1899
  }
1934
1900
  endIndex = Math.min(totalCount, endIndex + overscan);
1935
-
1936
- // Update the state to render the correct slice
1937
1901
  setRange({ startIndex, endIndex });
1938
1902
  };
1939
1903
 
1940
- // This function handles ONLY user-initiated scrolls.
1904
+ // If new items were added AND we are locked to the bottom, scroll smoothly.
1905
+ if (
1906
+ stickToBottom &&
1907
+ hasNewItems &&
1908
+ isLockedToBottomRef.current
1909
+ ) {
1910
+ // A timeout is essential for smooth scrolling to work after a render.
1911
+ const scrollTimeout = setTimeout(() => {
1912
+ if (container) {
1913
+ container.scrollTo({
1914
+ top: container.scrollHeight,
1915
+ behavior: "smooth",
1916
+ });
1917
+ }
1918
+ }, 50); // A small buffer (50ms) is more robust for measurement to catch up.
1919
+
1920
+ // Cleanup the timeout if the component unmounts or effect re-runs.
1921
+ return () => clearTimeout(scrollTimeout);
1922
+ }
1923
+
1924
+ // Always update the visible range.
1925
+ updateVirtualRange();
1926
+ }, [
1927
+ totalCount,
1928
+ positions,
1929
+ totalHeight,
1930
+ stickToBottom,
1931
+ itemHeight,
1932
+ ]); // Dependencies for scrolling/range.
1933
+
1934
+ // This effect is ONLY for handling USER SCROLLS and the initial scroll.
1935
+ useEffect(() => {
1936
+ const container = containerRef.current;
1937
+ if (!container) return;
1938
+
1939
+ // On initial mount, if we need to stick to the bottom, do it instantly.
1940
+ if (isLockedToBottomRef.current) {
1941
+ container.scrollTop = container.scrollHeight;
1942
+ }
1943
+
1941
1944
  const handleUserScroll = () => {
1942
- // When the user scrolls, update the ref so we know their intent for the *next* update.
1943
- isLockedToBottomRef.current =
1945
+ if (!container) return;
1946
+ // If the user scrolls up, break the lock.
1947
+ const isAtBottom =
1944
1948
  container.scrollHeight -
1945
1949
  container.scrollTop -
1946
1950
  container.clientHeight <
1947
1951
  1;
1948
- // Then, just render what's visible at the new position.
1949
- updateVirtualRange();
1952
+ if (!isAtBottom) {
1953
+ isLockedToBottomRef.current = false;
1954
+ }
1950
1955
  };
1951
1956
 
1952
- // Add the listener for user scrolling.
1953
1957
  container.addEventListener("scroll", handleUserScroll, {
1954
1958
  passive: true,
1955
1959
  });
1956
-
1957
- // --- STEP 2: Apply the scroll AFTER the render, based on what we remembered ---
1958
- if (
1959
- stickToBottom &&
1960
- (isLockedToBottomRef.current || wasScrolledToBottom)
1961
- ) {
1962
- // If we are "locked" OR if we were at the bottom just before this render,
1963
- // then scroll to the new bottom. This handles both initial load and new items.
1964
- container.scrollTop = container.scrollHeight;
1965
- }
1966
-
1967
- // Always calculate the visible range after any potential scroll changes.
1968
- updateVirtualRange();
1969
-
1970
- // Cleanup function is vital to prevent memory leaks.
1971
- return () => {
1960
+ return () =>
1972
1961
  container.removeEventListener("scroll", handleUserScroll);
1973
- };
1974
- }, [totalCount, positions, totalHeight, stickToBottom]);
1962
+ }, []); // This runs only once on mount.
1963
+
1964
+ // After every render, update the previous count ref.
1965
+ useEffect(() => {
1966
+ prevTotalCountRef.current = totalCount;
1967
+ });
1975
1968
 
1976
1969
  const scrollToBottom = useCallback(
1977
1970
  (behavior: ScrollBehavior = "smooth") => {
1978
1971
  if (containerRef.current) {
1972
+ // Re-enable the lock when the user explicitly asks to scroll to bottom.
1979
1973
  isLockedToBottomRef.current = true;
1980
1974
  containerRef.current.scrollTo({
1981
1975
  top: containerRef.current.scrollHeight,
@@ -1989,6 +1983,7 @@ function createProxyHandler<T>(
1989
1983
  const scrollToIndex = useCallback(
1990
1984
  (index: number, behavior: ScrollBehavior = "smooth") => {
1991
1985
  if (containerRef.current && positions[index] !== undefined) {
1986
+ // Scrolling to a specific index should break the lock.
1992
1987
  isLockedToBottomRef.current = false;
1993
1988
  containerRef.current.scrollTo({
1994
1989
  top: positions[index],