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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-state",
3
- "version": "0.5.330",
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,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 heights from shadow state. This runs when data or measurements change.
1839
- const { totalHeight, positions } = useMemo(() => {
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 { totalHeight: height, positions: pos };
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
- // Memoize the virtualized slice of data.
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
- // Then scroll to bottom after a short delay
1892
- setTimeout(() => {
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
- let scrollTimeoutId: NodeJS.Timeout;
1903
-
1904
- // This function determines what's visible in the viewport.
1905
- const updateVirtualRange = () => {
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
- // This function handles ONLY user-initiated scrolls.
1929
- const handleUserScroll = () => {
1930
- isLockedToBottomRef.current =
1931
- container.scrollHeight -
1932
- container.scrollTop -
1933
- container.clientHeight <
1934
- 1;
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", handleUserScroll, {
1939
- passive: true,
1940
- });
1888
+ container.addEventListener("scroll", handleScroll);
1889
+ handleScroll(); // Initial calculation
1941
1890
 
1942
- if (stickToBottom && isLockedToBottomRef.current) {
1943
- const currentScrollHeight = container.scrollHeight;
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
- lastScrollPositionRef.current = currentScrollHeight;
1961
- }
1895
+ const scrollToBottom = useCallback(() => {
1896
+ if (containerRef.current) {
1897
+ containerRef.current.scrollTop =
1898
+ containerRef.current.scrollHeight;
1962
1899
  }
1963
- updateVirtualRange();
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, behavior: ScrollBehavior = "smooth") => {
1987
- if (containerRef.current && positions[index] !== undefined) {
1988
- isLockedToBottomRef.current = false;
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
- [positions]
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
- transform: `translateY(${positions[range.startIndex] || 0}px)`,
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: arrayToMap.length }, (_, i) => i);
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 [ref, bounds] = useMeasure();
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 <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
+ );
3030
2971
  }