cogsbox-state 0.5.415 → 0.5.417

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.415",
3
+ "version": "0.5.417",
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
@@ -1821,7 +1821,7 @@ function createProxyHandler<T>(
1821
1821
  endIndex: 10,
1822
1822
  });
1823
1823
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
1824
- const wasAtBottomRef = useRef(false);
1824
+ const wasAtBottomRef = useRef(true);
1825
1825
  const previousCountRef = useRef(0);
1826
1826
 
1827
1827
  // Subscribe to shadow state updates
@@ -1832,7 +1832,7 @@ function createProxyHandler<T>(
1832
1832
  setShadowUpdateTrigger((prev) => prev + 1);
1833
1833
  });
1834
1834
  return unsubscribe;
1835
- }, []); // Empty deps - stateKey never changes
1835
+ }, [stateKey]);
1836
1836
 
1837
1837
  const sourceArray = getGlobalStore().getNestedState(
1838
1838
  stateKey,
@@ -1854,7 +1854,13 @@ function createProxyHandler<T>(
1854
1854
  height += measuredHeight || itemHeight;
1855
1855
  }
1856
1856
  return { totalHeight: height, positions: pos };
1857
- }, [totalCount, itemHeight, shadowUpdateTrigger]);
1857
+ }, [
1858
+ totalCount,
1859
+ stateKey,
1860
+ path.join("."),
1861
+ itemHeight,
1862
+ shadowUpdateTrigger,
1863
+ ]);
1858
1864
 
1859
1865
  // Create virtual state
1860
1866
  const virtualState = useMemo(() => {
@@ -1869,43 +1875,72 @@ function createProxyHandler<T>(
1869
1875
  ...meta,
1870
1876
  validIndices,
1871
1877
  });
1872
- }, [range.startIndex, range.endIndex, sourceArray]);
1878
+ }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1879
+
1880
+ // Helper to scroll to last item using stored ref
1881
+ const scrollToLastItem = useCallback(() => {
1882
+ const shadowArray =
1883
+ getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
1884
+ [];
1885
+ const lastIndex = totalCount - 1;
1886
+
1887
+ if (lastIndex >= 0) {
1888
+ const lastItemData = shadowArray[lastIndex];
1889
+ if (lastItemData?.virtualizer?.domRef) {
1890
+ const element = lastItemData.virtualizer.domRef;
1891
+ if (element && element.scrollIntoView) {
1892
+ element.scrollIntoView({
1893
+ behavior: "auto",
1894
+ block: "end",
1895
+ inline: "nearest",
1896
+ });
1897
+ return true;
1898
+ }
1899
+ }
1900
+ }
1901
+ return false;
1902
+ }, [stateKey, path, totalCount]);
1873
1903
 
1874
1904
  // Handle new items when at bottom
