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/dist/CogsState.jsx +672 -735
- package/dist/CogsState.jsx.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +88 -235
package/package.json
CHANGED
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
|
|
1832
|
-
const
|
|
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
|
-
|
|
1838
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
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
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
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
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
1918
|
+
const handleScroll = () => {
|
|
1919
|
+
// Clear existing timeout
|
|
1920
|
+
clearTimeout(scrollTimeout);
|
|
2062
1921
|
|
|
2063
|
-
|
|
2064
|
-
|
|
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
|
-
//
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
) {
|
|
2085
|
-
return;
|
|
2086
|
-
}
|
|
1925
|
+
// Reset after scrolling stops
|
|
1926
|
+
scrollTimeout = setTimeout(() => {
|
|
1927
|
+
isUserScrolling.current = false;
|
|
1928
|
+
}, 150);
|
|
2087
1929
|
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
);
|
|
1930
|
+
// Update visible range
|
|
1931
|
+
const { scrollTop, clientHeight } = container;
|
|
2091
1932
|
|
|
2092
|
-
//
|
|
2093
|
-
let
|
|
2094
|
-
let
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
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
|
-
|
|
1942
|
+
// Find last visible item
|
|
2107
1943
|
let endIndex = startIndex;
|
|
2108
|
-
const
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
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",
|
|
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
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
)
|
|
2135
|
-
|
|
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,
|