cogsbox-state 0.5.330 → 0.5.331
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 +711 -735
- package/dist/CogsState.jsx.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +65 -124
package/package.json
CHANGED
package/src/CogsState.tsx
CHANGED
|
@@ -1813,13 +1813,9 @@ function createProxyHandler<T>(
|
|
|
1813
1813
|
startIndex: 0,
|
|
1814
1814
|
endIndex: 10,
|
|
1815
1815
|
});
|
|
1816
|
-
|
|
1817
|
-
// This ref tracks if the user is locked to the bottom.
|
|
1818
|
-
const isLockedToBottomRef = useRef(stickToBottom);
|
|
1819
|
-
const lastScrollPositionRef = useRef(0);
|
|
1820
|
-
// This state triggers a re-render when item heights change.
|
|
1821
1816
|
const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
|
|
1822
1817
|
|
|
1818
|
+
// Subscribe to shadow updates
|
|
1823
1819
|
useEffect(() => {
|
|
1824
1820
|
const unsubscribe = getGlobalStore
|
|
1825
1821
|
.getState()
|
|
@@ -1835,20 +1831,18 @@ function createProxyHandler<T>(
|
|
|
1835
1831
|
) as any[];
|
|
1836
1832
|
const totalCount = sourceArray.length;
|
|
1837
1833
|
|
|
1838
|
-
// Calculate
|
|
1839
|
-
const
|
|
1834
|
+
// Calculate total height
|
|
1835
|
+
const totalHeight = useMemo(() => {
|
|
1840
1836
|
const shadowArray =
|
|
1841
1837
|
getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
|
|
1842
1838
|
[];
|
|
1843
1839
|
let height = 0;
|
|
1844
|
-
const pos: number[] = [];
|
|
1845
1840
|
for (let i = 0; i < totalCount; i++) {
|
|
1846
|
-
pos[i] = height;
|
|
1847
1841
|
const measuredHeight =
|
|
1848
1842
|
shadowArray[i]?.virtualizer?.itemHeight;
|
|
1849
1843
|
height += measuredHeight || itemHeight;
|
|
1850
1844
|
}
|
|
1851
|
-
return
|
|
1845
|
+
return height;
|
|
1852
1846
|
}, [
|
|
1853
1847
|
totalCount,
|
|
1854
1848
|
stateKey,
|
|
@@ -1857,7 +1851,7 @@ function createProxyHandler<T>(
|
|
|
1857
1851
|
shadowUpdateTrigger,
|
|
1858
1852
|
]);
|
|
1859
1853
|
|
|
1860
|
-
//
|
|
1854
|
+
// Create virtual slice
|
|
1861
1855
|
const virtualState = useMemo(() => {
|
|
1862
1856
|
const start = Math.max(0, range.startIndex);
|
|
1863
1857
|
const end = Math.min(totalCount, range.endIndex);
|
|
@@ -1871,128 +1865,47 @@ function createProxyHandler<T>(
|
|
|
1871
1865
|
validIndices,
|
|
1872
1866
|
});
|
|
1873
1867
|
}, [range.startIndex, range.endIndex, sourceArray, totalCount]);
|
|
1874
|
-
useEffect(() => {
|
|
1875
|
-
if (stickToBottom && totalCount > 0 && containerRef.current) {
|
|
1876
|
-
// When count increases, immediately adjust range to show bottom
|
|
1877
|
-
const container = containerRef.current;
|
|
1878
|
-
const visibleCount = Math.ceil(
|
|
1879
|
-
container.clientHeight / itemHeight
|
|
1880
|
-
);
|
|
1881
|
-
|
|
1882
|
-
// Set range to show the last items including the new one
|
|
1883
|
-
setRange({
|
|
1884
|
-
startIndex: Math.max(
|
|
1885
|
-
0,
|
|
1886
|
-
totalCount - visibleCount - overscan
|
|
1887
|
-
),
|
|
1888
|
-
endIndex: totalCount,
|
|
1889
|
-
});
|
|
1890
1868
|
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
container.scrollTop = container.scrollHeight;
|
|
1894
|
-
}, 100);
|
|
1895
|
-
}
|
|
1896
|
-
}, [totalCount]);
|
|
1897
|
-
// This is the main effect that handles all scrolling and updates.
|
|
1898
|
-
useLayoutEffect(() => {
|
|
1869
|
+
// Handle scroll
|
|
1870
|
+
useEffect(() => {
|
|
1899
1871
|
const container = containerRef.current;
|
|
1900
1872
|
if (!container) return;
|
|
1901
1873
|
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
if (!container) return;
|
|
1907
|
-
const { scrollTop } = container;
|
|
1908
|
-
let low = 0,
|
|
1909
|
-
high = totalCount - 1;
|
|
1910
|
-
while (low <= high) {
|
|
1911
|
-
const mid = Math.floor((low + high) / 2);
|
|
1912
|
-
if (positions[mid]! < scrollTop) low = mid + 1;
|
|
1913
|
-
else high = mid - 1;
|
|
1914
|
-
}
|
|
1915
|
-
const startIndex = Math.max(0, high - overscan);
|
|
1916
|
-
let endIndex = startIndex;
|
|
1917
|
-
const visibleEnd = scrollTop + container.clientHeight;
|
|
1918
|
-
while (
|
|
1919
|
-
endIndex < totalCount &&
|
|
1920
|
-
positions[endIndex]! < visibleEnd
|
|
1921
|
-
) {
|
|
1922
|
-
endIndex++;
|
|
1923
|
-
}
|
|
1924
|
-
endIndex = Math.min(totalCount, endIndex + overscan);
|
|
1925
|
-
setRange({ startIndex, endIndex });
|
|
1926
|
-
};
|
|
1874
|
+
const handleScroll = () => {
|
|
1875
|
+
const { scrollTop, clientHeight } = container;
|
|
1876
|
+
const startIndex = Math.floor(scrollTop / itemHeight);
|
|
1877
|
+
const visibleCount = Math.ceil(clientHeight / itemHeight);
|
|
1927
1878
|
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
updateVirtualRange();
|
|
1879
|
+
setRange({
|
|
1880
|
+
startIndex: Math.max(0, startIndex - overscan),
|
|
1881
|
+
endIndex: Math.min(
|
|
1882
|
+
totalCount,
|
|
1883
|
+
startIndex + visibleCount + overscan
|
|
1884
|
+
),
|
|
1885
|
+
});
|
|
1936
1886
|
};
|
|
1937
1887
|
|
|
1938
|
-
container.addEventListener("scroll",
|
|
1939
|
-
|
|
1940
|
-
});
|
|
1888
|
+
container.addEventListener("scroll", handleScroll);
|
|
1889
|
+
handleScroll(); // Initial calculation
|
|
1941
1890
|
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
// Only scroll if the height actually changed (new content rendered)
|
|
1946
|
-
if (currentScrollHeight > lastScrollPositionRef.current) {
|
|
1947
|
-
// Check if we're far from bottom (initial loads or large batches)
|
|
1948
|
-
const distanceFromBottom =
|
|
1949
|
-
currentScrollHeight -
|
|
1950
|
-
container.scrollTop -
|
|
1951
|
-
container.clientHeight;
|
|
1952
|
-
const isLargeGap =
|
|
1953
|
-
distanceFromBottom > container.clientHeight * 2;
|
|
1954
|
-
|
|
1955
|
-
container.scrollTo({
|
|
1956
|
-
top: 999999999,
|
|
1957
|
-
behavior: isLargeGap ? "auto" : "smooth", // Instant for big jumps, smooth for small
|
|
1958
|
-
});
|
|
1891
|
+
return () =>
|
|
1892
|
+
container.removeEventListener("scroll", handleScroll);
|
|
1893
|
+
}, [totalCount, itemHeight, overscan]);
|
|
1959
1894
|
|
|
1960
|
-
|
|
1961
|
-
|
|
1895
|
+
const scrollToBottom = useCallback(() => {
|
|
1896
|
+
if (containerRef.current) {
|
|
1897
|
+
containerRef.current.scrollTop =
|
|
1898
|
+
containerRef.current.scrollHeight;
|
|
1962
1899
|
}
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
// Cleanup function is vital to prevent memory leaks.
|
|
1966
|
-
return () => {
|
|
1967
|
-
container.removeEventListener("scroll", handleUserScroll);
|
|
1968
|
-
};
|
|
1969
|
-
// This effect re-runs whenever the list size or item heights change.
|
|
1970
|
-
}, [totalCount, positions, totalHeight, stickToBottom]);
|
|
1971
|
-
|
|
1972
|
-
const scrollToBottom = useCallback(
|
|
1973
|
-
(behavior: ScrollBehavior = "smooth") => {
|
|
1974
|
-
if (containerRef.current) {
|
|
1975
|
-
isLockedToBottomRef.current = true;
|
|
1976
|
-
containerRef.current.scrollTo({
|
|
1977
|
-
top: containerRef.current.scrollHeight,
|
|
1978
|
-
behavior,
|
|
1979
|
-
});
|
|
1980
|
-
}
|
|
1981
|
-
},
|
|
1982
|
-
[]
|
|
1983
|
-
);
|
|
1900
|
+
}, []);
|
|
1984
1901
|
|
|
1985
1902
|
const scrollToIndex = useCallback(
|
|
1986
|
-
(index: number
|
|
1987
|
-
if (containerRef.current
|
|
1988
|
-
|
|
1989
|
-
containerRef.current.scrollTo({
|
|
1990
|
-
top: positions[index],
|
|
1991
|
-
behavior,
|
|
1992
|
-
});
|
|
1903
|
+
(index: number) => {
|
|
1904
|
+
if (containerRef.current) {
|
|
1905
|
+
containerRef.current.scrollTop = index * itemHeight;
|
|
1993
1906
|
}
|
|
1994
1907
|
},
|
|
1995
|
-
[
|
|
1908
|
+
[itemHeight]
|
|
1996
1909
|
);
|
|
1997
1910
|
|
|
1998
1911
|
const virtualizerProps = {
|
|
@@ -2008,7 +1921,10 @@ function createProxyHandler<T>(
|
|
|
2008
1921
|
},
|
|
2009
1922
|
list: {
|
|
2010
1923
|
style: {
|
|
2011
|
-
|
|
1924
|
+
position: "absolute" as const,
|
|
1925
|
+
top: `${range.startIndex * itemHeight}px`,
|
|
1926
|
+
left: 0,
|
|
1927
|
+
right: 0,
|
|
2012
1928
|
},
|
|
2013
1929
|
},
|
|
2014
1930
|
};
|
|
@@ -2220,21 +2136,24 @@ function createProxyHandler<T>(
|
|
|
2220
2136
|
return null;
|
|
2221
2137
|
}
|
|
2222
2138
|
|
|
2139
|
+
const arrayLength = arrayToMap.length;
|
|
2223
2140
|
const indicesToMap =
|
|
2224
2141
|
meta?.validIndices ||
|
|
2225
|
-
Array.from({ length:
|
|
2142
|
+
Array.from({ length: arrayLength }, (_, i) => i);
|
|
2226
2143
|
|
|
2227
2144
|
return indicesToMap.map((originalIndex, localIndex) => {
|
|
2228
2145
|
const item = arrayToMap[originalIndex];
|
|
2229
2146
|
const finalPath = [...path, originalIndex.toString()];
|
|
2230
2147
|
const setter = rebuildStateShape(item, finalPath, meta);
|
|
2231
2148
|
const itemComponentId = `${componentId}-${path.join(".")}-${originalIndex}`;
|
|
2149
|
+
const isLastItem = originalIndex === arrayLength - 1;
|
|
2232
2150
|
|
|
2233
2151
|
return createElement(CogsItemWrapper, {
|
|
2234
2152
|
key: originalIndex,
|
|
2235
2153
|
stateKey,
|
|
2236
2154
|
itemComponentId,
|
|
2237
2155
|
itemPath: finalPath,
|
|
2156
|
+
isLastItem, // Pass it here!
|
|
2238
2157
|
children: callbackfn(
|
|
2239
2158
|
item,
|
|
2240
2159
|
setter,
|
|
@@ -2964,21 +2883,25 @@ export function $cogsSignalStore(proxy: {
|
|
|
2964
2883
|
);
|
|
2965
2884
|
return createElement("text", {}, String(value));
|
|
2966
2885
|
}
|
|
2886
|
+
|
|
2967
2887
|
function CogsItemWrapper({
|
|
2968
2888
|
stateKey,
|
|
2969
2889
|
itemComponentId,
|
|
2970
2890
|
itemPath,
|
|
2891
|
+
isLastItem,
|
|
2971
2892
|
children,
|
|
2972
2893
|
}: {
|
|
2973
2894
|
stateKey: string;
|
|
2974
2895
|
itemComponentId: string;
|
|
2975
2896
|
itemPath: string[];
|
|
2897
|
+
isLastItem: boolean;
|
|
2976
2898
|
children: React.ReactNode;
|
|
2977
2899
|
}) {
|
|
2978
2900
|
// This hook handles the re-rendering when the item's own data changes.
|
|
2979
2901
|
const [, forceUpdate] = useState({});
|
|
2980
2902
|
// This hook measures the element.
|
|
2981
|
-
const [
|
|
2903
|
+
const [measureRef, bounds] = useMeasure();
|
|
2904
|
+
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
2982
2905
|
// This ref prevents sending the same height update repeatedly.
|
|
2983
2906
|
const lastReportedHeight = useRef<number | null>(null);
|
|
2984
2907
|
|
|
@@ -3024,7 +2947,25 @@ function CogsItemWrapper({
|
|
|
3024
2947
|
}
|
|
3025
2948
|
};
|
|
3026
2949
|
}, [stateKey, itemComponentId, itemPath.join(".")]);
|
|
3027
|
-
|
|
2950
|
+
useEffect(() => {
|
|
2951
|
+
if (isLastItem && scrollRef.current) {
|
|
2952
|
+
setTimeout(() => {
|
|
2953
|
+
scrollRef.current?.scrollIntoView({
|
|
2954
|
+
behavior: "smooth",
|
|
2955
|
+
block: "end",
|
|
2956
|
+
});
|
|
2957
|
+
}, 50);
|
|
2958
|
+
}
|
|
2959
|
+
}, [isLastItem]);
|
|
3028
2960
|
// The rendered output is a simple div that gets measured.
|
|
3029
|
-
return
|
|
2961
|
+
return (
|
|
2962
|
+
<div
|
|
2963
|
+
ref={(el) => {
|
|
2964
|
+
measureRef(el);
|
|
2965
|
+
scrollRef.current = el;
|
|
2966
|
+
}}
|
|
2967
|
+
>
|
|
2968
|
+
{children}
|
|
2969
|
+
</div>
|
|
2970
|
+
);
|
|
3030
2971
|
}
|