cogsbox-state 0.5.293 → 0.5.295

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.293",
3
+ "version": "0.5.295",
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
@@ -1802,6 +1802,7 @@ function createProxyHandler<T>(
1802
1802
  return (
1803
1803
  options: VirtualViewOptions
1804
1804
  ): VirtualStateObjectResult<any[]> => {
1805
+ // --- CHANGE 1: itemHeight is now optional, with a default fallback.
1805
1806
  const {
1806
1807
  itemHeight = 50, // Default/estimated height
1807
1808
  overscan = 5,
@@ -1814,20 +1815,7 @@ function createProxyHandler<T>(
1814
1815
  endIndex: 10,
1815
1816
  });
1816
1817
 
1817
- // --- State Tracking Refs for Stability ---
1818
- const isAtBottomRef = useRef(stickToBottom);
1819
- // Store the scroll position before a new item is added
1820
- const scrollOffsetRef = useRef(0);
1821
- // Ref to track if the list has grown, to trigger scroll correction
1822
- const listGrewRef = useRef(false);
1823
-
1824
- const sourceArray = getGlobalStore().getNestedState(
1825
- stateKey,
1826
- path
1827
- ) as any[];
1828
- const totalCount = sourceArray.length;
1829
-
1830
- // Helper to get measured heights or the default
1818
+ // --- CHANGE 2: Add a helper to get the real height of each item. ---
1831
1819
  const getItemHeight = useCallback(
1832
1820
  (index: number): number => {
1833
1821
  const metadata = getGlobalStore
@@ -1838,20 +1826,30 @@ function createProxyHandler<T>(
1838
1826
  [itemHeight, stateKey, path]
1839
1827
  );
1840
1828
 
1841
- // Pre-calculate total height and the top offset of each item
1829
+ // --- These refs are from your original code. NO CHANGE. ---
1830
+ const isAtBottomRef = useRef(stickToBottom);
1831
+ const previousTotalCountRef = useRef(0);
1832
+ const isInitialMountRef = useRef(true);
1833
+
1834
+ const sourceArray = getGlobalStore().getNestedState(
1835
+ stateKey,
1836
+ path
1837
+ ) as any[];
1838
+ const totalCount = sourceArray.length;
1839
+
1840
+ // --- CHANGE 3: Calculate the total height and position of each item. ---
1841
+ // This is the only new block of logic required.
1842
1842
  const { totalHeight, positions } = useMemo(() => {
1843
- let currentHeight = 0;
1843
+ let height = 0;
1844
1844
  const pos: number[] = [];
1845
1845
  for (let i = 0; i < totalCount; i++) {
1846
- pos[i] = currentHeight;
1847
- currentHeight += getItemHeight(i);
1846
+ pos[i] = height;
1847
+ height += getItemHeight(i);
1848
1848
  }
1849
-
1850
- console.log("totalHeight", totalHeight);
1851
- return { totalHeight: currentHeight, positions: pos };
1849
+ return { totalHeight: height, positions: pos };
1852
1850
  }, [totalCount, getItemHeight]);
1853
1851
 
1854
- // This is identical to your original code
1852
+ // --- The virtualState logic is IDENTICAL to your original. NO CHANGE. ---
1855
1853
  const virtualState = useMemo(() => {
1856
1854
  const start = Math.max(0, range.startIndex);
1857
1855
  const end = Math.min(totalCount, range.endIndex);
@@ -1866,49 +1864,25 @@ function createProxyHandler<T>(
1866
1864
  });
1867
1865
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1868
1866
 
1869
- // --- STABLE SCROLL LOGIC ---
1870
- // useLayoutEffect runs after DOM mutations but before the browser paints.
1871
- // This is the perfect place to correct scroll positions.
1867
+ // --- This useLayoutEffect is from your original code. ---
1868
+ // --- We only change the math inside handleScroll. ---
1872
1869
  useLayoutEffect(() => {
1873
1870
  const container = containerRef.current;
1874
1871
  if (!container) return;
1875
1872
 
1876
- // If the list grew, we need to adjust the scroll position
1877
- // to prevent the content from jumping.
1878
- if (listGrewRef.current) {
1879
- listGrewRef.current = false; // Reset the flag
1880
-
1881
- if (isAtBottomRef.current) {
1882
- // If we were at the bottom, stay at the bottom.
1883
- // This is the fix for the auto-scroll issue.
1884
- container.scrollTop = container.scrollHeight;
1885
- } else {
1886
- // If we were in the middle, restore the previous scroll position
1887
- // plus the height of the content that was added above us.
1888
- // This is an advanced case, but for now, let's keep it simple
1889
- // as most use-cases are for chat-like views. For a simple list,
1890
- // just staying at the bottom is the main goal.
1891
- }
1892
- }
1893
- }, [totalHeight]); // This effect runs whenever the total height changes
1894
-
1895
- useEffect(() => {
1896
- const container = containerRef.current;
1897
- if (!container) return;
1898
-
1899
- // Track the previous total count to detect when new items are added
1900
- let previousTotalCount = totalCount;
1873
+ const wasAtBottom = isAtBottomRef.current;
1874
+ const listGrew = totalCount > previousTotalCountRef.current;
1875
+ previousTotalCountRef.current = totalCount;
1901
1876
 
1902
1877
  const handleScroll = () => {
1903
- if (!container) return;
1904
1878
  const { scrollTop, clientHeight, scrollHeight } = container;
1905
- // Update "is at bottom" status on every scroll
1906
1879
  isAtBottomRef.current =
1907
1880
  scrollHeight - scrollTop - clientHeight < 10;
1908
- scrollOffsetRef.current = scrollTop;
1909
1881
 
1910
- // Find start/end indices based on positions
1882
+ // --- CHANGE 4: The math to find the start and end index. ---
1883
+ // This replaces `scrollTop / itemHeight` with a more accurate search.
1911
1884
  let startIndex = 0;
1885
+ // Find the first item whose top position is past the scroll top.
1912
1886
  for (let i = 0; i < positions.length; i++) {
1913
1887
  if (positions[i]! >= scrollTop) {
1914
1888
  startIndex = i;
@@ -1917,6 +1891,7 @@ function createProxyHandler<T>(
1917
1891
  }
1918
1892
 
1919
1893
  let endIndex = startIndex;
1894
+ // Find the first item whose top position is past the bottom of the viewport.
1920
1895
  while (
1921
1896
  endIndex < totalCount &&
1922
1897
  positions[endIndex]! < scrollTop + clientHeight
@@ -1924,34 +1899,56 @@ function createProxyHandler<T>(
1924
1899
  endIndex++;
1925
1900
  }
1926
1901
 
1902
+ // Apply overscan, identical to your original code.
1927
1903
  startIndex = Math.max(0, startIndex - overscan);
1928
1904
  endIndex = Math.min(totalCount, endIndex + overscan);
1929
-
1905
+ console.log(
1906
+ "startIndex",
1907
+ startIndex,
1908
+ "endIndex",
1909
+ endIndex,
1910
+ "totalHeight",
1911
+ totalHeight
1912
+ );
1930
1913
  setRange((prevRange) => {
1931
1914
  if (
1932
1915
  prevRange.startIndex !== startIndex ||
1933
1916
  prevRange.endIndex !== endIndex
1934
1917
  ) {
1935
- return { startIndex, endIndex };
1918
+ return { startIndex: startIndex, endIndex: endIndex };
1936
1919
  }
1937
1920
  return prevRange;
1938
1921
  });
1939
1922
  };
1940
1923
 
1941
- // Check if the list has grown *before* the next render cycle
1942
- if (totalCount > previousTotalCount) {
1943
- listGrewRef.current = true;
1944
- }
1945
- previousTotalCount = totalCount;
1946
-
1947
1924
  container.addEventListener("scroll", handleScroll, {
1948
1925
  passive: true,
1949
1926
  });
1950
- handleScroll(); // Initial calculation
1927
+
1928
+ // --- This stickToBottom logic is IDENTICAL to your original. NO CHANGE. ---
1929
+ if (stickToBottom) {
1930
+ if (isInitialMountRef.current) {
1931
+ container.scrollTo({
1932
+ top: container.scrollHeight,
1933
+ behavior: "auto",
1934
+ });
1935
+ } else if (wasAtBottom && listGrew) {
1936
+ requestAnimationFrame(() => {
1937
+ container.scrollTo({
1938
+ top: container.scrollHeight,
1939
+ behavior: "smooth",
1940
+ });
1941
+ });
1942
+ }
1943
+ }
1944
+
1945
+ isInitialMountRef.current = false;
1946
+ handleScroll();
1951
1947
 
1952
1948
  return () =>
1953
1949
  container.removeEventListener("scroll", handleScroll);
1954
- }, [totalCount, overscan, positions]); // Depend on positions to re-run scroll logic
1950
+ // --- We swap `itemHeight` for `positions` in the dependency array. ---
1951
+ }, [totalCount, overscan, stickToBottom, positions]);
1955
1952
 
1956
1953
  const scrollToBottom = useCallback(
1957
1954
  (behavior: ScrollBehavior = "smooth") => {
@@ -1965,29 +1962,34 @@ function createProxyHandler<T>(
1965
1962
  []
1966
1963
  );
1967
1964
 
1965
+ // --- CHANGE 5: Update scrollToIndex to use the positions array. ---
1968
1966
  const scrollToIndex = useCallback(
1969
1967
  (index: number, behavior: ScrollBehavior = "smooth") => {
1970
- if (containerRef.current && positions[index] !== undefined) {
1968
+ if (containerRef.current) {
1971
1969
  containerRef.current.scrollTo({
1972
- top: positions[index],
1970
+ top: positions[index] || 0, // Use the calculated position
1973
1971
  behavior,
1974
1972
  });
1975
1973
  }
1976
1974
  },
1977
- [positions]
1975
+ [positions] // Dependency is now `positions`
1978
1976
  );
1979
1977
 
1978
+ // --- CHANGE 6: Update virtualizer props to use dynamic values. ---
1980
1979
  const virtualizerProps = {
1981
1980
  outer: {
1982
1981
  ref: containerRef,
1983
1982
  style: { overflowY: "auto", height: "100%" },
1984
1983
  },
1985
1984
  inner: {
1986
- style: { height: `${totalHeight}px`, position: "relative" },
1985
+ style: {
1986
+ height: `${totalHeight}px`, // Use calculated dynamic height
1987
+ position: "relative",
1988
+ },
1987
1989
  },
1988
1990
  list: {
1989
1991
  style: {
1990
- transform: `translateY(${positions[range.startIndex] || 0}px)`,
1992
+ transform: `translateY(${positions[range.startIndex] || 0}px)`, // Use calculated position
1991
1993
  },
1992
1994
  },
1993
1995
  };