cogsbox-state 0.5.388 → 0.5.390

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.388",
3
+ "version": "0.5.390",
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
@@ -261,8 +261,13 @@ export type ObjectEndType<T> = EndType<T> & {
261
261
  stateObject: (callbackfn: (value: T, setter: StateObject<T>) => void) => any;
262
262
  delete: () => void;
263
263
  };
264
+ export type ValidationError = {
265
+ path: (string | number)[];
266
+ message: string;
267
+ };
264
268
  type EffectFunction<T, R> = (state: T) => R;
265
269
  export type EndType<T, IsArrayElement = false> = {
270
+ addValidation: (errors: ValidationError[]) => void;
266
271
  applyJsonPatch: (patches: any[]) => void;
267
272
  update: UpdateType<T>;
268
273
  _path: string[];
@@ -1885,26 +1890,37 @@ function createProxyHandler<T>(
1885
1890
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1886
1891
 
1887
1892
  // --- 1. STATE CONTROLLER ---
1888
- // This effect decides which state to transition TO.
1893
+ // --- 1. STATE CONTROLLER (CORRECTED) ---
1889
1894
  useLayoutEffect(() => {
1890
- // THIS `IF` BLOCK IS THE NEW LOGIC
1891
1895
  const container = containerRef.current;
1896
+ if (!container) return;
1897
+
1892
1898
  const hasNewItems = totalCount > prevTotalCountRef.current;
1893
1899
 
1894
- if (hasNewItems && scrollAnchorRef.current && container) {
1895
- // User was scrolled up, new items arrived. Restore the scroll position.
1900
+ // THIS IS THE NEW, IMPORTANT LOGIC FOR ANCHORING
1901
+ if (hasNewItems && scrollAnchorRef.current) {
1896
1902
  const { top: prevScrollTop, height: prevScrollHeight } =
1897
1903
  scrollAnchorRef.current;
1904
+
1905
+ // This is the key: Tell the app we are about to programmatically scroll.
1906
+ isProgrammaticScroll.current = true;
1907
+
1908
+ // Restore the scroll position.
1898
1909
  container.scrollTop =
1899
1910
  prevScrollTop + (container.scrollHeight - prevScrollHeight);
1900
-
1901
- // IMPORTANT: Clear the anchor after using it.
1902
- scrollAnchorRef.current = null;
1903
1911
  console.log(
1904
1912
  `ANCHOR RESTORED to scrollTop: ${container.scrollTop}`
1905
1913
  );
1914
+
1915
+ // IMPORTANT: After the scroll, allow user scroll events again.
1916
+ // Use a timeout to ensure this runs after the scroll event has fired and been ignored.
1917
+ setTimeout(() => {
1918
+ isProgrammaticScroll.current = false;
1919
+ }, 100);
1920
+
1921
+ scrollAnchorRef.current = null; // Clear the anchor after using it.
1906
1922
  }
1907
- // YOUR EXISTING LOGIC CONTINUES BELOW in an else-if structure
1923
+ // YOUR ORIGINAL LOGIC CONTINUES UNCHANGED IN THE `ELSE` BLOCK
1908
1924
  else {
1909
1925
  const depsChanged = !isDeepEqual(
1910
1926
  dependencies,
@@ -1914,7 +1930,7 @@ function createProxyHandler<T>(
1914
1930
  if (depsChanged) {
1915
1931
  console.log("TRANSITION: Deps changed -> IDLE_AT_TOP");
1916
1932
  setStatus("IDLE_AT_TOP");
1917
- return; // Stop here, let the next effect handle the action for the new state.
1933
+ return;
1918
1934
  }
1919
1935
 
1920
1936
  if (
@@ -2021,35 +2037,35 @@ function createProxyHandler<T>(
2021
2037
  const scrollThreshold = itemHeight;
2022
2038
 
2023
2039
  const handleUserScroll = () => {
2040
+ // This guard is now critical. It will ignore our anchor restoration scroll.
2024
2041
  if (isProgrammaticScroll.current) {
2025
2042
  return;
2026
2043
  }
2027
2044
 
2028
2045
  const { scrollTop, scrollHeight, clientHeight } = container;
2029
2046
 
2047
+ // Part 1: Handle Status and Anchoring
2030
2048
  const isAtBottom =
2031
2049
  scrollHeight - scrollTop - clientHeight < 10;
2032
2050
 
2033
2051
  if (isAtBottom) {
2034
2052
  if (status !== "LOCKED_AT_BOTTOM") {
2035
2053
  setStatus("LOCKED_AT_BOTTOM");
2036
- // We are at the bottom, so we don't need an anchor.
2037
- scrollAnchorRef.current = null;
2038
2054
  }
2055
+ // If we are at the bottom, there is no anchor needed.
2056
+ scrollAnchorRef.current = null;
2039
2057
  } else {
2040
2058
  if (status !== "IDLE_NOT_AT_BOTTOM") {
2041
2059
  setStatus("IDLE_NOT_AT_BOTTOM");
2042
- // THE USER SCROLLED UP. SET THE ANCHOR.
2043
- scrollAnchorRef.current = {
2044
- top: scrollTop,
2045
- height: scrollHeight,
2046
- };
2047
- console.log(`ANCHOR SET at scrollTop: ${scrollTop}`);
2048
2060
  }
2061
+ // User is scrolled up. Continuously update the anchor with their latest position.
2062
+ scrollAnchorRef.current = {
2063
+ top: scrollTop,
2064
+ height: scrollHeight,
2065
+ };
2049
2066
  }
2050
- // --- END OF MINIMAL FIX ---
2051
2067
 
2052
- // The rest is YOUR original, working logic for updating the visible items.
2068
+ // Part 2: YOUR original, working logic for updating the visible range.
2053
2069
  if (
2054
2070
  Math.abs(scrollTop - lastUpdateAtScrollTop.current) <
2055
2071
  scrollThreshold
@@ -2061,7 +2077,7 @@ function createProxyHandler<T>(
2061
2077
  `Threshold passed at ${scrollTop}px. Recalculating range...`
2062
2078
  );
2063
2079
 
2064
- // NOW we do the expensive work.
2080
+ // ... your logic to find startIndex and endIndex ...
2065
2081
  let high = totalCount - 1;
2066
2082
  let low = 0;
2067
2083
  let topItemIndex = 0;
@@ -2090,7 +2106,6 @@ function createProxyHandler<T>(
2090
2106
  endIndex: Math.min(totalCount, endIndex + overscan),
2091
2107
  });
2092
2108
 
2093
- // Finally, we record that we did the work at THIS scroll position.
2094
2109
  lastUpdateAtScrollTop.current = scrollTop;
2095
2110
  };
2096
2111
 
@@ -2648,6 +2663,29 @@ function createProxyHandler<T>(
2648
2663
  };
2649
2664
  }
2650
2665
  if (path.length == 0) {
2666
+ if (prop === "addValidation") {
2667
+ return (errors: ValidationError[]) => {
2668
+ const init = getGlobalStore
2669
+ .getState()
2670
+ .getInitialOptions(stateKey)?.validation;
2671
+
2672
+ if (!init?.key) {
2673
+ throw new Error("Validation key not found");
2674
+ }
2675
+
2676
+ // Clear existing errors for this validation key
2677
+ removeValidationError(init.key);
2678
+
2679
+ // Add each new error
2680
+ errors.forEach((error) => {
2681
+ const fullErrorPath = [init.key, ...error.path].join(".");
2682
+ addValidationError(fullErrorPath, error.message);
2683
+ });
2684
+
2685
+ // Notify components to update
2686
+ notifyComponents(stateKey);
2687
+ };
2688
+ }
2651
2689
  if (prop === "applyJsonPatch") {
2652
2690
  return (patches: any[]) => {
2653
2691
  // This part is correct.