cogsbox-state 0.5.275 → 0.5.277
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 +639 -692
- package/dist/CogsState.jsx.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +103 -166
package/package.json
CHANGED
package/src/CogsState.tsx
CHANGED
|
@@ -37,7 +37,6 @@ import { z } from "zod";
|
|
|
37
37
|
import { formRefStore, getGlobalStore, type ComponentsType } from "./store.js";
|
|
38
38
|
import { useCogsConfig } from "./CogsStateClient.js";
|
|
39
39
|
import { applyPatch } from "fast-json-patch";
|
|
40
|
-
import useMeasure from "react-use-measure";
|
|
41
40
|
|
|
42
41
|
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
43
42
|
|
|
@@ -1471,13 +1470,7 @@ function createProxyHandler<T>(
|
|
|
1471
1470
|
function rebuildStateShape(
|
|
1472
1471
|
currentState: T,
|
|
1473
1472
|
path: string[] = [],
|
|
1474
|
-
meta?: {
|
|
1475
|
-
filtered?: string[][];
|
|
1476
|
-
validIndices?: number[];
|
|
1477
|
-
// --- NEW, OPTIONAL PROPERTIES ---
|
|
1478
|
-
isVirtual?: boolean;
|
|
1479
|
-
setVirtualItemHeight?: (index: number, height: number) => void;
|
|
1480
|
-
}
|
|
1473
|
+
meta?: { filtered?: string[][]; validIndices?: number[] }
|
|
1481
1474
|
): any {
|
|
1482
1475
|
const cacheKey = path.map(String).join(".");
|
|
1483
1476
|
|
|
@@ -1774,16 +1767,12 @@ function createProxyHandler<T>(
|
|
|
1774
1767
|
};
|
|
1775
1768
|
}
|
|
1776
1769
|
|
|
1777
|
-
// This replaces the old `useVirtualView` implementation completely.
|
|
1778
|
-
|
|
1779
1770
|
if (prop === "useVirtualView") {
|
|
1780
1771
|
return (
|
|
1781
1772
|
options: VirtualViewOptions
|
|
1782
1773
|
): VirtualStateObjectResult<any[]> => {
|
|
1783
|
-
// We rename `itemHeight` to `estimatedItemHeight` for clarity.
|
|
1784
|
-
// It's now just a fallback for items that haven't been measured yet.
|
|
1785
1774
|
const {
|
|
1786
|
-
itemHeight
|
|
1775
|
+
itemHeight,
|
|
1787
1776
|
overscan = 5,
|
|
1788
1777
|
stickToBottom = false,
|
|
1789
1778
|
} = options;
|
|
@@ -1794,48 +1783,31 @@ function createProxyHandler<T>(
|
|
|
1794
1783
|
endIndex: 10,
|
|
1795
1784
|
});
|
|
1796
1785
|
|
|
1797
|
-
// ---
|
|
1798
|
-
|
|
1799
|
-
const
|
|
1800
|
-
|
|
1801
|
-
);
|
|
1802
|
-
|
|
1803
|
-
// The "height setter" function that will be passed down.
|
|
1804
|
-
const setVirtualItemHeight = useCallback(
|
|
1805
|
-
(index: number, height: number) => {
|
|
1806
|
-
setMeasuredHeights((prev) => {
|
|
1807
|
-
if (prev.get(index) === height) return prev; // Avoids infinite loops
|
|
1808
|
-
const newMap = new Map(prev);
|
|
1809
|
-
newMap.set(index, height);
|
|
1810
|
-
return newMap;
|
|
1811
|
-
});
|
|
1812
|
-
},
|
|
1813
|
-
[]
|
|
1814
|
-
);
|
|
1786
|
+
// --- State Tracking Refs ---
|
|
1787
|
+
const isAtBottomRef = useRef(stickToBottom);
|
|
1788
|
+
const previousTotalCountRef = useRef(0);
|
|
1789
|
+
// NEW: Ref to explicitly track if this is the component's first render cycle.
|
|
1790
|
+
const isInitialMountRef = useRef(true);
|
|
1815
1791
|
|
|
1816
|
-
// --- 2. DATA AND META SETUP ---
|
|
1817
1792
|
const sourceArray = getGlobalStore().getNestedState(
|
|
1818
1793
|
stateKey,
|
|
1819
1794
|
path
|
|
1820
1795
|
) as any[];
|
|
1821
1796
|
const totalCount = sourceArray.length;
|
|
1822
1797
|
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
const
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
return
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
const isAtBottomRef = useRef(stickToBottom);
|
|
1837
|
-
const previousTotalCountRef = useRef(totalCount);
|
|
1838
|
-
const isInitialMountRef = useRef(true);
|
|
1798
|
+
const virtualState = useMemo(() => {
|
|
1799
|
+
const start = Math.max(0, range.startIndex);
|
|
1800
|
+
const end = Math.min(totalCount, range.endIndex);
|
|
1801
|
+
const validIndices = Array.from(
|
|
1802
|
+
{ length: end - start },
|
|
1803
|
+
(_, i) => start + i
|
|
1804
|
+
);
|
|
1805
|
+
const slicedArray = validIndices.map((idx) => sourceArray[idx]);
|
|
1806
|
+
return rebuildStateShape(slicedArray as any, path, {
|
|
1807
|
+
...meta,
|
|
1808
|
+
validIndices,
|
|
1809
|
+
});
|
|
1810
|
+
}, [range.startIndex, range.endIndex, sourceArray, totalCount]);
|
|
1839
1811
|
|
|
1840
1812
|
useLayoutEffect(() => {
|
|
1841
1813
|
const container = containerRef.current;
|
|
@@ -1849,41 +1821,43 @@ function createProxyHandler<T>(
|
|
|
1849
1821
|
const { scrollTop, clientHeight, scrollHeight } = container;
|
|
1850
1822
|
isAtBottomRef.current =
|
|
1851
1823
|
scrollHeight - scrollTop - clientHeight < 10;
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
setRange({
|
|
1872
|
-
startIndex: Math.max(0, startIndex - overscan),
|
|
1873
|
-
endIndex: Math.min(totalCount - 1, endIndex + overscan),
|
|
1824
|
+
const start = Math.max(
|
|
1825
|
+
0,
|
|
1826
|
+
Math.floor(scrollTop / itemHeight) - overscan
|
|
1827
|
+
);
|
|
1828
|
+
const end = Math.min(
|
|
1829
|
+
totalCount,
|
|
1830
|
+
Math.ceil((scrollTop + clientHeight) / itemHeight) +
|
|
1831
|
+
overscan
|
|
1832
|
+
);
|
|
1833
|
+
setRange((prevRange) => {
|
|
1834
|
+
if (
|
|
1835
|
+
prevRange.startIndex !== start ||
|
|
1836
|
+
prevRange.endIndex !== end
|
|
1837
|
+
) {
|
|
1838
|
+
return { startIndex: start, endIndex: end };
|
|
1839
|
+
}
|
|
1840
|
+
return prevRange;
|
|
1874
1841
|
});
|
|
1875
1842
|
};
|
|
1876
1843
|
|
|
1877
|
-
|
|
1844
|
+
container.addEventListener("scroll", handleScroll, {
|
|
1845
|
+
passive: true,
|
|
1846
|
+
});
|
|
1878
1847
|
|
|
1879
|
-
//
|
|
1848
|
+
// --- THE CORRECTED DECISION LOGIC ---
|
|
1880
1849
|
if (stickToBottom) {
|
|
1881
1850
|
if (isInitialMountRef.current) {
|
|
1851
|
+
// SCENARIO 1: First render of the component.
|
|
1852
|
+
// Go to the bottom unconditionally. Use `auto` scroll for an instant jump.
|
|
1882
1853
|
container.scrollTo({
|
|
1883
1854
|
top: container.scrollHeight,
|
|
1884
1855
|
behavior: "auto",
|
|
1885
1856
|
});
|
|
1886
1857
|
} else if (wasAtBottom && listGrew) {
|
|
1858
|
+
// SCENARIO 2: Subsequent renders (new messages arrive).
|
|
1859
|
+
// Only scroll if the user was already at the bottom.
|
|
1860
|
+
// Use `smooth` for a nice animated scroll for new messages.
|
|
1887
1861
|
requestAnimationFrame(() => {
|
|
1888
1862
|
container.scrollTo({
|
|
1889
1863
|
top: container.scrollHeight,
|
|
@@ -1892,16 +1866,17 @@ function createProxyHandler<T>(
|
|
|
1892
1866
|
});
|
|
1893
1867
|
}
|
|
1894
1868
|
}
|
|
1869
|
+
|
|
1870
|
+
// After the logic runs, it's no longer the initial mount.
|
|
1895
1871
|
isInitialMountRef.current = false;
|
|
1896
1872
|
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1873
|
+
// Always run handleScroll once to set the initial visible window.
|
|
1874
|
+
handleScroll();
|
|
1875
|
+
|
|
1900
1876
|
return () =>
|
|
1901
1877
|
container.removeEventListener("scroll", handleScroll);
|
|
1902
|
-
}, [totalCount, overscan, stickToBottom
|
|
1878
|
+
}, [totalCount, itemHeight, overscan, stickToBottom]);
|
|
1903
1879
|
|
|
1904
|
-
// --- 5. API AND PROPS ---
|
|
1905
1880
|
const scrollToBottom = useCallback(
|
|
1906
1881
|
(behavior: ScrollBehavior = "smooth") => {
|
|
1907
1882
|
if (containerRef.current) {
|
|
@@ -1916,57 +1891,38 @@ function createProxyHandler<T>(
|
|
|
1916
1891
|
|
|
1917
1892
|
const scrollToIndex = useCallback(
|
|
1918
1893
|
(index: number, behavior: ScrollBehavior = "smooth") => {
|
|
1919
|
-
if (
|
|
1920
|
-
containerRef.current &&
|
|
1921
|
-
itemPositions[index] !== undefined
|
|
1922
|
-
) {
|
|
1894
|
+
if (containerRef.current) {
|
|
1923
1895
|
containerRef.current.scrollTo({
|
|
1924
|
-
top:
|
|
1896
|
+
top: index * itemHeight,
|
|
1925
1897
|
behavior,
|
|
1926
1898
|
});
|
|
1927
1899
|
}
|
|
1928
1900
|
},
|
|
1929
|
-
[
|
|
1901
|
+
[itemHeight]
|
|
1930
1902
|
);
|
|
1931
1903
|
|
|
1904
|
+
// Same virtualizer props as before
|
|
1932
1905
|
const virtualizerProps = {
|
|
1933
1906
|
outer: {
|
|
1934
1907
|
ref: containerRef,
|
|
1935
|
-
style: { overflowY: "auto"
|
|
1908
|
+
style: { overflowY: "auto", height: "100%" },
|
|
1936
1909
|
},
|
|
1937
1910
|
inner: {
|
|
1938
1911
|
style: {
|
|
1939
|
-
height: `${
|
|
1940
|
-
position: "relative"
|
|
1912
|
+
height: `${totalCount * itemHeight}px`,
|
|
1913
|
+
position: "relative",
|
|
1941
1914
|
},
|
|
1942
1915
|
},
|
|
1943
|
-
// This is now just a slice of the full array to map over.
|
|
1944
1916
|
list: {
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
style: {},
|
|
1917
|
+
style: {
|
|
1918
|
+
transform: `translateY(${range.startIndex * itemHeight}px)`,
|
|
1919
|
+
},
|
|
1949
1920
|
},
|
|
1950
1921
|
};
|
|
1951
1922
|
|
|
1952
|
-
// We only want to render the items in the current virtual range.
|
|
1953
|
-
// We achieve this by setting the `validIndices` in the meta object for the final render pass.
|
|
1954
|
-
const finalVirtualState = useMemo(() => {
|
|
1955
|
-
const visibleIndices = [];
|
|
1956
|
-
for (let i = range.startIndex; i <= range.endIndex; i++) {
|
|
1957
|
-
if (i < totalCount) visibleIndices.push(i);
|
|
1958
|
-
}
|
|
1959
|
-
return rebuildStateShape(sourceArray as any, path, {
|
|
1960
|
-
...meta,
|
|
1961
|
-
isVirtual: true,
|
|
1962
|
-
setVirtualItemHeight,
|
|
1963
|
-
validIndices: visibleIndices,
|
|
1964
|
-
});
|
|
1965
|
-
}, [range, sourceArray, setVirtualItemHeight]);
|
|
1966
|
-
|
|
1967
1923
|
return {
|
|
1968
|
-
virtualState
|
|
1969
|
-
virtualizerProps,
|
|
1924
|
+
virtualState,
|
|
1925
|
+
virtualizerProps: virtualizerProps as any,
|
|
1970
1926
|
scrollToBottom,
|
|
1971
1927
|
scrollToIndex,
|
|
1972
1928
|
};
|
|
@@ -2018,16 +1974,12 @@ function createProxyHandler<T>(
|
|
|
2018
1974
|
};
|
|
2019
1975
|
}
|
|
2020
1976
|
|
|
2021
|
-
// This is the complete code for the `stateMap` property inside your proxy's `get` trap.
|
|
2022
|
-
|
|
2023
1977
|
if (prop === "stateMap") {
|
|
2024
1978
|
return (
|
|
2025
1979
|
callbackfn: (
|
|
2026
1980
|
value: InferArrayElement<T>,
|
|
2027
1981
|
setter: StateObject<InferArrayElement<T>>,
|
|
2028
1982
|
info: {
|
|
2029
|
-
// The `register` function is no longer needed for reactivity because
|
|
2030
|
-
// CogsItemWrapper handles it, but we keep it for API compatibility.
|
|
2031
1983
|
register: () => void;
|
|
2032
1984
|
index: number;
|
|
2033
1985
|
originalIndex: number;
|
|
@@ -2038,6 +1990,7 @@ function createProxyHandler<T>(
|
|
|
2038
1990
|
.getState()
|
|
2039
1991
|
.getNestedState(stateKey, path) as any[];
|
|
2040
1992
|
|
|
1993
|
+
// Defensive check to make sure we are mapping over an array
|
|
2041
1994
|
if (!Array.isArray(arrayToMap)) {
|
|
2042
1995
|
console.warn(
|
|
2043
1996
|
`stateMap called on a non-array value at path: ${path.join(".")}. The current value is:`,
|
|
@@ -2046,55 +1999,54 @@ function createProxyHandler<T>(
|
|
|
2046
1999
|
return null;
|
|
2047
2000
|
}
|
|
2048
2001
|
|
|
2002
|
+
// If we have validIndices, only map those items
|
|
2049
2003
|
const indicesToMap =
|
|
2050
2004
|
meta?.validIndices ||
|
|
2051
2005
|
Array.from({ length: arrayToMap.length }, (_, i) => i);
|
|
2052
2006
|
|
|
2053
2007
|
return indicesToMap.map((originalIndex, localIndex) => {
|
|
2054
2008
|
const item = arrayToMap[originalIndex];
|
|
2055
|
-
const
|
|
2056
|
-
const
|
|
2009
|
+
const finalPath = [...path, originalIndex.toString()];
|
|
2010
|
+
const setter = rebuildStateShape(item, finalPath, meta);
|
|
2011
|
+
|
|
2012
|
+
// Create the register function right here. It closes over the necessary variables.
|
|
2013
|
+
const register = () => {
|
|
2014
|
+
const [, forceUpdate] = useState({});
|
|
2015
|
+
const itemComponentId = `${componentId}-${path.join(".")}-${originalIndex}`;
|
|
2016
|
+
|
|
2017
|
+
useLayoutEffect(() => {
|
|
2018
|
+
const fullComponentId = `${stateKey}////${itemComponentId}`;
|
|
2019
|
+
const stateEntry = getGlobalStore
|
|
2020
|
+
.getState()
|
|
2021
|
+
.stateComponents.get(stateKey) || {
|
|
2022
|
+
components: new Map(),
|
|
2023
|
+
};
|
|
2024
|
+
|
|
2025
|
+
stateEntry.components.set(fullComponentId, {
|
|
2026
|
+
forceUpdate: () => forceUpdate({}),
|
|
2027
|
+
paths: new Set([finalPath.join(".")]),
|
|
2028
|
+
});
|
|
2057
2029
|
|
|
2058
|
-
|
|
2059
|
-
|
|
2030
|
+
getGlobalStore
|
|
2031
|
+
.getState()
|
|
2032
|
+
.stateComponents.set(stateKey, stateEntry);
|
|
2060
2033
|
|
|
2061
|
-
|
|
2034
|
+
return () => {
|
|
2035
|
+
const currentEntry = getGlobalStore
|
|
2036
|
+
.getState()
|
|
2037
|
+
.stateComponents.get(stateKey);
|
|
2038
|
+
if (currentEntry) {
|
|
2039
|
+
currentEntry.components.delete(fullComponentId);
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
2042
|
+
}, [stateKey, itemComponentId]);
|
|
2043
|
+
};
|
|
2062
2044
|
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
// This function is now a no-op because CogsItemWrapper handles registration.
|
|
2066
|
-
register: () => {
|
|
2067
|
-
console.warn(
|
|
2068
|
-
"The `register` function in stateMap is deprecated. Item reactivity is now automatic."
|
|
2069
|
-
);
|
|
2070
|
-
},
|
|
2045
|
+
return callbackfn(item, setter, {
|
|
2046
|
+
register,
|
|
2071
2047
|
index: localIndex,
|
|
2072
2048
|
originalIndex,
|
|
2073
2049
|
});
|
|
2074
|
-
|
|
2075
|
-
// --- This is the bridge between the virtualizer and the wrapper ---
|
|
2076
|
-
|
|
2077
|
-
// Check if we are in a virtual context by looking for the setter in the meta object.
|
|
2078
|
-
// `meta.setVirtualItemHeight` is passed down from `useVirtualView`.
|
|
2079
|
-
const onMeasure =
|
|
2080
|
-
meta?.isVirtual && meta!.setVirtualItemHeight
|
|
2081
|
-
? (height: number) =>
|
|
2082
|
-
meta!.setVirtualItemHeight!(originalIndex, height)
|
|
2083
|
-
: undefined; // If not in a virtual context, this will be undefined.
|
|
2084
|
-
|
|
2085
|
-
// --- Wrap the user's output in your enhanced CogsItemWrapper ---
|
|
2086
|
-
// This wrapper handles BOTH reactivity AND optional measurement.
|
|
2087
|
-
return (
|
|
2088
|
-
<CogsItemWrapper
|
|
2089
|
-
key={itemPathKey} // Use the unique path for React's key.
|
|
2090
|
-
stateKey={stateKey}
|
|
2091
|
-
itemComponentId={itemComponentId}
|
|
2092
|
-
itemPath={itemPath}
|
|
2093
|
-
onMeasure={onMeasure} // Pass the onMeasure function (or undefined)
|
|
2094
|
-
>
|
|
2095
|
-
{userRenderedItem}
|
|
2096
|
-
</CogsItemWrapper>
|
|
2097
|
-
);
|
|
2098
2050
|
});
|
|
2099
2051
|
};
|
|
2100
2052
|
}
|
|
@@ -2762,13 +2714,7 @@ function SignalMapRenderer({
|
|
|
2762
2714
|
rebuildStateShape: (
|
|
2763
2715
|
currentState: any,
|
|
2764
2716
|
path: string[],
|
|
2765
|
-
meta?: {
|
|
2766
|
-
filtered?: string[][];
|
|
2767
|
-
validIndices?: number[];
|
|
2768
|
-
// --- NEW, OPTIONAL PROPERTIES ---
|
|
2769
|
-
isVirtual?: boolean;
|
|
2770
|
-
setVirtualItemHeight?: (index: number, height: number) => void;
|
|
2771
|
-
}
|
|
2717
|
+
meta?: { filtered?: string[][]; validIndices?: number[] }
|
|
2772
2718
|
) => any;
|
|
2773
2719
|
}) {
|
|
2774
2720
|
const value = getGlobalStore().getNestedState(proxy._stateKey, proxy._path);
|
|
@@ -2885,13 +2831,11 @@ function CogsItemWrapper({
|
|
|
2885
2831
|
itemComponentId,
|
|
2886
2832
|
itemPath,
|
|
2887
2833
|
children,
|
|
2888
|
-
onMeasure, // OPTIONAL PROP
|
|
2889
2834
|
}: {
|
|
2890
2835
|
stateKey: string;
|
|
2891
2836
|
itemComponentId: string;
|
|
2892
2837
|
itemPath: string[];
|
|
2893
2838
|
children: React.ReactNode;
|
|
2894
|
-
onMeasure?: (height: number) => void; // The type definition
|
|
2895
2839
|
}) {
|
|
2896
2840
|
// This is a real component, so we can safely call hooks.
|
|
2897
2841
|
const [, forceUpdate] = useState({});
|
|
@@ -2923,14 +2867,7 @@ function CogsItemWrapper({
|
|
|
2923
2867
|
}
|
|
2924
2868
|
};
|
|
2925
2869
|
}, [stateKey, itemComponentId, itemPath.join(".")]); // Effect dependency array is stable.
|
|
2926
|
-
const [measureRef, bounds] = useMeasure();
|
|
2927
|
-
|
|
2928
|
-
useLayoutEffect(() => {
|
|
2929
|
-
if (!onMeasure || bounds.height === 0) return;
|
|
2930
|
-
|
|
2931
|
-
onMeasure(bounds.height);
|
|
2932
|
-
}, [bounds.height, onMeasure]);
|
|
2933
2870
|
|
|
2934
|
-
return <div ref={measureRef}>{children}</div>;
|
|
2935
2871
|
// Render the actual component the user provided.
|
|
2872
|
+
return <>{children}</>;
|
|
2936
2873
|
}
|