react-form-rewind 0.3.0 → 0.5.0

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/README.md CHANGED
@@ -2,16 +2,35 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/react-form-rewind.svg)](https://www.npmjs.com/package/react-form-rewind)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/react-form-rewind.svg)](https://www.npmjs.com/package/react-form-rewind)
5
- [![bundle size](https://img.shields.io/bundlephobia/minzip/react-form-rewind)](https://bundlephobia.com/package/react-form-rewind)
6
- [![license](https://img.shields.io/npm/l/react-form-rewind.svg)](LICENSE)
7
- [![react](https://img.shields.io/badge/react-%3E%3D18.0.0-blue.svg)](https://reactjs.org)
8
- [![typescript](https://img.shields.io/badge/typescript-5.3%2B-3178c6.svg)](https://www.typescriptlang.org/)
5
+ [![license](https://img.shields.io/npm/l/react-form-rewind.svg)](https://github.com/BazilSuhail/npm-react-form-rewind/blob/main/LICENSE)
6
+ [![types](https://img.shields.io/badge/types-typescript-blue.svg)](https://www.typescriptlang.org/)
7
+ [![zero deps](https://img.shields.io/badge/dependencies-zero-brightgreen.svg)]()
8
+ [![tree shakable](https://img.shields.io/badge/tree--shaking-yes-brightgreen.svg)]()
9
+ [![react](https://img.shields.io/badge/react-18%2B-61dafb.svg)](https://react.dev/)
10
+ [![bundle size](https://img.shields.io/bundlejs/size/react-form-rewind?label=min%2Bgzip)](https://bundlejs.com/?q=react-form-rewind)
11
+ [![github](https://img.shields.io/github/stars/BazilSuhail/npm-react-form-rewind?style=social)](https://github.com/BazilSuhail/npm-react-form-rewind)
9
12
 
10
13
  Zero-dependency, tree-shakable React state engine with auto-saved history stacks, time-traveling undo/redo, keyboard shortcuts, and draft persistence.
11
14
 
12
- ---
15
+ - **Undo/Redo** — full history stack with `Ctrl+Z` / `Ctrl+Shift+Z` keyboard shortcuts
16
+ - **Snapshot debouncing** — rapid keystrokes coalesced into logical history entries
17
+ - **Draft persistence** — auto-save to `localStorage` with schema versioning
18
+ - **Functional updates** — `setState(prev => prev + 1)` supported
19
+ - **History inspection** — access `past` and `future` arrays for custom UIs
20
+ - **Callbacks** — `onUndo`, `onRedo`, `onSnapshot` hooks
21
+ - Zero-config — no providers, no context, just a hook
22
+ - Tree-shakable — ESM + CJS with `sideEffects: false`
23
+ - TypeScript — full generics, all types exported
24
+
25
+ ## Install
13
26
 
14
- ## Why?
27
+ ```bash
28
+ npm install react-form-rewind
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ### Why?
15
34
 
16
35
  | Problem | Solution |
17
36
  |---------|----------|
@@ -20,7 +39,7 @@ Zero-dependency, tree-shakable React state engine with auto-saved history stacks
20
39
  | Manual debouncers for history snapshots | Automated keystroke coalescing into logical snapshots |
21
40
  | Heavy form libraries add validation bloat | Focused solely on history and state persistence |
22
41
 
23
- ## Quick Start
42
+ ## Usage
24
43
 
25
44
  ```bash
26
45
  npm install react-form-rewind
@@ -32,7 +51,7 @@ import { useFormHistory } from "react-form-rewind";
32
51
  function MyForm() {
33
52
  const { state, setState, undo, redo, canUndo, canRedo } = useFormHistory(
34
53
  { name: "", email: "" },
35
- { persist: { key: "my-form-draft" } }
54
+ { keyboard: true, persist: { key: "my-form-draft" } }
36
55
  );
37
56
 
38
57
  return (
@@ -89,6 +108,7 @@ The core hook that manages a history-backed state stack.
89
108
  | `maxHistory` | `number` | `100` | Maximum past entries to retain |
90
109
  | `debounceMs` | `number` | `300` | Debounce window for rapid state changes |
91
110
  | `persist` | `boolean \| PersistOptions` | `false` | Enable draft persistence |
111
+ | `keyboard` | `boolean` | `false` | Enable Ctrl+Z / Ctrl+Shift+Z keyboard shortcuts |
92
112
  | `onUndo` | `(state: T) => void` | — | Callback after undo |
93
113
  | `onRedo` | `(state: T) => void` | — | Callback after redo |
94
114
  | `onSnapshot` | `(entry: HistoryEntry<T>) => void` | — | Callback when a snapshot is committed |
@@ -107,7 +127,7 @@ The core hook that manages a history-backed state stack.
107
127
 
108
128
  ### Keyboard Shortcuts
109
129
 
110
- Shortcuts are enabled by default. Press **Ctrl+Z** to undo, **Ctrl+Shift+Z** or **Ctrl+Y** to redo. On macOS, **Ctrl** maps to **Cmd** automatically.
130
+ Pass `keyboard: true` to enable built-in shortcuts. Press **Ctrl+Z** to undo, **Ctrl+Shift+Z** or **Ctrl+Y** to redo. On macOS, **Ctrl** maps to **Cmd** automatically.
111
131
 
112
132
  ### Snapshot Debouncing
113
133
 
package/dist/index.d.mts CHANGED
@@ -3,15 +3,11 @@ interface HistoryEntry<T> {
3
3
  timestamp: number;
4
4
  label?: string;
5
5
  }
6
- interface HistoryStack<T> {
7
- past: HistoryEntry<T>[];
8
- present: T;
9
- future: HistoryEntry<T>[];
10
- }
11
6
  interface UseFormHistoryOptions<T> {
12
7
  maxHistory?: number;
13
8
  debounceMs?: number;
14
9
  persist?: boolean | PersistOptions;
10
+ keyboard?: boolean;
15
11
  onUndo?: (state: T) => void;
16
12
  onRedo?: (state: T) => void;
17
13
  onSnapshot?: (entry: HistoryEntry<T>) => void;
@@ -37,4 +33,4 @@ interface UseFormHistoryReturn<T> {
37
33
 
38
34
  declare function useFormHistory<T>(initialState: T, options?: UseFormHistoryOptions<T>): UseFormHistoryReturn<T>;
39
35
 
40
- export { type HistoryEntry, type HistoryStack, type PersistOptions, type UseFormHistoryOptions, type UseFormHistoryReturn, useFormHistory };
36
+ export { type HistoryEntry, type PersistOptions, type UseFormHistoryOptions, type UseFormHistoryReturn, useFormHistory };
package/dist/index.d.ts CHANGED
@@ -3,15 +3,11 @@ interface HistoryEntry<T> {
3
3
  timestamp: number;
4
4
  label?: string;
5
5
  }
6
- interface HistoryStack<T> {
7
- past: HistoryEntry<T>[];
8
- present: T;
9
- future: HistoryEntry<T>[];
10
- }
11
6
  interface UseFormHistoryOptions<T> {
12
7
  maxHistory?: number;
13
8
  debounceMs?: number;
14
9
  persist?: boolean | PersistOptions;
10
+ keyboard?: boolean;
15
11
  onUndo?: (state: T) => void;
16
12
  onRedo?: (state: T) => void;
17
13
  onSnapshot?: (entry: HistoryEntry<T>) => void;
@@ -37,4 +33,4 @@ interface UseFormHistoryReturn<T> {
37
33
 
38
34
  declare function useFormHistory<T>(initialState: T, options?: UseFormHistoryOptions<T>): UseFormHistoryReturn<T>;
39
35
 
40
- export { type HistoryEntry, type HistoryStack, type PersistOptions, type UseFormHistoryOptions, type UseFormHistoryReturn, useFormHistory };
36
+ export { type HistoryEntry, type PersistOptions, type UseFormHistoryOptions, type UseFormHistoryReturn, useFormHistory };
package/dist/index.js CHANGED
@@ -1,198 +1 @@
1
- 'use strict';
2
-
3
- var react = require('react');
4
-
5
- // src/useFormHistory.ts
6
- var DEFAULT_MAX_HISTORY = 100;
7
- var DEFAULT_DEBOUNCE_MS = 300;
8
- function useFormHistory(initialState, options = {}) {
9
- const {
10
- maxHistory = DEFAULT_MAX_HISTORY,
11
- debounceMs = DEFAULT_DEBOUNCE_MS,
12
- onUndo,
13
- onRedo,
14
- onSnapshot
15
- } = options;
16
- const pastRef = react.useRef([]);
17
- const futureRef = react.useRef([]);
18
- const [state, setStateRaw] = react.useState(initialState);
19
- const stateRef = react.useRef(initialState);
20
- stateRef.current = state;
21
- const [past, setPast] = react.useState([]);
22
- const [future, setFuture] = react.useState([]);
23
- const debounceTimerRef = react.useRef(null);
24
- const pendingStateRef = react.useRef(null);
25
- const preDebounceStateRef = react.useRef(null);
26
- const pushToPast = react.useCallback(
27
- (entry) => {
28
- const next = [...pastRef.current, entry];
29
- if (next.length > maxHistory) {
30
- next.splice(0, next.length - maxHistory);
31
- }
32
- pastRef.current = next;
33
- setPast(next);
34
- },
35
- [maxHistory]
36
- );
37
- react.useEffect(() => {
38
- return () => {
39
- if (debounceTimerRef.current !== null) {
40
- clearTimeout(debounceTimerRef.current);
41
- }
42
- };
43
- }, []);
44
- const setState = react.useCallback(
45
- (value, label) => {
46
- const prev = stateRef.current;
47
- const next = typeof value === "function" ? value(prev) : value;
48
- if (Object.is(prev, next)) return;
49
- setStateRaw(next);
50
- stateRef.current = next;
51
- if (debounceMs <= 0) {
52
- const entry = { state: prev, timestamp: Date.now(), label };
53
- pushToPast(entry);
54
- futureRef.current = [];
55
- setFuture([]);
56
- return;
57
- }
58
- if (pendingStateRef.current === null) {
59
- preDebounceStateRef.current = prev;
60
- }
61
- pendingStateRef.current = next;
62
- if (debounceTimerRef.current !== null) {
63
- clearTimeout(debounceTimerRef.current);
64
- }
65
- debounceTimerRef.current = setTimeout(() => {
66
- const entry = {
67
- state: preDebounceStateRef.current,
68
- timestamp: Date.now(),
69
- label
70
- };
71
- pushToPast(entry);
72
- futureRef.current = [];
73
- setFuture([]);
74
- debounceTimerRef.current = null;
75
- pendingStateRef.current = null;
76
- preDebounceStateRef.current = null;
77
- }, debounceMs);
78
- },
79
- [debounceMs, pushToPast]
80
- );
81
- const undo = react.useCallback(() => {
82
- if (debounceTimerRef.current !== null) {
83
- clearTimeout(debounceTimerRef.current);
84
- debounceTimerRef.current = null;
85
- }
86
- if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {
87
- const entry = {
88
- state: preDebounceStateRef.current,
89
- timestamp: Date.now()
90
- };
91
- pushToPast(entry);
92
- futureRef.current = [];
93
- setFuture([]);
94
- pendingStateRef.current = null;
95
- preDebounceStateRef.current = null;
96
- }
97
- if (pastRef.current.length === 0) return;
98
- const prev = pastRef.current[pastRef.current.length - 1];
99
- const newPast = pastRef.current.slice(0, -1);
100
- pastRef.current = newPast;
101
- setPast(newPast);
102
- const currentEntry = {
103
- state: stateRef.current,
104
- timestamp: Date.now()
105
- };
106
- futureRef.current = [...futureRef.current, currentEntry];
107
- setFuture(futureRef.current);
108
- setStateRaw(prev.state);
109
- stateRef.current = prev.state;
110
- onUndo?.(prev.state);
111
- }, [pushToPast, onUndo]);
112
- const redo = react.useCallback(() => {
113
- if (futureRef.current.length === 0) return;
114
- const next = futureRef.current[futureRef.current.length - 1];
115
- const newFuture = futureRef.current.slice(0, -1);
116
- futureRef.current = newFuture;
117
- setFuture(newFuture);
118
- const currentEntry = {
119
- state: stateRef.current,
120
- timestamp: Date.now()
121
- };
122
- pastRef.current = [...pastRef.current, currentEntry];
123
- setPast(pastRef.current);
124
- setStateRaw(next.state);
125
- stateRef.current = next.state;
126
- onRedo?.(next.state);
127
- }, [onRedo]);
128
- const clearHistory = react.useCallback(() => {
129
- if (debounceTimerRef.current !== null) {
130
- clearTimeout(debounceTimerRef.current);
131
- debounceTimerRef.current = null;
132
- }
133
- pendingStateRef.current = null;
134
- preDebounceStateRef.current = null;
135
- pastRef.current = [];
136
- futureRef.current = [];
137
- setPast([]);
138
- setFuture([]);
139
- }, []);
140
- const snapshot = react.useCallback(
141
- (label) => {
142
- let didFlush = false;
143
- if (debounceTimerRef.current !== null) {
144
- clearTimeout(debounceTimerRef.current);
145
- debounceTimerRef.current = null;
146
- }
147
- if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {
148
- const entry = {
149
- state: preDebounceStateRef.current,
150
- timestamp: Date.now()
151
- };
152
- pushToPast(entry);
153
- futureRef.current = [];
154
- setFuture([]);
155
- pendingStateRef.current = null;
156
- preDebounceStateRef.current = null;
157
- didFlush = true;
158
- }
159
- if (!didFlush) {
160
- const entry = {
161
- state: stateRef.current,
162
- timestamp: Date.now(),
163
- label
164
- };
165
- pushToPast(entry);
166
- futureRef.current = [];
167
- setFuture([]);
168
- onSnapshot?.(entry);
169
- } else if (label && pastRef.current.length > 0) {
170
- pastRef.current[pastRef.current.length - 1].label = label;
171
- setPast([...pastRef.current]);
172
- }
173
- },
174
- [pushToPast, onSnapshot]
175
- );
176
- const clearDraft = react.useCallback(() => {
177
- clearHistory();
178
- setStateRaw(initialState);
179
- stateRef.current = initialState;
180
- }, [clearHistory, initialState]);
181
- return {
182
- state,
183
- setState,
184
- undo,
185
- redo,
186
- canUndo: past.length > 0,
187
- canRedo: future.length > 0,
188
- clearHistory,
189
- clearDraft,
190
- snapshot,
191
- past,
192
- future
193
- };
194
- }
195
-
196
- exports.useFormHistory = useFormHistory;
197
- //# sourceMappingURL=index.js.map
198
- //# sourceMappingURL=index.js.map
1
+ 'use strict';var react=require('react');var j=100,V=300,X=500,Y=1;function q(e){return !e||typeof e=="boolean"?null:{key:e.key,debounceMs:e.debounceMs??X,version:e.version??Y}}function U(){return typeof window<"u"&&!!window.localStorage}function G(e,g){try{if(!U())return null;let f=window.localStorage.getItem(e);if(!f)return null;let E=JSON.parse(f);return E.__v!==g?(window.localStorage.removeItem(e),null):E.__s}catch{return null}}function K(e,g,f){try{if(!U())return;window.localStorage.setItem(e,JSON.stringify({__s:g,__v:f}));}catch{}}function Q(e){try{if(!U())return;window.localStorage.removeItem(e);}catch{}}function W(e,g={}){let{maxHistory:f=j,debounceMs:E=V,persist:C,keyboard:P=false,onUndo:k,onRedo:I,onSnapshot:M}=g,n=q(C),[R,h]=react.useState(()=>{if(n){let t=G(n.key,n.version);if(t!==null)return t}return e}),a=react.useRef(R);a.current=R;let u=react.useRef([]),c=react.useRef([]),[x,H]=react.useState([]),[L,S]=react.useState([]),o=react.useRef(null),m=react.useRef(null),i=react.useRef(null),y=react.useRef(null),T=react.useCallback(()=>{c.current=[],S([]);},[]),p=react.useCallback(t=>{let r=[...u.current,t];r.length>f&&r.splice(0,r.length-f),u.current=r,H(r);},[f]);react.useEffect(()=>()=>{o.current!==null&&clearTimeout(o.current),y.current!==null&&clearTimeout(y.current);},[]);let N=react.useCallback(t=>{n&&(y.current!==null&&clearTimeout(y.current),y.current=setTimeout(()=>{K(n.key,t,n.version),y.current=null;},n.debounceMs));},[n]),v=react.useCallback(t=>{n&&(y.current!==null&&(clearTimeout(y.current),y.current=null),K(n.key,t,n.version));},[n]),z=react.useCallback((t,r)=>{let s=a.current,d=typeof t=="function"?t(s):t;if(!Object.is(s,d)){if(h(d),a.current=d,N(d),E<=0){let F={state:s,timestamp:Date.now(),label:r};p(F),T();return}m.current===null&&(i.current=s),m.current=d,o.current!==null&&clearTimeout(o.current),o.current=setTimeout(()=>{let F={state:i.current,timestamp:Date.now(),label:r};p(F),T(),o.current=null,m.current=null,i.current=null;},E);}},[E,p,N,T]),D=react.useCallback(()=>{if(o.current!==null&&(clearTimeout(o.current),o.current=null),m.current!==null&&i.current!==null){let d={state:i.current,timestamp:Date.now()};p(d),T(),m.current=null,i.current=null;}if(u.current.length===0)return;let t=u.current[u.current.length-1],r=u.current.slice(0,-1);u.current=r,H(r);let s={state:a.current,timestamp:Date.now()};c.current=[...c.current,s],S(c.current),h(t.state),a.current=t.state,v(t.state),k?.(t.state);},[p,k,v,T]),_=react.useCallback(()=>{if(c.current.length===0)return;let t=c.current[c.current.length-1],r=c.current.slice(0,-1);c.current=r,S(r);let s={state:a.current,timestamp:Date.now()};u.current=[...u.current,s],H(u.current),h(t.state),a.current=t.state,v(t.state),I?.(t.state);},[I,v]);react.useEffect(()=>{if(!P)return;let t=r=>{!(r.metaKey||r.ctrlKey)||r.key!=="z"||(r.preventDefault(),r.shiftKey?_():D());};return window.addEventListener("keydown",t),()=>window.removeEventListener("keydown",t)},[P,D,_]);let b=react.useCallback(()=>{o.current!==null&&(clearTimeout(o.current),o.current=null),m.current=null,i.current=null,u.current=[],c.current=[],H([]),S([]);},[]),B=react.useCallback(t=>{let r=false;if(o.current!==null&&(clearTimeout(o.current),o.current=null),m.current!==null&&i.current!==null){let s={state:i.current,timestamp:Date.now()};p(s),T(),m.current=null,i.current=null,r=true;}if(r)t&&u.current.length>0&&(u.current[u.current.length-1].label=t,H([...u.current]));else {let s={state:a.current,timestamp:Date.now(),label:t};p(s),T(),M?.(s);}},[p,M,T]),J=react.useCallback(()=>{b(),n&&Q(n.key),h(e),a.current=e;},[b,e,n]);return {state:R,setState:z,undo:D,redo:_,canUndo:x.length>0,canRedo:L.length>0,clearHistory:b,clearDraft:J,snapshot:B,past:x,future:L}}exports.useFormHistory=W;
package/dist/index.mjs CHANGED
@@ -1,196 +1 @@
1
- import { useRef, useState, useCallback, useEffect } from 'react';
2
-
3
- // src/useFormHistory.ts
4
- var DEFAULT_MAX_HISTORY = 100;
5
- var DEFAULT_DEBOUNCE_MS = 300;
6
- function useFormHistory(initialState, options = {}) {
7
- const {
8
- maxHistory = DEFAULT_MAX_HISTORY,
9
- debounceMs = DEFAULT_DEBOUNCE_MS,
10
- onUndo,
11
- onRedo,
12
- onSnapshot
13
- } = options;
14
- const pastRef = useRef([]);
15
- const futureRef = useRef([]);
16
- const [state, setStateRaw] = useState(initialState);
17
- const stateRef = useRef(initialState);
18
- stateRef.current = state;
19
- const [past, setPast] = useState([]);
20
- const [future, setFuture] = useState([]);
21
- const debounceTimerRef = useRef(null);
22
- const pendingStateRef = useRef(null);
23
- const preDebounceStateRef = useRef(null);
24
- const pushToPast = useCallback(
25
- (entry) => {
26
- const next = [...pastRef.current, entry];
27
- if (next.length > maxHistory) {
28
- next.splice(0, next.length - maxHistory);
29
- }
30
- pastRef.current = next;
31
- setPast(next);
32
- },
33
- [maxHistory]
34
- );
35
- useEffect(() => {
36
- return () => {
37
- if (debounceTimerRef.current !== null) {
38
- clearTimeout(debounceTimerRef.current);
39
- }
40
- };
41
- }, []);
42
- const setState = useCallback(
43
- (value, label) => {
44
- const prev = stateRef.current;
45
- const next = typeof value === "function" ? value(prev) : value;
46
- if (Object.is(prev, next)) return;
47
- setStateRaw(next);
48
- stateRef.current = next;
49
- if (debounceMs <= 0) {
50
- const entry = { state: prev, timestamp: Date.now(), label };
51
- pushToPast(entry);
52
- futureRef.current = [];
53
- setFuture([]);
54
- return;
55
- }
56
- if (pendingStateRef.current === null) {
57
- preDebounceStateRef.current = prev;
58
- }
59
- pendingStateRef.current = next;
60
- if (debounceTimerRef.current !== null) {
61
- clearTimeout(debounceTimerRef.current);
62
- }
63
- debounceTimerRef.current = setTimeout(() => {
64
- const entry = {
65
- state: preDebounceStateRef.current,
66
- timestamp: Date.now(),
67
- label
68
- };
69
- pushToPast(entry);
70
- futureRef.current = [];
71
- setFuture([]);
72
- debounceTimerRef.current = null;
73
- pendingStateRef.current = null;
74
- preDebounceStateRef.current = null;
75
- }, debounceMs);
76
- },
77
- [debounceMs, pushToPast]
78
- );
79
- const undo = useCallback(() => {
80
- if (debounceTimerRef.current !== null) {
81
- clearTimeout(debounceTimerRef.current);
82
- debounceTimerRef.current = null;
83
- }
84
- if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {
85
- const entry = {
86
- state: preDebounceStateRef.current,
87
- timestamp: Date.now()
88
- };
89
- pushToPast(entry);
90
- futureRef.current = [];
91
- setFuture([]);
92
- pendingStateRef.current = null;
93
- preDebounceStateRef.current = null;
94
- }
95
- if (pastRef.current.length === 0) return;
96
- const prev = pastRef.current[pastRef.current.length - 1];
97
- const newPast = pastRef.current.slice(0, -1);
98
- pastRef.current = newPast;
99
- setPast(newPast);
100
- const currentEntry = {
101
- state: stateRef.current,
102
- timestamp: Date.now()
103
- };
104
- futureRef.current = [...futureRef.current, currentEntry];
105
- setFuture(futureRef.current);
106
- setStateRaw(prev.state);
107
- stateRef.current = prev.state;
108
- onUndo?.(prev.state);
109
- }, [pushToPast, onUndo]);
110
- const redo = useCallback(() => {
111
- if (futureRef.current.length === 0) return;
112
- const next = futureRef.current[futureRef.current.length - 1];
113
- const newFuture = futureRef.current.slice(0, -1);
114
- futureRef.current = newFuture;
115
- setFuture(newFuture);
116
- const currentEntry = {
117
- state: stateRef.current,
118
- timestamp: Date.now()
119
- };
120
- pastRef.current = [...pastRef.current, currentEntry];
121
- setPast(pastRef.current);
122
- setStateRaw(next.state);
123
- stateRef.current = next.state;
124
- onRedo?.(next.state);
125
- }, [onRedo]);
126
- const clearHistory = useCallback(() => {
127
- if (debounceTimerRef.current !== null) {
128
- clearTimeout(debounceTimerRef.current);
129
- debounceTimerRef.current = null;
130
- }
131
- pendingStateRef.current = null;
132
- preDebounceStateRef.current = null;
133
- pastRef.current = [];
134
- futureRef.current = [];
135
- setPast([]);
136
- setFuture([]);
137
- }, []);
138
- const snapshot = useCallback(
139
- (label) => {
140
- let didFlush = false;
141
- if (debounceTimerRef.current !== null) {
142
- clearTimeout(debounceTimerRef.current);
143
- debounceTimerRef.current = null;
144
- }
145
- if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {
146
- const entry = {
147
- state: preDebounceStateRef.current,
148
- timestamp: Date.now()
149
- };
150
- pushToPast(entry);
151
- futureRef.current = [];
152
- setFuture([]);
153
- pendingStateRef.current = null;
154
- preDebounceStateRef.current = null;
155
- didFlush = true;
156
- }
157
- if (!didFlush) {
158
- const entry = {
159
- state: stateRef.current,
160
- timestamp: Date.now(),
161
- label
162
- };
163
- pushToPast(entry);
164
- futureRef.current = [];
165
- setFuture([]);
166
- onSnapshot?.(entry);
167
- } else if (label && pastRef.current.length > 0) {
168
- pastRef.current[pastRef.current.length - 1].label = label;
169
- setPast([...pastRef.current]);
170
- }
171
- },
172
- [pushToPast, onSnapshot]
173
- );
174
- const clearDraft = useCallback(() => {
175
- clearHistory();
176
- setStateRaw(initialState);
177
- stateRef.current = initialState;
178
- }, [clearHistory, initialState]);
179
- return {
180
- state,
181
- setState,
182
- undo,
183
- redo,
184
- canUndo: past.length > 0,
185
- canRedo: future.length > 0,
186
- clearHistory,
187
- clearDraft,
188
- snapshot,
189
- past,
190
- future
191
- };
192
- }
193
-
194
- export { useFormHistory };
195
- //# sourceMappingURL=index.mjs.map
196
- //# sourceMappingURL=index.mjs.map
1
+ import {useState,useRef,useCallback,useEffect}from'react';var j=100,V=300,X=500,Y=1;function q(e){return !e||typeof e=="boolean"?null:{key:e.key,debounceMs:e.debounceMs??X,version:e.version??Y}}function U(){return typeof window<"u"&&!!window.localStorage}function G(e,g){try{if(!U())return null;let f=window.localStorage.getItem(e);if(!f)return null;let E=JSON.parse(f);return E.__v!==g?(window.localStorage.removeItem(e),null):E.__s}catch{return null}}function K(e,g,f){try{if(!U())return;window.localStorage.setItem(e,JSON.stringify({__s:g,__v:f}));}catch{}}function Q(e){try{if(!U())return;window.localStorage.removeItem(e);}catch{}}function W(e,g={}){let{maxHistory:f=j,debounceMs:E=V,persist:C,keyboard:P=false,onUndo:k,onRedo:I,onSnapshot:M}=g,n=q(C),[R,h]=useState(()=>{if(n){let t=G(n.key,n.version);if(t!==null)return t}return e}),a=useRef(R);a.current=R;let u=useRef([]),c=useRef([]),[x,H]=useState([]),[L,S]=useState([]),o=useRef(null),m=useRef(null),i=useRef(null),y=useRef(null),T=useCallback(()=>{c.current=[],S([]);},[]),p=useCallback(t=>{let r=[...u.current,t];r.length>f&&r.splice(0,r.length-f),u.current=r,H(r);},[f]);useEffect(()=>()=>{o.current!==null&&clearTimeout(o.current),y.current!==null&&clearTimeout(y.current);},[]);let N=useCallback(t=>{n&&(y.current!==null&&clearTimeout(y.current),y.current=setTimeout(()=>{K(n.key,t,n.version),y.current=null;},n.debounceMs));},[n]),v=useCallback(t=>{n&&(y.current!==null&&(clearTimeout(y.current),y.current=null),K(n.key,t,n.version));},[n]),z=useCallback((t,r)=>{let s=a.current,d=typeof t=="function"?t(s):t;if(!Object.is(s,d)){if(h(d),a.current=d,N(d),E<=0){let F={state:s,timestamp:Date.now(),label:r};p(F),T();return}m.current===null&&(i.current=s),m.current=d,o.current!==null&&clearTimeout(o.current),o.current=setTimeout(()=>{let F={state:i.current,timestamp:Date.now(),label:r};p(F),T(),o.current=null,m.current=null,i.current=null;},E);}},[E,p,N,T]),D=useCallback(()=>{if(o.current!==null&&(clearTimeout(o.current),o.current=null),m.current!==null&&i.current!==null){let d={state:i.current,timestamp:Date.now()};p(d),T(),m.current=null,i.current=null;}if(u.current.length===0)return;let t=u.current[u.current.length-1],r=u.current.slice(0,-1);u.current=r,H(r);let s={state:a.current,timestamp:Date.now()};c.current=[...c.current,s],S(c.current),h(t.state),a.current=t.state,v(t.state),k?.(t.state);},[p,k,v,T]),_=useCallback(()=>{if(c.current.length===0)return;let t=c.current[c.current.length-1],r=c.current.slice(0,-1);c.current=r,S(r);let s={state:a.current,timestamp:Date.now()};u.current=[...u.current,s],H(u.current),h(t.state),a.current=t.state,v(t.state),I?.(t.state);},[I,v]);useEffect(()=>{if(!P)return;let t=r=>{!(r.metaKey||r.ctrlKey)||r.key!=="z"||(r.preventDefault(),r.shiftKey?_():D());};return window.addEventListener("keydown",t),()=>window.removeEventListener("keydown",t)},[P,D,_]);let b=useCallback(()=>{o.current!==null&&(clearTimeout(o.current),o.current=null),m.current=null,i.current=null,u.current=[],c.current=[],H([]),S([]);},[]),B=useCallback(t=>{let r=false;if(o.current!==null&&(clearTimeout(o.current),o.current=null),m.current!==null&&i.current!==null){let s={state:i.current,timestamp:Date.now()};p(s),T(),m.current=null,i.current=null,r=true;}if(r)t&&u.current.length>0&&(u.current[u.current.length-1].label=t,H([...u.current]));else {let s={state:a.current,timestamp:Date.now(),label:t};p(s),T(),M?.(s);}},[p,M,T]),J=useCallback(()=>{b(),n&&Q(n.key),h(e),a.current=e;},[b,e,n]);return {state:R,setState:z,undo:D,redo:_,canUndo:x.length>0,canRedo:L.length>0,clearHistory:b,clearDraft:J,snapshot:B,past:x,future:L}}export{W as useFormHistory};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-form-rewind",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Zero-dependency React state engine with auto-saved history stacks, time-traveling undo/redo, keyboard shortcuts, and draft persistence for forms.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -60,11 +60,11 @@
60
60
  ],
61
61
  "repository": {
62
62
  "type": "git",
63
- "url": "https://github.com/your-username/react-form-rewind.git"
63
+ "url": "https://github.com/BazilSuhail/npm-react-form-rewind.git"
64
64
  },
65
65
  "bugs": {
66
- "url": "https://github.com/your-username/react-form-rewind/issues"
66
+ "url": "https://github.com/BazilSuhail/npm-react-form-rewind/issues"
67
67
  },
68
- "homepage": "https://github.com/your-username/react-form-rewind#readme",
68
+ "homepage": "https://github.com/BazilSuhail/npm-react-form-rewind#readme",
69
69
  "license": "MIT"
70
70
  }
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/useFormHistory.ts"],"names":["useRef","useState","useCallback","useEffect"],"mappings":"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","file":"index.js","sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { HistoryEntry, UseFormHistoryOptions, UseFormHistoryReturn } from \"./types\";\n\nconst DEFAULT_MAX_HISTORY = 100;\nconst DEFAULT_DEBOUNCE_MS = 300;\n\nexport function useFormHistory<T>(\n initialState: T,\n options: UseFormHistoryOptions<T> = {},\n): UseFormHistoryReturn<T> {\n const {\n maxHistory = DEFAULT_MAX_HISTORY,\n debounceMs = DEFAULT_DEBOUNCE_MS,\n onUndo,\n onRedo,\n onSnapshot,\n } = options;\n\n const pastRef = useRef<HistoryEntry<T>[]>([]);\n const futureRef = useRef<HistoryEntry<T>[]>([]);\n\n const [state, setStateRaw] = useState<T>(initialState);\n const stateRef = useRef<T>(initialState);\n stateRef.current = state;\n\n const [past, setPast] = useState<HistoryEntry<T>[]>([]);\n const [future, setFuture] = useState<HistoryEntry<T>[]>([]);\n\n const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const pendingStateRef = useRef<T | null>(null);\n const preDebounceStateRef = useRef<T | null>(null);\n\n const pushToPast = useCallback(\n (entry: HistoryEntry<T>) => {\n const next = [...pastRef.current, entry];\n if (next.length > maxHistory) {\n next.splice(0, next.length - maxHistory);\n }\n pastRef.current = next;\n setPast(next);\n },\n [maxHistory],\n );\n\n useEffect(() => {\n return () => {\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n }\n };\n }, []);\n\n const setState = useCallback(\n (value: T | ((prev: T) => T), label?: string) => {\n const prev = stateRef.current;\n const next = typeof value === \"function\" ? (value as (prev: T) => T)(prev) : value;\n\n if (Object.is(prev, next)) return;\n\n setStateRaw(next);\n stateRef.current = next;\n\n if (debounceMs <= 0) {\n const entry: HistoryEntry<T> = { state: prev, timestamp: Date.now(), label };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n return;\n }\n\n if (pendingStateRef.current === null) {\n preDebounceStateRef.current = prev;\n }\n\n pendingStateRef.current = next;\n\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n }\n\n debounceTimerRef.current = setTimeout(() => {\n const entry: HistoryEntry<T> = {\n state: preDebounceStateRef.current!,\n timestamp: Date.now(),\n label,\n };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n debounceTimerRef.current = null;\n pendingStateRef.current = null;\n preDebounceStateRef.current = null;\n }, debounceMs);\n },\n [debounceMs, pushToPast],\n );\n\n const undo = useCallback(() => {\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = null;\n }\n\n if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {\n const entry: HistoryEntry<T> = {\n state: preDebounceStateRef.current,\n timestamp: Date.now(),\n };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n pendingStateRef.current = null;\n preDebounceStateRef.current = null;\n }\n\n if (pastRef.current.length === 0) return;\n\n const prev = pastRef.current[pastRef.current.length - 1];\n const newPast = pastRef.current.slice(0, -1);\n pastRef.current = newPast;\n setPast(newPast);\n\n const currentEntry: HistoryEntry<T> = {\n state: stateRef.current,\n timestamp: Date.now(),\n };\n futureRef.current = [...futureRef.current, currentEntry];\n setFuture(futureRef.current);\n\n setStateRaw(prev.state);\n stateRef.current = prev.state;\n onUndo?.(prev.state);\n }, [pushToPast, onUndo]);\n\n const redo = useCallback(() => {\n if (futureRef.current.length === 0) return;\n\n const next = futureRef.current[futureRef.current.length - 1];\n const newFuture = futureRef.current.slice(0, -1);\n futureRef.current = newFuture;\n setFuture(newFuture);\n\n const currentEntry: HistoryEntry<T> = {\n state: stateRef.current,\n timestamp: Date.now(),\n };\n pastRef.current = [...pastRef.current, currentEntry];\n setPast(pastRef.current);\n\n setStateRaw(next.state);\n stateRef.current = next.state;\n onRedo?.(next.state);\n }, [onRedo]);\n\n const clearHistory = useCallback(() => {\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = null;\n }\n pendingStateRef.current = null;\n preDebounceStateRef.current = null;\n pastRef.current = [];\n futureRef.current = [];\n setPast([]);\n setFuture([]);\n }, []);\n\n const snapshot = useCallback(\n (label?: string) => {\n let didFlush = false;\n\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = null;\n }\n\n if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {\n const entry: HistoryEntry<T> = {\n state: preDebounceStateRef.current,\n timestamp: Date.now(),\n };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n pendingStateRef.current = null;\n preDebounceStateRef.current = null;\n didFlush = true;\n }\n\n if (!didFlush) {\n const entry: HistoryEntry<T> = {\n state: stateRef.current,\n timestamp: Date.now(),\n label,\n };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n onSnapshot?.(entry);\n } else if (label && pastRef.current.length > 0) {\n pastRef.current[pastRef.current.length - 1].label = label;\n setPast([...pastRef.current]);\n }\n },\n [pushToPast, onSnapshot],\n );\n\n const clearDraft = useCallback(() => {\n clearHistory();\n setStateRaw(initialState);\n stateRef.current = initialState;\n }, [clearHistory, initialState]);\n\n return {\n state,\n setState,\n undo,\n redo,\n canUndo: past.length > 0,\n canRedo: future.length > 0,\n clearHistory,\n clearDraft,\n snapshot,\n past,\n future,\n };\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/useFormHistory.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { HistoryEntry, UseFormHistoryOptions, UseFormHistoryReturn } from \"./types\";\n\nconst DEFAULT_MAX_HISTORY = 100;\nconst DEFAULT_DEBOUNCE_MS = 300;\n\nexport function useFormHistory<T>(\n initialState: T,\n options: UseFormHistoryOptions<T> = {},\n): UseFormHistoryReturn<T> {\n const {\n maxHistory = DEFAULT_MAX_HISTORY,\n debounceMs = DEFAULT_DEBOUNCE_MS,\n onUndo,\n onRedo,\n onSnapshot,\n } = options;\n\n const pastRef = useRef<HistoryEntry<T>[]>([]);\n const futureRef = useRef<HistoryEntry<T>[]>([]);\n\n const [state, setStateRaw] = useState<T>(initialState);\n const stateRef = useRef<T>(initialState);\n stateRef.current = state;\n\n const [past, setPast] = useState<HistoryEntry<T>[]>([]);\n const [future, setFuture] = useState<HistoryEntry<T>[]>([]);\n\n const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const pendingStateRef = useRef<T | null>(null);\n const preDebounceStateRef = useRef<T | null>(null);\n\n const pushToPast = useCallback(\n (entry: HistoryEntry<T>) => {\n const next = [...pastRef.current, entry];\n if (next.length > maxHistory) {\n next.splice(0, next.length - maxHistory);\n }\n pastRef.current = next;\n setPast(next);\n },\n [maxHistory],\n );\n\n useEffect(() => {\n return () => {\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n }\n };\n }, []);\n\n const setState = useCallback(\n (value: T | ((prev: T) => T), label?: string) => {\n const prev = stateRef.current;\n const next = typeof value === \"function\" ? (value as (prev: T) => T)(prev) : value;\n\n if (Object.is(prev, next)) return;\n\n setStateRaw(next);\n stateRef.current = next;\n\n if (debounceMs <= 0) {\n const entry: HistoryEntry<T> = { state: prev, timestamp: Date.now(), label };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n return;\n }\n\n if (pendingStateRef.current === null) {\n preDebounceStateRef.current = prev;\n }\n\n pendingStateRef.current = next;\n\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n }\n\n debounceTimerRef.current = setTimeout(() => {\n const entry: HistoryEntry<T> = {\n state: preDebounceStateRef.current!,\n timestamp: Date.now(),\n label,\n };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n debounceTimerRef.current = null;\n pendingStateRef.current = null;\n preDebounceStateRef.current = null;\n }, debounceMs);\n },\n [debounceMs, pushToPast],\n );\n\n const undo = useCallback(() => {\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = null;\n }\n\n if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {\n const entry: HistoryEntry<T> = {\n state: preDebounceStateRef.current,\n timestamp: Date.now(),\n };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n pendingStateRef.current = null;\n preDebounceStateRef.current = null;\n }\n\n if (pastRef.current.length === 0) return;\n\n const prev = pastRef.current[pastRef.current.length - 1];\n const newPast = pastRef.current.slice(0, -1);\n pastRef.current = newPast;\n setPast(newPast);\n\n const currentEntry: HistoryEntry<T> = {\n state: stateRef.current,\n timestamp: Date.now(),\n };\n futureRef.current = [...futureRef.current, currentEntry];\n setFuture(futureRef.current);\n\n setStateRaw(prev.state);\n stateRef.current = prev.state;\n onUndo?.(prev.state);\n }, [pushToPast, onUndo]);\n\n const redo = useCallback(() => {\n if (futureRef.current.length === 0) return;\n\n const next = futureRef.current[futureRef.current.length - 1];\n const newFuture = futureRef.current.slice(0, -1);\n futureRef.current = newFuture;\n setFuture(newFuture);\n\n const currentEntry: HistoryEntry<T> = {\n state: stateRef.current,\n timestamp: Date.now(),\n };\n pastRef.current = [...pastRef.current, currentEntry];\n setPast(pastRef.current);\n\n setStateRaw(next.state);\n stateRef.current = next.state;\n onRedo?.(next.state);\n }, [onRedo]);\n\n const clearHistory = useCallback(() => {\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = null;\n }\n pendingStateRef.current = null;\n preDebounceStateRef.current = null;\n pastRef.current = [];\n futureRef.current = [];\n setPast([]);\n setFuture([]);\n }, []);\n\n const snapshot = useCallback(\n (label?: string) => {\n let didFlush = false;\n\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = null;\n }\n\n if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {\n const entry: HistoryEntry<T> = {\n state: preDebounceStateRef.current,\n timestamp: Date.now(),\n };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n pendingStateRef.current = null;\n preDebounceStateRef.current = null;\n didFlush = true;\n }\n\n if (!didFlush) {\n const entry: HistoryEntry<T> = {\n state: stateRef.current,\n timestamp: Date.now(),\n label,\n };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n onSnapshot?.(entry);\n } else if (label && pastRef.current.length > 0) {\n pastRef.current[pastRef.current.length - 1].label = label;\n setPast([...pastRef.current]);\n }\n },\n [pushToPast, onSnapshot],\n );\n\n const clearDraft = useCallback(() => {\n clearHistory();\n setStateRaw(initialState);\n stateRef.current = initialState;\n }, [clearHistory, initialState]);\n\n return {\n state,\n setState,\n undo,\n redo,\n canUndo: past.length > 0,\n canRedo: future.length > 0,\n clearHistory,\n clearDraft,\n snapshot,\n past,\n future,\n };\n}\n"]}