cogsbox-state 0.5.401 → 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.401",
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,249 +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
- });
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
+ }
1978
1898
 
1979
- intervalId = setInterval(() => {
1980
- const lastItemIndex = totalCount - 1;
1981
- const shadowArray =
1982
- getGlobalStore
1983
- .getState()
1984
- .getShadowMetadata(stateKey, path) || [];
1985
- const lastItemHeight =
1986
- shadowArray[lastItemIndex]?.virtualizer?.itemHeight || 0;
1987
- console.log(
1988
- "ACTION (GETTING_HEIGHTS): lastItemHeight =",
1989
- lastItemHeight,
1990
- " index =",
1991
- lastItemIndex
1992
- );
1993
- if (lastItemHeight > 0) {
1994
- clearInterval(intervalId);
1995
- if (!shouldNotScroll.current) {
1996
- console.log(
1997
- "ACTION (GETTING_HEIGHTS): Measurement success -> SCROLLING_TO_BOTTOM"
1998
- );
1999
-
2000
- setStatus("SCROLLING_TO_BOTTOM");
2001
- }
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
+ });
2002
1906
  }
2003
1907
  }, 100);
2004
- } else if (status === "SCROLLING_TO_BOTTOM") {
2005
- console.log(
2006
- "ACTION (SCROLLING_TO_BOTTOM): Executing scroll."
2007
- );
2008
- isProgrammaticScroll.current = true;
2009
- // Use 'auto' for initial load, 'smooth' for new messages.
2010
- const scrollBehavior =
2011
- prevTotalCountRef.current === 0 ? "auto" : "smooth";
2012
-
2013
- container.scrollTo({
2014
- top: container.scrollHeight,
2015
- behavior: scrollBehavior,
2016
- });
2017
-
2018
- const timeoutId = setTimeout(
2019
- () => {
2020
- console.log(
2021
- "ACTION (SCROLLING_TO_BOTTOM): Scroll finished -> LOCKED_AT_BOTTOM"
2022
- );
2023
- isProgrammaticScroll.current = false;
2024
- shouldNotScroll.current = false;
2025
- setStatus("LOCKED_AT_BOTTOM");
2026
- },
2027
- scrollBehavior === "smooth" ? 500 : 50
2028
- );
2029
-
2030
- return () => clearTimeout(timeoutId);
2031
1908
  }
1909
+ }, [totalCount, stickToBottom]);
2032
1910
 
2033
- return () => {
2034
- if (intervalId) clearInterval(intervalId);
2035
- };
2036
- }, [status, totalCount, positions]);
2037
-
1911
+ // Handle scroll events
2038
1912
  useEffect(() => {
2039
1913
  const container = containerRef.current;
2040
1914
  if (!container) return;
2041
1915
 
2042
- const scrollThreshold = itemHeight;
1916
+ let scrollTimeout: NodeJS.Timeout;
2043
1917
 
2044
- const handleUserScroll = () => {
2045
- // This guard is now critical. It will ignore our anchor restoration scroll.
2046
- if (isProgrammaticScroll.current) {
2047
- return;
2048
- }
1918
+ const handleScroll = () => {
1919
+ // Clear existing timeout
1920
+ clearTimeout(scrollTimeout);
2049
1921
 
2050
- const { scrollTop, scrollHeight, clientHeight } = container;
1922
+ // Mark as user scrolling
1923
+ isUserScrolling.current = true;
2051
1924
 
2052
- // Part 1: Handle Status and Anchoring
2053
- const isAtBottom =
2054
- scrollHeight - scrollTop - clientHeight < 10;
1925
+ // Reset after scrolling stops
1926
+ scrollTimeout = setTimeout(() => {
1927
+ isUserScrolling.current = false;
1928
+ }, 150);
2055
1929
 
2056
- if (isAtBottom) {
2057
- if (status !== "LOCKED_AT_BOTTOM") {
2058
- setStatus("LOCKED_AT_BOTTOM");
2059
- }
2060
- // If we are at the bottom, there is no anchor needed.
2061
- scrollAnchorRef.current = null;
2062
- } else {
2063
- if (status !== "IDLE_NOT_AT_BOTTOM") {
2064
- setStatus("IDLE_NOT_AT_BOTTOM");
2065
- }
2066
- // User is scrolled up. Continuously update the anchor with their latest position.
2067
- scrollAnchorRef.current = {
2068
- top: scrollTop,
2069
- height: scrollHeight,
2070
- };
2071
- }
2072
-
2073
- // Part 2: YOUR original, working logic for updating the visible range.
2074
- if (
2075
- Math.abs(scrollTop - lastUpdateAtScrollTop.current) <
2076
- scrollThreshold
2077
- ) {
2078
- return;
2079
- }
2080
-
2081
- console.log(
2082
- `Threshold passed at ${scrollTop}px. Recalculating range...`
2083
- );
1930
+ // Update visible range
1931
+ const { scrollTop, clientHeight } = container;
2084
1932
 
2085
- // ... your logic to find startIndex and endIndex ...
2086
- let high = totalCount - 1;
2087
- let low = 0;
2088
- let topItemIndex = 0;
2089
- while (low <= high) {
2090
- const mid = Math.floor((low + high) / 2);
2091
- if (positions[mid]! < scrollTop) {
2092
- topItemIndex = mid;
2093
- low = mid + 1;
2094
- } else {
2095
- 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;
2096
1939
  }
2097
1940
  }
2098
1941
 
2099
- const startIndex = Math.max(0, topItemIndex - overscan);
1942
+ // Find last visible item
2100
1943
  let endIndex = startIndex;
2101
- const visibleEnd = scrollTop + clientHeight;
2102
- while (
2103
- endIndex < totalCount &&
2104
- positions[endIndex]! < visibleEnd
2105
- ) {
2106
- 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;
2107
1950
  }
2108
1951
 
2109
1952
  setRange({
2110
- startIndex,
2111
- endIndex: Math.min(totalCount, endIndex + overscan),
1953
+ startIndex: Math.max(0, startIndex),
1954
+ endIndex: Math.min(totalCount, endIndex + 1 + overscan),
2112
1955
  });
2113
-
2114
- lastUpdateAtScrollTop.current = scrollTop;
2115
1956
  };
2116
1957
 
2117
- container.addEventListener("scroll", handleUserScroll, {
1958
+ container.addEventListener("scroll", handleScroll, {
2118
1959
  passive: true,
2119
1960
  });
2120
- return () =>
2121
- container.removeEventListener("scroll", handleUserScroll);
2122
- }, [totalCount, positions, itemHeight, overscan, status]);
2123
1961
 
2124
- const scrollToBottom = useCallback(() => {
2125
- console.log(
2126
- "USER ACTION: Clicked scroll button -> SCROLLING_TO_BOTTOM"
2127
- );
2128
- 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
+ };
2129
1978
  }, []);
2130
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
+
2131
1992
  const scrollToIndex = useCallback(
2132
1993
  (index: number, behavior: ScrollBehavior = "smooth") => {
2133
1994
  if (containerRef.current && positions[index] !== undefined) {
2134
- setStatus("IDLE_NOT_AT_BOTTOM");
2135
1995
  containerRef.current.scrollTo({
2136
1996
  top: positions[index],
2137
1997
  behavior,