1875
1905
  useEffect(() => {
1876
1906
  if (!stickToBottom || totalCount === 0) return;
1877
1907
 
1878
1908
  const hasNewItems = totalCount > previousCountRef.current;
1909
+ const isInitialLoad =
1910
+ previousCountRef.current === 0 && totalCount > 0;
1879
1911
 
1880
- // Only scroll if we were at bottom AND new items arrived
1881
- if (
1882
- hasNewItems &&
1883
- wasAtBottomRef.current &&
1884
- previousCountRef.current > 0
1885
- ) {
1886
- const container = containerRef.current;
1887
- if (!container) return;
1888
-
1889
- // Update range to show end
1912
+ if ((hasNewItems || isInitialLoad) && wasAtBottomRef.current) {
1913
+ // First, ensure the last items are in range
1890
1914
  const visibleCount = Math.ceil(
1891
- container.clientHeight / itemHeight
1915
+ containerRef.current?.clientHeight || 0 / itemHeight
1892
1916
  );
1893
- setRange({
1917
+ const newRange = {
1894
1918
  startIndex: Math.max(
1895
1919
  0,
1896
1920
  totalCount - visibleCount - overscan
1897
1921
  ),
1898
1922
  endIndex: totalCount,
1899
- });
1923
+ };
1924
+
1925
+ setRange(newRange);
1900
1926
 
1901
- // Scroll to bottom after render
1902
- setTimeout(() => {
1903
- container.scrollTop = container.scrollHeight;
1927
+ // Then scroll to the last item after it renders
1928
+ const timeoutId = setTimeout(() => {
1929
+ const scrolled = scrollToLastItem();
1930
+ if (!scrolled && containerRef.current) {
1931
+ // Fallback if ref not available yet
1932
+ containerRef.current.scrollTop =
1933
+ containerRef.current.scrollHeight;
1934
+ }
1904
1935
  }, 50);
1936
+
1937
+ previousCountRef.current = totalCount;
1938
+
1939
+ return () => clearTimeout(timeoutId);
1905
1940
  }
1906
1941
 
1907
1942
  previousCountRef.current = totalCount;
1908
- }, [totalCount]); // ONLY totalCount - nothing else matters
1943
+ }, [totalCount]);
1909
1944
 
1910
1945
  // Handle scroll events
1911
1946
  useEffect(() => {
@@ -1914,42 +1949,79 @@ function createProxyHandler<T>(
1914
1949
 
1915
1950
  const handleScroll = () => {
1916
1951
  const { scrollTop, scrollHeight, clientHeight } = container;
1952
+ const distanceFromBottom =
1953
+ scrollHeight - scrollTop - clientHeight;
1954
+
1955
+ // Only consider "at bottom" if we're VERY close (like 5px)
1956
+ // This prevents the snap-back behavior when scrolling up
1957
+ wasAtBottomRef.current = distanceFromBottom < 5;
1958
+
1959
+ // Update visible range based on scroll position
1960
+ let startIndex = 0;
1961
+ for (let i = 0; i < positions.length; i++) {
1962
+ if (positions[i]! > scrollTop - itemHeight * overscan) {
1963
+ startIndex = Math.max(0, i - 1);
1964
+ break;
1965
+ }
1966
+ }
1917
1967
 
1918
- // Check if at bottom
1919
- wasAtBottomRef.current =
1920
- scrollHeight - scrollTop - clientHeight < 100;
1921
-
1922
- // Calculate visible range
1923
- const firstVisible = Math.floor(scrollTop / itemHeight);
1924
- const lastVisible = Math.ceil(
1925
- (scrollTop + clientHeight) / itemHeight
1926
- );
1968
+ let endIndex = startIndex;
1969
+ const viewportEnd = scrollTop + clientHeight;
1970
+ for (let i = startIndex; i < positions.length; i++) {
1971
+ if (positions[i]! > viewportEnd + itemHeight * overscan) {
1972
+ break;
1973
+ }
1974
+ endIndex = i;
1975
+ }
1927
1976
 
1928
1977
  setRange({
1929
- startIndex: Math.max(0, firstVisible - overscan),
1930
- endIndex: Math.min(totalCount, lastVisible + overscan),
1978
+ startIndex: Math.max(0, startIndex),
1979
+ endIndex: Math.min(totalCount, endIndex + 1 + overscan),
1931
1980
  });
1932
1981
  };
1933
1982
 
1934
1983
  container.addEventListener("scroll", handleScroll, {
1935
1984
  passive: true,
1936
1985
  });
1986
+
1987
+ // Initial setup
1988
+ if (stickToBottom && totalCount > 0) {
1989
+ // For initial load, jump to bottom
1990
+ container.scrollTop = container.scrollHeight;
1991
+ }
1937
1992
  handleScroll();
1938
1993
 
1939
- return () =>
1994
+ return () => {
1940
1995
  container.removeEventListener("scroll", handleScroll);
1941
- }, [positions]); // Only re-attach when positions change
1996
+ };
1997
+ }, [positions, totalCount, itemHeight, overscan, stickToBottom]);
1942
1998
 
1943
1999
  const scrollToBottom = useCallback(() => {
1944
- if (containerRef.current) {
2000
+ wasAtBottomRef.current = true;
2001
+ const scrolled = scrollToLastItem();
2002
+ if (!scrolled && containerRef.current) {
1945
2003
  containerRef.current.scrollTop =
1946
2004
  containerRef.current.scrollHeight;
1947
- wasAtBottomRef.current = true;
1948
2005
  }
1949
- }, []);
2006
+ }, [scrollToLastItem]);
1950
2007
 
1951
2008
  const scrollToIndex = useCallback(
1952
2009
  (index: number, behavior: ScrollBehavior = "smooth") => {
2010
+ const shadowArray =
2011
+ getGlobalStore
2012
+ .getState()
2013
+ .getShadowMetadata(stateKey, path) || [];
2014
+ const itemData = shadowArray[index];
2015
+
2016
+ if (itemData?.virtualizer?.domRef) {
2017
+ const element = itemData.virtualizer.domRef;
2018
+ if (element && element.scrollIntoView) {
2019
+ element.scrollIntoView({ behavior, block: "center" });
2020
+ return;
2021
+ }
2022
+ }
2023
+
2024
+ // Fallback to position-based scrolling
1953
2025
  if (containerRef.current && positions[index] !== undefined) {
1954
2026
  containerRef.current.scrollTo({
1955
2027
  top: positions[index],
@@ -1957,7 +2029,7 @@ function createProxyHandler<T>(
1957
2029
  });
1958
2030
  }
1959
2031
  },
1960
- [positions]
2032
+ [positions, stateKey, path]
1961
2033
  );
1962
2034
 
1963
2035
  const virtualizerProps = {