cogsbox-state 0.5.283 → 0.5.284

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.283",
3
+ "version": "0.5.284",
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
@@ -1802,7 +1802,7 @@ function createProxyHandler<T>(
1802
1802
  options: VirtualViewOptions
1803
1803
  ): VirtualStateObjectResult<any[]> => {
1804
1804
  const {
1805
- itemHeight,
1805
+ itemHeight = 50, // Now optional with default
1806
1806
  overscan = 5,
1807
1807
  stickToBottom = false,
1808
1808
  } = options;
@@ -1812,16 +1812,38 @@ function createProxyHandler<T>(
1812
1812
  startIndex: 0,
1813
1813
  endIndex: 10,
1814
1814
  });
1815
- const getItemHeight = useCallback((index: number) => {
1816
- const metadata = getGlobalStore
1817
- .getState()
1818
- .getShadowMetadata(stateKey, [...path, index.toString()]);
1819
- return metadata?.virtualizer?.itemHeight || options.itemHeight;
1820
- }, []);
1821
- // --- State Tracking Refs ---
1815
+
1816
+ // Get item height from shadow state or fall back to default
1817
+ const getItemHeight = useCallback(
1818
+ (index: number) => {
1819
+ const metadata = getGlobalStore
1820
+ .getState()
1821
+ .getShadowMetadata(stateKey, [...path, index.toString()]);
1822
+ return metadata?.virtualizer?.itemHeight || itemHeight;
1823
+ },
1824
+ [itemHeight]
1825
+ );
1826
+
1827
+ // Calculate total height and item positions
1828
+ const calculateHeights = useCallback(() => {
1829
+ const sourceArray = getGlobalStore().getNestedState(
1830
+ stateKey,
1831
+ path
1832
+ ) as any[];
1833
+
1834
+ let totalHeight = 0;
1835
+ const positions: number[] = [];
1836
+
1837
+ for (let i = 0; i < sourceArray.length; i++) {
1838
+ positions[i] = totalHeight;
1839
+ totalHeight += getItemHeight(i);
1840
+ }
1841
+
1842
+ return { totalHeight, positions };
1843
+ }, [getItemHeight]);
1844
+
1822
1845
  const isAtBottomRef = useRef(stickToBottom);
1823
1846
  const previousTotalCountRef = useRef(0);
1824
- // NEW: Ref to explicitly track if this is the component's first render cycle.
1825
1847
  const isInitialMountRef = useRef(true);
1826
1848
 
