cogsbox-state 0.5.329 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-state",
3
- "version": "0.5.329",
3
+ "version": "0.5.331",
4
4
  "description": "React state management library with form controls and server sync",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/CogsState.tsx CHANGED
@@ -1813,15 +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
-
1820
- // This state triggers a re-render when item heights change.
1821
1816
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
1822
- const hasInitiallyLoadedRef = useRef(false);
1823
- const prevTotalCountRef = useRef(0);
1824
1817
 
1818
+ // Subscribe to shadow updates
1825
1819
  useEffect(() => {
1826
1820
  const unsubscribe = getGlobalStore
1827
1821
  .getState()
@@ -1837,20 +1831,18 @@ function createProxyHandler<T>(
1837
1831
  ) as any[];
1838
1832
  const totalCount = sourceArray.length;
1839
1833
 
1840
- // Calculate heights from shadow state. This runs when data or measurements change.
1841
- const { totalHeight, positions } = useMemo(() => {
1834
+ // Calculate total height
1835
+ const totalHeight = useMemo(() => {
1842
1836
  const shadowArray =
1843
1837
  getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
1844
1838
  [];
1845
1839
  let height = 0;
1846
- const pos: number[] = [];
1847
1840
  for (let i = 0; i < totalCount; i++) {
1848
- pos[i] = height;
1849
1841
  const measuredHeight =
1850
1842
  shadowArray[i]?.virtualizer?.itemHeight;
1851
1843
  height += measuredHeight || itemHeight;
1852
1844
  }
1853
- return { totalHeight: height, positions: pos };
1845
+ return height;
1854
1846
  }, [
1855
1847
  totalCount,
1856
1848
  stateKey,
@@ -1859,7 +1851,7 @@ function createProxyHandler<T>(
1859
1851
  shadowUpdateTrigger,
1860
1852
  ]);
1861
1853
 
1862
- // Memoize the virtualized slice of data.
1854
+ // Create virtual slice
1863
1855
  const virtualState = useMemo(() => {
1864
1856
  const start = Math.max(0, range.startIndex);
1865
1857
  const end = Math.min(totalCount, range.endIndex);
@@ -1873,135 +1865,47 @@ function createProxyHandler<T>(
1873
1865
  validIndices,
1874
1866
  });
1875
1867
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1876
- useEffect(() => {
1877
- if (stickToBottom && totalCount > 0 && containerRef.current) {
1878
- // When count increases, immediately adjust range to show bottom
1879
- const container = containerRef.current;
1880
- const visibleCount = Math.ceil(
1881
- container.clientHeight / itemHeight
1882
- );
1883
-
1884
- // Set range to show the last items including the new one
1885
- setRange({
1886
- startIndex: Math.max(
1887
- 0,
1888
- totalCount - visibleCount - overscan
1889
- ),
1890
- endIndex: totalCount,
1891
- });
1892
1868
 
1893
- // Then scroll to bottom after a short delay
1894
- setTimeout(() => {
1895
- container.scrollTop = container.scrollHeight;
1896
- }, 100);
1897
- }
1898
- }, [totalCount]);
1899
- // This is the main effect that handles all scrolling and updates.
1900
- useLayoutEffect(() => {
1869
+ // Handle scroll
1870
+ useEffect(() => {
1901
1871
  const container = containerRef.current;
1902
1872
  if (!container) return;
1903
1873
 
1904
- let scrollTimeoutId: NodeJS.Timeout;
1905
-
1906
- // This function determines what's visible in the viewport.
1907
- const updateVirtualRange = () => {
1908
- if (!container) return;
1909
- const { scrollTop } = container;
1910
- let low = 0,
1911
- high = totalCount - 1;
1912
- while (low <= high) {
1913
- const mid = Math.floor((low + high) / 2);
1914
- if (positions[mid]! < scrollTop) low = mid + 1;
1915
- else high = mid - 1;
1916
- }
1917
- const startIndex = Math.max(0, high - overscan);
1918
- let endIndex = startIndex;
1919
- const visibleEnd = scrollTop + container.clientHeight;
1920
- while (
1921
- endIndex < totalCount &&
1922
- positions[endIndex]! < visibleEnd
1923
- ) {
1924
- endIndex++;
1925
- }
1926
- endIndex = Math.min(totalCount, endIndex + overscan);
1927
- setRange({ startIndex, endIndex });
1928
- };
1874
+ const handleScroll = () => {
1875
+ const { scrollTop, clientHeight } = container;
1876
+ const startIndex = Math.floor(scrollTop / itemHeight);
1877
+ const visibleCount = Math.ceil(clientHeight / itemHeight);
1929
1878
 
1930
- // This function handles ONLY user-initiated scrolls.
1931
- const handleUserScroll = () => {
1932
- isLockedToBottomRef.current =
1933
- container.scrollHeight -
1934
- container.scrollTop -
1935
- container.clientHeight <
1936
- 1;
1937
- updateVirtualRange();
1879
+ setRange({
1880
+ startIndex: Math.max(0, startIndex - overscan),
1881
+ endIndex: Math.min(
1882
+ totalCount,
1883
+ startIndex + visibleCount + overscan
1884
+ ),
1885
+ });
1938
1886
  };
1939
1887
 
1940
- container.addEventListener("scroll", handleUserScroll, {
1941
- passive: true,
1942
- });
1888
+ container.addEventListener("scroll", handleScroll);
1889
+ handleScroll(); // Initial calculation
1943
1890
 
1944
- // In your useLayoutEffect:
1945
- if (stickToBottom) {
1946
- // Check if this is initial load or new item
1947
- const isInitialLoad =
1948
- !hasInitiallyLoadedRef.current && totalCount > 0;
1949
- const isNewItem =
1950
- hasInitiallyLoadedRef.current &&
1951
- totalCount > prevTotalCountRef.current;
1952
-
1953
- scrollTimeoutId = setTimeout(() => {
1954
- console.log("totalHeight", totalHeight);
1955
- if (isLockedToBottomRef.current) {
1956
- container.scrollTo({
1957
- top: 999999999,
1958
- behavior: isNewItem ? "smooth" : "auto", // Only smooth for NEW items after initial load
1959
- });
1960
- }
1961
- }, 200);
1891
+ return () =>
1892
+ container.removeEventListener("scroll", handleScroll);
1893
+ }, [totalCount, itemHeight, overscan]);
1962
1894
 
1963
- // Mark as initially loaded after first run
1964
- if (isInitialLoad) {
1965
- hasInitiallyLoadedRef.current = true;
1966
- }
1895
+ const scrollToBottom = useCallback(() => {
1896
+ if (containerRef.current) {
1897
+ containerRef.current.scrollTop =
1898
+ containerRef.current.scrollHeight;
1967
1899
  }
1968
-
1969
- // Update ref at the end
1970
- prevTotalCountRef.current = totalCount;
1971
- updateVirtualRange();
1972
-
1973
- // Cleanup function is vital to prevent memory leaks.
1974
- return () => {
1975
- clearTimeout(scrollTimeoutId);
1976
- container.removeEventListener("scroll", handleUserScroll);
1977
- };
1978
- // This effect re-runs whenever the list size or item heights change.
1979
- }, [totalCount, positions, totalHeight, stickToBottom]);
1980
-
1981
- const scrollToBottom = useCallback(
1982
- (behavior: ScrollBehavior = "smooth") => {
1983
- if (containerRef.current) {
1984
- isLockedToBottomRef.current = true;
1985
- containerRef.current.scrollTo({
1986
- top: containerRef.current.scrollHeight,
1987
- behavior,
1988
- });
1989
- }
1990
- },
1991
- []
1992
- );
1900
+ }, []);
1993
1901
 
1994
1902
  const scrollToIndex = useCallback(
1995
- (index: number, behavior: ScrollBehavior = "smooth") => {
1996
- if (containerRef.current && positions[index] !== undefined) {
1997
- isLockedToBottomRef.current = false;
1998
- containerRef.current.scrollTo({
1999
- top: positions[index],
2000
- behavior,
2001
- });
1903
+ (index: number) => {
1904
+ if (containerRef.current) {
1905
+ containerRef.current.scrollTop = index * itemHeight;
2002
1906
  }
2003
1907
  },
2004
- [positions]
1908
+ [itemHeight]
2005
1909
  );
2006
1910
 
2007
1911
  const virtualizerProps = {
@@ -2017,7 +1921,10 @@ function createProxyHandler<T>(
2017
1921
  },
2018
1922
  list: {
2019
1923
  style: {
2020
- transform: `translateY(${positions[range.startIndex] || 0}px)`,
1924
+ position: "absolute" as const,
1925
+ top: `${range.startIndex * itemHeight}px`,
1926
+ left: 0,
1927
+ right: 0,
2021
1928
  },
2022
1929
  },
2023
1930
  };
@@ -2229,21 +2136,24 @@ function createProxyHandler<T>(
2229
2136
  return null;
2230
2137
  }
2231
2138
 
2139
+ const arrayLength = arrayToMap.length;
2232
2140
  const indicesToMap =
2233
2141
  meta?.validIndices ||
2234
- Array.from({ length: arrayToMap.length }, (_, i) => i);
2142
+ Array.from({ length: arrayLength }, (_, i) => i);
2235
2143
 
2236
2144
  return indicesToMap.map((originalIndex, localIndex) => {
2237
2145
  const item = arrayToMap[originalIndex];
2238
2146
  const finalPath = [...path, originalIndex.toString()];
2239
2147
  const setter = rebuildStateShape(item, finalPath, meta);
2240
2148
  const itemComponentId = `${componentId}-${path.join(".")}-${originalIndex}`;
2149
+ const isLastItem = originalIndex === arrayLength - 1;
2241
2150
 
2242
2151
  return createElement(CogsItemWrapper, {
2243
2152
  key: originalIndex,
2244
2153
  stateKey,
2245
2154
  itemComponentId,
2246
2155
  itemPath: finalPath,
2156
+ isLastItem, // Pass it here!
2247
2157
  children: callbackfn(
2248
2158
  item,
2249
2159
  setter,
@@ -2973,21 +2883,25 @@ export function $cogsSignalStore(proxy: {
2973
2883
  );
2974
2884
  return createElement("text", {}, String(value));
2975
2885
  }
2886
+
2976
2887
  function CogsItemWrapper({
2977
2888
  stateKey,
2978
2889
  itemComponentId,
2979
2890
  itemPath,
2891
+ isLastItem,
2980
2892
  children,
2981
2893
  }: {
2982
2894
  stateKey: string;
2983
2895
  itemComponentId: string;
2984
2896
  itemPath: string[];
2897
+ isLastItem: boolean;
2985
2898
  children: React.ReactNode;
2986
2899
  }) {
2987
2900
  // This hook handles the re-rendering when the item's own data changes.
2988
2901
  const [, forceUpdate] = useState({});
2989
2902
  // This hook measures the element.
2990
- const [ref, bounds] = useMeasure();
2903
+ const [measureRef, bounds] = useMeasure();
2904
+ const scrollRef = useRef<HTMLDivElement | null>(null);
2991
2905
  // This ref prevents sending the same height update repeatedly.
2992
2906
  const lastReportedHeight = useRef<number | null>(null);
2993
2907
 
@@ -3033,7 +2947,25 @@ function CogsItemWrapper({
3033
2947
  }
3034
2948
  };
3035
2949
  }, [stateKey, itemComponentId, itemPath.join(".")]);
3036
-
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]);
3037
2960
  // The rendered output is a simple div that gets measured.
3038
- return <div ref={ref}>{children}</div>;
2961
+ return (
2962
+ <div
2963
+ ref={(el) => {
2964
+ measureRef(el);
2965
+ scrollRef.current = el;
2966
+ }}
2967
+ >
2968
+ {children}
2969
+ </div>
2970
+ );
3039
2971
  }