react-form-rewind 0.2.0 → 0.4.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
package/dist/index.js CHANGED
@@ -4,15 +4,78 @@ var react = require('react');
4
4
 
5
5
  // src/useFormHistory.ts
6
6
  var DEFAULT_MAX_HISTORY = 100;
7
+ var DEFAULT_DEBOUNCE_MS = 300;
8
+ var DEFAULT_PERSIST_DEBOUNCE_MS = 500;
9
+ var DEFAULT_PERSIST_VERSION = 1;
10
+ function normalizePersist(persist) {
11
+ if (!persist) return null;
12
+ if (typeof persist === "boolean") return null;
13
+ return {
14
+ key: persist.key,
15
+ debounceMs: persist.debounceMs ?? DEFAULT_PERSIST_DEBOUNCE_MS,
16
+ version: persist.version ?? DEFAULT_PERSIST_VERSION
17
+ };
18
+ }
19
+ function readDraft(key, version) {
20
+ try {
21
+ if (typeof window === "undefined" || !window.localStorage) return null;
22
+ const raw = window.localStorage.getItem(key);
23
+ if (!raw) return null;
24
+ const parsed = JSON.parse(raw);
25
+ if (parsed.__version !== version) {
26
+ window.localStorage.removeItem(key);
27
+ return null;
28
+ }
29
+ return parsed.__state;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+ function writeDraft(key, state, version) {
35
+ try {
36
+ if (typeof window === "undefined" || !window.localStorage) return;
37
+ window.localStorage.setItem(key, JSON.stringify({ __state: state, __version: version }));
38
+ } catch {
39
+ }
40
+ }
41
+ function removeDraft(key) {
42
+ try {
43
+ if (typeof window === "undefined" || !window.localStorage) return;
44
+ window.localStorage.removeItem(key);
45
+ } catch {
46
+ }
47
+ }
7
48
  function useFormHistory(initialState, options = {}) {
8
- const { maxHistory = DEFAULT_MAX_HISTORY, onUndo, onRedo, onSnapshot } = options;
49
+ const {
50
+ maxHistory = DEFAULT_MAX_HISTORY,
51
+ debounceMs = DEFAULT_DEBOUNCE_MS,
52
+ persist: persistOption,
53
+ onUndo,
54
+ onRedo,
55
+ onSnapshot
56
+ } = options;
57
+ const persist = normalizePersist(persistOption);
58
+ const initializedRef = react.useRef(false);
59
+ const [state, setStateRaw] = react.useState(() => {
60
+ if (persist) {
61
+ const draft = readDraft(persist.key, persist.version);
62
+ if (draft !== null) {
63
+ initializedRef.current = true;
64
+ return draft;
65
+ }
66
+ }
67
+ return initialState;
68
+ });
69
+ const stateRef = react.useRef(state);
70
+ stateRef.current = state;
9
71
  const pastRef = react.useRef([]);
10
72
  const futureRef = react.useRef([]);
11
- const [state, setStateRaw] = react.useState(initialState);
12
- const stateRef = react.useRef(initialState);
13
- stateRef.current = state;
14
73
  const [past, setPast] = react.useState([]);
15
74
  const [future, setFuture] = react.useState([]);
75
+ const debounceTimerRef = react.useRef(null);
76
+ const pendingStateRef = react.useRef(null);
77
+ const preDebounceStateRef = react.useRef(null);
78
+ const persistTimerRef = react.useRef(null);
16
79
  const pushToPast = react.useCallback(
17
80
  (entry) => {
18
81
  const next = [...pastRef.current, entry];
@@ -24,21 +87,94 @@ function useFormHistory(initialState, options = {}) {
24
87
  },
25
88
  [maxHistory]
26
89
  );
90
+ react.useEffect(() => {
91
+ return () => {
92
+ if (debounceTimerRef.current !== null) {
93
+ clearTimeout(debounceTimerRef.current);
94
+ }
95
+ if (persistTimerRef.current !== null) {
96
+ clearTimeout(persistTimerRef.current);
97
+ }
98
+ };
99
+ }, []);
100
+ const schedulePersist = react.useCallback(
101
+ (nextState) => {
102
+ if (!persist) return;
103
+ if (persistTimerRef.current !== null) {
104
+ clearTimeout(persistTimerRef.current);
105
+ }
106
+ persistTimerRef.current = setTimeout(() => {
107
+ writeDraft(persist.key, nextState, persist.version);
108
+ persistTimerRef.current = null;
109
+ }, persist.debounceMs);
110
+ },
111
+ [persist]
112
+ );
113
+ const persistNow = react.useCallback(
114
+ (nextState) => {
115
+ if (!persist) return;
116
+ if (persistTimerRef.current !== null) {
117
+ clearTimeout(persistTimerRef.current);
118
+ persistTimerRef.current = null;
119
+ }
120
+ writeDraft(persist.key, nextState, persist.version);
121
+ },
122
+ [persist]
123
+ );
27
124
  const setState = react.useCallback(
28
125
  (value, label) => {
29
126
  const prev = stateRef.current;
30
127
  const next = typeof value === "function" ? value(prev) : value;
31
128
  if (Object.is(prev, next)) return;
32
- const entry = { state: prev, timestamp: Date.now(), label };
33
- pushToPast(entry);
34
- futureRef.current = [];
35
- setFuture([]);
36
129
  setStateRaw(next);
37
130
  stateRef.current = next;
131
+ schedulePersist(next);
132
+ if (debounceMs <= 0) {
133
+ const entry = { state: prev, timestamp: Date.now(), label };
134
+ pushToPast(entry);
135
+ futureRef.current = [];
136
+ setFuture([]);
137
+ return;
138
+ }
139
+ if (pendingStateRef.current === null) {
140
+ preDebounceStateRef.current = prev;
141
+ }
142
+ pendingStateRef.current = next;
143
+ if (debounceTimerRef.current !== null) {
144
+ clearTimeout(debounceTimerRef.current);
145
+ }
146
+ debounceTimerRef.current = setTimeout(() => {
147
+ const entry = {
148
+ state: preDebounceStateRef.current,
149
+ timestamp: Date.now(),
150
+ label
151
+ };
152
+ pushToPast(entry);
153
+ futureRef.current = [];
154
+ setFuture([]);
155
+ debounceTimerRef.current = null;
156
+ pendingStateRef.current = null;
157
+ preDebounceStateRef.current = null;
158
+ }, debounceMs);
38
159
  },
39
- [pushToPast]
160
+ [debounceMs, pushToPast, schedulePersist]
40
161
  );
41
162
  const undo = react.useCallback(() => {
163
+ if (debounceTimerRef.current !== null) {
164
+ clearTimeout(debounceTimerRef.current);
165
+ debounceTimerRef.current = null;
166
+ }
167
+ if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {
168
+ const entry = {
169
+ state: preDebounceStateRef.current,
170
+ timestamp: Date.now()
171
+ };
172
+ pushToPast(entry);
173
+ futureRef.current = [];
174
+ setFuture([]);
175
+ pendingStateRef.current = null;
176
+ preDebounceStateRef.current = null;
177
+ }
42
178
  if (pastRef.current.length === 0) return;
43
179
  const prev = pastRef.current[pastRef.current.length - 1];
44
180
  const newPast = pastRef.current.slice(0, -1);
@@ -52,8 +188,9 @@ function useFormHistory(initialState, options = {}) {
52
188
  setFuture(futureRef.current);
53
189
  setStateRaw(prev.state);
54
190
  stateRef.current = prev.state;
191
+ persistNow(prev.state);
55
192
  onUndo?.(prev.state);
56
- }, [onUndo]);
193
+ }, [pushToPast, onUndo, persistNow]);
57
194
  const redo = react.useCallback(() => {
58
195
  if (futureRef.current.length === 0) return;
59
196
  const next = futureRef.current[futureRef.current.length - 1];
@@ -68,9 +205,16 @@ function useFormHistory(initialState, options = {}) {
68
205
  setPast(pastRef.current);
69
206
  setStateRaw(next.state);
70
207
  stateRef.current = next.state;
208
+ persistNow(next.state);
71
209
  onRedo?.(next.state);
72
- }, [onRedo]);
210
+ }, [onRedo, persistNow]);
73
211
  const clearHistory = react.useCallback(() => {
212
+ if (debounceTimerRef.current !== null) {
213
+ clearTimeout(debounceTimerRef.current);
214
+ debounceTimerRef.current = null;
215
+ }
216
+ pendingStateRef.current = null;
217
+ preDebounceStateRef.current = null;
74
218
  pastRef.current = [];
75
219
  futureRef.current = [];
76
220
  setPast([]);
@@ -78,23 +222,48 @@ function useFormHistory(initialState, options = {}) {
78
222
  }, []);
79
223
  const snapshot = react.useCallback(
80
224
  (label) => {
81
- const entry = {
82
- state: stateRef.current,
83
- timestamp: Date.now(),
84
- label
85
- };
86
- pushToPast(entry);
87
- futureRef.current = [];
88
- setFuture([]);
89
- onSnapshot?.(entry);
225
+ let didFlush = false;
226
+ if (debounceTimerRef.current !== null) {
227
+ clearTimeout(debounceTimerRef.current);
228
+ debounceTimerRef.current = null;
229
+ }
230
+ if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {
231
+ const entry = {
232
+ state: preDebounceStateRef.current,
233
+ timestamp: Date.now()
234
+ };
235
+ pushToPast(entry);
236
+ futureRef.current = [];
237
+ setFuture([]);
238
+ pendingStateRef.current = null;
239
+ preDebounceStateRef.current = null;
240
+ didFlush = true;
241
+ }
242
+ if (!didFlush) {
243
+ const entry = {
244
+ state: stateRef.current,
245
+ timestamp: Date.now(),
246
+ label
247
+ };
248
+ pushToPast(entry);
249
+ futureRef.current = [];
250
+ setFuture([]);
251
+ onSnapshot?.(entry);
252
+ } else if (label && pastRef.current.length > 0) {
253
+ pastRef.current[pastRef.current.length - 1].label = label;
254
+ setPast([...pastRef.current]);
255
+ }
90
256
  },
91
257
  [pushToPast, onSnapshot]
92
258
  );
93
259
  const clearDraft = react.useCallback(() => {
94
260
  clearHistory();
261
+ if (persist) {
262
+ removeDraft(persist.key);
263
+ }
95
264
  setStateRaw(initialState);
96
265
  stateRef.current = initialState;
97
- }, [clearHistory, initialState]);
266
+ }, [clearHistory, initialState, persist]);
98
267
  return {
99
268
  state,
100
269
  setState,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/useFormHistory.ts"],"names":["useRef","useState","useCallback"],"mappings":"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","file":"index.js","sourcesContent":["import { useCallback, useRef, useState } from \"react\";\nimport type { HistoryEntry, UseFormHistoryOptions, UseFormHistoryReturn } from \"./types\";\n\nconst DEFAULT_MAX_HISTORY = 100;\n\nexport function useFormHistory<T>(\n initialState: T,\n options: UseFormHistoryOptions<T> = {},\n): UseFormHistoryReturn<T> {\n const { maxHistory = DEFAULT_MAX_HISTORY, onUndo, onRedo, onSnapshot } = 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 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 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 const entry: HistoryEntry<T> = { state: prev, timestamp: Date.now(), label };\n\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n setStateRaw(next);\n stateRef.current = next;\n },\n [pushToPast],\n );\n\n const undo = useCallback(() => {\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 }, [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 pastRef.current = [];\n futureRef.current = [];\n setPast([]);\n setFuture([]);\n }, []);\n\n const snapshot = useCallback(\n (label?: string) => {\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 },\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
+ {"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, PersistOptions, UseFormHistoryOptions, UseFormHistoryReturn } from \"./types\";\n\nconst DEFAULT_MAX_HISTORY = 100;\nconst DEFAULT_DEBOUNCE_MS = 300;\nconst DEFAULT_PERSIST_DEBOUNCE_MS = 500;\nconst DEFAULT_PERSIST_VERSION = 1;\n\ninterface PersistStorage {\n key: string;\n debounceMs: number;\n version: number;\n}\n\nfunction normalizePersist(persist: boolean | PersistOptions | undefined): PersistStorage | null {\n if (!persist) return null;\n if (typeof persist === \"boolean\") return null;\n return {\n key: persist.key,\n debounceMs: persist.debounceMs ?? DEFAULT_PERSIST_DEBOUNCE_MS,\n version: persist.version ?? DEFAULT_PERSIST_VERSION,\n };\n}\n\nfunction readDraft<T>(key: string, version: number): T | null {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) return null;\n const raw = window.localStorage.getItem(key);\n if (!raw) return null;\n const parsed = JSON.parse(raw);\n if (parsed.__version !== version) {\n window.localStorage.removeItem(key);\n return null;\n }\n return parsed.__state as T;\n } catch {\n return null;\n }\n}\n\nfunction writeDraft<T>(key: string, state: T, version: number): void {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) return;\n window.localStorage.setItem(key, JSON.stringify({ __state: state, __version: version }));\n } catch {\n // localStorage full or disabled — silently ignore\n }\n}\n\nfunction removeDraft(key: string): void {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) return;\n window.localStorage.removeItem(key);\n } catch {\n // silently ignore\n }\n}\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 persist: persistOption,\n onUndo,\n onRedo,\n onSnapshot,\n } = options;\n\n const persist = normalizePersist(persistOption);\n const initializedRef = useRef(false);\n\n // Hydrate from localStorage on first render\n const [state, setStateRaw] = useState<T>(() => {\n if (persist) {\n const draft = readDraft<T>(persist.key, persist.version);\n if (draft !== null) {\n initializedRef.current = true;\n return draft;\n }\n }\n return initialState;\n });\n\n const stateRef = useRef<T>(state);\n stateRef.current = state;\n\n const pastRef = useRef<HistoryEntry<T>[]>([]);\n const futureRef = useRef<HistoryEntry<T>[]>([]);\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 // Persist debounce timer\n const persistTimerRef = useRef<ReturnType<typeof setTimeout> | 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 // Cleanup timers on unmount\n useEffect(() => {\n return () => {\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n }\n if (persistTimerRef.current !== null) {\n clearTimeout(persistTimerRef.current);\n }\n };\n }, []);\n\n // Debounced persist helper\n const schedulePersist = useCallback(\n (nextState: T) => {\n if (!persist) return;\n if (persistTimerRef.current !== null) {\n clearTimeout(persistTimerRef.current);\n }\n persistTimerRef.current = setTimeout(() => {\n writeDraft(persist.key, nextState, persist.version);\n persistTimerRef.current = null;\n }, persist.debounceMs);\n },\n [persist],\n );\n\n // Persist immediately (used by undo/redo/snapshot/clearDraft which bypass debounce)\n const persistNow = useCallback(\n (nextState: T) => {\n if (!persist) return;\n if (persistTimerRef.current !== null) {\n clearTimeout(persistTimerRef.current);\n persistTimerRef.current = null;\n }\n writeDraft(persist.key, nextState, persist.version);\n },\n [persist],\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 schedulePersist(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, schedulePersist],\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 persistNow(prev.state);\n onUndo?.(prev.state);\n }, [pushToPast, onUndo, persistNow]);\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 persistNow(next.state);\n onRedo?.(next.state);\n }, [onRedo, persistNow]);\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 if (persist) {\n removeDraft(persist.key);\n }\n setStateRaw(initialState);\n stateRef.current = initialState;\n }, [clearHistory, initialState, persist]);\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"]}
package/dist/index.mjs CHANGED
@@ -1,16 +1,79 @@
1
- import { useRef, useState, useCallback } from 'react';
1
+ import { useRef, useState, useCallback, useEffect } from 'react';
2
2
 
3
3
  // src/useFormHistory.ts
4
4
  var DEFAULT_MAX_HISTORY = 100;
5
+ var DEFAULT_DEBOUNCE_MS = 300;
6
+ var DEFAULT_PERSIST_DEBOUNCE_MS = 500;
7
+ var DEFAULT_PERSIST_VERSION = 1;
8
+ function normalizePersist(persist) {
9
+ if (!persist) return null;
10
+ if (typeof persist === "boolean") return null;
11
+ return {
12
+ key: persist.key,
13
+ debounceMs: persist.debounceMs ?? DEFAULT_PERSIST_DEBOUNCE_MS,
14
+ version: persist.version ?? DEFAULT_PERSIST_VERSION
15
+ };
16
+ }
17
+ function readDraft(key, version) {
18
+ try {
19
+ if (typeof window === "undefined" || !window.localStorage) return null;
20
+ const raw = window.localStorage.getItem(key);
21
+ if (!raw) return null;
22
+ const parsed = JSON.parse(raw);
23
+ if (parsed.__version !== version) {
24
+ window.localStorage.removeItem(key);
25
+ return null;
26
+ }
27
+ return parsed.__state;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+ function writeDraft(key, state, version) {
33
+ try {
34
+ if (typeof window === "undefined" || !window.localStorage) return;
35
+ window.localStorage.setItem(key, JSON.stringify({ __state: state, __version: version }));
36
+ } catch {
37
+ }
38
+ }
39
+ function removeDraft(key) {
40
+ try {
41
+ if (typeof window === "undefined" || !window.localStorage) return;
42
+ window.localStorage.removeItem(key);
43
+ } catch {
44
+ }
45
+ }
5
46
  function useFormHistory(initialState, options = {}) {
6
- const { maxHistory = DEFAULT_MAX_HISTORY, onUndo, onRedo, onSnapshot } = options;
47
+ const {
48
+ maxHistory = DEFAULT_MAX_HISTORY,
49
+ debounceMs = DEFAULT_DEBOUNCE_MS,
50
+ persist: persistOption,
51
+ onUndo,
52
+ onRedo,
53
+ onSnapshot
54
+ } = options;
55
+ const persist = normalizePersist(persistOption);
56
+ const initializedRef = useRef(false);
57
+ const [state, setStateRaw] = useState(() => {
58
+ if (persist) {
59
+ const draft = readDraft(persist.key, persist.version);
60
+ if (draft !== null) {
61
+ initializedRef.current = true;
62
+ return draft;
63
+ }
64
+ }
65
+ return initialState;
66
+ });
67
+ const stateRef = useRef(state);
68
+ stateRef.current = state;
7
69
  const pastRef = useRef([]);
8
70
  const futureRef = useRef([]);
9
- const [state, setStateRaw] = useState(initialState);
10
- const stateRef = useRef(initialState);
11
- stateRef.current = state;
12
71
  const [past, setPast] = useState([]);
13
72
  const [future, setFuture] = useState([]);
73
+ const debounceTimerRef = useRef(null);
74
+ const pendingStateRef = useRef(null);
75
+ const preDebounceStateRef = useRef(null);
76
+ const persistTimerRef = useRef(null);
14
77
  const pushToPast = useCallback(
15
78
  (entry) => {
16
79
  const next = [...pastRef.current, entry];
@@ -22,21 +85,94 @@ function useFormHistory(initialState, options = {}) {
22
85
  },
23
86
  [maxHistory]
24
87
  );
88
+ useEffect(() => {
89
+ return () => {
90
+ if (debounceTimerRef.current !== null) {
91
+ clearTimeout(debounceTimerRef.current);
92
+ }
93
+ if (persistTimerRef.current !== null) {
94
+ clearTimeout(persistTimerRef.current);
95
+ }
96
+ };
97
+ }, []);
98
+ const schedulePersist = useCallback(
99
+ (nextState) => {
100
+ if (!persist) return;
101
+ if (persistTimerRef.current !== null) {
102
+ clearTimeout(persistTimerRef.current);
103
+ }
104
+ persistTimerRef.current = setTimeout(() => {
105
+ writeDraft(persist.key, nextState, persist.version);
106
+ persistTimerRef.current = null;
107
+ }, persist.debounceMs);
108
+ },
109
+ [persist]
110
+ );
111
+ const persistNow = useCallback(
112
+ (nextState) => {
113
+ if (!persist) return;
114
+ if (persistTimerRef.current !== null) {
115
+ clearTimeout(persistTimerRef.current);
116
+ persistTimerRef.current = null;
117
+ }
118
+ writeDraft(persist.key, nextState, persist.version);
119
+ },
120
+ [persist]
121
+ );
25
122
  const setState = useCallback(
26
123
  (value, label) => {
27
124
  const prev = stateRef.current;
28
125
  const next = typeof value === "function" ? value(prev) : value;
29
126
  if (Object.is(prev, next)) return;
30
- const entry = { state: prev, timestamp: Date.now(), label };
31
- pushToPast(entry);
32
- futureRef.current = [];
33
- setFuture([]);
34
127
  setStateRaw(next);
35
128
  stateRef.current = next;
129
+ schedulePersist(next);
130
+ if (debounceMs <= 0) {
131
+ const entry = { state: prev, timestamp: Date.now(), label };
132
+ pushToPast(entry);
133
+ futureRef.current = [];
134
+ setFuture([]);
135
+ return;
136
+ }
137
+ if (pendingStateRef.current === null) {
138
+ preDebounceStateRef.current = prev;
139
+ }
140
+ pendingStateRef.current = next;
141
+ if (debounceTimerRef.current !== null) {
142
+ clearTimeout(debounceTimerRef.current);
143
+ }
144
+ debounceTimerRef.current = setTimeout(() => {
145
+ const entry = {
146
+ state: preDebounceStateRef.current,
147
+ timestamp: Date.now(),
148
+ label
149
+ };
150
+ pushToPast(entry);
151
+ futureRef.current = [];
152
+ setFuture([]);
153
+ debounceTimerRef.current = null;
154
+ pendingStateRef.current = null;
155
+ preDebounceStateRef.current = null;
156
+ }, debounceMs);
36
157
  },
37
- [pushToPast]
158
+ [debounceMs, pushToPast, schedulePersist]
38
159
  );
39
160
  const undo = useCallback(() => {
161
+ if (debounceTimerRef.current !== null) {
162
+ clearTimeout(debounceTimerRef.current);
163
+ debounceTimerRef.current = null;
164
+ }
165
+ if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {
166
+ const entry = {
167
+ state: preDebounceStateRef.current,
168
+ timestamp: Date.now()
169
+ };
170
+ pushToPast(entry);
171
+ futureRef.current = [];
172
+ setFuture([]);
173
+ pendingStateRef.current = null;
174
+ preDebounceStateRef.current = null;
175
+ }
40
176
  if (pastRef.current.length === 0) return;
41
177
  const prev = pastRef.current[pastRef.current.length - 1];
42
178
  const newPast = pastRef.current.slice(0, -1);
@@ -50,8 +186,9 @@ function useFormHistory(initialState, options = {}) {
50
186
  setFuture(futureRef.current);
51
187
  setStateRaw(prev.state);
52
188
  stateRef.current = prev.state;
189
+ persistNow(prev.state);
53
190
  onUndo?.(prev.state);
54
- }, [onUndo]);
191
+ }, [pushToPast, onUndo, persistNow]);
55
192
  const redo = useCallback(() => {
56
193
  if (futureRef.current.length === 0) return;
57
194
  const next = futureRef.current[futureRef.current.length - 1];
@@ -66,9 +203,16 @@ function useFormHistory(initialState, options = {}) {
66
203
  setPast(pastRef.current);
67
204
  setStateRaw(next.state);
68
205
  stateRef.current = next.state;
206
+ persistNow(next.state);
69
207
  onRedo?.(next.state);
70
- }, [onRedo]);
208
+ }, [onRedo, persistNow]);
71
209
  const clearHistory = useCallback(() => {
210
+ if (debounceTimerRef.current !== null) {
211
+ clearTimeout(debounceTimerRef.current);
212
+ debounceTimerRef.current = null;
213
+ }
214
+ pendingStateRef.current = null;
215
+ preDebounceStateRef.current = null;
72
216
  pastRef.current = [];
73
217
  futureRef.current = [];
74
218
  setPast([]);
@@ -76,23 +220,48 @@ function useFormHistory(initialState, options = {}) {
76
220
  }, []);
77
221
  const snapshot = useCallback(
78
222
  (label) => {
79
- const entry = {
80
- state: stateRef.current,
81
- timestamp: Date.now(),
82
- label
83
- };
84
- pushToPast(entry);
85
- futureRef.current = [];
86
- setFuture([]);
87
- onSnapshot?.(entry);
223
+ let didFlush = false;
224
+ if (debounceTimerRef.current !== null) {
225
+ clearTimeout(debounceTimerRef.current);
226
+ debounceTimerRef.current = null;
227
+ }
228
+ if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {
229
+ const entry = {
230
+ state: preDebounceStateRef.current,
231
+ timestamp: Date.now()
232
+ };
233
+ pushToPast(entry);
234
+ futureRef.current = [];
235
+ setFuture([]);
236
+ pendingStateRef.current = null;
237
+ preDebounceStateRef.current = null;
238
+ didFlush = true;
239
+ }
240
+ if (!didFlush) {
241
+ const entry = {
242
+ state: stateRef.current,
243
+ timestamp: Date.now(),
244
+ label
245
+ };
246
+ pushToPast(entry);
247
+ futureRef.current = [];
248
+ setFuture([]);
249
+ onSnapshot?.(entry);
250
+ } else if (label && pastRef.current.length > 0) {
251
+ pastRef.current[pastRef.current.length - 1].label = label;
252
+ setPast([...pastRef.current]);
253
+ }
88
254
  },
89
255
  [pushToPast, onSnapshot]
90
256
  );
91
257
  const clearDraft = useCallback(() => {
92
258
  clearHistory();
259
+ if (persist) {
260
+ removeDraft(persist.key);
261
+ }
93
262
  setStateRaw(initialState);
94
263
  stateRef.current = initialState;
95
- }, [clearHistory, initialState]);
264
+ }, [clearHistory, initialState, persist]);
96
265
  return {
97
266
  state,
98
267
  setState,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/useFormHistory.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import { useCallback, useRef, useState } from \"react\";\nimport type { HistoryEntry, UseFormHistoryOptions, UseFormHistoryReturn } from \"./types\";\n\nconst DEFAULT_MAX_HISTORY = 100;\n\nexport function useFormHistory<T>(\n initialState: T,\n options: UseFormHistoryOptions<T> = {},\n): UseFormHistoryReturn<T> {\n const { maxHistory = DEFAULT_MAX_HISTORY, onUndo, onRedo, onSnapshot } = 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 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 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 const entry: HistoryEntry<T> = { state: prev, timestamp: Date.now(), label };\n\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n setStateRaw(next);\n stateRef.current = next;\n },\n [pushToPast],\n );\n\n const undo = useCallback(() => {\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 }, [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 pastRef.current = [];\n futureRef.current = [];\n setPast([]);\n setFuture([]);\n }, []);\n\n const snapshot = useCallback(\n (label?: string) => {\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 },\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
+ {"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, PersistOptions, UseFormHistoryOptions, UseFormHistoryReturn } from \"./types\";\n\nconst DEFAULT_MAX_HISTORY = 100;\nconst DEFAULT_DEBOUNCE_MS = 300;\nconst DEFAULT_PERSIST_DEBOUNCE_MS = 500;\nconst DEFAULT_PERSIST_VERSION = 1;\n\ninterface PersistStorage {\n key: string;\n debounceMs: number;\n version: number;\n}\n\nfunction normalizePersist(persist: boolean | PersistOptions | undefined): PersistStorage | null {\n if (!persist) return null;\n if (typeof persist === \"boolean\") return null;\n return {\n key: persist.key,\n debounceMs: persist.debounceMs ?? DEFAULT_PERSIST_DEBOUNCE_MS,\n version: persist.version ?? DEFAULT_PERSIST_VERSION,\n };\n}\n\nfunction readDraft<T>(key: string, version: number): T | null {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) return null;\n const raw = window.localStorage.getItem(key);\n if (!raw) return null;\n const parsed = JSON.parse(raw);\n if (parsed.__version !== version) {\n window.localStorage.removeItem(key);\n return null;\n }\n return parsed.__state as T;\n } catch {\n return null;\n }\n}\n\nfunction writeDraft<T>(key: string, state: T, version: number): void {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) return;\n window.localStorage.setItem(key, JSON.stringify({ __state: state, __version: version }));\n } catch {\n // localStorage full or disabled — silently ignore\n }\n}\n\nfunction removeDraft(key: string): void {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) return;\n window.localStorage.removeItem(key);\n } catch {\n // silently ignore\n }\n}\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 persist: persistOption,\n onUndo,\n onRedo,\n onSnapshot,\n } = options;\n\n const persist = normalizePersist(persistOption);\n const initializedRef = useRef(false);\n\n // Hydrate from localStorage on first render\n const [state, setStateRaw] = useState<T>(() => {\n if (persist) {\n const draft = readDraft<T>(persist.key, persist.version);\n if (draft !== null) {\n initializedRef.current = true;\n return draft;\n }\n }\n return initialState;\n });\n\n const stateRef = useRef<T>(state);\n stateRef.current = state;\n\n const pastRef = useRef<HistoryEntry<T>[]>([]);\n const futureRef = useRef<HistoryEntry<T>[]>([]);\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 // Persist debounce timer\n const persistTimerRef = useRef<ReturnType<typeof setTimeout> | 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 // Cleanup timers on unmount\n useEffect(() => {\n return () => {\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n }\n if (persistTimerRef.current !== null) {\n clearTimeout(persistTimerRef.current);\n }\n };\n }, []);\n\n // Debounced persist helper\n const schedulePersist = useCallback(\n (nextState: T) => {\n if (!persist) return;\n if (persistTimerRef.current !== null) {\n clearTimeout(persistTimerRef.current);\n }\n persistTimerRef.current = setTimeout(() => {\n writeDraft(persist.key, nextState, persist.version);\n persistTimerRef.current = null;\n }, persist.debounceMs);\n },\n [persist],\n );\n\n // Persist immediately (used by undo/redo/snapshot/clearDraft which bypass debounce)\n const persistNow = useCallback(\n (nextState: T) => {\n if (!persist) return;\n if (persistTimerRef.current !== null) {\n clearTimeout(persistTimerRef.current);\n persistTimerRef.current = null;\n }\n writeDraft(persist.key, nextState, persist.version);\n },\n [persist],\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 schedulePersist(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, schedulePersist],\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 persistNow(prev.state);\n onUndo?.(prev.state);\n }, [pushToPast, onUndo, persistNow]);\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 persistNow(next.state);\n onRedo?.(next.state);\n }, [onRedo, persistNow]);\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 if (persist) {\n removeDraft(persist.key);\n }\n setStateRaw(initialState);\n stateRef.current = initialState;\n }, [clearHistory, initialState, persist]);\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-form-rewind",
3
- "version": "0.2.0",
3
+ "version": "0.4.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
  }