1827
1849
  const sourceArray = getGlobalStore().getNestedState(
@@ -1852,25 +1874,43 @@ function createProxyHandler<T>(
1852
1874
  const listGrew = totalCount > previousTotalCountRef.current;
1853
1875
  previousTotalCountRef.current = totalCount;
1854
1876
 
1877
+ const { totalHeight, positions } = calculateHeights();
1878
+
1855
1879
  const handleScroll = () => {
1856
1880
  const { scrollTop, clientHeight, scrollHeight } = container;
1857
1881
  isAtBottomRef.current =
1858
1882
  scrollHeight - scrollTop - clientHeight < 10;
1859
- const start = Math.max(
1860
- 0,
1861
- Math.floor(scrollTop / itemHeight) - overscan
1862
- );
1863
- const end = Math.min(
1864
- totalCount,
1865
- Math.ceil((scrollTop + clientHeight) / itemHeight) +
1866
- overscan
1867
- );
1883
+
1884
+ // Find start index using binary search
1885
+ let start = 0;
1886
+ let end = positions.length - 1;
1887
+ while (start < end) {
1888
+ const mid = Math.floor((start + end) / 2);
1889
+ if (positions[mid]! < scrollTop) {
1890
+ start = mid + 1;
1891
+ } else {
1892
+ end = mid;
1893
+ }
1894
+ }
1895
+ start = Math.max(0, start - overscan);
1896
+
1897
+ // Find end index
1898
+ const visibleEnd = scrollTop + clientHeight;
1899
+ let endIndex = start;
1900
+ while (
1901
+ endIndex < positions.length &&
1902
+ positions[endIndex]! < visibleEnd
1903
+ ) {
1904
+ endIndex++;
1905
+ }
1906
+ endIndex = Math.min(totalCount, endIndex + overscan);
1907
+
1868
1908
  setRange((prevRange) => {
1869
1909
  if (
1870
1910
  prevRange.startIndex !== start ||
1871
- prevRange.endIndex !== end
1911
+ prevRange.endIndex !== endIndex
1872
1912
  ) {
1873
- return { startIndex: start, endIndex: end };
1913
+ return { startIndex: start, endIndex: endIndex };
1874
1914
  }
1875
1915
  return prevRange;
1876
1916
  });
@@ -1880,19 +1920,13 @@ function createProxyHandler<T>(
1880
1920
  passive: true,
1881
1921
  });
1882
1922
 
1883
- // --- THE CORRECTED DECISION LOGIC ---
1884
1923
  if (stickToBottom) {
1885
1924
  if (isInitialMountRef.current) {
1886
- // SCENARIO 1: First render of the component.
1887
- // Go to the bottom unconditionally. Use `auto` scroll for an instant jump.
1888
1925
  container.scrollTo({
1889
1926
  top: container.scrollHeight,
1890
1927
  behavior: "auto",
1891
1928
  });
1892
1929
  } else if (wasAtBottom && listGrew) {
1893
- // SCENARIO 2: Subsequent renders (new messages arrive).
1894
- // Only scroll if the user was already at the bottom.
1895
- // Use `smooth` for a nice animated scroll for new messages.
1896
1930
  requestAnimationFrame(() => {
1897
1931
  container.scrollTo({
1898
1932
  top: container.scrollHeight,
@@ -1902,15 +1936,12 @@ function createProxyHandler<T>(
1902
1936
  }
1903
1937
  }
1904
1938
 
1905
- // After the logic runs, it's no longer the initial mount.
1906
1939
  isInitialMountRef.current = false;
1907
-
1908
- // Always run handleScroll once to set the initial visible window.
1909
1940
  handleScroll();
1910
1941
 
1911
1942
  return () =>
1912
1943
  container.removeEventListener("scroll", handleScroll);
1913
- }, [totalCount, itemHeight, overscan, stickToBottom]);
1944
+ }, [totalCount, calculateHeights, overscan, stickToBottom]);
1914
1945
 
1915
1946
  const scrollToBottom = useCallback(
1916
1947
  (behavior: ScrollBehavior = "smooth") => {
@@ -1927,37 +1958,44 @@ function createProxyHandler<T>(
1927
1958
  const scrollToIndex = useCallback(
1928
1959
  (index: number, behavior: ScrollBehavior = "smooth") => {
1929
1960
  if (containerRef.current) {
1961
+ const { positions } = calculateHeights();
1930
1962
  containerRef.current.scrollTo({
1931
- top: index * itemHeight,
1963
+ top: positions[index] || 0,
1932
1964
  behavior,
1933
1965
  });
1934
1966
  }
1935
1967
  },
1936
- [itemHeight]
1968
+ [calculateHeights]
1937
1969
  );
1938
1970
 
1939
- // Same virtualizer props as before
1971
+ // Calculate actual heights for rendering
1972
+ const {
1973
+ totalHeight: totalHeightForRender,
1974
+ positions: positionsForRender,
1975
+ } = calculateHeights();
1976
+ const offsetY = positionsForRender[range.startIndex] || 0;
1977
+
1940
1978
  const virtualizerProps = {
1941
1979
  outer: {
1942
1980
  ref: containerRef,
1943
- style: { overflowY: "auto", height: "100%" },
1981
+ style: { overflowY: "auto", height: "100%" } as CSSProperties,
1944
1982
  },
1945
1983
  inner: {
1946
1984
  style: {
1947
- height: `${totalCount * itemHeight}px`,
1985
+ height: `${totalHeightForRender}px`,
1948
1986
  position: "relative",
1949
- },
1987
+ } as CSSProperties,
1950
1988
  },
1951
1989
  list: {
1952
1990
  style: {
1953
- transform: `translateY(${range.startIndex * itemHeight}px)`,
1954
- },
1991
+ transform: `translateY(${offsetY}px)`,
1992
+ } as CSSProperties,
1955
1993
  },
1956
1994
  };
1957
1995
 
1958
1996
  return {
1959
1997
  virtualState,
1960
- virtualizerProps: virtualizerProps as any,
1998
+ virtualizerProps,
1961
1999
  scrollToBottom,
1962
2000
  scrollToIndex,
1963
2001
  };