cogsbox-state 0.5.331 → 0.5.333

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.331",
3
+ "version": "0.5.333",
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,9 +1813,11 @@ function createProxyHandler<T>(
1813
1813
  startIndex: 0,
1814
1814
  endIndex: 10,
1815
1815
  });
1816
+
1817
+ const isLockedToBottomRef = useRef(false); // Always start false
1816
1818
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
1819
+ const lastTotalCountRef = useRef(0); // Track previous count
1817
1820
 
1818
- // Subscribe to shadow updates
1819
1821
  useEffect(() => {
1820
1822
  const unsubscribe = getGlobalStore
1821
1823
  .getState()
@@ -1831,18 +1833,19 @@ function createProxyHandler<T>(
1831
1833
  ) as any[];
1832
1834
  const totalCount = sourceArray.length;
1833
1835
 
1834
- // Calculate total height
1835
- const totalHeight = useMemo(() => {
1836
+ const { totalHeight, positions } = useMemo(() => {
1836
1837
  const shadowArray =
1837
1838
  getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
1838
1839
  [];
1839
1840
  let height = 0;
1841
+ const pos: number[] = [];
1840
1842
  for (let i = 0; i < totalCount; i++) {
1843
+ pos[i] = height;
1841
1844
  const measuredHeight =
1842
1845
  shadowArray[i]?.virtualizer?.itemHeight;
1843
1846
  height += measuredHeight || itemHeight;
1844
1847
  }
1845
- return height;
1848
+ return { totalHeight: height, positions: pos };
1846
1849
  }, [
1847
1850
  totalCount,
1848
1851
  stateKey,
@@ -1851,7 +1854,6 @@ function createProxyHandler<T>(
1851
1854
  shadowUpdateTrigger,
1852
1855
  ]);
1853
1856
 
1854
- // Create virtual slice
1855
1857
  const virtualState = useMemo(() => {
1856
1858
  const start = Math.max(0, range.startIndex);
1857
1859
  const end = Math.min(totalCount, range.endIndex);
@@ -1866,46 +1868,102 @@ function createProxyHandler<T>(
1866
1868
  });
1867
1869
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1868
1870
 
1869
- // Handle scroll
1870
- useEffect(() => {
1871
+ useLayoutEffect(() => {
1871
1872
  const container = containerRef.current;
1872
1873
  if (!container) return;
1873
1874
 
1874
- const handleScroll = () => {
1875
- const { scrollTop, clientHeight } = container;
1876
- const startIndex = Math.floor(scrollTop / itemHeight);
1877
- const visibleCount = Math.ceil(clientHeight / itemHeight);
1878
-
1879
- setRange({
1880
- startIndex: Math.max(0, startIndex - overscan),
1881
- endIndex: Math.min(
1882
- totalCount,
1883
- startIndex + visibleCount + overscan
1884
- ),
1885
- });
1875
+ const updateVirtualRange = () => {
1876
+ if (!container) return;
1877
+ const { scrollTop } = container;
1878
+ let low = 0,
1879
+ high = totalCount - 1;
1880
+ while (low <= high) {
1881
+ const mid = Math.floor((low + high) / 2);
1882
+ if (positions[mid]! < scrollTop) low = mid + 1;
1883
+ else high = mid - 1;
1884
+ }
1885
+ const startIndex = Math.max(0, high - overscan);
1886
+ let endIndex = startIndex;
1887
+ const visibleEnd = scrollTop + container.clientHeight;
1888
+ while (
1889
+ endIndex < totalCount &&
1890
+ positions[endIndex]! < visibleEnd
1891
+ ) {
1892
+ endIndex++;
1893
+ }
1894
+ endIndex = Math.min(totalCount, endIndex + overscan);
1895
+ setRange({ startIndex, endIndex });
1886
1896
  };
1887
1897
 
1888
- container.addEventListener("scroll", handleScroll);
1889
- handleScroll(); // Initial calculation
1898
+ const handleUserScroll = () => {
1899
+ isLockedToBottomRef.current =
1900
+ container.scrollHeight -
1901
+ container.scrollTop -
1902
+ container.clientHeight <
1903
+ 1;
1904
+ updateVirtualRange();
1905
+ };
1890
1906
 
1891
- return () =>
1892
- container.removeEventListener("scroll", handleScroll);
1893
- }, [totalCount, itemHeight, overscan]);
1907
+ container.addEventListener("scroll", handleUserScroll, {
1908
+ passive: true,
1909
+ });
1894
1910
 
1895
- const scrollToBottom = useCallback(() => {
1896
- if (containerRef.current) {
1897
- containerRef.current.scrollTop =
1898
- containerRef.current.scrollHeight;
1911
+ // Handle scrolling
1912
+ if (stickToBottom && totalCount > 0) {
1913
+ const isInitialLoad =
1914
+ lastTotalCountRef.current === 0 && totalCount > 0;
1915
+ const hasNewItems = totalCount > lastTotalCountRef.current;
1916
+
1917
+ if (isInitialLoad) {
1918
+ // First load - always scroll to bottom
1919
+ setTimeout(() => {
1920
+ container.scrollTop = container.scrollHeight;
1921
+ isLockedToBottomRef.current = true;
1922
+ }, 1000); // Longer delay for initial load
1923
+ } else if (hasNewItems && isLockedToBottomRef.current) {
1924
+ // New items added and user is at bottom - smooth scroll
1925
+ setTimeout(() => {
1926
+ container.scrollTo({
1927
+ top: container.scrollHeight,
1928
+ behavior: "smooth",
1929
+ });
1930
+ }, 100);
1931
+ }
1932
+ // If user has scrolled up, don't auto-scroll
1899
1933
  }
1900
- }, []);
1901
1934
 
1902
- const scrollToIndex = useCallback(
1903
- (index: number) => {
1935
+ updateVirtualRange();
1936
+ lastTotalCountRef.current = totalCount;
1937
+
1938
+ return () => {
1939
+ container.removeEventListener("scroll", handleUserScroll);
1940
+ };
1941
+ }, [totalCount, positions, stickToBottom]);
1942
+
1943
+ const scrollToBottom = useCallback(
1944
+ (behavior: ScrollBehavior = "smooth") => {
1904
1945
  if (containerRef.current) {
1905
- containerRef.current.scrollTop = index * itemHeight;
1946
+ isLockedToBottomRef.current = true;
1947
+ containerRef.current.scrollTo({
1948
+ top: containerRef.current.scrollHeight,
1949
+ behavior,
1950
+ });
1906
1951
  }
1907
1952
  },
1908
- [itemHeight]
1953
+ []
1954
+ );
1955
+
1956
+ const scrollToIndex = useCallback(
1957
+ (index: number, behavior: ScrollBehavior = "smooth") => {
1958
+ if (containerRef.current && positions[index] !== undefined) {
1959
+ isLockedToBottomRef.current = false;
1960
+ containerRef.current.scrollTo({
1961
+ top: positions[index],
1962
+ behavior,
1963
+ });
1964
+ }
1965
+ },
1966
+ [positions]
1909
1967
  );
1910
1968
 
1911
1969
  const virtualizerProps = {
@@ -1921,10 +1979,7 @@ function createProxyHandler<T>(
1921
1979
  },
1922
1980
  list: {
1923
1981
  style: {
1924
- position: "absolute" as const,
1925
- top: `${range.startIndex * itemHeight}px`,
1926
- left: 0,
1927
- right: 0,
1982
+ transform: `translateY(${positions[range.startIndex] || 0}px)`,
1928
1983
  },
1929
1984
  },
1930
1985
  };
@@ -2136,24 +2191,21 @@ function createProxyHandler<T>(
2136
2191
  return null;
2137
2192
  }
2138
2193
 
2139
- const arrayLength = arrayToMap.length;
2140
2194
  const indicesToMap =
2141
2195
  meta?.validIndices ||
2142
- Array.from({ length: arrayLength }, (_, i) => i);
2196
+ Array.from({ length: arrayToMap.length }, (_, i) => i);
2143
2197
 
2144
2198
  return indicesToMap.map((originalIndex, localIndex) => {
2145
2199
  const item = arrayToMap[originalIndex];
2146
2200
  const finalPath = [...path, originalIndex.toString()];
2147
2201
  const setter = rebuildStateShape(item, finalPath, meta);
2148
2202
  const itemComponentId = `${componentId}-${path.join(".")}-${originalIndex}`;
2149
- const isLastItem = originalIndex === arrayLength - 1;
2150
2203
 
2151
2204
  return createElement(CogsItemWrapper, {
2152
2205
  key: originalIndex,
2153
2206
  stateKey,
2154
2207
  itemComponentId,
2155
2208
  itemPath: finalPath,
2156
- isLastItem, // Pass it here!
2157
2209
  children: callbackfn(
2158
2210
  item,
2159
2211
  setter,
@@ -2883,25 +2935,21 @@ export function $cogsSignalStore(proxy: {
2883
2935
  );
2884
2936
  return createElement("text", {}, String(value));
2885
2937
  }
2886
-
2887
2938
  function CogsItemWrapper({
2888
2939
  stateKey,
2889
2940
  itemComponentId,
2890
2941
  itemPath,
2891
- isLastItem,
2892
2942
  children,
2893
2943
  }: {
2894
2944
  stateKey: string;
2895
2945
  itemComponentId: string;
2896
2946
  itemPath: string[];
2897
- isLastItem: boolean;
2898
2947
  children: React.ReactNode;
2899
2948
  }) {
2900
2949
  // This hook handles the re-rendering when the item's own data changes.
2901
2950
  const [, forceUpdate] = useState({});
2902
2951
  // This hook measures the element.
2903
- const [measureRef, bounds] = useMeasure();
2904
- const scrollRef = useRef<HTMLDivElement | null>(null);
2952
+ const [ref, bounds] = useMeasure();
2905
2953
  // This ref prevents sending the same height update repeatedly.
2906
2954
  const lastReportedHeight = useRef<number | null>(null);
2907
2955
 
@@ -2947,25 +2995,7 @@ function CogsItemWrapper({
2947
2995
  }
2948
2996
  };
2949
2997
  }, [stateKey, itemComponentId, itemPath.join(".")]);
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]);
2998
+
2960
2999
  // The rendered output is a simple div that gets measured.
2961
- return (
2962
- <div
2963
- ref={(el) => {
2964
- measureRef(el);
2965
- scrollRef.current = el;
2966
- }}
2967
- >
2968
- {children}
2969
- </div>
2970
- );
3000
+ return <div ref={ref}>{children}</div>;
2971
3001
  }