cogsbox-state 0.5.414 → 0.5.416

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.414",
3
+ "version": "0.5.416",
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,78 @@ 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
+ // Track if we're at bottom (with tolerance)
1956
+ wasAtBottomRef.current = distanceFromBottom < 100;
1957
+
1958
+ // Update visible range based on scroll position
1959
+ let startIndex = 0;
1960
+ for (let i = 0; i < positions.length; i++) {
1961
+ if (positions[i]! > scrollTop - itemHeight * overscan) {
1962
+ startIndex = Math.max(0, i - 1);
1963
+ break;
1964
+ }
1965
+ }
1917
1966
 
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
- );
1967
+ let endIndex = startIndex;
1968
+ const viewportEnd = scrollTop + clientHeight;
1969
+ for (let i = startIndex; i < positions.length; i++) {
1970
+ if (positions[i]! > viewportEnd + itemHeight * overscan) {
1971
+ break;
1972
+ }
1973
+ endIndex = i;
1974
+ }
1927
1975
 
1928
1976
  setRange({
1929
- startIndex: Math.max(0, firstVisible - overscan),
1930
- endIndex: Math.min(totalCount, lastVisible + overscan),
1977
+ startIndex: Math.max(0, startIndex),
1978
+ endIndex: Math.min(totalCount, endIndex + 1 + overscan),
1931
1979
  });
1932
1980
  };
1933
1981
 
1934
1982
  container.addEventListener("scroll", handleScroll, {
1935
1983
  passive: true,
1936
1984
  });
1985
+
1986
+ // Initial setup
1987
+ if (stickToBottom && totalCount > 0) {
1988
+ // For initial load, jump to bottom
1989
+ container.scrollTop = container.scrollHeight;
1990
+ }
1937
1991
  handleScroll();
1938
1992
 
1939
- return () =>
1993
+ return () => {
1940
1994
  container.removeEventListener("scroll", handleScroll);
1941
- }, [positions]); // Only re-attach when positions change
1995
+ };
1996
+ }, [positions, totalCount, itemHeight, overscan, stickToBottom]);
1942
1997
 
1943
1998
  const scrollToBottom = useCallback(() => {
1944
- if (containerRef.current) {
1999
+ wasAtBottomRef.current = true;
2000
+ const scrolled = scrollToLastItem();
2001
+ if (!scrolled && containerRef.current) {
1945
2002
  containerRef.current.scrollTop =
1946
2003
  containerRef.current.scrollHeight;
1947
- wasAtBottomRef.current = true;
1948
2004
  }
1949
- }, []);
2005
+ }, [scrollToLastItem]);
1950
2006
 
1951
2007
  const scrollToIndex = useCallback(
1952
2008
  (index: number, behavior: ScrollBehavior = "smooth") => {
2009
+ const shadowArray =
2010
+ getGlobalStore
2011
+ .getState()
2012
+ .getShadowMetadata(stateKey, path) || [];
2013
+ const itemData = shadowArray[index];
2014
+
2015
+ if (itemData?.virtualizer?.domRef) {
2016
+ const element = itemData.virtualizer.domRef;
2017
+ if (element && element.scrollIntoView) {
2018
+ element.scrollIntoView({ behavior, block: "center" });
2019
+ return;
2020
+ }
2021
+ }
2022
+
2023
+ // Fallback to position-based scrolling
1953
2024
  if (containerRef.current && positions[index] !== undefined) {
1954
2025
  containerRef.current.scrollTo({
1955
2026
  top: positions[index],
@@ -1957,7 +2028,7 @@ function createProxyHandler<T>(
1957
2028
  });
1958
2029
  }
1959
2030
  },
1960
- [positions]
2031
+ [positions, stateKey, path]
1961
2032
  );
1962
2033
 
1963
2034
  const virtualizerProps = {