cogsbox-state 0.5.366 → 0.5.368
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 +692 -712
- package/dist/CogsState.jsx.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +72 -125
package/package.json
CHANGED
package/src/CogsState.tsx
CHANGED
|
@@ -1810,18 +1810,15 @@ function createProxyHandler<T>(
|
|
|
1810
1810
|
} = options;
|
|
1811
1811
|
|
|
1812
1812
|
type Status =
|
|
1813
|
-
| "
|
|
1814
|
-
| "
|
|
1815
|
-
| "MOVING_TO_BOTTOM"
|
|
1816
|
-
| "LOCKED_AT_BOTTOM"
|
|
1817
|
-
| "IDLE_NOT_AT_BOTTOM";
|
|
1813
|
+
| "IDLE" // User is in control, or we are resting at the bottom.
|
|
1814
|
+
| "SCROLLING_TO_BOTTOM"; // Performing a scroll animation.
|
|
1818
1815
|
|
|
1819
1816
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
1820
1817
|
const [range, setRange] = useState({
|
|
1821
1818
|
startIndex: 0,
|
|
1822
1819
|
endIndex: 10,
|
|
1823
1820
|
});
|
|
1824
|
-
const [status, setStatus] = useState<Status>("
|
|
1821
|
+
const [status, setStatus] = useState<Status>("IDLE");
|
|
1825
1822
|
const prevTotalCountRef = useRef(0);
|
|
1826
1823
|
const prevDepsRef = useRef(dependencies);
|
|
1827
1824
|
|
|
@@ -1877,141 +1874,82 @@ function createProxyHandler<T>(
|
|
|
1877
1874
|
});
|
|
1878
1875
|
}, [range.startIndex, range.endIndex, sourceArray, totalCount]);
|
|
1879
1876
|
|
|
1880
|
-
// ---
|
|
1881
|
-
// This effect decides which state to transition to based on external changes.
|
|
1877
|
+
// --- Main Controller Effect ---
|
|
1882
1878
|
useLayoutEffect(() => {
|
|
1879
|
+
const container = containerRef.current;
|
|
1880
|
+
if (!container) return;
|
|
1881
|
+
|
|
1883
1882
|
const depsChanged = !isDeepEqual(
|
|
1884
1883
|
dependencies,
|
|
1885
1884
|
prevDepsRef.current
|
|
1886
1885
|
);
|
|
1887
1886
|
const hasNewItems = totalCount > prevTotalCountRef.current;
|
|
1888
1887
|
|
|
1888
|
+
// 1. On Chat Change: Reset to a clean state and trigger an instant scroll.
|
|
1889
1889
|
if (depsChanged) {
|
|
1890
1890
|
console.log(
|
|
1891
|
-
"
|
|
1891
|
+
"EVENT: Chat changed. Resetting state and scrolling."
|
|
1892
1892
|
);
|
|
1893
|
-
setStatus("
|
|
1893
|
+
setStatus("SCROLLING_TO_BOTTOM");
|
|
1894
|
+
// This return is important. It lets the effect re-run with the new status.
|
|
1894
1895
|
return;
|
|
1895
1896
|
}
|
|
1896
1897
|
|
|
1897
|
-
if
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1898
|
+
// 2. On New Message: Check if we SHOULD scroll.
|
|
1899
|
+
// This will only happen if we are IDLE (meaning we are already at the bottom).
|
|
1900
|
+
if (hasNewItems && status === "IDLE") {
|
|
1901
|
+
const isAtBottom =
|
|
1902
|
+
container.scrollHeight -
|
|
1903
|
+
container.scrollTop -
|
|
1904
|
+
container.clientHeight <
|
|
1905
|
+
itemHeight;
|
|
1906
|
+
if (isAtBottom && stickToBottom) {
|
|
1907
|
+
console.log(
|
|
1908
|
+
"EVENT: New message arrived while at bottom. -> SCROLLING_TO_BOTTOM"
|
|
1909
|
+
);
|
|
1910
|
+
setStatus("SCROLLING_TO_BOTTOM");
|
|
1911
|
+
return; // Let the effect re-run with the new status.
|
|
1912
|
+
}
|
|
1906
1913
|
}
|
|
1907
1914
|
|
|
1908
|
-
prevTotalCountRef.current = totalCount;
|
|
1909
|
-
prevDepsRef.current = dependencies;
|
|
1910
|
-
}, [totalCount, ...dependencies]);
|
|
1911
|
-
|
|
1912
|
-
// --- STATE MACHINE ACTIONS ---
|
|
1913
|
-
// This effect handles the actions for each state.
|
|
1914
|
-
useLayoutEffect(() => {
|
|
1915
|
-
const container = containerRef.current;
|
|
1916
|
-
if (!container) return;
|
|
1917
|
-
|
|
1918
|
-
let intervalId: NodeJS.Timeout | undefined;
|
|
1919
1915
|
let scrollTimeoutId: NodeJS.Timeout | undefined;
|
|
1920
1916
|
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1917
|
+
// 3. If we are in the SCROLLING state, perform the scroll action.
|
|
1918
|
+
if (status === "SCROLLING_TO_BOTTOM") {
|
|
1919
|
+
// A chat change or initial load should be instant. New messages should be smooth.
|
|
1920
|
+
const isInitial =
|
|
1921
|
+
prevTotalCountRef.current === 0 || depsChanged;
|
|
1922
|
+
const scrollBehavior = isInitial ? "auto" : "smooth";
|
|
1926
1923
|
console.log(
|
|
1927
|
-
|
|
1924
|
+
`ACTION: Scrolling to bottom. Behavior: ${scrollBehavior}`
|
|
1928
1925
|
);
|
|
1929
|
-
setStatus("GETTING_ARRAY_HEIGHTS");
|
|
1930
|
-
} else if (status === "GETTING_ARRAY_HEIGHTS") {
|
|
1931
|
-
console.log(
|
|
1932
|
-
"ACTION: GETTING_ARRAY_HEIGHTS. Setting range and starting loop."
|
|
1933
|
-
);
|
|
1934
|
-
setRange({
|
|
1935
|
-
startIndex: Math.max(0, totalCount - 10 - overscan),
|
|
1936
|
-
endIndex: totalCount,
|
|
1937
|
-
});
|
|
1938
|
-
|
|
1939
|
-
intervalId = setInterval(() => {
|
|
1940
|
-
const lastItemIndex = totalCount - 1;
|
|
1941
|
-
const shadowArray =
|
|
1942
|
-
getGlobalStore
|
|
1943
|
-
.getState()
|
|
1944
|
-
.getShadowMetadata(stateKey, path) || [];
|
|
1945
|
-
const lastItemHeight =
|
|
1946
|
-
shadowArray[lastItemIndex]?.virtualizer?.itemHeight || 0;
|
|
1947
|
-
|
|
1948
|
-
if (lastItemHeight > 0) {
|
|
1949
|
-
clearInterval(intervalId);
|
|
1950
|
-
console.log(
|
|
1951
|
-
"ACTION: Measurement success. -> MOVING_TO_BOTTOM"
|
|
1952
|
-
);
|
|
1953
|
-
setStatus("MOVING_TO_BOTTOM");
|
|
1954
|
-
}
|
|
1955
|
-
}, 100);
|
|
1956
|
-
} else if (status === "MOVING_TO_BOTTOM") {
|
|
1957
|
-
console.log("ACTION: MOVING_TO_BOTTOM. Executing scroll.");
|
|
1958
|
-
const scrollBehavior =
|
|
1959
|
-
prevTotalCountRef.current === 0 ? "auto" : "smooth";
|
|
1960
1926
|
|
|
1961
1927
|
container.scrollTo({
|
|
1962
1928
|
top: container.scrollHeight,
|
|
1963
1929
|
behavior: scrollBehavior,
|
|
1964
1930
|
});
|
|
1965
1931
|
|
|
1932
|
+
// After the scroll, transition back to IDLE.
|
|
1933
|
+
// The timeout lets the animation finish so this doesn't happen too early.
|
|
1966
1934
|
scrollTimeoutId = setTimeout(
|
|
1967
1935
|
() => {
|
|
1968
|
-
console.log(
|
|
1969
|
-
|
|
1970
|
-
);
|
|
1971
|
-
setStatus("LOCKED_AT_BOTTOM");
|
|
1936
|
+
console.log("ACTION: Scroll finished. -> IDLE");
|
|
1937
|
+
setStatus("IDLE");
|
|
1972
1938
|
},
|
|
1973
|
-
|
|
1939
|
+
isInitial ? 50 : 500
|
|
1974
1940
|
);
|
|
1975
1941
|
}
|
|
1976
1942
|
|
|
1977
|
-
//
|
|
1978
|
-
return () => {
|
|
1979
|
-
if (intervalId) {
|
|
1980
|
-
console.log("CLEANUP: Clearing measurement loop timer.");
|
|
1981
|
-
clearInterval(intervalId);
|
|
1982
|
-
}
|
|
1983
|
-
if (scrollTimeoutId) {
|
|
1984
|
-
console.log("CLEANUP: Clearing scroll-end timer.");
|
|
1985
|
-
clearTimeout(scrollTimeoutId);
|
|
1986
|
-
}
|
|
1987
|
-
};
|
|
1988
|
-
}, [status, totalCount, positions]);
|
|
1989
|
-
|
|
1990
|
-
// --- USER INTERACTION ---
|
|
1991
|
-
// This effect only handles user scrolls.
|
|
1992
|
-
useEffect(() => {
|
|
1993
|
-
const container = containerRef.current;
|
|
1994
|
-
if (!container) return;
|
|
1995
|
-
|
|
1943
|
+
// --- User Scroll Handling & Range Update ---
|
|
1996
1944
|
const handleUserScroll = () => {
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
// If the user scrolls up from a locked or scrolling state, move to idle.
|
|
2003
|
-
if (
|
|
2004
|
-
!isAtBottom &&
|
|
2005
|
-
(status === "LOCKED_AT_BOTTOM" ||
|
|
2006
|
-
status === "MOVING_TO_BOTTOM")
|
|
2007
|
-
) {
|
|
2008
|
-
console.log(
|
|
2009
|
-
"USER_ACTION: Scrolled up. -> IDLE_NOT_AT_BOTTOM"
|
|
2010
|
-
);
|
|
2011
|
-
setStatus("IDLE_NOT_AT_BOTTOM");
|
|
1945
|
+
// If the user scrolls, they are in control. Immediately go to IDLE.
|
|
1946
|
+
// This will also cancel any pending scroll-end timeouts.
|
|
1947
|
+
if (status === "SCROLLING_TO_BOTTOM") {
|
|
1948
|
+
console.log("USER ACTION: Interrupted scroll. -> IDLE");
|
|
1949
|
+
setStatus("IDLE");
|
|
2012
1950
|
}
|
|
2013
1951
|
|
|
2014
|
-
//
|
|
1952
|
+
// This is the full range update function.
|
|
2015
1953
|
const { scrollTop, clientHeight } = container;
|
|
2016
1954
|
let low = 0,
|
|
2017
1955
|
high = totalCount - 1;
|
|
@@ -2038,29 +1976,38 @@ function createProxyHandler<T>(
|
|
|
2038
1976
|
container.addEventListener("scroll", handleUserScroll, {
|
|
2039
1977
|
passive: true,
|
|
2040
1978
|
});
|
|
2041
|
-
|
|
1979
|
+
|
|
1980
|
+
// Always update range on render if we are IDLE.
|
|
1981
|
+
if (status === "IDLE") {
|
|
1982
|
+
handleUserScroll();
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// Update refs for the next render cycle.
|
|
1986
|
+
prevTotalCountRef.current = totalCount;
|
|
1987
|
+
prevDepsRef.current = dependencies;
|
|
1988
|
+
|
|
1989
|
+
return () => {
|
|
2042
1990
|
container.removeEventListener("scroll", handleUserScroll);
|
|
2043
|
-
|
|
1991
|
+
if (scrollTimeoutId) clearTimeout(scrollTimeoutId);
|
|
1992
|
+
};
|
|
1993
|
+
}, [totalCount, positions, status, ...dependencies]);
|
|
2044
1994
|
|
|
2045
|
-
const scrollToBottom = useCallback(
|
|
2046
|
-
(
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
},
|
|
2052
|
-
[]
|
|
2053
|
-
);
|
|
1995
|
+
const scrollToBottom = useCallback(() => {
|
|
1996
|
+
console.log(
|
|
1997
|
+
"USER ACTION: Clicked scroll button -> SCROLLING_TO_BOTTOM"
|
|
1998
|
+
);
|
|
1999
|
+
setStatus("SCROLLING_TO_BOTTOM");
|
|
2000
|
+
}, []);
|
|
2054
2001
|
|
|
2055
2002
|
const scrollToIndex = useCallback(
|
|
2056
2003
|
(index: number, behavior: ScrollBehavior = "smooth") => {
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2004
|
+
if (containerRef.current && positions[index] !== undefined) {
|
|
2005
|
+
setStatus("IDLE");
|
|
2006
|
+
containerRef.current.scrollTo({
|
|
2007
|
+
top: positions[index],
|
|
2008
|
+
behavior,
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2064
2011
|
},
|
|
2065
2012
|
[positions]
|
|
2066
2013
|
);
|