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/dist/CogsState.jsx +678 -737
- package/dist/CogsState.jsx.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +88 -228
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,249 +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
|
-
});
|
|
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
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
.
|
|
1984
|
-
|
|
1985
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1916
|
+
let scrollTimeout: NodeJS.Timeout;
|
|
2043
1917
|
|
|
2044
|
-
const
|
|
2045
|
-
//
|
|
2046
|
-
|
|
2047
|
-
return;
|
|
2048
|
-
}
|
|
1918
|
+
const handleScroll = () => {
|
|
1919
|
+
// Clear existing timeout
|
|
1920
|
+
clearTimeout(scrollTimeout);
|
|
2049
1921
|
|
|
2050
|
-
|
|
1922
|
+
// Mark as user scrolling
|
|
1923
|
+
isUserScrolling.current = true;
|
|
2051
1924
|
|
|
2052
|
-
//
|
|
2053
|
-
|
|
2054
|
-
|
|
1925
|
+
// Reset after scrolling stops
|
|
1926
|
+
scrollTimeout = setTimeout(() => {
|
|
1927
|
+
isUserScrolling.current = false;
|
|
1928
|
+
}, 150);
|
|
2055
1929
|
|
|
2056
|
-
|
|
2057
|
-
|
|
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
|
-
//
|
|
2086
|
-
let
|
|
2087
|
-
let
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
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
|
-
|
|
1942
|
+
// Find last visible item
|
|
2100
1943
|
let endIndex = startIndex;
|
|
2101
|
-
const
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
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",
|
|
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
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
)
|
|
2128
|
-
|
|
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,
|