cogsbox-state 0.5.402 → 0.5.403

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.402",
3
+ "version": "0.5.403",
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
@@ -1803,6 +1803,7 @@ function createProxyHandler<T>(
1803
1803
  return selectedIndex ?? -1;
1804
1804
  };
1805
1805
  }
1806
+ // Simplified useVirtualView approach
1806
1807
  if (prop === "useVirtualView") {
1807
1808
  return (
1808
1809
  options: VirtualViewOptions
@@ -1814,30 +1815,16 @@ function createProxyHandler<T>(
1814
1815
  dependencies = [],
1815
1816
  } = options;
1816
1817
 
1817
- // YOUR STATE MACHINE STATES
1818
- type Status =
1819
- | "IDLE_AT_TOP"
1820
- | "GETTING_HEIGHTS"
1821
- | "SCROLLING_TO_BOTTOM"
1822
- | "LOCKED_AT_BOTTOM"
1823
- | "IDLE_NOT_AT_BOTTOM";
1824
-
1825
- const shouldNotScroll = useRef(false);
1826
1818
  const containerRef = useRef<HTMLDivElement | null>(null);
1827
1819
  const [range, setRange] = useState({
1828
1820
  startIndex: 0,
1829
1821
  endIndex: 10,
1830
1822
  });
1831
- const [status, setStatus] = useState<Status>("IDLE_AT_TOP");
1832
- const isProgrammaticScroll = useRef(false);
1833
- const prevTotalCountRef = useRef(0);
1834
- const prevDepsRef = useRef(dependencies);
1835
- const lastUpdateAtScrollTop = useRef(0);
1823
+ const isUserScrolling = useRef(false);
1824
+ const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
1836
1825
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
1837
- const scrollAnchorRef = useRef<{
1838
- top: number;
1839
- height: number;
1840
- } | null>(null);
1826
+
1827
+ // Subscribe to shadow state updates
1841
1828
  useEffect(() => {
1842
1829
  const unsubscribe = getGlobalStore
1843
1830
  .getState()
@@ -1853,6 +1840,7 @@ function createProxyHandler<T>(
1853
1840
  ) as any[];
1854
1841
  const totalCount = sourceArray.length;
1855
1842
 
1843
+ // Calculate heights and positions
1856
1844
  const { totalHeight, positions } = useMemo(() => {
1857
1845
  const shadowArray =
1858
1846
  getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
@@ -1874,7 +1862,7 @@ function createProxyHandler<T>(
1874
1862
  shadowUpdateTrigger,
1875
1863
  ]);
1876
1864
 
1877
- // THIS IS THE FULL, NON-PLACEHOLDER FUNCTION
1865
+ // Create virtual state
1878
1866
  const virtualState = useMemo(() => {
1879
1867
  const start = Math.max(0, range.startIndex);
1880
1868
  const end = Math.min(totalCount, range.endIndex);
@@ -1889,256 +1877,121 @@ function createProxyHandler<T>(
1889
1877
  });
1890
1878
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1891
1879
 
1892
- // --- 1. STATE CONTROLLER ---
1893
- // --- 1. STATE CONTROLLER (CORRECTED) ---
1894
- useLayoutEffect(() => {
1895
- const container = containerRef.current;
1896
- if (!container) return;
1897
-
1898
- const hasNewItems = totalCount > prevTotalCountRef.current;
1899
-
1900
- // THIS IS THE NEW, IMPORTANT LOGIC FOR ANCHORING
1901
- if (hasNewItems && scrollAnchorRef.current) {
1902
- const { top: prevScrollTop, height: prevScrollHeight } =
1903
- scrollAnchorRef.current;
1904
-
1905
- // This is the key: Tell the app we are about to programmatically scroll.
1906
- isProgrammaticScroll.current = true;
1907
-
1908
- // Restore the scroll position.
1909
- container.scrollTop =
1910
- prevScrollTop + (container.scrollHeight - prevScrollHeight);
1911
- console.log(
1912
- `ANCHOR RESTORED to scrollTop: ${container.scrollTop}`
1913
- );
1914
-
1915
- // IMPORTANT: After the scroll, allow user scroll events again.
1916
- // Use a timeout to ensure this runs after the scroll event has fired and been ignored.
1917
- setTimeout(() => {
1918
- isProgrammaticScroll.current = false;
1919
- }, 100);
1920
-
1921
- scrollAnchorRef.current = null; // Clear the anchor after using it.
1922
- }
1923
- // YOUR ORIGINAL LOGIC CONTINUES UNCHANGED IN THE `ELSE` BLOCK
1924
- else {
1925
- const depsChanged = !isDeepEqual(
1926
- dependencies,
1927
- prevDepsRef.current
1928
- );
1929
-
1930
- if (depsChanged) {
1931
- console.log("TRANSITION: Deps changed -> IDLE_AT_TOP");
1932
- setStatus("IDLE_AT_TOP");
1933
- return;
1934
- }
1935
-
1936
- if (
1937
- hasNewItems &&
1938
- status === "LOCKED_AT_BOTTOM" &&
1939
- stickToBottom
1940
- ) {
1941
- console.log(
1942
- "TRANSITION: New items arrived while locked -> GETTING_HEIGHTS"
1943
- );
1944
- setStatus("GETTING_HEIGHTS");
1945
- }
1946
- }
1947
-
1948
- prevTotalCountRef.current = totalCount;
1949
- prevDepsRef.current = dependencies;
1950
- }, [totalCount, ...dependencies]);
1880
+ // Simple scroll to bottom when items are added
1881
+ useEffect(() => {
1882
+ if (!stickToBottom || !containerRef.current || totalCount === 0)
1883
+ return;
1951
1884
 
1952
- // --- 2. STATE ACTION HANDLER ---
1953
- // This effect performs the ACTION for the current state.
1954
- useLayoutEffect(() => {
1955
1885
  const container = containerRef.current;
1956
- if (!container) return;
1957
-
1958
- let intervalId: NodeJS.Timeout | undefined;
1959
-
1960
- if (
1961
- status === "IDLE_AT_TOP" &&
1962
- stickToBottom &&
1963
- totalCount > 0
1964
- ) {
1965
- // If we just loaded a new chat, start the process.
1966
- console.log(
1967
- "ACTION (IDLE_AT_TOP): Data has arrived -> GETTING_HEIGHTS"
1968
- );
1969
- setStatus("GETTING_HEIGHTS");
1970
- } else if (status === "GETTING_HEIGHTS") {
1971
- console.log(
1972
- "ACTION (GETTING_HEIGHTS): Setting range to end and starting loop."
1973
- );
1974
- setRange({
1975
- startIndex: Math.max(0, totalCount - 10 - overscan),
1976
- endIndex: totalCount,
1977
- });
1978
-
1979
- let attemptCount = 0;
1980
- const maxAttempts = 50;
1981
-
1982
- intervalId = setInterval(() => {
1983
- attemptCount++;
1984
- const lastItemIndex = totalCount - 1;
1985
- const shadowArray =
1986
- getGlobalStore
1987
- .getState()
1988
- .getShadowMetadata(stateKey, path) || [];
1989
- const lastItemHeight =
1990
- shadowArray[lastItemIndex]?.virtualizer?.itemHeight || 0;
1991
-
1992
- console.log(
1993
- `ACTION (GETTING_HEIGHTS): attempt ${attemptCount}, lastItemHeight =`,
1994
- lastItemHeight
1995
- );
1886
+ const isNearBottom =
1887
+ container.scrollHeight -
1888
+ container.scrollTop -
1889
+ container.clientHeight <
1890
+ 100;
1891
+
1892
+ // If user is near bottom or we're auto-scrolling, scroll to bottom
1893
+ if (isNearBottom || !isUserScrolling.current) {
1894
+ // Clear any pending scroll
1895
+ if (scrollTimeoutRef.current) {
1896
+ clearTimeout(scrollTimeoutRef.current);
1897
+ }
1996
1898
 
1997
- if (lastItemHeight > 0) {
1998
- clearInterval(intervalId);
1999
- console.log(
2000
- "ACTION (GETTING_HEIGHTS): Measurement success -> SCROLLING_TO_BOTTOM"
2001
- );
2002
- setStatus("SCROLLING_TO_BOTTOM");
2003
- } else if (attemptCount >= maxAttempts) {
2004
- clearInterval(intervalId);
2005
- console.log(
2006
- "ACTION (GETTING_HEIGHTS): Timeout - proceeding anyway"
2007
- );
2008
- setStatus("SCROLLING_TO_BOTTOM");
1899
+ // Delay scroll to allow items to render and measure
1900
+ scrollTimeoutRef.current = setTimeout(() => {
1901
+ if (containerRef.current) {
1902
+ containerRef.current.scrollTo({
1903
+ top: containerRef.current.scrollHeight,
1904
+ behavior: "smooth",
1905
+ });
2009
1906
  }
2010
1907
  }, 100);
2011
- } else if (status === "SCROLLING_TO_BOTTOM") {
2012
- console.log(
2013
- "ACTION (SCROLLING_TO_BOTTOM): Executing scroll."
2014
- );
2015
- isProgrammaticScroll.current = true;
2016
- // Use 'auto' for initial load, 'smooth' for new messages.
2017
- const scrollBehavior =
2018
- prevTotalCountRef.current === 0 ? "auto" : "smooth";
2019
-
2020
- container.scrollTo({
2021
- top: container.scrollHeight,
2022
- behavior: scrollBehavior,
2023
- });
2024
-
2025
- const timeoutId = setTimeout(
2026
- () => {
2027
- console.log(
2028
- "ACTION (SCROLLING_TO_BOTTOM): Scroll finished -> LOCKED_AT_BOTTOM"
2029
- );
2030
- isProgrammaticScroll.current = false;
2031
- shouldNotScroll.current = false;
2032
- setStatus("LOCKED_AT_BOTTOM");
2033
- },
2034
- scrollBehavior === "smooth" ? 500 : 50
2035
- );
2036
-
2037
- return () => clearTimeout(timeoutId);
2038
1908
  }
1909
+ }, [totalCount, stickToBottom]);
2039
1910
 
2040
- return () => {
2041
- if (intervalId) clearInterval(intervalId);
2042
- };
2043
- }, [status, totalCount, positions]);
2044
-
1911
+ // Handle scroll events
2045
1912
  useEffect(() => {
2046
1913
  const container = containerRef.current;
2047
1914
  if (!container) return;
2048
1915
 
2049
- const scrollThreshold = itemHeight;
2050
-
2051
- const handleUserScroll = () => {
2052
- // This guard is now critical. It will ignore our anchor restoration scroll.
2053
- if (isProgrammaticScroll.current) {
2054
- return;
2055
- }
2056
-
2057
- const { scrollTop, scrollHeight, clientHeight } = container;
1916
+ let scrollTimeout: NodeJS.Timeout;
2058
1917
 
2059
- // Part 1: Handle Status and Anchoring
2060
- const isAtBottom =
2061
- scrollHeight - scrollTop - clientHeight < 10;
1918
+ const handleScroll = () => {
1919
+ // Clear existing timeout
1920
+ clearTimeout(scrollTimeout);
2062
1921
 
2063
- if (isAtBottom) {
2064
- if (status !== "LOCKED_AT_BOTTOM") {
2065
- setStatus("LOCKED_AT_BOTTOM");
2066
- }
2067
- // If we are at the bottom, there is no anchor needed.
2068
- scrollAnchorRef.current = null;
2069
- } else {
2070
- if (status !== "IDLE_NOT_AT_BOTTOM") {
2071
- setStatus("IDLE_NOT_AT_BOTTOM");
2072
- }
2073
- // User is scrolled up. Continuously update the anchor with their latest position.
2074
- scrollAnchorRef.current = {
2075
- top: scrollTop,
2076
- height: scrollHeight,
2077
- };
2078
- }
1922
+ // Mark as user scrolling
1923
+ isUserScrolling.current = true;
2079
1924
 
2080
- // Part 2: YOUR original, working logic for updating the visible range.
2081
- if (
2082
- Math.abs(scrollTop - lastUpdateAtScrollTop.current) <
2083
- scrollThreshold
2084
- ) {
2085
- return;
2086
- }
1925
+ // Reset after scrolling stops
1926
+ scrollTimeout = setTimeout(() => {
1927
+ isUserScrolling.current = false;
1928
+ }, 150);
2087
1929
 
2088
- console.log(
2089
- `Threshold passed at ${scrollTop}px. Recalculating range...`
2090
- );
1930
+ // Update visible range
1931
+ const { scrollTop, clientHeight } = container;
2091
1932
 
2092
- // ... your logic to find startIndex and endIndex ...
2093
- let high = totalCount - 1;
2094
- let low = 0;
2095
- let topItemIndex = 0;
2096
- while (low <= high) {
2097
- const mid = Math.floor((low + high) / 2);
2098
- if (positions[mid]! < scrollTop) {
2099
- topItemIndex = mid;
2100
- low = mid + 1;
2101
- } else {
2102
- high = mid - 1;
1933
+ // Find first visible item
1934
+ let startIndex = 0;
1935
+ for (let i = 0; i < positions.length; i++) {
1936
+ if (positions[i]! > scrollTop - itemHeight * overscan) {
1937
+ startIndex = Math.max(0, i - 1);
1938
+ break;
2103
1939
  }
2104
1940
  }
2105
1941
 
2106
- const startIndex = Math.max(0, topItemIndex - overscan);
1942
+ // Find last visible item
2107
1943
  let endIndex = startIndex;
2108
- const visibleEnd = scrollTop + clientHeight;
2109
- while (
2110
- endIndex < totalCount &&
2111
- positions[endIndex]! < visibleEnd
2112
- ) {
2113
- endIndex++;
1944
+ const viewportEnd = scrollTop + clientHeight;
1945
+ for (let i = startIndex; i < positions.length; i++) {
1946
+ if (positions[i]! > viewportEnd + itemHeight * overscan) {
1947
+ break;
1948
+ }
1949
+ endIndex = i;
2114
1950
  }
2115
1951
 
2116
1952
  setRange({
2117
- startIndex,
2118
- endIndex: Math.min(totalCount, endIndex + overscan),
1953
+ startIndex: Math.max(0, startIndex),
1954
+ endIndex: Math.min(totalCount, endIndex + 1 + overscan),
2119
1955
  });
2120
-
2121
- lastUpdateAtScrollTop.current = scrollTop;
2122
1956
  };
2123
1957
 
2124
- container.addEventListener("scroll", handleUserScroll, {
1958
+ container.addEventListener("scroll", handleScroll, {
2125
1959
  passive: true,
2126
1960
  });
2127
- return () =>
2128
- container.removeEventListener("scroll", handleUserScroll);
2129
- }, [totalCount, positions, itemHeight, overscan, status]);
2130
1961
 
2131
- const scrollToBottom = useCallback(() => {
2132
- console.log(
2133
- "USER ACTION: Clicked scroll button -> SCROLLING_TO_BOTTOM"
2134
- );
2135
- setStatus("SCROLLING_TO_BOTTOM");
1962
+ // Initial range calculation
1963
+ handleScroll();
1964
+
1965
+ return () => {
1966
+ container.removeEventListener("scroll", handleScroll);
1967
+ clearTimeout(scrollTimeout);
1968
+ };
1969
+ }, [positions, totalCount, itemHeight, overscan]);
1970
+
1971
+ // Cleanup scroll timeout on unmount
1972
+ useEffect(() => {
1973
+ return () => {
1974
+ if (scrollTimeoutRef.current) {
1975
+ clearTimeout(scrollTimeoutRef.current);
1976
+ }
1977
+ };
2136
1978
  }, []);
2137
1979
 
1980
+ const scrollToBottom = useCallback(
1981
+ (behavior: ScrollBehavior = "smooth") => {
1982
+ if (containerRef.current) {
1983
+ containerRef.current.scrollTo({
1984
+ top: containerRef.current.scrollHeight,
1985
+ behavior,
1986
+ });
1987
+ }
1988
+ },
1989
+ []
1990
+ );
1991
+
2138
1992
  const scrollToIndex = useCallback(
2139
1993
  (index: number, behavior: ScrollBehavior = "smooth") => {
2140
1994
  if (containerRef.current && positions[index] !== undefined) {
2141
- setStatus("IDLE_NOT_AT_BOTTOM");
2142
1995
  containerRef.current.scrollTo({
2143
1996
  top: positions[index],
2144
1997
  behavior,