cogsbox-state 0.5.381 → 0.5.383
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 +644 -640
- package/dist/CogsState.jsx.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +95 -55
package/package.json
CHANGED
package/src/CogsState.tsx
CHANGED
|
@@ -1817,7 +1817,7 @@ function createProxyHandler<T>(
|
|
|
1817
1817
|
| "LOCKED_AT_BOTTOM"
|
|
1818
1818
|
| "IDLE_NOT_AT_BOTTOM";
|
|
1819
1819
|
|
|
1820
|
-
|
|
1820
|
+
// Refs and State
|
|
1821
1821
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
1822
1822
|
const [range, setRange] = useState({
|
|
1823
1823
|
startIndex: 0,
|
|
@@ -1827,9 +1827,15 @@ function createProxyHandler<T>(
|
|
|
1827
1827
|
const isProgrammaticScroll = useRef(false);
|
|
1828
1828
|
const prevTotalCountRef = useRef(0);
|
|
1829
1829
|
const prevDepsRef = useRef(dependencies);
|
|
1830
|
-
const lastUpdateAtScrollTop = useRef(0);
|
|
1831
1830
|
const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
|
|
1832
|
-
|
|
1831
|
+
// CHANGE: Add a ref to store the scroll position BEFORE new items are added.
|
|
1832
|
+
// This is the key to preventing the jump when scrolled up.
|
|
1833
|
+
const scrollAnchorRef = useRef<{
|
|
1834
|
+
scrollTop: number;
|
|
1835
|
+
scrollHeight: number;
|
|
1836
|
+
} | null>(null);
|
|
1837
|
+
|
|
1838
|
+
// ... (Your existing useEffect for shadow state and useMemo for data are fine) ...
|
|
1833
1839
|
useEffect(() => {
|
|
1834
1840
|
const unsubscribe = getGlobalStore
|
|
1835
1841
|
.getState()
|
|
@@ -1866,7 +1872,6 @@ function createProxyHandler<T>(
|
|
|
1866
1872
|
shadowUpdateTrigger,
|
|
1867
1873
|
]);
|
|
1868
1874
|
|
|
1869
|
-
// THIS IS THE FULL, NON-PLACEHOLDER FUNCTION
|
|
1870
1875
|
const virtualState = useMemo(() => {
|
|
1871
1876
|
const start = Math.max(0, range.startIndex);
|
|
1872
1877
|
const end = Math.min(totalCount, range.endIndex);
|
|
@@ -1881,22 +1886,28 @@ function createProxyHandler<T>(
|
|
|
1881
1886
|
});
|
|
1882
1887
|
}, [range.startIndex, range.endIndex, sourceArray, totalCount]);
|
|
1883
1888
|
|
|
1884
|
-
// --- 1. STATE CONTROLLER ---
|
|
1885
|
-
// This effect decides which state to transition TO.
|
|
1889
|
+
// --- 1. STATE CONTROLLER (REVISED LOGIC) ---
|
|
1886
1890
|
useLayoutEffect(() => {
|
|
1891
|
+
const container = containerRef.current;
|
|
1892
|
+
if (!container) return;
|
|
1893
|
+
|
|
1894
|
+
const hasNewItems = totalCount > prevTotalCountRef.current;
|
|
1887
1895
|
const depsChanged = !isDeepEqual(
|
|
1888
1896
|
dependencies,
|
|
1889
1897
|
prevDepsRef.current
|
|
1890
1898
|
);
|
|
1891
|
-
const hasNewItems = totalCount > prevTotalCountRef.current;
|
|
1892
1899
|
|
|
1900
|
+
// Condition 1: Hard Reset.
|
|
1901
|
+
// This happens when you load a completely new list (e.g., switch chats).
|
|
1893
1902
|
if (depsChanged) {
|
|
1894
|
-
console.log(
|
|
1903
|
+
console.log(
|
|
1904
|
+
"TRANSITION (Hard Reset): Deps changed -> IDLE_AT_TOP"
|
|
1905
|
+
);
|
|
1895
1906
|
setStatus("IDLE_AT_TOP");
|
|
1896
|
-
|
|
1907
|
+
container.scrollTo({ top: 0 }); // Reset scroll position
|
|
1897
1908
|
}
|
|
1898
|
-
|
|
1899
|
-
if (
|
|
1909
|
+
// Condition 2: New items arrive while we're locked to the bottom.
|
|
1910
|
+
else if (
|
|
1900
1911
|
hasNewItems &&
|
|
1901
1912
|
status === "LOCKED_AT_BOTTOM" &&
|
|
1902
1913
|
stickToBottom
|
|
@@ -1906,14 +1917,29 @@ function createProxyHandler<T>(
|
|
|
1906
1917
|
);
|
|
1907
1918
|
setStatus("GETTING_HEIGHTS");
|
|
1908
1919
|
}
|
|
1920
|
+
// CHANGE: Condition 3: New items arrive while we are scrolled up.
|
|
1921
|
+
// This is the "scroll anchoring" logic that prevents the jump.
|
|
1922
|
+
else if (hasNewItems && scrollAnchorRef.current) {
|
|
1923
|
+
console.log(
|
|
1924
|
+
"ACTION: Maintaining scroll position after new items added."
|
|
1925
|
+
);
|
|
1926
|
+
// We adjust the scroll position by the amount of height that was just added.
|
|
1927
|
+
container.scrollTop =
|
|
1928
|
+
scrollAnchorRef.current.scrollTop +
|
|
1929
|
+
(container.scrollHeight -
|
|
1930
|
+
scrollAnchorRef.current.scrollHeight);
|
|
1931
|
+
}
|
|
1909
1932
|
|
|
1933
|
+
// Finally, update the refs for the next render.
|
|
1910
1934
|
prevTotalCountRef.current = totalCount;
|
|
1911
1935
|
prevDepsRef.current = dependencies;
|
|
1912
|
-
|
|
1936
|
+
// Clear the anchor after using it. It will be re-set by the user scroll handler.
|
|
1937
|
+
scrollAnchorRef.current = null;
|
|
1938
|
+
}, [totalCount, ...dependencies]); // This dependency array is correct.
|
|
1913
1939
|
|
|
1914
|
-
// --- 2. STATE ACTION HANDLER ---
|
|
1915
|
-
// This effect performs the ACTION for the current state.
|
|
1940
|
+
// --- 2. STATE ACTION HANDLER (Mostly Unchanged) ---
|
|
1916
1941
|
useLayoutEffect(() => {
|
|
1942
|
+
// ... (This effect's logic for GETTING_HEIGHTS and SCROLLING_TO_BOTTOM is correct and can remain the same)
|
|
1917
1943
|
const container = containerRef.current;
|
|
1918
1944
|
if (!container) return;
|
|
1919
1945
|
|
|
@@ -1924,15 +1950,12 @@ function createProxyHandler<T>(
|
|
|
1924
1950
|
stickToBottom &&
|
|
1925
1951
|
totalCount > 0
|
|
1926
1952
|
) {
|
|
1927
|
-
// If we just loaded a new chat, start the process.
|
|
1928
1953
|
console.log(
|
|
1929
|
-
"ACTION (IDLE_AT_TOP): Data
|
|
1954
|
+
"ACTION (IDLE_AT_TOP): Data arrived -> GETTING_HEIGHTS"
|
|
1930
1955
|
);
|
|
1931
1956
|
setStatus("GETTING_HEIGHTS");
|
|
1932
1957
|
} else if (status === "GETTING_HEIGHTS") {
|
|
1933
|
-
console.log(
|
|
1934
|
-
"ACTION (GETTING_HEIGHTS): Setting range to end and starting loop."
|
|
1935
|
-
);
|
|
1958
|
+
console.log("ACTION (GETTING_HEIGHTS): Setting range to end");
|
|
1936
1959
|
setRange({
|
|
1937
1960
|
startIndex: Math.max(0, totalCount - 10 - overscan),
|
|
1938
1961
|
endIndex: totalCount,
|
|
@@ -1949,13 +1972,10 @@ function createProxyHandler<T>(
|
|
|
1949
1972
|
|
|
1950
1973
|
if (lastItemHeight > 0) {
|
|
1951
1974
|
clearInterval(intervalId);
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
setStatus("SCROLLING_TO_BOTTOM");
|
|
1958
|
-
}
|
|
1975
|
+
console.log(
|
|
1976
|
+
"ACTION (GETTING_HEIGHTS): Measurement success -> SCROLLING_TO_BOTTOM"
|
|
1977
|
+
);
|
|
1978
|
+
setStatus("SCROLLING_TO_BOTTOM");
|
|
1959
1979
|
}
|
|
1960
1980
|
}, 100);
|
|
1961
1981
|
} else if (status === "SCROLLING_TO_BOTTOM") {
|
|
@@ -1963,7 +1983,6 @@ function createProxyHandler<T>(
|
|
|
1963
1983
|
"ACTION (SCROLLING_TO_BOTTOM): Executing scroll."
|
|
1964
1984
|
);
|
|
1965
1985
|
isProgrammaticScroll.current = true;
|
|
1966
|
-
// Use 'auto' for initial load, 'smooth' for new messages.
|
|
1967
1986
|
const scrollBehavior =
|
|
1968
1987
|
prevTotalCountRef.current === 0 ? "auto" : "smooth";
|
|
1969
1988
|
|
|
@@ -1974,16 +1993,11 @@ function createProxyHandler<T>(
|
|
|
1974
1993
|
|
|
1975
1994
|
const timeoutId = setTimeout(
|
|
1976
1995
|
() => {
|
|
1977
|
-
console.log(
|
|
1978
|
-
"ACTION (SCROLLING_TO_BOTTOM): Scroll finished -> LOCKED_AT_BOTTOM"
|
|
1979
|
-
);
|
|
1980
1996
|
isProgrammaticScroll.current = false;
|
|
1981
|
-
shouldNotScroll.current = false;
|
|
1982
1997
|
setStatus("LOCKED_AT_BOTTOM");
|
|
1983
1998
|
},
|
|
1984
1999
|
scrollBehavior === "smooth" ? 500 : 50
|
|
1985
2000
|
);
|
|
1986
|
-
|
|
1987
2001
|
return () => clearTimeout(timeoutId);
|
|
1988
2002
|
}
|
|
1989
2003
|
|
|
@@ -1992,46 +2006,70 @@ function createProxyHandler<T>(
|
|
|
1992
2006
|
};
|
|
1993
2007
|
}, [status, totalCount, positions]);
|
|
1994
2008
|
|
|
2009
|
+
// --- 3. USER INTERACTION & RANGE UPDATER (REVISED) ---
|
|
1995
2010
|
useEffect(() => {
|
|
1996
2011
|
const container = containerRef.current;
|
|
1997
2012
|
if (!container) return;
|
|
1998
2013
|
|
|
1999
|
-
|
|
2014
|
+
// CHANGE: Add a buffer to prevent jittering at the bottom.
|
|
2015
|
+
const bottomLockThreshold = 10; // 10px buffer
|
|
2000
2016
|
|
|
2001
2017
|
const handleUserScroll = () => {
|
|
2002
2018
|
if (isProgrammaticScroll.current) {
|
|
2003
2019
|
return;
|
|
2004
2020
|
}
|
|
2005
2021
|
|
|
2006
|
-
const scrollTop = container
|
|
2007
|
-
if (
|
|
2008
|
-
Math.abs(scrollTop - lastUpdateAtScrollTop.current) <
|
|
2009
|
-
scrollThreshold
|
|
2010
|
-
) {
|
|
2011
|
-
return;
|
|
2012
|
-
}
|
|
2022
|
+
const { scrollTop, clientHeight, scrollHeight } = container;
|
|
2013
2023
|
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2024
|
+
// CHANGE: Before the next state update, save the current scroll positions.
|
|
2025
|
+
// This 'anchors' our view, so the State Controller knows where we were.
|
|
2026
|
+
scrollAnchorRef.current = { scrollTop, scrollHeight };
|
|
2027
|
+
|
|
2028
|
+
// Part 1: Update the state machine with a tolerance
|
|
2029
|
+
const isAtBottom =
|
|
2030
|
+
scrollHeight - scrollTop - clientHeight <
|
|
2031
|
+
bottomLockThreshold;
|
|
2017
2032
|
|
|
2018
|
-
|
|
2019
|
-
|
|
2033
|
+
if (isAtBottom) {
|
|
2034
|
+
if (status !== "LOCKED_AT_BOTTOM") {
|
|
2035
|
+
console.log(
|
|
2036
|
+
"SCROLL EVENT: Reached bottom -> LOCKED_AT_BOTTOM"
|
|
2037
|
+
);
|
|
2038
|
+
setStatus("LOCKED_AT_BOTTOM");
|
|
2039
|
+
}
|
|
2040
|
+
} else {
|
|
2041
|
+
if (status !== "IDLE_NOT_AT_BOTTOM") {
|
|
2042
|
+
console.log(
|
|
2043
|
+
"SCROLL EVENT: Scrolled up -> IDLE_NOT_AT_BOTTOM"
|
|
2044
|
+
);
|
|
2045
|
+
setStatus("IDLE_NOT_AT_BOTTOM");
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// Part 2: Efficiently update the rendered range (this logic is good)
|
|
2020
2050
|
let high = totalCount - 1;
|
|
2021
2051
|
let low = 0;
|
|
2022
|
-
let
|
|
2052
|
+
let potentialTopIndex = 0;
|
|
2023
2053
|
while (low <= high) {
|
|
2024
2054
|
const mid = Math.floor((low + high) / 2);
|
|
2025
2055
|
if (positions[mid]! < scrollTop) {
|
|
2026
|
-
|
|
2056
|
+
potentialTopIndex = mid;
|
|
2027
2057
|
low = mid + 1;
|
|
2028
2058
|
} else {
|
|
2029
2059
|
high = mid - 1;
|
|
2030
2060
|
}
|
|
2031
2061
|
}
|
|
2032
2062
|
|
|
2033
|
-
const
|
|
2034
|
-
|
|
2063
|
+
const potentialStartIndex = Math.max(
|
|
2064
|
+
0,
|
|
2065
|
+
potentialTopIndex - overscan
|
|
2066
|
+
);
|
|
2067
|
+
|
|
2068
|
+
if (potentialStartIndex === range.startIndex) {
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
let endIndex = potentialStartIndex;
|
|
2035
2073
|
const visibleEnd = scrollTop + clientHeight;
|
|
2036
2074
|
while (
|
|
2037
2075
|
endIndex < totalCount &&
|
|
@@ -2040,13 +2078,13 @@ function createProxyHandler<T>(
|
|
|
2040
2078
|
endIndex++;
|
|
2041
2079
|
}
|
|
2042
2080
|
|
|
2081
|
+
console.log(
|
|
2082
|
+
`Index changed from ${range.startIndex} to ${potentialStartIndex}. Updating range.`
|
|
2083
|
+
);
|
|
2043
2084
|
setRange({
|
|
2044
|
-
startIndex,
|
|
2085
|
+
startIndex: potentialStartIndex,
|
|
2045
2086
|
endIndex: Math.min(totalCount, endIndex + overscan),
|
|
2046
2087
|
});
|
|
2047
|
-
|
|
2048
|
-
// Finally, we record that we did the work at THIS scroll position.
|
|
2049
|
-
lastUpdateAtScrollTop.current = scrollTop;
|
|
2050
2088
|
};
|
|
2051
2089
|
|
|
2052
2090
|
container.addEventListener("scroll", handleUserScroll, {
|
|
@@ -2054,14 +2092,16 @@ function createProxyHandler<T>(
|
|
|
2054
2092
|
});
|
|
2055
2093
|
return () =>
|
|
2056
2094
|
container.removeEventListener("scroll", handleUserScroll);
|
|
2057
|
-
}, [totalCount, positions,
|
|
2095
|
+
}, [totalCount, positions, status, range.startIndex]);
|
|
2058
2096
|
|
|
2097
|
+
// --- (The rest of your code: scrollToBottom, scrollToIndex, virtualizerProps is fine) ---
|
|
2059
2098
|
const scrollToBottom = useCallback(() => {
|
|
2060
2099
|
console.log(
|
|
2061
2100
|
"USER ACTION: Clicked scroll button -> SCROLLING_TO_BOTTOM"
|
|
2062
2101
|
);
|
|
2102
|
+
if (totalCount === 0) return;
|
|
2063
2103
|
setStatus("SCROLLING_TO_BOTTOM");
|
|
2064
|
-
}, []);
|
|
2104
|
+
}, [totalCount]);
|
|
2065
2105
|
|
|
2066
2106
|
const scrollToIndex = useCallback(
|
|
2067
2107
|
(index: number, behavior: ScrollBehavior = "smooth") => {
|