cogsbox-state 0.5.292 → 0.5.293

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.292",
3
+ "version": "0.5.293",
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
@@ -42,7 +42,7 @@ import useMeasure from "react-use-measure";
42
42
  type Prettify<T> = { [K in keyof T]: T[K] } & {};
43
43
 
44
44
  export type VirtualViewOptions = {
45
- itemHeight: number;
45
+ itemHeight?: number;
46
46
  overscan?: number;
47
47
  stickToBottom?: boolean;
48
48
  };
@@ -1802,9 +1802,8 @@ function createProxyHandler<T>(
1802
1802
  return (
1803
1803
  options: VirtualViewOptions
1804
1804
  ): VirtualStateObjectResult<any[]> => {
1805
- // --- CHANGE 1: itemHeight is now optional with a default ---
1806
1805
  const {
1807
- itemHeight = 50, // Serves as a fallback for unmeasured items
1806
+ itemHeight = 50, // Default/estimated height
1808
1807
  overscan = 5,
1809
1808
  stickToBottom = false,
1810
1809
  } = options;
@@ -1815,7 +1814,20 @@ function createProxyHandler<T>(
1815
1814
  endIndex: 10,
1816
1815
  });
1817
1816
 
1818
- // --- CHANGE 2: Add a helper to get heights from shadow store ---
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
1819
1831
  const getItemHeight = useCallback(
1820
1832
  (index: number): number => {
1821
1833
  const metadata = getGlobalStore
@@ -1826,18 +1838,7 @@ function createProxyHandler<T>(
1826
1838
  [itemHeight, stateKey, path]
1827
1839
  );
1828
1840
 
1829
- const isAtBottomRef = useRef(stickToBottom);
1830
- const previousTotalCountRef = useRef(0);
1831
- const isInitialMountRef = useRef(true);
1832
-
1833
- const sourceArray = getGlobalStore().getNestedState(
1834
- stateKey,
1835
- path
1836
- ) as any[];
1837
- const totalCount = sourceArray.length;
1838
-
1839
- // --- CHANGE 3: Pre-calculate total height and item positions ---
1840
- // This replaces all instances of `totalCount * itemHeight`.
1841
+ // Pre-calculate total height and the top offset of each item
1841
1842
  const { totalHeight, positions } = useMemo(() => {
1842
1843
  let currentHeight = 0;
1843
1844
  const pos: number[] = [];
@@ -1845,10 +1846,12 @@ function createProxyHandler<T>(
1845
1846
  pos[i] = currentHeight;
1846
1847
  currentHeight += getItemHeight(i);
1847
1848
  }
1849
+
1850
+ console.log("totalHeight", totalHeight);
1848
1851
  return { totalHeight: currentHeight, positions: pos };
1849
1852
  }, [totalCount, getItemHeight]);
1850
1853
 
1851
- // This part is IDENTICAL to your original code
1854
+ // This is identical to your original code
1852
1855
  const virtualState = useMemo(() => {
1853
1856
  const start = Math.max(0, range.startIndex);
1854
1857
  const end = Math.min(totalCount, range.endIndex);
@@ -1863,23 +1866,49 @@ function createProxyHandler<T>(
1863
1866
  });
1864
1867
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1865
1868
 
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.
1866
1872
  useLayoutEffect(() => {
1867
1873
  const container = containerRef.current;
1868
1874
  if (!container) return;
1869
1875
 
1870
- const wasAtBottom = isAtBottomRef.current;
1871
- const listGrew = totalCount > previousTotalCountRef.current;
1872
- previousTotalCountRef.current = totalCount;
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
1901
 
1874
1902
  const handleScroll = () => {
1903
+ if (!container) return;
1875
1904
  const { scrollTop, clientHeight, scrollHeight } = container;
1905
+ // Update "is at bottom" status on every scroll
1876
1906
  isAtBottomRef.current =
1877
1907
  scrollHeight - scrollTop - clientHeight < 10;
1908
+ scrollOffsetRef.current = scrollTop;
1878
1909
 
1879
- // --- CHANGE 4: Update scroll logic to use positions array ---
1880
- // This is the dynamic equivalent of `Math.floor(scrollTop / itemHeight)`.
1910
+ // Find start/end indices based on positions
1881
1911
  let startIndex = 0;
1882
- // A simple loop is robust and easy to understand.
1883
1912
  for (let i = 0; i < positions.length; i++) {
1884
1913
  if (positions[i]! >= scrollTop) {
1885
1914
  startIndex = i;
@@ -1890,7 +1919,6 @@ function createProxyHandler<T>(
1890
1919
  let endIndex = startIndex;
1891
1920
  while (
1892
1921
  endIndex < totalCount &&
1893
- positions[endIndex] &&
1894
1922
  positions[endIndex]! < scrollTop + clientHeight
1895
1923
  ) {
1896
1924
  endIndex++;
@@ -1904,39 +1932,26 @@ function createProxyHandler<T>(
1904
1932
  prevRange.startIndex !== startIndex ||
1905
1933
  prevRange.endIndex !== endIndex
1906
1934
  ) {
1907
- return { startIndex: startIndex, endIndex: endIndex };
1935
+ return { startIndex, endIndex };
1908
1936
  }
1909
1937
  return prevRange;
1910
1938
  });
1911
1939
  };
1912
1940
 
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
+
1913
1947
  container.addEventListener("scroll", handleScroll, {
1914
1948
  passive: true,
1915
1949
  });
1916
-
1917
- // This logic is IDENTICAL to your original code
1918
- if (stickToBottom) {
1919
- if (isInitialMountRef.current) {
1920
- container.scrollTo({
1921
- top: container.scrollHeight,
1922
- behavior: "auto",
1923
- });
1924
- } else if (wasAtBottom && listGrew) {
1925
- requestAnimationFrame(() => {
1926
- container.scrollTo({
1927
- top: container.scrollHeight,
1928
- behavior: "smooth",
1929
- });
1930
- });
1931
- }
1932
- }
1933
- isInitialMountRef.current = false;
1934
- handleScroll();
1950
+ handleScroll(); // Initial calculation
1935
1951
 
1936
1952
  return () =>
1937
1953
  container.removeEventListener("scroll", handleScroll);
1938
- // The dependencies are almost identical, just swapping itemHeight for `positions`
1939
- }, [totalCount, overscan, stickToBottom, positions]);
1954
+ }, [totalCount, overscan, positions]); // Depend on positions to re-run scroll logic
1940
1955
 
1941
1956
  const scrollToBottom = useCallback(
1942
1957
  (behavior: ScrollBehavior = "smooth") => {
@@ -1950,34 +1965,28 @@ function createProxyHandler<T>(
1950
1965
  []
1951
1966
  );
1952
1967
 
1953
- // --- CHANGE 5: Update scrollToIndex to use positions array ---
1954
1968
  const scrollToIndex = useCallback(
1955
1969
  (index: number, behavior: ScrollBehavior = "smooth") => {
1956
1970
  if (containerRef.current && positions[index] !== undefined) {
1957
1971
  containerRef.current.scrollTo({
1958
- top: positions[index], // Instead of `index * itemHeight`
1972
+ top: positions[index],
1959
1973
  behavior,
1960
1974
  });
1961
1975
  }
1962
1976
  },
1963
- [positions] // Depends on `positions` now instead of `itemHeight`
1977
+ [positions]
1964
1978
  );
1965
1979
 
1966
- // --- CHANGE 6: Update props to use dynamic totalHeight and offsets ---
1967
1980
  const virtualizerProps = {
1968
1981
  outer: {
1969
1982
  ref: containerRef,
1970
1983
  style: { overflowY: "auto", height: "100%" },
1971
1984
  },
1972
1985
  inner: {
1973
- style: {
1974
- height: `${totalHeight}px`, // Use calculated total height
1975
- position: "relative",
1976
- },
1986
+ style: { height: `${totalHeight}px`, position: "relative" },
1977
1987
  },
1978
1988
  list: {
1979
1989
  style: {
1980
- // Use the pre-calculated position of the first visible item
1981
1990
  transform: `translateY(${positions[range.startIndex] || 0}px)`,
1982
1991
  },
1983
1992
  },