cogsbox-state 0.5.335 → 0.5.336

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.335",
3
+ "version": "0.5.336",
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
@@ -1814,9 +1814,11 @@ function createProxyHandler<T>(
1814
1814
  endIndex: 10,
1815
1815
  });
1816
1816
 
1817
- const isLockedToBottomRef = useRef(false); // Always start false
1817
+ // This ref tracks if the user is locked to the bottom.
1818
+ const isLockedToBottomRef = useRef(stickToBottom);
1819
+
1820
+ // This state triggers a re-render when item heights change.
1818
1821
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
1819
- const lastTotalCountRef = useRef(0); // Track previous count
1820
1822
 
1821
1823
  useEffect(() => {
1822
1824
  const unsubscribe = getGlobalStore
@@ -1833,6 +1835,7 @@ function createProxyHandler<T>(
1833
1835
  ) as any[];
1834
1836
  const totalCount = sourceArray.length;
1835
1837
 
1838
+ // Calculate heights from shadow state. This runs when data or measurements change.
1836
1839
  const { totalHeight, positions } = useMemo(() => {
1837
1840
  const shadowArray =
1838
1841
  getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
@@ -1854,6 +1857,7 @@ function createProxyHandler<T>(
1854
1857
  shadowUpdateTrigger,
1855
1858
  ]);
1856
1859
 
1860
+ // Memoize the virtualized slice of data.
1857
1861
  const virtualState = useMemo(() => {
1858
1862
  const start = Math.max(0, range.startIndex);
1859
1863
  const end = Math.min(totalCount, range.endIndex);
@@ -1867,14 +1871,40 @@ function createProxyHandler<T>(
1867
1871
  validIndices,
1868
1872
  });
1869
1873
  }, [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
+ );
1870
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;
1894
+ }, 100);
1895
+ }
1896
+ }, [totalCount]);
1897
+ // This is the main effect that handles all scrolling and updates.
1871
1898
  useLayoutEffect(() => {
1872
1899
  const container = containerRef.current;
1873
1900
  if (!container) return;
1874
1901
 
1902
+ // This function determines what's visible in the viewport.
1875
1903
  const updateVirtualRange = () => {
1876
1904
  if (!container) return;
1877
- const { scrollTop } = container;
1905
+ const { scrollTop, clientHeight } = container;
1906
+
1907
+ // Find the first visible item
1878
1908
  let low = 0,
1879
1909
  high = totalCount - 1;
1880
1910
  while (low <= high) {
@@ -1883,8 +1913,10 @@ function createProxyHandler<T>(
1883
1913
  else high = mid - 1;
1884
1914
  }
1885
1915
  const startIndex = Math.max(0, high - overscan);
1916
+
1917
+ // Find the last visible item
1886
1918
  let endIndex = startIndex;
1887
- const visibleEnd = scrollTop + container.clientHeight;
1919
+ const visibleEnd = scrollTop + clientHeight;
1888
1920
  while (
1889
1921
  endIndex < totalCount &&
1890
1922
  positions[endIndex]! < visibleEnd
@@ -1892,54 +1924,44 @@ function createProxyHandler<T>(
1892
1924
  endIndex++;
1893
1925
  }
1894
1926
  endIndex = Math.min(totalCount, endIndex + overscan);
1927
+
1895
1928
  setRange({ startIndex, endIndex });
1896
1929
  };
1897
1930
 
1931
+ // This function handles ONLY user-initiated scrolls.
1898
1932
  const handleUserScroll = () => {
1933
+ // The key: check if we are scrolled to the bottom.
1899
1934
  isLockedToBottomRef.current =
1900
1935
  container.scrollHeight -
1901
1936
  container.scrollTop -
1902
1937
  container.clientHeight <
1903
1938
  1;
1939
+
1940
+ // After any scroll, update what's visible.
1904
1941
  updateVirtualRange();
1905
1942
  };
1906
1943
 
1944
+ // Add the listener for user scrolling.
1907
1945
  container.addEventListener("scroll", handleUserScroll, {
1908
1946
  passive: true,
1909
1947
  });
1910
- console.log("totalCount", totalCount);
1911
- // Handle scrolling
1912
- if (stickToBottom && totalCount > 0) {
1913
- const isInitialLoad =
1914
- lastTotalCountRef.current === 0 && totalCount > 0;
1915
- const hasNewItems = totalCount > lastTotalCountRef.current;
1916
- console.log("isInitialLoad", isInitialLoad);
1917
- console.log("hasNewItems", hasNewItems);
1918
- if (isInitialLoad) {
1919
- // First load - always scroll to bottom
1920
- setTimeout(() => {
1921
- container.scrollTop = container.scrollHeight;
1922
- isLockedToBottomRef.current = true;
1923
- }, 1000); // Longer delay for initial load
1924
- } else if (hasNewItems && isLockedToBottomRef.current) {
1925
- // New items added and user is at bottom - smooth scroll
1926
- setTimeout(() => {
1927
- container.scrollTo({
1928
- top: container.scrollHeight,
1929
- behavior: "smooth",
1930
- });
1931
- }, 100);
1932
- }
1933
- // If user has scrolled up, don't auto-scroll
1948
+
1949
+ // --- THE AUTO-SCROLL LOGIC ---
1950
+ // This checks if we should auto-scroll *after* a render has occurred.
1951
+ if (stickToBottom && isLockedToBottomRef.current) {
1952
+ // If we were at the bottom before this new item was added,
1953
+ // scroll to the new bottom.
1954
+ container.scrollTop = container.scrollHeight; // <--- THIS IS THE SCROLL LOGIC
1934
1955
  }
1935
1956
 
1957
+ // Always calculate the initial visible range.
1936
1958
  updateVirtualRange();
1937
- lastTotalCountRef.current = totalCount;
1938
1959
 
1960
+ // Cleanup function is vital to prevent memory leaks.
1939
1961
  return () => {
1940
1962
  container.removeEventListener("scroll", handleUserScroll);
1941
1963
  };
1942
- }, [totalCount, positions, stickToBottom]);
1964
+ }, [totalCount, positions, totalHeight, stickToBottom]);
1943
1965
 
1944
1966
  const scrollToBottom = useCallback(
1945
1967
  (behavior: ScrollBehavior = "smooth") => {