react-form-rewind 0.4.0 → 1.0.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
@@ -51,7 +51,7 @@ import { useFormHistory } from "react-form-rewind";
51
51
  function MyForm() {
52
52
  const { state, setState, undo, redo, canUndo, canRedo } = useFormHistory(
53
53
  { name: "", email: "" },
54
- { persist: { key: "my-form-draft" } }
54
+ { keyboard: true, persist: { key: "my-form-draft" } }
55
55
  );
56
56
 
57
57
  return (
@@ -108,6 +108,7 @@ The core hook that manages a history-backed state stack.
108
108
  | `maxHistory` | `number` | `100` | Maximum past entries to retain |
109
109
  | `debounceMs` | `number` | `300` | Debounce window for rapid state changes |
110
110
  | `persist` | `boolean \| PersistOptions` | `false` | Enable draft persistence |
111
+ | `keyboard` | `boolean` | `false` | Enable Ctrl+Z / Ctrl+Shift+Z keyboard shortcuts |
111
112
  | `onUndo` | `(state: T) => void` | — | Callback after undo |
112
113
  | `onRedo` | `(state: T) => void` | — | Callback after redo |
113
114
  | `onSnapshot` | `(entry: HistoryEntry<T>) => void` | — | Callback when a snapshot is committed |
@@ -126,7 +127,7 @@ The core hook that manages a history-backed state stack.
126
127
 
127
128
  ### Keyboard Shortcuts
128
129
 
129
- 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.
130
131
 
131
132
  ### Snapshot Debouncing
132
133
 
package/dist/index.d.mts CHANGED
@@ -1,17 +1,47 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface FieldRules {
5
+ required?: boolean | string;
6
+ pattern?: RegExp | {
7
+ value: RegExp;
8
+ message: string;
9
+ };
10
+ minLength?: number | {
11
+ value: number;
12
+ message: string;
13
+ };
14
+ maxLength?: number | {
15
+ value: number;
16
+ message: string;
17
+ };
18
+ min?: number | {
19
+ value: number;
20
+ message: string;
21
+ };
22
+ max?: number | {
23
+ value: number;
24
+ message: string;
25
+ };
26
+ validate?: (value: unknown) => string | null;
27
+ }
28
+ interface FieldError {
29
+ message: string;
30
+ type: string;
31
+ }
32
+ declare function validateField(value: unknown, rules: FieldRules): FieldError | null;
33
+ declare function validateAll(state: Record<string, unknown>, fieldRules: Record<string, FieldRules>): Record<string, FieldError>;
34
+
1
35
  interface HistoryEntry<T> {
2
36
  state: T;
3
37
  timestamp: number;
4
38
  label?: string;
5
39
  }
6
- interface HistoryStack<T> {
7
- past: HistoryEntry<T>[];
8
- present: T;
9
- future: HistoryEntry<T>[];
10
- }
11
40
  interface UseFormHistoryOptions<T> {
12
41
  maxHistory?: number;
13
42
  debounceMs?: number;
14
43
  persist?: boolean | PersistOptions;
44
+ keyboard?: boolean;
15
45
  onUndo?: (state: T) => void;
16
46
  onRedo?: (state: T) => void;
17
47
  onSnapshot?: (entry: HistoryEntry<T>) => void;
@@ -34,7 +64,65 @@ interface UseFormHistoryReturn<T> {
34
64
  past: HistoryEntry<T>[];
35
65
  future: HistoryEntry<T>[];
36
66
  }
67
+ interface FieldMeta {
68
+ name: string;
69
+ rules?: FieldRules;
70
+ touched: boolean;
71
+ }
72
+ interface FormRewindContextValue {
73
+ state: Record<string, unknown>;
74
+ setState: (name: string, value: unknown) => void;
75
+ errors: Record<string, FieldError>;
76
+ setError: (name: string, error: FieldError) => void;
77
+ clearError: (name: string) => void;
78
+ touched: Record<string, boolean>;
79
+ touch: (name: string) => void;
80
+ fields: Record<string, FieldMeta>;
81
+ registerField: (name: string, rules?: FieldRules) => void;
82
+ unregisterField: (name: string) => void;
83
+ }
37
84
 
38
85
  declare function useFormHistory<T>(initialState: T, options?: UseFormHistoryOptions<T>): UseFormHistoryReturn<T>;
39
86
 
40
- export { type HistoryEntry, type HistoryStack, type PersistOptions, type UseFormHistoryOptions, type UseFormHistoryReturn, useFormHistory };
87
+ declare function useFormRewindContext(): FormRewindContextValue;
88
+ interface FormRewindProps<T extends Record<string, unknown>> {
89
+ initialState: T;
90
+ children: ReactNode;
91
+ onSubmit?: (state: T) => void;
92
+ maxHistory?: UseFormHistoryOptions<T>["maxHistory"];
93
+ debounceMs?: UseFormHistoryOptions<T>["debounceMs"];
94
+ persist?: UseFormHistoryOptions<T>["persist"];
95
+ keyboard?: UseFormHistoryOptions<T>["keyboard"];
96
+ }
97
+ declare function FormRewind<T extends Record<string, unknown>>({ initialState, children, onSubmit, maxHistory, debounceMs, persist, keyboard, }: FormRewindProps<T>): react.JSX.Element;
98
+
99
+ interface BaseFieldProps {
100
+ name: string;
101
+ label?: string;
102
+ rules?: FieldRules;
103
+ className?: string;
104
+ style?: React.CSSProperties;
105
+ }
106
+ interface TextFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "onChange"> {
107
+ }
108
+ declare function TextField({ name, label, rules, className, style, ...inputProps }: TextFieldProps): react.JSX.Element;
109
+ interface NumberFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "onChange" | "type"> {
110
+ }
111
+ declare function NumberField({ name, label, rules, className, style, ...inputProps }: NumberFieldProps): react.JSX.Element;
112
+ interface CheckboxFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "checked" | "onChange"> {
113
+ }
114
+ declare function CheckboxField({ name, label, rules, className, style, ...inputProps }: CheckboxFieldProps): react.JSX.Element;
115
+ interface SelectOption {
116
+ value: string;
117
+ label: string;
118
+ }
119
+ interface SelectFieldProps extends BaseFieldProps {
120
+ options: SelectOption[];
121
+ placeholder?: string;
122
+ }
123
+ declare function SelectField({ name, label, rules, options, placeholder, className, style }: SelectFieldProps): react.JSX.Element;
124
+ interface TextareaFieldProps extends BaseFieldProps, Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "name" | "value" | "onChange"> {
125
+ }
126
+ declare function TextareaField({ name, label, rules, className, style, ...textareaProps }: TextareaFieldProps): react.JSX.Element;
127
+
128
+ export { CheckboxField, type FieldError, type FieldMeta, type FieldRules, FormRewind, type FormRewindContextValue, type FormRewindProps, type HistoryEntry, NumberField, type PersistOptions, SelectField, TextField, TextareaField, type UseFormHistoryOptions, type UseFormHistoryReturn, useFormHistory, useFormRewindContext, validateAll, validateField };
package/dist/index.d.ts CHANGED
@@ -1,17 +1,47 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface FieldRules {
5
+ required?: boolean | string;
6
+ pattern?: RegExp | {
7
+ value: RegExp;
8
+ message: string;
9
+ };
10
+ minLength?: number | {
11
+ value: number;
12
+ message: string;
13
+ };
14
+ maxLength?: number | {
15
+ value: number;
16
+ message: string;
17
+ };
18
+ min?: number | {
19
+ value: number;
20
+ message: string;
21
+ };
22
+ max?: number | {
23
+ value: number;
24
+ message: string;
25
+ };
26
+ validate?: (value: unknown) => string | null;
27
+ }
28
+ interface FieldError {
29
+ message: string;
30
+ type: string;
31
+ }
32
+ declare function validateField(value: unknown, rules: FieldRules): FieldError | null;
33
+ declare function validateAll(state: Record<string, unknown>, fieldRules: Record<string, FieldRules>): Record<string, FieldError>;
34
+
1
35
  interface HistoryEntry<T> {
2
36
  state: T;
3
37
  timestamp: number;
4
38
  label?: string;
5
39
  }
6
- interface HistoryStack<T> {
7
- past: HistoryEntry<T>[];
8
- present: T;
9
- future: HistoryEntry<T>[];
10
- }
11
40
  interface UseFormHistoryOptions<T> {
12
41
  maxHistory?: number;
13
42
  debounceMs?: number;
14
43
  persist?: boolean | PersistOptions;
44
+ keyboard?: boolean;
15
45
  onUndo?: (state: T) => void;
16
46
  onRedo?: (state: T) => void;
17
47
  onSnapshot?: (entry: HistoryEntry<T>) => void;
@@ -34,7 +64,65 @@ interface UseFormHistoryReturn<T> {
34
64
  past: HistoryEntry<T>[];
35
65
  future: HistoryEntry<T>[];
36
66
  }
67
+ interface FieldMeta {
68
+ name: string;
69
+ rules?: FieldRules;
70
+ touched: boolean;
71
+ }
72
+ interface FormRewindContextValue {
73
+ state: Record<string, unknown>;
74
+ setState: (name: string, value: unknown) => void;
75
+ errors: Record<string, FieldError>;
76
+ setError: (name: string, error: FieldError) => void;
77
+ clearError: (name: string) => void;
78
+ touched: Record<string, boolean>;
79
+ touch: (name: string) => void;
80
+ fields: Record<string, FieldMeta>;
81
+ registerField: (name: string, rules?: FieldRules) => void;
82
+ unregisterField: (name: string) => void;
83
+ }
37
84
 
38
85
  declare function useFormHistory<T>(initialState: T, options?: UseFormHistoryOptions<T>): UseFormHistoryReturn<T>;
39
86
 
40
- export { type HistoryEntry, type HistoryStack, type PersistOptions, type UseFormHistoryOptions, type UseFormHistoryReturn, useFormHistory };
87
+ declare function useFormRewindContext(): FormRewindContextValue;
88
+ interface FormRewindProps<T extends Record<string, unknown>> {
89
+ initialState: T;
90
+ children: ReactNode;
91
+ onSubmit?: (state: T) => void;
92
+ maxHistory?: UseFormHistoryOptions<T>["maxHistory"];
93
+ debounceMs?: UseFormHistoryOptions<T>["debounceMs"];
94
+ persist?: UseFormHistoryOptions<T>["persist"];
95
+ keyboard?: UseFormHistoryOptions<T>["keyboard"];
96
+ }
97
+ declare function FormRewind<T extends Record<string, unknown>>({ initialState, children, onSubmit, maxHistory, debounceMs, persist, keyboard, }: FormRewindProps<T>): react.JSX.Element;
98
+
99
+ interface BaseFieldProps {
100
+ name: string;
101
+ label?: string;
102
+ rules?: FieldRules;
103
+ className?: string;
104
+ style?: React.CSSProperties;
105
+ }
106
+ interface TextFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "onChange"> {
107
+ }
108
+ declare function TextField({ name, label, rules, className, style, ...inputProps }: TextFieldProps): react.JSX.Element;
109
+ interface NumberFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "onChange" | "type"> {
110
+ }
111
+ declare function NumberField({ name, label, rules, className, style, ...inputProps }: NumberFieldProps): react.JSX.Element;
112
+ interface CheckboxFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "checked" | "onChange"> {
113
+ }
114
+ declare function CheckboxField({ name, label, rules, className, style, ...inputProps }: CheckboxFieldProps): react.JSX.Element;
115
+ interface SelectOption {
116
+ value: string;
117
+ label: string;
118
+ }
119
+ interface SelectFieldProps extends BaseFieldProps {
120
+ options: SelectOption[];
121
+ placeholder?: string;
122
+ }
123
+ declare function SelectField({ name, label, rules, options, placeholder, className, style }: SelectFieldProps): react.JSX.Element;
124
+ interface TextareaFieldProps extends BaseFieldProps, Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "name" | "value" | "onChange"> {
125
+ }
126
+ declare function TextareaField({ name, label, rules, className, style, ...textareaProps }: TextareaFieldProps): react.JSX.Element;
127
+
128
+ export { CheckboxField, type FieldError, type FieldMeta, type FieldRules, FormRewind, type FormRewindContextValue, type FormRewindProps, type HistoryEntry, NumberField, type PersistOptions, SelectField, TextField, TextareaField, type UseFormHistoryOptions, type UseFormHistoryReturn, useFormHistory, useFormRewindContext, validateAll, validateField };
package/dist/index.js CHANGED
@@ -1,284 +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
- 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
- }
48
- function useFormHistory(initialState, 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;
71
- const pastRef = react.useRef([]);
72
- const futureRef = react.useRef([]);
73
- const [past, setPast] = react.useState([]);
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);
79
- const pushToPast = react.useCallback(
80
- (entry) => {
81
- const next = [...pastRef.current, entry];
82
- if (next.length > maxHistory) {
83
- next.splice(0, next.length - maxHistory);
84
- }
85
- pastRef.current = next;
86
- setPast(next);
87
- },
88
- [maxHistory]
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
- );
124
- const setState = react.useCallback(
125
- (value, label) => {
126
- const prev = stateRef.current;
127
- const next = typeof value === "function" ? value(prev) : value;
128
- if (Object.is(prev, next)) return;
129
- setStateRaw(next);
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);
159
- },
160
- [debounceMs, pushToPast, schedulePersist]
161
- );
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
- }
178
- if (pastRef.current.length === 0) return;
179
- const prev = pastRef.current[pastRef.current.length - 1];
180
- const newPast = pastRef.current.slice(0, -1);
181
- pastRef.current = newPast;
182
- setPast(newPast);
183
- const currentEntry = {
184
- state: stateRef.current,
185
- timestamp: Date.now()
186
- };
187
- futureRef.current = [...futureRef.current, currentEntry];
188
- setFuture(futureRef.current);
189
- setStateRaw(prev.state);
190
- stateRef.current = prev.state;
191
- persistNow(prev.state);
192
- onUndo?.(prev.state);
193
- }, [pushToPast, onUndo, persistNow]);
194
- const redo = react.useCallback(() => {
195
- if (futureRef.current.length === 0) return;
196
- const next = futureRef.current[futureRef.current.length - 1];
197
- const newFuture = futureRef.current.slice(0, -1);
198
- futureRef.current = newFuture;
199
- setFuture(newFuture);
200
- const currentEntry = {
201
- state: stateRef.current,
202
- timestamp: Date.now()
203
- };
204
- pastRef.current = [...pastRef.current, currentEntry];
205
- setPast(pastRef.current);
206
- setStateRaw(next.state);
207
- stateRef.current = next.state;
208
- persistNow(next.state);
209
- onRedo?.(next.state);
210
- }, [onRedo, persistNow]);
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;
218
- pastRef.current = [];
219
- futureRef.current = [];
220
- setPast([]);
221
- setFuture([]);
222
- }, []);
223
- const snapshot = react.useCallback(
224
- (label) => {
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
- }
256
- },
257
- [pushToPast, onSnapshot]
258
- );
259
- const clearDraft = react.useCallback(() => {
260
- clearHistory();
261
- if (persist) {
262
- removeDraft(persist.key);
263
- }
264
- setStateRaw(initialState);
265
- stateRef.current = initialState;
266
- }, [clearHistory, initialState, persist]);
267
- return {
268
- state,
269
- setState,
270
- undo,
271
- redo,
272
- canUndo: past.length > 0,
273
- canRedo: future.length > 0,
274
- clearHistory,
275
- clearDraft,
276
- snapshot,
277
- past,
278
- future
279
- };
280
- }
281
-
282
- exports.useFormHistory = useFormHistory;
283
- //# sourceMappingURL=index.js.map
284
- //# sourceMappingURL=index.js.map
1
+ 'use strict';var react=require('react'),jsxRuntime=require('react/jsx-runtime');var ue=100,ce=300,ae=500,de=1;function pe(e){return !e||typeof e=="boolean"?null:{key:e.key,debounceMs:e.debounceMs??ae,version:e.version??de}}function K(){return typeof window<"u"&&!!window.localStorage}function me(e,t){try{if(!K())return null;let s=window.localStorage.getItem(e);if(!s)return null;let u=JSON.parse(s);return u.__v!==t?(window.localStorage.removeItem(e),null):u.__s}catch{return null}}function Y(e,t,s){try{if(!K())return;window.localStorage.setItem(e,JSON.stringify({__s:t,__v:s}));}catch{}}function ge(e){try{if(!K())return;window.localStorage.removeItem(e);}catch{}}function z(e,t={}){let{maxHistory:s=ue,debounceMs:u=ce,persist:i,keyboard:a=false,onUndo:r,onRedo:c,onSnapshot:T}=t,n=pe(i),[m,x]=react.useState(()=>{if(n){let l=me(n.key,n.version);if(l!==null)return l}return e}),d=react.useRef(m);d.current=m;let y=react.useRef([]),b=react.useRef([]),[U,S]=react.useState([]),[I,L]=react.useState([]),F=react.useRef(null),E=react.useRef(null),o=react.useRef(null),p=react.useRef(null),f=react.useCallback(()=>{b.current=[],L([]);},[]),R=react.useCallback(l=>{let g=[...y.current,l];g.length>s&&g.splice(0,g.length-s),y.current=g,S(g);},[s]);react.useEffect(()=>()=>{F.current!==null&&clearTimeout(F.current),p.current!==null&&clearTimeout(p.current);},[]);let M=react.useCallback(l=>{n&&(p.current!==null&&clearTimeout(p.current),p.current=setTimeout(()=>{Y(n.key,l,n.version),p.current=null;},n.debounceMs));},[n]),_=react.useCallback(l=>{n&&(p.current!==null&&(clearTimeout(p.current),p.current=null),Y(n.key,l,n.version));},[n]),se=react.useCallback((l,g)=>{let v=d.current,C=typeof l=="function"?l(v):l;if(!Object.is(v,C)){if(x(C),d.current=C,M(C),u<=0){let q={state:v,timestamp:Date.now(),label:g};R(q),f();return}E.current===null&&(o.current=v),E.current=C,F.current!==null&&clearTimeout(F.current),F.current=setTimeout(()=>{let q={state:o.current,timestamp:Date.now(),label:g};R(q),f(),F.current=null,E.current=null,o.current=null;},u);}},[u,R,M,f]),A=react.useCallback(()=>{if(F.current!==null&&(clearTimeout(F.current),F.current=null),E.current!==null&&o.current!==null){let C={state:o.current,timestamp:Date.now()};R(C),f(),E.current=null,o.current=null;}if(y.current.length===0)return;let l=y.current[y.current.length-1],g=y.current.slice(0,-1);y.current=g,S(g);let v={state:d.current,timestamp:Date.now()};b.current=[...b.current,v],L(b.current),x(l.state),d.current=l.state,_(l.state),r?.(l.state);},[R,r,_,f]),$=react.useCallback(()=>{if(b.current.length===0)return;let l=b.current[b.current.length-1],g=b.current.slice(0,-1);b.current=g,L(g);let v={state:d.current,timestamp:Date.now()};y.current=[...y.current,v],S(y.current),x(l.state),d.current=l.state,_(l.state),c?.(l.state);},[c,_]);react.useEffect(()=>{if(!a)return;let l=g=>{!(g.metaKey||g.ctrlKey)||g.key!=="z"||(g.preventDefault(),g.shiftKey?$():A());};return window.addEventListener("keydown",l),()=>window.removeEventListener("keydown",l)},[a,A,$]);let V=react.useCallback(()=>{F.current!==null&&(clearTimeout(F.current),F.current=null),E.current=null,o.current=null,y.current=[],b.current=[],S([]),L([]);},[]),ie=react.useCallback(l=>{let g=false;if(F.current!==null&&(clearTimeout(F.current),F.current=null),E.current!==null&&o.current!==null){let v={state:o.current,timestamp:Date.now()};R(v),f(),E.current=null,o.current=null,g=true;}if(g)l&&y.current.length>0&&(y.current[y.current.length-1].label=l,S([...y.current]));else {let v={state:d.current,timestamp:Date.now(),label:l};R(v),f(),T?.(v);}},[R,T,f]),le=react.useCallback(()=>{V(),n&&ge(n.key),x(e),d.current=e;},[V,e,n]);return {state:m,setState:se,undo:A,redo:$,canUndo:U.length>0,canRedo:I.length>0,clearHistory:V,clearDraft:le,snapshot:ie,past:U,future:I}}function N(e,t){return e&&typeof e=="object"&&"value"in e?e:{value:e,message:t}}function B(e,t){let u=typeof e=="number"?e:Number(e);if(t.required&&(typeof e=="string"?e.trim()==="":e==null||e===false))return {message:typeof t.required=="string"?t.required:"Required",type:"required"};if(t.pattern!=null){let{value:i,message:a}=N(t.pattern,"Invalid format");if(typeof e=="string"&&!i.test(e))return {message:a,type:"pattern"}}if(t.minLength!=null&&typeof e=="string"){let{value:i,message:a}=N(t.minLength,`Min ${t.minLength} characters`);if(e.length<i)return {message:a,type:"minLength"}}if(t.maxLength!=null&&typeof e=="string"){let{value:i,message:a}=N(t.maxLength,`Max ${t.maxLength} characters`);if(e.length>i)return {message:a,type:"maxLength"}}if(t.min!=null&&!isNaN(u)){let{value:i,message:a}=N(t.min,`Min ${t.min}`);if(u<i)return {message:a,type:"min"}}if(t.max!=null&&!isNaN(u)){let{value:i,message:a}=N(t.max,`Max ${t.max}`);if(u>i)return {message:a,type:"max"}}if(t.validate){let i=t.validate(e);if(i)return {message:i,type:"validate"}}return null}function J(e,t){let s={};for(let u in t){let i=B(e[u],t[u]);i&&(s[u]=i);}return s}var W=react.createContext(null);function H(){let e=react.useContext(W);if(!e)throw new Error("useFormRewindContext must be used within <FormRewind>");return e}function Z({initialState:e,children:t,onSubmit:s,maxHistory:u,debounceMs:i,persist:a,keyboard:r}){let c=z(e,{maxHistory:u,debounceMs:i,persist:a,keyboard:r}),[T,n]=react.useState({}),[m,x]=react.useState({}),d=react.useRef({}),y=react.useCallback((o,p)=>{d.current[o]=p??{};},[]),b=react.useCallback(o=>{delete d.current[o];},[]),U=react.useCallback((o,p)=>{n(f=>({...f,[o]:p}));},[]),S=react.useCallback(o=>{n(p=>{let f={...p};return delete f[o],f});},[]),I=react.useCallback(o=>{x(p=>({...p,[o]:true}));},[]),L=react.useCallback((o,p)=>{if(c.setState(f=>({...f,[o]:p}),o),m[o]&&d.current[o]){let f=B(p,d.current[o]);n(f?R=>({...R,[o]:f}):R=>{let M={...R};return delete M[o],M});}},[c.setState,m]),F=react.useCallback(o=>{o&&o.preventDefault();let p={};for(let R in d.current)p[R]=true;x(p);let f=J(c.state,d.current);return n(f),Object.keys(f).length===0&&s&&s(c.state),Object.keys(f).length===0},[c.state,s]),E={state:c.state,setState:L,errors:T,setError:U,clearError:S,touched:m,touch:I,fields:Object.fromEntries(Object.entries(d.current).map(([o,p])=>[o,{name:o,rules:p,touched:!!m[o]}])),registerField:y,unregisterField:b};return jsxRuntime.jsx(W.Provider,{value:E,children:jsxRuntime.jsx("form",{onSubmit:F,noValidate:true,children:t})})}function ee({name:e,label:t,rules:s,className:u,style:i,...a}){let r=H();react.useEffect(()=>(r.registerField(e,s),()=>r.unregisterField(e)),[e,s,r.registerField,r.unregisterField]);let c=x=>{r.setState(e,x.target.value);},T=()=>{r.touch(e);},n=r.errors[e],m=String(r.state[e]??"");return jsxRuntime.jsxs("div",{className:u,style:i,children:[t&&jsxRuntime.jsx("label",{htmlFor:e,children:t}),jsxRuntime.jsx("input",{id:e,type:"text",value:m,onChange:c,onBlur:T,"aria-invalid":!!n,"aria-describedby":n?`${e}-error`:void 0,...a}),n&&jsxRuntime.jsx("span",{id:`${e}-error`,role:"alert",children:n.message})]})}function te({name:e,label:t,rules:s,className:u,style:i,...a}){let r=H();react.useEffect(()=>(r.registerField(e,s),()=>r.unregisterField(e)),[e,s,r.registerField,r.unregisterField]);let c=x=>{let d=x.target.value===""?"":Number(x.target.value);r.setState(e,d);},T=()=>{r.touch(e);},n=r.errors[e],m=r.state[e];return jsxRuntime.jsxs("div",{className:u,style:i,children:[t&&jsxRuntime.jsx("label",{htmlFor:e,children:t}),jsxRuntime.jsx("input",{id:e,type:"number",value:m===""||m==null?"":Number(m),onChange:c,onBlur:T,"aria-invalid":!!n,"aria-describedby":n?`${e}-error`:void 0,...a}),n&&jsxRuntime.jsx("span",{id:`${e}-error`,role:"alert",children:n.message})]})}function re({name:e,label:t,rules:s,className:u,style:i,...a}){let r=H();react.useEffect(()=>(r.registerField(e,s),()=>r.unregisterField(e)),[e,s,r.registerField,r.unregisterField]);let c=m=>{r.setState(e,m.target.checked);},T=r.errors[e],n=!!r.state[e];return jsxRuntime.jsxs("div",{className:u,style:i,children:[jsxRuntime.jsxs("label",{children:[jsxRuntime.jsx("input",{type:"checkbox",checked:n,onChange:c,"aria-invalid":!!T,...a}),t]}),T&&jsxRuntime.jsx("span",{role:"alert",children:T.message})]})}function ne({name:e,label:t,rules:s,options:u,placeholder:i,className:a,style:r}){let c=H();react.useEffect(()=>(c.registerField(e,s),()=>c.unregisterField(e)),[e,s,c.registerField,c.unregisterField]);let T=d=>{c.setState(e,d.target.value);},n=()=>{c.touch(e);},m=c.errors[e],x=String(c.state[e]??"");return jsxRuntime.jsxs("div",{className:a,style:r,children:[t&&jsxRuntime.jsx("label",{htmlFor:e,children:t}),jsxRuntime.jsxs("select",{id:e,value:x,onChange:T,onBlur:n,"aria-invalid":!!m,"aria-describedby":m?`${e}-error`:void 0,children:[i&&jsxRuntime.jsx("option",{value:"",disabled:true,children:i}),u.map(d=>jsxRuntime.jsx("option",{value:d.value,children:d.label},d.value))]}),m&&jsxRuntime.jsx("span",{id:`${e}-error`,role:"alert",children:m.message})]})}function oe({name:e,label:t,rules:s,className:u,style:i,...a}){let r=H();react.useEffect(()=>(r.registerField(e,s),()=>r.unregisterField(e)),[e,s,r.registerField,r.unregisterField]);let c=x=>{r.setState(e,x.target.value);},T=()=>{r.touch(e);},n=r.errors[e],m=String(r.state[e]??"");return jsxRuntime.jsxs("div",{className:u,style:i,children:[t&&jsxRuntime.jsx("label",{htmlFor:e,children:t}),jsxRuntime.jsx("textarea",{id:e,value:m,onChange:c,onBlur:T,"aria-invalid":!!n,"aria-describedby":n?`${e}-error`:void 0,...a}),n&&jsxRuntime.jsx("span",{id:`${e}-error`,role:"alert",children:n.message})]})}exports.CheckboxField=re;exports.FormRewind=Z;exports.NumberField=te;exports.SelectField=ne;exports.TextField=ee;exports.TextareaField=oe;exports.useFormHistory=z;exports.useFormRewindContext=H;exports.validateAll=J;exports.validateField=B;
package/dist/index.mjs CHANGED
@@ -1,282 +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
- 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
- }
46
- function useFormHistory(initialState, 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;
69
- const pastRef = useRef([]);
70
- const futureRef = useRef([]);
71
- const [past, setPast] = useState([]);
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);
77
- const pushToPast = useCallback(
78
- (entry) => {
79
- const next = [...pastRef.current, entry];
80
- if (next.length > maxHistory) {
81
- next.splice(0, next.length - maxHistory);
82
- }
83
- pastRef.current = next;
84
- setPast(next);
85
- },
86
- [maxHistory]
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
- );
122
- const setState = useCallback(
123
- (value, label) => {
124
- const prev = stateRef.current;
125
- const next = typeof value === "function" ? value(prev) : value;
126
- if (Object.is(prev, next)) return;
127
- setStateRaw(next);
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);
157
- },
158
- [debounceMs, pushToPast, schedulePersist]
159
- );
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
- }
176
- if (pastRef.current.length === 0) return;
177
- const prev = pastRef.current[pastRef.current.length - 1];
178
- const newPast = pastRef.current.slice(0, -1);
179
- pastRef.current = newPast;
180
- setPast(newPast);
181
- const currentEntry = {
182
- state: stateRef.current,
183
- timestamp: Date.now()
184
- };
185
- futureRef.current = [...futureRef.current, currentEntry];
186
- setFuture(futureRef.current);
187
- setStateRaw(prev.state);
188
- stateRef.current = prev.state;
189
- persistNow(prev.state);
190
- onUndo?.(prev.state);
191
- }, [pushToPast, onUndo, persistNow]);
192
- const redo = useCallback(() => {
193
- if (futureRef.current.length === 0) return;
194
- const next = futureRef.current[futureRef.current.length - 1];
195
- const newFuture = futureRef.current.slice(0, -1);
196
- futureRef.current = newFuture;
197
- setFuture(newFuture);
198
- const currentEntry = {
199
- state: stateRef.current,
200
- timestamp: Date.now()
201
- };
202
- pastRef.current = [...pastRef.current, currentEntry];
203
- setPast(pastRef.current);
204
- setStateRaw(next.state);
205
- stateRef.current = next.state;
206
- persistNow(next.state);
207
- onRedo?.(next.state);
208
- }, [onRedo, persistNow]);
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;
216
- pastRef.current = [];
217
- futureRef.current = [];
218
- setPast([]);
219
- setFuture([]);
220
- }, []);
221
- const snapshot = useCallback(
222
- (label) => {
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
- }
254
- },
255
- [pushToPast, onSnapshot]
256
- );
257
- const clearDraft = useCallback(() => {
258
- clearHistory();
259
- if (persist) {
260
- removeDraft(persist.key);
261
- }
262
- setStateRaw(initialState);
263
- stateRef.current = initialState;
264
- }, [clearHistory, initialState, persist]);
265
- return {
266
- state,
267
- setState,
268
- undo,
269
- redo,
270
- canUndo: past.length > 0,
271
- canRedo: future.length > 0,
272
- clearHistory,
273
- clearDraft,
274
- snapshot,
275
- past,
276
- future
277
- };
278
- }
279
-
280
- export { useFormHistory };
281
- //# sourceMappingURL=index.mjs.map
282
- //# sourceMappingURL=index.mjs.map
1
+ import {createContext,useState,useRef,useCallback,useEffect,useContext}from'react';import {jsx,jsxs}from'react/jsx-runtime';var ue=100,ce=300,ae=500,de=1;function pe(e){return !e||typeof e=="boolean"?null:{key:e.key,debounceMs:e.debounceMs??ae,version:e.version??de}}function K(){return typeof window<"u"&&!!window.localStorage}function me(e,t){try{if(!K())return null;let s=window.localStorage.getItem(e);if(!s)return null;let u=JSON.parse(s);return u.__v!==t?(window.localStorage.removeItem(e),null):u.__s}catch{return null}}function Y(e,t,s){try{if(!K())return;window.localStorage.setItem(e,JSON.stringify({__s:t,__v:s}));}catch{}}function ge(e){try{if(!K())return;window.localStorage.removeItem(e);}catch{}}function z(e,t={}){let{maxHistory:s=ue,debounceMs:u=ce,persist:i,keyboard:a=false,onUndo:r,onRedo:c,onSnapshot:T}=t,n=pe(i),[m,x]=useState(()=>{if(n){let l=me(n.key,n.version);if(l!==null)return l}return e}),d=useRef(m);d.current=m;let y=useRef([]),b=useRef([]),[U,S]=useState([]),[I,L]=useState([]),F=useRef(null),E=useRef(null),o=useRef(null),p=useRef(null),f=useCallback(()=>{b.current=[],L([]);},[]),R=useCallback(l=>{let g=[...y.current,l];g.length>s&&g.splice(0,g.length-s),y.current=g,S(g);},[s]);useEffect(()=>()=>{F.current!==null&&clearTimeout(F.current),p.current!==null&&clearTimeout(p.current);},[]);let M=useCallback(l=>{n&&(p.current!==null&&clearTimeout(p.current),p.current=setTimeout(()=>{Y(n.key,l,n.version),p.current=null;},n.debounceMs));},[n]),_=useCallback(l=>{n&&(p.current!==null&&(clearTimeout(p.current),p.current=null),Y(n.key,l,n.version));},[n]),se=useCallback((l,g)=>{let v=d.current,C=typeof l=="function"?l(v):l;if(!Object.is(v,C)){if(x(C),d.current=C,M(C),u<=0){let q={state:v,timestamp:Date.now(),label:g};R(q),f();return}E.current===null&&(o.current=v),E.current=C,F.current!==null&&clearTimeout(F.current),F.current=setTimeout(()=>{let q={state:o.current,timestamp:Date.now(),label:g};R(q),f(),F.current=null,E.current=null,o.current=null;},u);}},[u,R,M,f]),A=useCallback(()=>{if(F.current!==null&&(clearTimeout(F.current),F.current=null),E.current!==null&&o.current!==null){let C={state:o.current,timestamp:Date.now()};R(C),f(),E.current=null,o.current=null;}if(y.current.length===0)return;let l=y.current[y.current.length-1],g=y.current.slice(0,-1);y.current=g,S(g);let v={state:d.current,timestamp:Date.now()};b.current=[...b.current,v],L(b.current),x(l.state),d.current=l.state,_(l.state),r?.(l.state);},[R,r,_,f]),$=useCallback(()=>{if(b.current.length===0)return;let l=b.current[b.current.length-1],g=b.current.slice(0,-1);b.current=g,L(g);let v={state:d.current,timestamp:Date.now()};y.current=[...y.current,v],S(y.current),x(l.state),d.current=l.state,_(l.state),c?.(l.state);},[c,_]);useEffect(()=>{if(!a)return;let l=g=>{!(g.metaKey||g.ctrlKey)||g.key!=="z"||(g.preventDefault(),g.shiftKey?$():A());};return window.addEventListener("keydown",l),()=>window.removeEventListener("keydown",l)},[a,A,$]);let V=useCallback(()=>{F.current!==null&&(clearTimeout(F.current),F.current=null),E.current=null,o.current=null,y.current=[],b.current=[],S([]),L([]);},[]),ie=useCallback(l=>{let g=false;if(F.current!==null&&(clearTimeout(F.current),F.current=null),E.current!==null&&o.current!==null){let v={state:o.current,timestamp:Date.now()};R(v),f(),E.current=null,o.current=null,g=true;}if(g)l&&y.current.length>0&&(y.current[y.current.length-1].label=l,S([...y.current]));else {let v={state:d.current,timestamp:Date.now(),label:l};R(v),f(),T?.(v);}},[R,T,f]),le=useCallback(()=>{V(),n&&ge(n.key),x(e),d.current=e;},[V,e,n]);return {state:m,setState:se,undo:A,redo:$,canUndo:U.length>0,canRedo:I.length>0,clearHistory:V,clearDraft:le,snapshot:ie,past:U,future:I}}function N(e,t){return e&&typeof e=="object"&&"value"in e?e:{value:e,message:t}}function B(e,t){let u=typeof e=="number"?e:Number(e);if(t.required&&(typeof e=="string"?e.trim()==="":e==null||e===false))return {message:typeof t.required=="string"?t.required:"Required",type:"required"};if(t.pattern!=null){let{value:i,message:a}=N(t.pattern,"Invalid format");if(typeof e=="string"&&!i.test(e))return {message:a,type:"pattern"}}if(t.minLength!=null&&typeof e=="string"){let{value:i,message:a}=N(t.minLength,`Min ${t.minLength} characters`);if(e.length<i)return {message:a,type:"minLength"}}if(t.maxLength!=null&&typeof e=="string"){let{value:i,message:a}=N(t.maxLength,`Max ${t.maxLength} characters`);if(e.length>i)return {message:a,type:"maxLength"}}if(t.min!=null&&!isNaN(u)){let{value:i,message:a}=N(t.min,`Min ${t.min}`);if(u<i)return {message:a,type:"min"}}if(t.max!=null&&!isNaN(u)){let{value:i,message:a}=N(t.max,`Max ${t.max}`);if(u>i)return {message:a,type:"max"}}if(t.validate){let i=t.validate(e);if(i)return {message:i,type:"validate"}}return null}function J(e,t){let s={};for(let u in t){let i=B(e[u],t[u]);i&&(s[u]=i);}return s}var W=createContext(null);function H(){let e=useContext(W);if(!e)throw new Error("useFormRewindContext must be used within <FormRewind>");return e}function Z({initialState:e,children:t,onSubmit:s,maxHistory:u,debounceMs:i,persist:a,keyboard:r}){let c=z(e,{maxHistory:u,debounceMs:i,persist:a,keyboard:r}),[T,n]=useState({}),[m,x]=useState({}),d=useRef({}),y=useCallback((o,p)=>{d.current[o]=p??{};},[]),b=useCallback(o=>{delete d.current[o];},[]),U=useCallback((o,p)=>{n(f=>({...f,[o]:p}));},[]),S=useCallback(o=>{n(p=>{let f={...p};return delete f[o],f});},[]),I=useCallback(o=>{x(p=>({...p,[o]:true}));},[]),L=useCallback((o,p)=>{if(c.setState(f=>({...f,[o]:p}),o),m[o]&&d.current[o]){let f=B(p,d.current[o]);n(f?R=>({...R,[o]:f}):R=>{let M={...R};return delete M[o],M});}},[c.setState,m]),F=useCallback(o=>{o&&o.preventDefault();let p={};for(let R in d.current)p[R]=true;x(p);let f=J(c.state,d.current);return n(f),Object.keys(f).length===0&&s&&s(c.state),Object.keys(f).length===0},[c.state,s]),E={state:c.state,setState:L,errors:T,setError:U,clearError:S,touched:m,touch:I,fields:Object.fromEntries(Object.entries(d.current).map(([o,p])=>[o,{name:o,rules:p,touched:!!m[o]}])),registerField:y,unregisterField:b};return jsx(W.Provider,{value:E,children:jsx("form",{onSubmit:F,noValidate:true,children:t})})}function ee({name:e,label:t,rules:s,className:u,style:i,...a}){let r=H();useEffect(()=>(r.registerField(e,s),()=>r.unregisterField(e)),[e,s,r.registerField,r.unregisterField]);let c=x=>{r.setState(e,x.target.value);},T=()=>{r.touch(e);},n=r.errors[e],m=String(r.state[e]??"");return jsxs("div",{className:u,style:i,children:[t&&jsx("label",{htmlFor:e,children:t}),jsx("input",{id:e,type:"text",value:m,onChange:c,onBlur:T,"aria-invalid":!!n,"aria-describedby":n?`${e}-error`:void 0,...a}),n&&jsx("span",{id:`${e}-error`,role:"alert",children:n.message})]})}function te({name:e,label:t,rules:s,className:u,style:i,...a}){let r=H();useEffect(()=>(r.registerField(e,s),()=>r.unregisterField(e)),[e,s,r.registerField,r.unregisterField]);let c=x=>{let d=x.target.value===""?"":Number(x.target.value);r.setState(e,d);},T=()=>{r.touch(e);},n=r.errors[e],m=r.state[e];return jsxs("div",{className:u,style:i,children:[t&&jsx("label",{htmlFor:e,children:t}),jsx("input",{id:e,type:"number",value:m===""||m==null?"":Number(m),onChange:c,onBlur:T,"aria-invalid":!!n,"aria-describedby":n?`${e}-error`:void 0,...a}),n&&jsx("span",{id:`${e}-error`,role:"alert",children:n.message})]})}function re({name:e,label:t,rules:s,className:u,style:i,...a}){let r=H();useEffect(()=>(r.registerField(e,s),()=>r.unregisterField(e)),[e,s,r.registerField,r.unregisterField]);let c=m=>{r.setState(e,m.target.checked);},T=r.errors[e],n=!!r.state[e];return jsxs("div",{className:u,style:i,children:[jsxs("label",{children:[jsx("input",{type:"checkbox",checked:n,onChange:c,"aria-invalid":!!T,...a}),t]}),T&&jsx("span",{role:"alert",children:T.message})]})}function ne({name:e,label:t,rules:s,options:u,placeholder:i,className:a,style:r}){let c=H();useEffect(()=>(c.registerField(e,s),()=>c.unregisterField(e)),[e,s,c.registerField,c.unregisterField]);let T=d=>{c.setState(e,d.target.value);},n=()=>{c.touch(e);},m=c.errors[e],x=String(c.state[e]??"");return jsxs("div",{className:a,style:r,children:[t&&jsx("label",{htmlFor:e,children:t}),jsxs("select",{id:e,value:x,onChange:T,onBlur:n,"aria-invalid":!!m,"aria-describedby":m?`${e}-error`:void 0,children:[i&&jsx("option",{value:"",disabled:true,children:i}),u.map(d=>jsx("option",{value:d.value,children:d.label},d.value))]}),m&&jsx("span",{id:`${e}-error`,role:"alert",children:m.message})]})}function oe({name:e,label:t,rules:s,className:u,style:i,...a}){let r=H();useEffect(()=>(r.registerField(e,s),()=>r.unregisterField(e)),[e,s,r.registerField,r.unregisterField]);let c=x=>{r.setState(e,x.target.value);},T=()=>{r.touch(e);},n=r.errors[e],m=String(r.state[e]??"");return jsxs("div",{className:u,style:i,children:[t&&jsx("label",{htmlFor:e,children:t}),jsx("textarea",{id:e,value:m,onChange:c,onBlur:T,"aria-invalid":!!n,"aria-describedby":n?`${e}-error`:void 0,...a}),n&&jsx("span",{id:`${e}-error`,role:"alert",children:n.message})]})}export{re as CheckboxField,Z as FormRewind,te as NumberField,ne as SelectField,ee as TextField,oe as TextareaField,z as useFormHistory,H as useFormRewindContext,J as validateAll,B as validateField};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-form-rewind",
3
- "version": "0.4.0",
3
+ "version": "1.0.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",
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, 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"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/useFormHistory.ts"],"names":[],"mappings":";;;AAGA,IAAM,mBAAA,GAAsB,GAAA;AAC5B,IAAM,mBAAA,GAAsB,GAAA;AAC5B,IAAM,2BAAA,GAA8B,GAAA;AACpC,IAAM,uBAAA,GAA0B,CAAA;AAQhC,SAAS,iBAAiB,OAAA,EAAsE;AAC9F,EAAA,IAAI,CAAC,SAAS,OAAO,IAAA;AACrB,EAAA,IAAI,OAAO,OAAA,KAAY,SAAA,EAAW,OAAO,IAAA;AACzC,EAAA,OAAO;AAAA,IACL,KAAK,OAAA,CAAQ,GAAA;AAAA,IACb,UAAA,EAAY,QAAQ,UAAA,IAAc,2BAAA;AAAA,IAClC,OAAA,EAAS,QAAQ,OAAA,IAAW;AAAA,GAC9B;AACF;AAEA,SAAS,SAAA,CAAa,KAAa,OAAA,EAA2B;AAC5D,EAAA,IAAI;AACF,IAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,CAAC,MAAA,CAAO,cAAc,OAAO,IAAA;AAClE,IAAA,MAAM,GAAA,GAAM,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,GAAG,CAAA;AAC3C,IAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AACjB,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC7B,IAAA,IAAI,MAAA,CAAO,cAAc,OAAA,EAAS;AAChC,MAAA,MAAA,CAAO,YAAA,CAAa,WAAW,GAAG,CAAA;AAClC,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,OAAO,MAAA,CAAO,OAAA;AAAA,EAChB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAEA,SAAS,UAAA,CAAc,GAAA,EAAa,KAAA,EAAU,OAAA,EAAuB;AACnE,EAAA,IAAI;AACF,IAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,CAAC,OAAO,YAAA,EAAc;AAC3D,IAAA,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,EAAE,OAAA,EAAS,KAAA,EAAO,SAAA,EAAW,OAAA,EAAS,CAAC,CAAA;AAAA,EACzF,CAAA,CAAA,MAAQ;AAAA,EAER;AACF;AAEA,SAAS,YAAY,GAAA,EAAmB;AACtC,EAAA,IAAI;AACF,IAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,CAAC,OAAO,YAAA,EAAc;AAC3D,IAAA,MAAA,CAAO,YAAA,CAAa,WAAW,GAAG,CAAA;AAAA,EACpC,CAAA,CAAA,MAAQ;AAAA,EAER;AACF;AAEO,SAAS,cAAA,CACd,YAAA,EACA,OAAA,GAAoC,EAAC,EACZ;AACzB,EAAA,MAAM;AAAA,IACJ,UAAA,GAAa,mBAAA;AAAA,IACb,UAAA,GAAa,mBAAA;AAAA,IACb,OAAA,EAAS,aAAA;AAAA,IACT,MAAA;AAAA,IACA,MAAA;AAAA,IACA;AAAA,GACF,GAAI,OAAA;AAEJ,EAAA,MAAM,OAAA,GAAU,iBAAiB,aAAa,CAAA;AAC9C,EAAA,MAAM,cAAA,GAAiB,OAAO,KAAK,CAAA;AAGnC,EAAA,MAAM,CAAC,KAAA,EAAO,WAAW,CAAA,GAAI,SAAY,MAAM;AAC7C,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,MAAM,KAAA,GAAQ,SAAA,CAAa,OAAA,CAAQ,GAAA,EAAK,QAAQ,OAAO,CAAA;AACvD,MAAA,IAAI,UAAU,IAAA,EAAM;AAClB,QAAA,cAAA,CAAe,OAAA,GAAU,IAAA;AACzB,QAAA,OAAO,KAAA;AAAA,MACT;AAAA,IACF;AACA,IAAA,OAAO,YAAA;AAAA,EACT,CAAC,CAAA;AAED,EAAA,MAAM,QAAA,GAAW,OAAU,KAAK,CAAA;AAChC,EAAA,QAAA,CAAS,OAAA,GAAU,KAAA;AAEnB,EAAA,MAAM,OAAA,GAAU,MAAA,CAA0B,EAAE,CAAA;AAC5C,EAAA,MAAM,SAAA,GAAY,MAAA,CAA0B,EAAE,CAAA;AAE9C,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAI,QAAA,CAA4B,EAAE,CAAA;AACtD,EAAA,MAAM,CAAC,MAAA,EAAQ,SAAS,CAAA,GAAI,QAAA,CAA4B,EAAE,CAAA;AAE1D,EAAA,MAAM,gBAAA,GAAmB,OAA6C,IAAI,CAAA;AAC1E,EAAA,MAAM,eAAA,GAAkB,OAAiB,IAAI,CAAA;AAC7C,EAAA,MAAM,mBAAA,GAAsB,OAAiB,IAAI,CAAA;AAGjD,EAAA,MAAM,eAAA,GAAkB,OAA6C,IAAI,CAAA;AAEzE,EAAA,MAAM,UAAA,GAAa,WAAA;AAAA,IACjB,CAAC,KAAA,KAA2B;AAC1B,MAAA,MAAM,IAAA,GAAO,CAAC,GAAG,OAAA,CAAQ,SAAS,KAAK,CAAA;AACvC,MAAA,IAAI,IAAA,CAAK,SAAS,UAAA,EAAY;AAC5B,QAAA,IAAA,CAAK,MAAA,CAAO,CAAA,EAAG,IAAA,CAAK,MAAA,GAAS,UAAU,CAAA;AAAA,MACzC;AACA,MAAA,OAAA,CAAQ,OAAA,GAAU,IAAA;AAClB,MAAA,OAAA,CAAQ,IAAI,CAAA;AAAA,IACd,CAAA;AAAA,IACA,CAAC,UAAU;AAAA,GACb;AAGA,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,OAAO,MAAM;AACX,MAAA,IAAI,gBAAA,CAAiB,YAAY,IAAA,EAAM;AACrC,QAAA,YAAA,CAAa,iBAAiB,OAAO,CAAA;AAAA,MACvC;AACA,MAAA,IAAI,eAAA,CAAgB,YAAY,IAAA,EAAM;AACpC,QAAA,YAAA,CAAa,gBAAgB,OAAO,CAAA;AAAA,MACtC;AAAA,IACF,CAAA;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAGL,EAAA,MAAM,eAAA,GAAkB,WAAA;AAAA,IACtB,CAAC,SAAA,KAAiB;AAChB,MAAA,IAAI,CAAC,OAAA,EAAS;AACd,MAAA,IAAI,eAAA,CAAgB,YAAY,IAAA,EAAM;AACpC,QAAA,YAAA,CAAa,gBAAgB,OAAO,CAAA;AAAA,MACtC;AACA,MAAA,eAAA,CAAgB,OAAA,GAAU,WAAW,MAAM;AACzC,QAAA,UAAA,CAAW,OAAA,CAAQ,GAAA,EAAK,SAAA,EAAW,OAAA,CAAQ,OAAO,CAAA;AAClD,QAAA,eAAA,CAAgB,OAAA,GAAU,IAAA;AAAA,MAC5B,CAAA,EAAG,QAAQ,UAAU,CAAA;AAAA,IACvB,CAAA;AAAA,IACA,CAAC,OAAO;AAAA,GACV;AAGA,EAAA,MAAM,UAAA,GAAa,WAAA;AAAA,IACjB,CAAC,SAAA,KAAiB;AAChB,MAAA,IAAI,CAAC,OAAA,EAAS;AACd,MAAA,IAAI,eAAA,CAAgB,YAAY,IAAA,EAAM;AACpC,QAAA,YAAA,CAAa,gBAAgB,OAAO,CAAA;AACpC,QAAA,eAAA,CAAgB,OAAA,GAAU,IAAA;AAAA,MAC5B;AACA,MAAA,UAAA,CAAW,OAAA,CAAQ,GAAA,EAAK,SAAA,EAAW,OAAA,CAAQ,OAAO,CAAA;AAAA,IACpD,CAAA;AAAA,IACA,CAAC,OAAO;AAAA,GACV;AAEA,EAAA,MAAM,QAAA,GAAW,WAAA;AAAA,IACf,CAAC,OAA6B,KAAA,KAAmB;AAC/C,MAAA,MAAM,OAAO,QAAA,CAAS,OAAA;AACtB,MAAA,MAAM,OAAO,OAAO,KAAA,KAAU,UAAA,GAAc,KAAA,CAAyB,IAAI,CAAA,GAAI,KAAA;AAE7E,MAAA,IAAI,MAAA,CAAO,EAAA,CAAG,IAAA,EAAM,IAAI,CAAA,EAAG;AAE3B,MAAA,WAAA,CAAY,IAAI,CAAA;AAChB,MAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AACnB,MAAA,eAAA,CAAgB,IAAI,CAAA;AAEpB,MAAA,IAAI,cAAc,CAAA,EAAG;AACnB,QAAA,MAAM,KAAA,GAAyB,EAAE,KAAA,EAAO,IAAA,EAAM,WAAW,IAAA,CAAK,GAAA,IAAO,KAAA,EAAM;AAC3E,QAAA,UAAA,CAAW,KAAK,CAAA;AAChB,QAAA,SAAA,CAAU,UAAU,EAAC;AACrB,QAAA,SAAA,CAAU,EAAE,CAAA;AACZ,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,eAAA,CAAgB,YAAY,IAAA,EAAM;AACpC,QAAA,mBAAA,CAAoB,OAAA,GAAU,IAAA;AAAA,MAChC;AAEA,MAAA,eAAA,CAAgB,OAAA,GAAU,IAAA;AAE1B,MAAA,IAAI,gBAAA,CAAiB,YAAY,IAAA,EAAM;AACrC,QAAA,YAAA,CAAa,iBAAiB,OAAO,CAAA;AAAA,MACvC;AAEA,MAAA,gBAAA,CAAiB,OAAA,GAAU,WAAW,MAAM;AAC1C,QAAA,MAAM,KAAA,GAAyB;AAAA,UAC7B,OAAO,mBAAA,CAAoB,OAAA;AAAA,UAC3B,SAAA,EAAW,KAAK,GAAA,EAAI;AAAA,UACpB;AAAA,SACF;AACA,QAAA,UAAA,CAAW,KAAK,CAAA;AAChB,QAAA,SAAA,CAAU,UAAU,EAAC;AACrB,QAAA,SAAA,CAAU,EAAE,CAAA;AACZ,QAAA,gBAAA,CAAiB,OAAA,GAAU,IAAA;AAC3B,QAAA,eAAA,CAAgB,OAAA,GAAU,IAAA;AAC1B,QAAA,mBAAA,CAAoB,OAAA,GAAU,IAAA;AAAA,MAChC,GAAG,UAAU,CAAA;AAAA,IACf,CAAA;AAAA,IACA,CAAC,UAAA,EAAY,UAAA,EAAY,eAAe;AAAA,GAC1C;AAEA,EAAA,MAAM,IAAA,GAAO,YAAY,MAAM;AAC7B,IAAA,IAAI,gBAAA,CAAiB,YAAY,IAAA,EAAM;AACrC,MAAA,YAAA,CAAa,iBAAiB,OAAO,CAAA;AACrC,MAAA,gBAAA,CAAiB,OAAA,GAAU,IAAA;AAAA,IAC7B;AAEA,IAAA,IAAI,eAAA,CAAgB,OAAA,KAAY,IAAA,IAAQ,mBAAA,CAAoB,YAAY,IAAA,EAAM;AAC5E,MAAA,MAAM,KAAA,GAAyB;AAAA,QAC7B,OAAO,mBAAA,CAAoB,OAAA;AAAA,QAC3B,SAAA,EAAW,KAAK,GAAA;AAAI,OACtB;AACA,MAAA,UAAA,CAAW,KAAK,CAAA;AAChB,MAAA,SAAA,CAAU,UAAU,EAAC;AACrB,MAAA,SAAA,CAAU,EAAE,CAAA;AACZ,MAAA,eAAA,CAAgB,OAAA,GAAU,IAAA;AAC1B,MAAA,mBAAA,CAAoB,OAAA,GAAU,IAAA;AAAA,IAChC;AAEA,IAAA,IAAI,OAAA,CAAQ,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG;AAElC,IAAA,MAAM,OAAO,OAAA,CAAQ,OAAA,CAAQ,OAAA,CAAQ,OAAA,CAAQ,SAAS,CAAC,CAAA;AACvD,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,CAAQ,KAAA,CAAM,GAAG,EAAE,CAAA;AAC3C,IAAA,OAAA,CAAQ,OAAA,GAAU,OAAA;AAClB,IAAA,OAAA,CAAQ,OAAO,CAAA;AAEf,IAAA,MAAM,YAAA,GAAgC;AAAA,MACpC,OAAO,QAAA,CAAS,OAAA;AAAA,MAChB,SAAA,EAAW,KAAK,GAAA;AAAI,KACtB;AACA,IAAA,SAAA,CAAU,OAAA,GAAU,CAAC,GAAG,SAAA,CAAU,SAAS,YAAY,CAAA;AACvD,IAAA,SAAA,CAAU,UAAU,OAAO,CAAA;AAE3B,IAAA,WAAA,CAAY,KAAK,KAAK,CAAA;AACtB,IAAA,QAAA,CAAS,UAAU,IAAA,CAAK,KAAA;AACxB,IAAA,UAAA,CAAW,KAAK,KAAK,CAAA;AACrB,IAAA,MAAA,GAAS,KAAK,KAAK,CAAA;AAAA,EACrB,CAAA,EAAG,CAAC,UAAA,EAAY,MAAA,EAAQ,UAAU,CAAC,CAAA;AAEnC,EAAA,MAAM,IAAA,GAAO,YAAY,MAAM;AAC7B,IAAA,IAAI,SAAA,CAAU,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG;AAEpC,IAAA,MAAM,OAAO,SAAA,CAAU,OAAA,CAAQ,SAAA,CAAU,OAAA,CAAQ,SAAS,CAAC,CAAA;AAC3D,IAAA,MAAM,SAAA,GAAY,SAAA,CAAU,OAAA,CAAQ,KAAA,CAAM,GAAG,EAAE,CAAA;AAC/C,IAAA,SAAA,CAAU,OAAA,GAAU,SAAA;AACpB,IAAA,SAAA,CAAU,SAAS,CAAA;AAEnB,IAAA,MAAM,YAAA,GAAgC;AAAA,MACpC,OAAO,QAAA,CAAS,OAAA;AAAA,MAChB,SAAA,EAAW,KAAK,GAAA;AAAI,KACtB;AACA,IAAA,OAAA,CAAQ,OAAA,GAAU,CAAC,GAAG,OAAA,CAAQ,SAAS,YAAY,CAAA;AACnD,IAAA,OAAA,CAAQ,QAAQ,OAAO,CAAA;AAEvB,IAAA,WAAA,CAAY,KAAK,KAAK,CAAA;AACtB,IAAA,QAAA,CAAS,UAAU,IAAA,CAAK,KAAA;AACxB,IAAA,UAAA,CAAW,KAAK,KAAK,CAAA;AACrB,IAAA,MAAA,GAAS,KAAK,KAAK,CAAA;AAAA,EACrB,CAAA,EAAG,CAAC,MAAA,EAAQ,UAAU,CAAC,CAAA;AAEvB,EAAA,MAAM,YAAA,GAAe,YAAY,MAAM;AACrC,IAAA,IAAI,gBAAA,CAAiB,YAAY,IAAA,EAAM;AACrC,MAAA,YAAA,CAAa,iBAAiB,OAAO,CAAA;AACrC,MAAA,gBAAA,CAAiB,OAAA,GAAU,IAAA;AAAA,IAC7B;AACA,IAAA,eAAA,CAAgB,OAAA,GAAU,IAAA;AAC1B,IAAA,mBAAA,CAAoB,OAAA,GAAU,IAAA;AAC9B,IAAA,OAAA,CAAQ,UAAU,EAAC;AACnB,IAAA,SAAA,CAAU,UAAU,EAAC;AACrB,IAAA,OAAA,CAAQ,EAAE,CAAA;AACV,IAAA,SAAA,CAAU,EAAE,CAAA;AAAA,EACd,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,QAAA,GAAW,WAAA;AAAA,IACf,CAAC,KAAA,KAAmB;AAClB,MAAA,IAAI,QAAA,GAAW,KAAA;AAEf,MAAA,IAAI,gBAAA,CAAiB,YAAY,IAAA,EAAM;AACrC,QAAA,YAAA,CAAa,iBAAiB,OAAO,CAAA;AACrC,QAAA,gBAAA,CAAiB,OAAA,GAAU,IAAA;AAAA,MAC7B;AAEA,MAAA,IAAI,eAAA,CAAgB,OAAA,KAAY,IAAA,IAAQ,mBAAA,CAAoB,YAAY,IAAA,EAAM;AAC5E,QAAA,MAAM,KAAA,GAAyB;AAAA,UAC7B,OAAO,mBAAA,CAAoB,OAAA;AAAA,UAC3B,SAAA,EAAW,KAAK,GAAA;AAAI,SACtB;AACA,QAAA,UAAA,CAAW,KAAK,CAAA;AAChB,QAAA,SAAA,CAAU,UAAU,EAAC;AACrB,QAAA,SAAA,CAAU,EAAE,CAAA;AACZ,QAAA,eAAA,CAAgB,OAAA,GAAU,IAAA;AAC1B,QAAA,mBAAA,CAAoB,OAAA,GAAU,IAAA;AAC9B,QAAA,QAAA,GAAW,IAAA;AAAA,MACb;AAEA,MAAA,IAAI,CAAC,QAAA,EAAU;AACb,QAAA,MAAM,KAAA,GAAyB;AAAA,UAC7B,OAAO,QAAA,CAAS,OAAA;AAAA,UAChB,SAAA,EAAW,KAAK,GAAA,EAAI;AAAA,UACpB;AAAA,SACF;AACA,QAAA,UAAA,CAAW,KAAK,CAAA;AAChB,QAAA,SAAA,CAAU,UAAU,EAAC;AACrB,QAAA,SAAA,CAAU,EAAE,CAAA;AACZ,QAAA,UAAA,GAAa,KAAK,CAAA;AAAA,MACpB,CAAA,MAAA,IAAW,KAAA,IAAS,OAAA,CAAQ,OAAA,CAAQ,SAAS,CAAA,EAAG;AAC9C,QAAA,OAAA,CAAQ,QAAQ,OAAA,CAAQ,OAAA,CAAQ,MAAA,GAAS,CAAC,EAAE,KAAA,GAAQ,KAAA;AACpD,QAAA,OAAA,CAAQ,CAAC,GAAG,OAAA,CAAQ,OAAO,CAAC,CAAA;AAAA,MAC9B;AAAA,IACF,CAAA;AAAA,IACA,CAAC,YAAY,UAAU;AAAA,GACzB;AAEA,EAAA,MAAM,UAAA,GAAa,YAAY,MAAM;AACnC,IAAA,YAAA,EAAa;AACb,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,WAAA,CAAY,QAAQ,GAAG,CAAA;AAAA,IACzB;AACA,IAAA,WAAA,CAAY,YAAY,CAAA;AACxB,IAAA,QAAA,CAAS,OAAA,GAAU,YAAA;AAAA,EACrB,CAAA,EAAG,CAAC,YAAA,EAAc,YAAA,EAAc,OAAO,CAAC,CAAA;AAExC,EAAA,OAAO;AAAA,IACL,KAAA;AAAA,IACA,QAAA;AAAA,IACA,IAAA;AAAA,IACA,IAAA;AAAA,IACA,OAAA,EAAS,KAAK,MAAA,GAAS,CAAA;AAAA,IACvB,OAAA,EAAS,OAAO,MAAA,GAAS,CAAA;AAAA,IACzB,YAAA;AAAA,IACA,UAAA;AAAA,IACA,QAAA;AAAA,IACA,IAAA;AAAA,IACA;AAAA,GACF;AACF","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"]}