cogsbox-state 0.5.402 → 0.5.404
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 +611 -663
- package/dist/CogsState.jsx.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +106 -223
package/package.json
CHANGED
package/src/CogsState.tsx
CHANGED
|
@@ -1803,6 +1803,8 @@ function createProxyHandler<T>(
|
|
|
1803
1803
|
return selectedIndex ?? -1;
|
|
1804
1804
|
};
|
|
1805
1805
|
}
|
|
1806
|
+
// Simplified useVirtualView approach
|
|
1807
|
+
// Optimal approach - replace the useVirtualView implementation
|
|
1806
1808
|
if (prop === "useVirtualView") {
|
|
1807
1809
|
return (
|
|
1808
1810
|
options: VirtualViewOptions
|
|
@@ -1814,30 +1816,19 @@ function createProxyHandler<T>(
|
|
|
1814
1816
|
dependencies = [],
|
|
1815
1817
|
} = options;
|
|
1816
1818
|
|
|
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
1819
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
1827
1820
|
const [range, setRange] = useState({
|
|
1828
1821
|
startIndex: 0,
|
|
1829
1822
|
endIndex: 10,
|
|
1830
1823
|
});
|
|
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);
|
|
1836
1824
|
const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1825
|
+
const isUserScrollingRef = useRef(false);
|
|
1826
|
+
const shouldStickToBottomRef = useRef(true);
|
|
1827
|
+
const scrollToBottomIntervalRef = useRef<NodeJS.Timeout | null>(
|
|
1828
|
+
null
|
|
1829
|
+
);
|
|
1830
|
+
|
|
1831
|
+
// Subscribe to shadow state updates
|
|
1841
1832
|
useEffect(() => {
|
|
1842
1833
|
const unsubscribe = getGlobalStore
|
|
1843
1834
|
.getState()
|
|
@@ -1853,6 +1844,7 @@ function createProxyHandler<T>(
|
|
|
1853
1844
|
) as any[];
|
|
1854
1845
|
const totalCount = sourceArray.length;
|
|
1855
1846
|
|
|
1847
|
+
// Calculate heights and positions
|
|
1856
1848
|
const { totalHeight, positions } = useMemo(() => {
|
|
1857
1849
|
const shadowArray =
|
|
1858
1850
|
getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
|
|
@@ -1874,7 +1866,7 @@ function createProxyHandler<T>(
|
|
|
1874
1866
|
shadowUpdateTrigger,
|
|
1875
1867
|
]);
|
|
1876
1868
|
|
|
1877
|
-
//
|
|
1869
|
+
// Create virtual state
|
|
1878
1870
|
const virtualState = useMemo(() => {
|
|
1879
1871
|
const start = Math.max(0, range.startIndex);
|
|
1880
1872
|
const end = Math.min(totalCount, range.endIndex);
|
|
@@ -1889,256 +1881,147 @@ function createProxyHandler<T>(
|
|
|
1889
1881
|
});
|
|
1890
1882
|
}, [range.startIndex, range.endIndex, sourceArray, totalCount]);
|
|
1891
1883
|
|
|
1892
|
-
//
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
if (!
|
|
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
|
-
}
|
|
1884
|
+
// Handle auto-scroll to bottom
|
|
1885
|
+
useEffect(() => {
|
|
1886
|
+
if (!stickToBottom || !containerRef.current || totalCount === 0)
|
|
1887
|
+
return;
|
|
1888
|
+
if (!shouldStickToBottomRef.current) return;
|
|
1935
1889
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
stickToBottom
|
|
1940
|
-
) {
|
|
1941
|
-
console.log(
|
|
1942
|
-
"TRANSITION: New items arrived while locked -> GETTING_HEIGHTS"
|
|
1943
|
-
);
|
|
1944
|
-
setStatus("GETTING_HEIGHTS");
|
|
1945
|
-
}
|
|
1890
|
+
// Clear any existing interval
|
|
1891
|
+
if (scrollToBottomIntervalRef.current) {
|
|
1892
|
+
clearInterval(scrollToBottomIntervalRef.current);
|
|
1946
1893
|
}
|
|
1947
1894
|
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
// --- 2. STATE ACTION HANDLER ---
|
|
1953
|
-
// This effect performs the ACTION for the current state.
|
|
1954
|
-
useLayoutEffect(() => {
|
|
1955
|
-
const container = containerRef.current;
|
|
1956
|
-
if (!container) return;
|
|
1957
|
-
|
|
1958
|
-
let intervalId: NodeJS.Timeout | undefined;
|
|
1895
|
+
// For initial load or big jumps, show the end immediately
|
|
1896
|
+
const jumpThreshold = 50;
|
|
1897
|
+
const isInitialLoad = range.endIndex < jumpThreshold;
|
|
1898
|
+
const isBigJump = totalCount > range.endIndex + jumpThreshold;
|
|
1959
1899
|
|
|
1960
|
-
if (
|
|
1961
|
-
|
|
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
|
-
);
|
|
1900
|
+
if (isInitialLoad || isBigJump) {
|
|
1901
|
+
// Jump to show the last items immediately
|
|
1974
1902
|
setRange({
|
|
1975
|
-
startIndex: Math.max(0, totalCount -
|
|
1903
|
+
startIndex: Math.max(0, totalCount - 20),
|
|
1976
1904
|
endIndex: totalCount,
|
|
1977
1905
|
});
|
|
1906
|
+
}
|
|
1978
1907
|
|
|
1979
|
-
|
|
1980
|
-
|
|
1908
|
+
// Keep scrolling to bottom until we're actually there
|
|
1909
|
+
let attempts = 0;
|
|
1910
|
+
const maxAttempts = 50; // 5 seconds max
|
|
1981
1911
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
const shadowArray =
|
|
1986
|
-
getGlobalStore
|
|
1987
|
-
.getState()
|
|
1988
|
-
.getShadowMetadata(stateKey, path) || [];
|
|
1989
|
-
const lastItemHeight =
|
|
1990
|
-
shadowArray[lastItemIndex]?.virtualizer?.itemHeight || 0;
|
|
1912
|
+
scrollToBottomIntervalRef.current = setInterval(() => {
|
|
1913
|
+
const container = containerRef.current;
|
|
1914
|
+
if (!container) return;
|
|
1991
1915
|
|
|
1992
|
-
|
|
1993
|
-
`ACTION (GETTING_HEIGHTS): attempt ${attemptCount}, lastItemHeight =`,
|
|
1994
|
-
lastItemHeight
|
|
1995
|
-
);
|
|
1916
|
+
attempts++;
|
|
1996
1917
|
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
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");
|
|
2009
|
-
}
|
|
2010
|
-
}, 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
|
-
});
|
|
1918
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
1919
|
+
const currentBottom = scrollTop + clientHeight;
|
|
1920
|
+
const actualBottom = scrollHeight;
|
|
1921
|
+
const isAtBottom = actualBottom - currentBottom < 5;
|
|
2024
1922
|
|
|
2025
|
-
|
|
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
|
|
1923
|
+
console.log(
|
|
1924
|
+
`Scroll attempt ${attempts}: currentBottom=${currentBottom}, actualBottom=${actualBottom}, isAtBottom=${isAtBottom}`
|
|
2035
1925
|
);
|
|
2036
1926
|
|
|
2037
|
-
|
|
2038
|
-
|
|
1927
|
+
if (isAtBottom || attempts >= maxAttempts) {
|
|
1928
|
+
clearInterval(scrollToBottomIntervalRef.current!);
|
|
1929
|
+
scrollToBottomIntervalRef.current = null;
|
|
1930
|
+
console.log(
|
|
1931
|
+
isAtBottom ? "Reached bottom!" : "Timeout - giving up"
|
|
1932
|
+
);
|
|
1933
|
+
} else {
|
|
1934
|
+
// Use instant scroll, not smooth
|
|
1935
|
+
container.scrollTop = container.scrollHeight;
|
|
1936
|
+
}
|
|
1937
|
+
}, 100);
|
|
2039
1938
|
|
|
1939
|
+
// Cleanup
|
|
2040
1940
|
return () => {
|
|
2041
|
-
if (
|
|
1941
|
+
if (scrollToBottomIntervalRef.current) {
|
|
1942
|
+
clearInterval(scrollToBottomIntervalRef.current);
|
|
1943
|
+
scrollToBottomIntervalRef.current = null;
|
|
1944
|
+
}
|
|
2042
1945
|
};
|
|
2043
|
-
}, [
|
|
1946
|
+
}, [totalCount, stickToBottom]);
|
|
2044
1947
|
|
|
1948
|
+
// Handle user scroll
|
|
2045
1949
|
useEffect(() => {
|
|
2046
1950
|
const container = containerRef.current;
|
|
2047
1951
|
if (!container) return;
|
|
2048
1952
|
|
|
2049
|
-
|
|
1953
|
+
let scrollTimeout: NodeJS.Timeout;
|
|
2050
1954
|
|
|
2051
|
-
const
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
1955
|
+
const handleScroll = () => {
|
|
1956
|
+
if (scrollToBottomIntervalRef.current) {
|
|
1957
|
+
// Stop auto-scrolling if user scrolls
|
|
1958
|
+
clearInterval(scrollToBottomIntervalRef.current);
|
|
1959
|
+
scrollToBottomIntervalRef.current = null;
|
|
2055
1960
|
}
|
|
2056
1961
|
|
|
2057
1962
|
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
2058
|
-
|
|
2059
|
-
// Part 1: Handle Status and Anchoring
|
|
2060
1963
|
const isAtBottom =
|
|
2061
1964
|
scrollHeight - scrollTop - clientHeight < 10;
|
|
2062
1965
|
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
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
|
-
}
|
|
2087
|
-
|
|
2088
|
-
console.log(
|
|
2089
|
-
`Threshold passed at ${scrollTop}px. Recalculating range...`
|
|
2090
|
-
);
|
|
2091
|
-
|
|
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;
|
|
1966
|
+
// Update whether we should stick to bottom
|
|
1967
|
+
shouldStickToBottomRef.current = isAtBottom;
|
|
1968
|
+
|
|
1969
|
+
// Mark as user scrolling
|
|
1970
|
+
clearTimeout(scrollTimeout);
|
|
1971
|
+
isUserScrollingRef.current = true;
|
|
1972
|
+
scrollTimeout = setTimeout(() => {
|
|
1973
|
+
isUserScrollingRef.current = false;
|
|
1974
|
+
}, 150);
|
|
1975
|
+
|
|
1976
|
+
// Update visible range
|
|
1977
|
+
let startIndex = 0;
|
|
1978
|
+
for (let i = 0; i < positions.length; i++) {
|
|
1979
|
+
if (positions[i]! > scrollTop - itemHeight * overscan) {
|
|
1980
|
+
startIndex = Math.max(0, i - 1);
|
|
1981
|
+
break;
|
|
2103
1982
|
}
|
|
2104
1983
|
}
|
|
2105
1984
|
|
|
2106
|
-
const startIndex = Math.max(0, topItemIndex - overscan);
|
|
2107
1985
|
let endIndex = startIndex;
|
|
2108
|
-
const
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
endIndex
|
|
1986
|
+
const viewportEnd = scrollTop + clientHeight;
|
|
1987
|
+
for (let i = startIndex; i < positions.length; i++) {
|
|
1988
|
+
if (positions[i]! > viewportEnd + itemHeight * overscan) {
|
|
1989
|
+
break;
|
|
1990
|
+
}
|
|
1991
|
+
endIndex = i;
|
|
2114
1992
|
}
|
|
2115
1993
|
|
|
2116
1994
|
setRange({
|
|
2117
|
-
startIndex,
|
|
2118
|
-
endIndex: Math.min(totalCount, endIndex + overscan),
|
|
1995
|
+
startIndex: Math.max(0, startIndex),
|
|
1996
|
+
endIndex: Math.min(totalCount, endIndex + 1 + overscan),
|
|
2119
1997
|
});
|
|
2120
|
-
|
|
2121
|
-
lastUpdateAtScrollTop.current = scrollTop;
|
|
2122
1998
|
};
|
|
2123
1999
|
|
|
2124
|
-
container.addEventListener("scroll",
|
|
2000
|
+
container.addEventListener("scroll", handleScroll, {
|
|
2125
2001
|
passive: true,
|
|
2126
2002
|
});
|
|
2127
|
-
|
|
2128
|
-
container.removeEventListener("scroll", handleUserScroll);
|
|
2129
|
-
}, [totalCount, positions, itemHeight, overscan, status]);
|
|
2003
|
+
handleScroll(); // Initial calculation
|
|
2130
2004
|
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2005
|
+
return () => {
|
|
2006
|
+
container.removeEventListener("scroll", handleScroll);
|
|
2007
|
+
clearTimeout(scrollTimeout);
|
|
2008
|
+
};
|
|
2009
|
+
}, [positions, totalCount, itemHeight, overscan]);
|
|
2010
|
+
|
|
2011
|
+
const scrollToBottom = useCallback(
|
|
2012
|
+
(behavior: ScrollBehavior = "auto") => {
|
|
2013
|
+
shouldStickToBottomRef.current = true;
|
|
2014
|
+
if (containerRef.current) {
|
|
2015
|
+
containerRef.current.scrollTop =
|
|
2016
|
+
containerRef.current.scrollHeight;
|
|
2017
|
+
}
|
|
2018
|
+
},
|
|
2019
|
+
[]
|
|
2020
|
+
);
|
|
2137
2021
|
|
|
2138
2022
|
const scrollToIndex = useCallback(
|
|
2139
2023
|
(index: number, behavior: ScrollBehavior = "smooth") => {
|
|
2140
2024
|
if (containerRef.current && positions[index] !== undefined) {
|
|
2141
|
-
setStatus("IDLE_NOT_AT_BOTTOM");
|
|
2142
2025
|
containerRef.current.scrollTo({
|
|
2143
2026
|
top: positions[index],
|
|
2144
2027
|
behavior,
|