cogsbox-state 0.5.342 → 0.5.344

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.342",
3
+ "version": "0.5.344",
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,19 +1804,27 @@ function createProxyHandler<T>(
1804
1804
  ): VirtualStateObjectResult<any[]> => {
1805
1805
  const {
1806
1806
  itemHeight = 50,
1807
- overscan = 6, // Keeping your working overscan value
1807
+ overscan = 6,
1808
1808
  stickToBottom = false,
1809
1809
  } = options;
1810
1810
 
1811
1811
  const containerRef = useRef<HTMLDivElement | null>(null);
1812
- const [range, setRange] = useState({
1813
- startIndex: 0,
1814
- endIndex: 10,
1815
- });
1816
- // The MASTER SWITCH for auto-scrolling. Starts true.
1812
+ // We'll set the range to the end first, then let an effect handle the scroll.
1813
+ const initialRange = () => {
1814
+ if (stickToBottom) {
1815
+ const visibleCount = 10; // A reasonable guess for initial render
1816
+ return {
1817
+ startIndex: Math.max(
1818
+ 0,
1819
+ sourceArray.length - visibleCount - overscan
1820
+ ),
1821
+ endIndex: sourceArray.length,
1822
+ };
1823
+ }
1824
+ return { startIndex: 0, endIndex: 10 };
1825
+ };
1826
+ const [range, setRange] = useState(initialRange);
1817
1827
  const isLockedToBottomRef = useRef(stickToBottom);
1818
- // Store the previous item count to detect when new items are added.
1819
- const prevTotalCountRef = useRef(0);
1820
1828
 
1821
1829
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
1822
1830
 
@@ -1870,16 +1878,48 @@ function createProxyHandler<T>(
1870
1878
  });
1871
1879
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1872
1880
 
1873
- // This layout effect is for SCROLLING and RANGE updates ONLY.
1881
+ // This is the implementation of YOUR ALGORITHM.
1874
1882
  useLayoutEffect(() => {
1875
1883
  const container = containerRef.current;
1876
- if (!container) return;
1884
+ if (
1885
+ !container ||
1886
+ !stickToBottom ||
1887
+ !isLockedToBottomRef.current
1888
+ ) {
1889
+ return;
1890
+ }
1877
1891
 
1878
- const hasNewItems = totalCount > prevTotalCountRef.current;
1892
+ // STEP 1: Check if the last item is measured. This is our "ready" signal.
1893
+ const lastItemIndex = totalCount - 1;
1894
+ const shadowArray =
1895
+ getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
1896
+ [];
1897
+ const lastItemIsMeasured =
1898
+ lastItemIndex >= 0 &&
1899
+ shadowArray[lastItemIndex]?.virtualizer?.itemHeight > 0;
1900
+
1901
+ // STEP 2: If it's measured, we know totalHeight is correct. We can now scroll.
1902
+ if (lastItemIsMeasured || totalCount === 0) {
1903
+ // A timeout is essential for 'smooth' to work reliably after a render.
1904
+ const scrollTimeout = setTimeout(() => {
1905
+ container.scrollTo({
1906
+ top: container.scrollHeight,
1907
+ behavior: "smooth",
1908
+ });
1909
+ }, 50); // A small buffer is safer than 0ms.
1910
+
1911
+ return () => clearTimeout(scrollTimeout);
1912
+ }
1913
+ // If the last item is NOT measured, this effect does nothing and simply waits.
1914
+ // It will automatically re-run when the measurement comes in (via shadowUpdateTrigger).
1915
+ }, [totalCount, totalHeight, stickToBottom]); // Re-run when layout changes.
1916
+
1917
+ // This effect ONLY handles user interaction and range updates.
1918
+ useEffect(() => {
1919
+ const container = containerRef.current;
1920
+ if (!container) return;
1879
1921
 
1880
- // This function determines what's visible in the viewport.
1881
1922
  const updateVirtualRange = () => {
1882
- if (!container) return;
1883
1923
  const { scrollTop, clientHeight } = container;
1884
1924
  let low = 0,
1885
1925
  high = totalCount - 1;
@@ -1897,53 +1937,13 @@ function createProxyHandler<T>(
1897
1937
  ) {
1898
1938
  endIndex++;
1899
1939
  }
1900
- endIndex = Math.min(totalCount, endIndex + overscan);
1901
- setRange({ startIndex, endIndex });
1940
+ setRange({
1941
+ startIndex,
1942
+ endIndex: Math.min(totalCount, endIndex + overscan),
1943
+ });
1902
1944
  };
1903
1945
 
1904
- // If new items were added AND we are locked to the bottom, scroll smoothly.
1905
- if (
1906
- stickToBottom &&
1907
- hasNewItems &&
1908
- isLockedToBottomRef.current
1909
- ) {
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);
1922
- }
1923
-
1924
- // Always update the visible range.
1925
- updateVirtualRange();
1926
- }, [
1927
- totalCount,
1928
- positions,
1929
- totalHeight,
1930
- stickToBottom,
1931
- itemHeight,
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
1946
  const handleUserScroll = () => {
1945
- if (!container) return;
1946
- // If the user scrolls up, break the lock.
1947
1947
  const isAtBottom =
1948
1948
  container.scrollHeight -
1949
1949
  container.scrollTop -
@@ -1952,24 +1952,22 @@ function createProxyHandler<T>(
1952
1952
  if (!isAtBottom) {
1953
1953
  isLockedToBottomRef.current = false;
1954
1954
  }
1955
+ updateVirtualRange();
1955
1956
  };
1956
1957
 
1957
1958
  container.addEventListener("scroll", handleUserScroll, {
1958
1959
  passive: true,
1959
1960
  });
1961
+ // Initial range calculation
1962
+ updateVirtualRange();
1963
+
1960
1964
  return () =>
1961
1965
  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
- });
1966
+ }, [totalCount, positions]);
1968
1967
 
1969
1968
  const scrollToBottom = useCallback(
1970
1969
  (behavior: ScrollBehavior = "smooth") => {
1971
1970
  if (containerRef.current) {
1972
- // Re-enable the lock when the user explicitly asks to scroll to bottom.
1973
1971
  isLockedToBottomRef.current = true;
1974
1972
  containerRef.current.scrollTo({
1975
1973
  top: containerRef.current.scrollHeight,
@@ -1983,7 +1981,6 @@ function createProxyHandler<T>(
1983
1981
  const scrollToIndex = useCallback(
1984
1982
  (index: number, behavior: ScrollBehavior = "smooth") => {
1985
1983
  if (containerRef.current && positions[index] !== undefined) {
1986
- // Scrolling to a specific index should break the lock.
1987
1984
  isLockedToBottomRef.current = false;
1988
1985
  containerRef.current.scrollTo({
1989
1986
  top: positions[index],