cogsbox-state 0.5.340 → 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.340",
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.
1898
- // This is the main effect that handles all scrolling and updates.
1872
+
1873
+ // This layout effect is for SCROLLING and RANGE updates ONLY.
1899
1874
  useLayoutEffect(() => {
1900
1875
  const container = containerRef.current;
1901
1876
  if (!container) return;
1902
1877
 
1903
- // --- STEP 1: Remember if we were scrolled to the bottom BEFORE this render ---
1904
- // The check is made more forgiving (< itemHeight) to handle measurement delays.
1905
- const wasScrolledToBottom =
1906
- container.scrollHeight -
1907
- container.scrollTop -
1908
- container.clientHeight <
1909
- itemHeight;
1878
+ const hasNewItems = totalCount > prevTotalCountRef.current;
1910
1879
 
1911
1880
  // This function determines what's visible in the viewport.
1912
1881
  const updateVirtualRange = () => {
1913
1882
  if (!container) return;
1914
1883
  const { scrollTop, clientHeight } = container;
1915
- // ... (rest of the function is the same, no changes needed)
1916
1884
  let low = 0,
1917
1885
  high = totalCount - 1;
1918
1886
  while (low <= high) {
@@ -1933,61 +1901,75 @@ function createProxyHandler<T>(
1933
1901
  setRange({ startIndex, endIndex });
1934
1902
  };
1935
1903
 
1936
- // This function handles ONLY user-initiated scrolls.
1937
- const handleUserScroll = () => {
1938
- // When the user scrolls, update the ref so we know their intent for the *next* update.
1939
- isLockedToBottomRef.current =
1940
- container.scrollHeight -
1941
- container.scrollTop -
1942
- container.clientHeight <
1943
- itemHeight; // Strict check for user action
1944
- // Then, just render what's visible at the new position.
1945
- updateVirtualRange();
1946
- };
1947
-
1948
- // Add the listener for user scrolling.
1949
- container.addEventListener("scroll", handleUserScroll, {
1950
- passive: true,
1951
- });
1952
-
1953
- let scrollTimeoutId: NodeJS.Timeout | undefined;
1954
-
1955
- // --- STEP 2: Conditionally schedule the SMOOTH scroll ---
1904
+ // If new items were added AND we are locked to the bottom, scroll smoothly.
1956
1905
  if (
1957
1906
  stickToBottom &&
1958
- (isLockedToBottomRef.current || wasScrolledToBottom)
1907
+ hasNewItems &&
1908
+ isLockedToBottomRef.current
1959
1909
  ) {
1960
- // A timeout is crucial for smooth scroll. It schedules the scroll
1961
- // for *after* the browser has painted the new items, preventing jank.
1962
- scrollTimeoutId = setTimeout(() => {
1963
- container.scrollTo({
1964
- top: container.scrollHeight,
1965
- behavior: "smooth",
1966
- });
1967
- }, 0); // 0ms is enough to defer it to the next event loop tick.
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);
1968
1922
  }
1969
1923
 
1970
- // Always calculate the visible range after any potential scroll changes.
1924
+ // Always update the visible range.
1971
1925
  updateVirtualRange();
1972
-
1973
- // Cleanup function is vital to prevent memory leaks.
1974
- return () => {
1975
- container.removeEventListener("scroll", handleUserScroll);
1976
- if (scrollTimeoutId) {
1977
- clearTimeout(scrollTimeoutId);
1978
- }
1979
- };
1980
1926
  }, [
1981
1927
  totalCount,
1982
1928
  positions,
1983
1929
  totalHeight,
1984
1930
  stickToBottom,
1985
1931
  itemHeight,
1986
- ]); // Add itemHeight to dependencies
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
+
1944
+ const handleUserScroll = () => {
1945
+ if (!container) return;
1946
+ // If the user scrolls up, break the lock.
1947
+ const isAtBottom =
1948
+ container.scrollHeight -
1949
+ container.scrollTop -
1950
+ container.clientHeight <
1951
+ 1;
1952
+ if (!isAtBottom) {
1953
+ isLockedToBottomRef.current = false;
1954
+ }
1955
+ };
1956
+
1957
+ container.addEventListener("scroll", handleUserScroll, {
1958
+ passive: true,
1959
+ });
1960
+ return () =>
1961
+ container.removeEventListener("scroll", handleUserScroll);
1962
+ }, []); // This runs only once on mount.
1963
+
1964
+ // After every render, update the previous count ref.
1965
+ useEffect(() => {
1966
+ prevTotalCountRef.current = totalCount;
1967
+ });
1987
1968
 
1988
1969
  const scrollToBottom = useCallback(
1989
1970
  (behavior: ScrollBehavior = "smooth") => {
1990
1971
  if (containerRef.current) {
1972
+ // Re-enable the lock when the user explicitly asks to scroll to bottom.
1991
1973
  isLockedToBottomRef.current = true;
1992
1974
  containerRef.current.scrollTo({
1993
1975
  top: containerRef.current.scrollHeight,
@@ -2001,6 +1983,7 @@ function createProxyHandler<T>(
2001
1983
  const scrollToIndex = useCallback(
2002
1984
  (index: number, behavior: ScrollBehavior = "smooth") => {
2003
1985
  if (containerRef.current && positions[index] !== undefined) {
1986
+ // Scrolling to a specific index should break the lock.
2004
1987
  isLockedToBottomRef.current = false;
2005
1988
  containerRef.current.scrollTo({
2006
1989
  top: positions[index],