react-form-rewind 0.5.0 → 1.1.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
@@ -10,17 +10,11 @@
10
10
  [![bundle size](https://img.shields.io/bundlejs/size/react-form-rewind?label=min%2Bgzip)](https://bundlejs.com/?q=react-form-rewind)
11
11
  [![github](https://img.shields.io/github/stars/BazilSuhail/npm-react-form-rewind?style=social)](https://github.com/BazilSuhail/npm-react-form-rewind)
12
12
 
13
- Zero-dependency, tree-shakable React state engine with auto-saved history stacks, time-traveling undo/redo, keyboard shortcuts, and draft persistence.
14
-
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
13
+ Zero-dependency, tree-shakable React state engine with **field-level undo/redo**, per-field history stacks, validation, draft persistence, and keyboard shortcuts.
14
+
15
+ Two APIs: a standalone hook for full control, or field components for zero-boilerplate forms.
16
+
17
+ ---
24
18
 
25
19
  ## Install
26
20
 
@@ -28,131 +22,287 @@ Zero-dependency, tree-shakable React state engine with auto-saved history stacks
28
22
  npm install react-form-rewind
29
23
  ```
30
24
 
31
- ## Quick Start
25
+ ---
32
26
 
33
- ### Why?
27
+ ## Two Ways to Use
34
28
 
35
- | Problem | Solution |
36
- |---------|----------|
37
- | No native Ctrl+Z / Ctrl+Y in React forms | Built-in keyboard shortcuts with history tracking |
38
- | User progress lost on tab reload | Auto-save drafts to `localStorage` with schema versioning |
39
- | Manual debouncers for history snapshots | Automated keystroke coalescing into logical snapshots |
40
- | Heavy form libraries add validation bloat | Focused solely on history and state persistence |
29
+ ### 1. Field Components (recommended for forms)
41
30
 
42
- ## Usage
31
+ Zero boilerplate. Field components handle registration, onChange, validation, and per-field undo/redo automatically.
43
32
 
44
- ```bash
45
- npm install react-form-rewind
33
+ ```tsx
34
+ import { FormRewind, TextField, NumberField } from "react-form-rewind";
35
+
36
+ function SignupForm() {
37
+ return (
38
+ <FormRewind
39
+ initialState={{ name: "", email: "", age: 0 }}
40
+ keyboard
41
+ persist={{ key: "signup-draft" }}
42
+ onSubmit={(data) => console.log(data)}
43
+ >
44
+ <TextField name="name" label="Name" rules={{ required: true }} />
45
+ <TextField
46
+ name="email"
47
+ label="Email"
48
+ rules={{
49
+ required: true,
50
+ pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "Invalid email" },
51
+ }}
52
+ />
53
+ <NumberField name="age" label="Age" rules={{ min: 18, max: 120 }} />
54
+ <button type="submit">Submit</button>
55
+ </FormRewind>
56
+ );
57
+ }
46
58
  ```
47
59
 
60
+ **What happens:**
61
+ - Click into Name, type "John", press **Ctrl+Z** — only Name undoes, Email stays
62
+ - Type in both fields, submit — validation runs, errors show per field
63
+ - Close tab, reopen — draft restored from localStorage
64
+
65
+ ### 2. Standalone Hook (full control)
66
+
67
+ Use `useFormHistory` for any state — not just forms. Form-level undo/redo on the entire state object.
68
+
48
69
  ```tsx
49
70
  import { useFormHistory } from "react-form-rewind";
50
71
 
51
- function MyForm() {
72
+ function Counter() {
52
73
  const { state, setState, undo, redo, canUndo, canRedo } = useFormHistory(
53
- { name: "", email: "" },
54
- { keyboard: true, persist: { key: "my-form-draft" } }
74
+ { count: 0 },
75
+ { keyboard: true }
55
76
  );
56
77
 
57
78
  return (
58
- <form>
59
- <input
60
- value={state.name}
61
- onChange={(e) => setState({ ...state, name: e.target.value })}
62
- />
63
- <input
64
- value={state.email}
65
- onChange={(e) => setState({ ...state, email: e.target.value })}
66
- />
67
- <button type="button" onClick={undo} disabled={!canUndo}>
68
- Undo
69
- </button>
70
- <button type="button" onClick={redo} disabled={!canRedo}>
71
- Redo
72
- </button>
73
- </form>
79
+ <div>
80
+ <button onClick={() => setState({ count: state.count - 1 })} disabled={!canUndo}>-</button>
81
+ <span>{state.count}</span>
82
+ <button onClick={() => setState({ count: state.count + 1 })} disabled={!canRedo}>+</button>
83
+ </div>
74
84
  );
75
85
  }
76
86
  ```
77
87
 
78
- Press **Ctrl+Z** to undo, **Ctrl+Shift+Z** or **Ctrl+Y** to redo.
88
+ **Ctrl+Z** reverts the entire state. **Ctrl+Shift+Z** redoes.
79
89
 
80
90
  ---
81
91
 
82
- ## API Reference
92
+ ## Field-Level Undo/Redo
83
93
 
84
- ### `useFormHistory<T>(initialState, options?)`
94
+ The key feature. When using `<FormRewind>` with `keyboard`, **Ctrl+Z undoes only the field your cursor is in**. Other fields stay untouched.
95
+
96
+ ```
97
+ Name: [John|] <-- cursor here, Ctrl+Z reverts just Name
98
+ Email: [john@test.com] <-- stays exactly as-is
99
+ Age: [25] <-- untouched
100
+ ```
101
+
102
+ Each field maintains its own independent history stack:
103
+ - **Per-field debounce** — typing "hello" fast = one undo step, not five
104
+ - **Per-field redo** — Ctrl+Shift+Z redoes only the focused field
105
+ - **Independent stacks** — undoing Name doesn't affect Email's history
106
+
107
+ ---
108
+
109
+ ## Field Components
110
+
111
+ All field components auto-register with the `<FormRewind>` context, track their own history, validate on blur, and display errors.
112
+
113
+ ### TextField
114
+
115
+ ```tsx
116
+ <TextField name="name" label="Name" placeholder="John" rules={{ required: true }} />
117
+ ```
118
+
119
+ Props: `name`, `label?`, `rules?`, `placeholder?`, `className?`, `style?`, plus all native `<input>` props.
120
+
121
+ ### NumberField
122
+
123
+ ```tsx
124
+ <NumberField name="age" label="Age" rules={{ min: 0, max: 150 }} />
125
+ ```
126
+
127
+ Same as TextField but type="number". Value is stored as a number.
128
+
129
+ ### CheckboxField
130
+
131
+ ```tsx
132
+ <CheckboxField name="agree" label="I agree to terms" rules={{ required: true }} />
133
+ ```
134
+
135
+ Boolean field. `required` means the checkbox must be checked.
136
+
137
+ ### SelectField
138
+
139
+ ```tsx
140
+ <SelectField
141
+ name="country"
142
+ label="Country"
143
+ placeholder="Select..."
144
+ options={[
145
+ { value: "us", label: "United States" },
146
+ { value: "uk", label: "United Kingdom" },
147
+ ]}
148
+ rules={{ required: true }}
149
+ />
150
+ ```
151
+
152
+ ### TextareaField
153
+
154
+ ```tsx
155
+ <TextareaField name="bio" label="Bio" rows={4} rules={{ maxLength: 500 }} />
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Validation
161
+
162
+ Pass a `rules` prop to any field component. Validation runs on blur (when the field is touched) and on form submit.
163
+
164
+ ### Built-in Rules
165
+
166
+ | Rule | Type | Description |
167
+ |------|------|-------------|
168
+ | `required` | `boolean \| string` | Field must be non-empty. Pass a string for custom error message. |
169
+ | `pattern` | `RegExp \| { value: RegExp, message: string }` | Must match regex |
170
+ | `minLength` | `number \| { value: number, message: string }` | String min length |
171
+ | `maxLength` | `number \| { value: number, message: string }` | String max length |
172
+ | `min` | `number \| { value: number, message: string }` | Number minimum |
173
+ | `max` | `number \| { value: number, message: string }` | Number maximum |
174
+ | `validate` | `(value) => string \| null` | Custom validator. Return error message or null. |
175
+
176
+ ### Examples
177
+
178
+ ```tsx
179
+ // Required with custom message
180
+ <TextField name="name" rules={{ required: "Name is required" }} />
181
+
182
+ // Email pattern with custom message
183
+ <TextField name="email" rules={{ pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "Bad email" } }} />
184
+
185
+ // Number range
186
+ <NumberField name="score" rules={{ min: 0, max: 100 }} />
187
+
188
+ // Custom validator
189
+ <TextField
190
+ name="username"
191
+ rules={{
192
+ validate: (val) => (val as string).length < 3 ? "Too short" : null,
193
+ }}
194
+ />
195
+ ```
196
+
197
+ ---
198
+
199
+ ## FormRewind Provider
200
+
201
+ The `<FormRewind>` component wraps your form and provides context to all field components.
202
+
203
+ | Prop | Type | Default | Description |
204
+ |------|------|---------|-------------|
205
+ | `initialState` | `Record<string, unknown>` | (required) | Initial form values |
206
+ | `keyboard` | `boolean` | `false` | Enable field-level Ctrl+Z / Ctrl+Shift+Z |
207
+ | `debounceMs` | `number` | `300` | Per-field debounce window |
208
+ | `maxHistory` | `number` | `100` | Max history entries per field |
209
+ | `persist` | `{ key, debounceMs?, version? }` or `false` | `false` | localStorage draft persistence |
210
+ | `onSubmit` | `(state) => void` | — | Called after validation passes |
211
+ | `children` | `ReactNode` | (required) | Form fields |
212
+
213
+ Renders a `<form>` element with `noValidate`. Handles submit, runs validation, calls `onSubmit` only if all fields pass.
214
+
215
+ ---
85
216
 
86
- The core hook that manages a history-backed state stack.
217
+ ## Standalone Hook API
87
218
 
88
- **Returns:**
219
+ ### `useFormHistory<T>(initialState, options?)`
89
220
 
90
221
  | Property | Type | Description |
91
222
  |----------|------|-------------|
92
- | `state` | `T` | Current present state |
93
- | `setState` | `(value: T \| ((prev: T) => T), label?: string) => void` | Update state (pushes to history) |
223
+ | `state` | `T` | Current state |
224
+ | `setState` | `(value \| updater, label?) => void` | Update state |
94
225
  | `undo` | `() => void` | Revert to previous state |
95
226
  | `redo` | `() => void` | Re-apply undone state |
96
227
  | `canUndo` | `boolean` | Whether undo is available |
97
228
  | `canRedo` | `boolean` | Whether redo is available |
98
229
  | `clearHistory` | `() => void` | Reset history, keep current state |
99
230
  | `clearDraft` | `() => void` | Clear persisted draft from storage |
100
- | `snapshot` | `(label?: string) => void` | Force-commit current state to history |
231
+ | `snapshot` | `(label?) => void` | Force-commit current state to history |
101
232
  | `past` | `HistoryEntry<T>[]` | Past history entries |
102
233
  | `future` | `HistoryEntry<T>[]` | Future (undone) entries |
103
234
 
104
- **Options:**
235
+ Options:
105
236
 
106
237
  | Option | Type | Default | Description |
107
238
  |--------|------|---------|-------------|
108
- | `maxHistory` | `number` | `100` | Maximum past entries to retain |
109
- | `debounceMs` | `number` | `300` | Debounce window for rapid state changes |
110
- | `persist` | `boolean \| PersistOptions` | `false` | Enable draft persistence |
111
- | `keyboard` | `boolean` | `false` | Enable Ctrl+Z / Ctrl+Shift+Z keyboard shortcuts |
239
+ | `maxHistory` | `number` | `100` | Max past entries |
240
+ | `debounceMs` | `number` | `300` | Debounce window (0 = no debounce) |
241
+ | `persist` | `boolean \| PersistOptions` | `false` | Draft persistence |
242
+ | `keyboard` | `boolean` | `false` | Ctrl+Z / Ctrl+Shift+Z (form-level) |
112
243
  | `onUndo` | `(state: T) => void` | — | Callback after undo |
113
244
  | `onRedo` | `(state: T) => void` | — | Callback after redo |
114
- | `onSnapshot` | `(entry: HistoryEntry<T>) => void` | — | Callback when a snapshot is committed |
245
+ | `onSnapshot` | `(entry) => void` | — | Callback on snapshot |
115
246
 
116
- ### `PersistOptions`
247
+ ---
117
248
 
118
- | Property | Type | Default | Description |
119
- |----------|------|---------|-------------|
120
- | `key` | `string` | — | `localStorage` key for draft storage |
121
- | `debounceMs` | `number` | `500` | Debounce for auto-save writes |
122
- | `version` | `number` | `1` | Schema version (mismatches discard draft) |
249
+ ## useFormRewindContext
123
250
 
124
- ---
251
+ Access form context from outside field components:
125
252
 
126
- ## Features
253
+ ```tsx
254
+ import { useFormRewindContext } from "react-form-rewind";
127
255
 
128
- ### Keyboard Shortcuts
256
+ function UndoButton() {
257
+ const { undoField, fields } = useFormRewindContext();
258
+ // undoField("name") — undo just the name field
259
+ // fields.name.canUndo — check if name has undo history
260
+ }
261
+ ```
129
262
 
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.
263
+ | Property | Type | Description |
264
+ |----------|------|-------------|
265
+ | `state` | `Record<string, unknown>` | Full form state |
266
+ | `setState` | `(name, value) => void` | Set a single field |
267
+ | `errors` | `Record<string, FieldError>` | Current validation errors |
268
+ | `touched` | `Record<string, boolean>` | Which fields have been blurred |
269
+ | `fields` | `Record<string, FieldMeta>` | Per-field metadata (touched, canUndo, canRedo) |
270
+ | `undoField` | `(name) => void` | Undo a specific field |
271
+ | `redoField` | `(name) => void` | Redo a specific field |
272
+ | `setError` | `(name, error) => void` | Manually set a field error |
273
+ | `clearError` | `(name) => void` | Clear a field error |
131
274
 
132
- ### Snapshot Debouncing
275
+ ---
133
276
 
134
- Rapid keystrokes (typing "hello" quickly) are coalesced into a single history entry instead of one per keystroke. The debounce window defaults to 300ms.
277
+ ## Draft Persistence
135
278
 
136
- ### Draft Persistence
279
+ Enable with `persist: { key: "my-form" }`. Drafts auto-save to `localStorage` (debounced) and restore on mount.
137
280
 
138
- Enable with `persist: { key: "my-form" }`. Drafts are auto-saved to `localStorage` and restored on mount. Schema versioning prevents stale drafts from hydrating incorrectly.
281
+ | Property | Type | Default | Description |
282
+ |----------|------|---------|-------------|
283
+ | `key` | `string` | (required) | localStorage key |
284
+ | `debounceMs` | `number` | `500` | Auto-save debounce |
285
+ | `version` | `number` | `1` | Schema version (mismatches discard draft) |
139
286
 
140
287
  ---
141
288
 
142
289
  ## Tree-Shaking
143
290
 
144
- `react-form-rewind` uses pure ES module exports with `sideEffects: false` in `package.json`. Bundlers like Webpack, Rollup, and esbuild will only include code you actually import.
291
+ Pure ES module exports with `sideEffects: false`. Only import what you use:
145
292
 
146
293
  ```ts
147
- // Only the hook is bundled — no extra code
294
+ // Just the hook — no field components bundled
148
295
  import { useFormHistory } from "react-form-rewind";
296
+
297
+ // Just field components — no standalone hook logic
298
+ import { FormRewind, TextField } from "react-form-rewind";
149
299
  ```
150
300
 
151
301
  ---
152
302
 
153
303
  ## TypeScript
154
304
 
155
- Full type definitions are included. All generics are inferred from your initial state:
305
+ Full generics, all types exported. State is inferred from `initialState`:
156
306
 
157
307
  ```ts
158
308
  const { state } = useFormHistory({ count: 0 });
@@ -161,17 +311,6 @@ const { state } = useFormHistory({ count: 0 });
161
311
 
162
312
  ---
163
313
 
164
- ## Browser Support
165
-
166
- - Chrome 80+
167
- - Firefox 78+
168
- - Safari 14+
169
- - Edge 80+
170
-
171
- Requires `React 18+` and native `Array`, `localStorage`, and `addEventListener` APIs.
172
-
173
- ---
174
-
175
314
  ## License
176
315
 
177
316
  [MIT](LICENSE)
package/dist/index.d.mts CHANGED
@@ -1,3 +1,37 @@
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;
@@ -30,7 +64,77 @@ interface UseFormHistoryReturn<T> {
30
64
  past: HistoryEntry<T>[];
31
65
  future: HistoryEntry<T>[];
32
66
  }
67
+ interface FieldMeta {
68
+ name: string;
69
+ rules?: FieldRules;
70
+ touched: boolean;
71
+ canUndo: boolean;
72
+ canRedo: boolean;
73
+ }
74
+ interface FieldHistory {
75
+ past: unknown[];
76
+ future: unknown[];
77
+ }
78
+ interface FormRewindContextValue {
79
+ state: Record<string, unknown>;
80
+ setState: (name: string, value: unknown) => void;
81
+ errors: Record<string, FieldError>;
82
+ setError: (name: string, error: FieldError) => void;
83
+ clearError: (name: string) => void;
84
+ touched: Record<string, boolean>;
85
+ touch: (name: string) => void;
86
+ fields: Record<string, FieldMeta>;
87
+ registerField: (name: string, rules?: FieldRules) => void;
88
+ unregisterField: (name: string) => void;
89
+ undoField: (name: string) => void;
90
+ redoField: (name: string) => void;
91
+ }
33
92
 
34
93
  declare function useFormHistory<T>(initialState: T, options?: UseFormHistoryOptions<T>): UseFormHistoryReturn<T>;
35
94
 
36
- export { type HistoryEntry, type PersistOptions, type UseFormHistoryOptions, type UseFormHistoryReturn, useFormHistory };
95
+ declare function useFormRewindContext(): FormRewindContextValue;
96
+ interface FormRewindProps {
97
+ initialState: Record<string, unknown>;
98
+ children: ReactNode;
99
+ onSubmit?: (state: Record<string, unknown>) => void;
100
+ maxHistory?: number;
101
+ debounceMs?: number;
102
+ persist?: {
103
+ key: string;
104
+ debounceMs?: number;
105
+ version?: number;
106
+ } | false;
107
+ keyboard?: boolean;
108
+ }
109
+ declare function FormRewind({ initialState, children, onSubmit, maxHistory, debounceMs, persist, keyboard, }: FormRewindProps): react.JSX.Element;
110
+
111
+ interface BaseFieldProps {
112
+ name: string;
113
+ label?: string;
114
+ rules?: FieldRules;
115
+ className?: string;
116
+ style?: React.CSSProperties;
117
+ }
118
+ interface TextFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "onChange"> {
119
+ }
120
+ declare function TextField({ name, label, rules, className, style, ...inputProps }: TextFieldProps): react.JSX.Element;
121
+ interface NumberFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "onChange" | "type"> {
122
+ }
123
+ declare function NumberField({ name, label, rules, className, style, ...inputProps }: NumberFieldProps): react.JSX.Element;
124
+ interface CheckboxFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "checked" | "onChange"> {
125
+ }
126
+ declare function CheckboxField({ name, label, rules, className, style, ...inputProps }: CheckboxFieldProps): react.JSX.Element;
127
+ interface SelectOption {
128
+ value: string;
129
+ label: string;
130
+ }
131
+ interface SelectFieldProps extends BaseFieldProps {
132
+ options: SelectOption[];
133
+ placeholder?: string;
134
+ }
135
+ declare function SelectField({ name, label, rules, options, placeholder, className, style }: SelectFieldProps): react.JSX.Element;
136
+ interface TextareaFieldProps extends BaseFieldProps, Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "name" | "value" | "onChange"> {
137
+ }
138
+ declare function TextareaField({ name, label, rules, className, style, ...textareaProps }: TextareaFieldProps): react.JSX.Element;
139
+
140
+ export { CheckboxField, type FieldError, type FieldHistory, 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,3 +1,37 @@
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;
@@ -30,7 +64,77 @@ interface UseFormHistoryReturn<T> {
30
64
  past: HistoryEntry<T>[];
31
65
  future: HistoryEntry<T>[];
32
66
  }
67
+ interface FieldMeta {
68
+ name: string;
69
+ rules?: FieldRules;
70
+ touched: boolean;
71
+ canUndo: boolean;
72
+ canRedo: boolean;
73
+ }
74
+ interface FieldHistory {
75
+ past: unknown[];
76
+ future: unknown[];
77
+ }
78
+ interface FormRewindContextValue {
79
+ state: Record<string, unknown>;
80
+ setState: (name: string, value: unknown) => void;
81
+ errors: Record<string, FieldError>;
82
+ setError: (name: string, error: FieldError) => void;
83
+ clearError: (name: string) => void;
84
+ touched: Record<string, boolean>;
85
+ touch: (name: string) => void;
86
+ fields: Record<string, FieldMeta>;
87
+ registerField: (name: string, rules?: FieldRules) => void;
88
+ unregisterField: (name: string) => void;
89
+ undoField: (name: string) => void;
90
+ redoField: (name: string) => void;
91
+ }
33
92
 
34
93
  declare function useFormHistory<T>(initialState: T, options?: UseFormHistoryOptions<T>): UseFormHistoryReturn<T>;
35
94
 
36
- export { type HistoryEntry, type PersistOptions, type UseFormHistoryOptions, type UseFormHistoryReturn, useFormHistory };
95
+ declare function useFormRewindContext(): FormRewindContextValue;
96
+ interface FormRewindProps {
97
+ initialState: Record<string, unknown>;
98
+ children: ReactNode;
99
+ onSubmit?: (state: Record<string, unknown>) => void;
100
+ maxHistory?: number;
101
+ debounceMs?: number;
102
+ persist?: {
103
+ key: string;
104
+ debounceMs?: number;
105
+ version?: number;
106
+ } | false;
107
+ keyboard?: boolean;
108
+ }
109
+ declare function FormRewind({ initialState, children, onSubmit, maxHistory, debounceMs, persist, keyboard, }: FormRewindProps): react.JSX.Element;
110
+
111
+ interface BaseFieldProps {
112
+ name: string;
113
+ label?: string;
114
+ rules?: FieldRules;
115
+ className?: string;
116
+ style?: React.CSSProperties;
117
+ }
118
+ interface TextFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "onChange"> {
119
+ }
120
+ declare function TextField({ name, label, rules, className, style, ...inputProps }: TextFieldProps): react.JSX.Element;
121
+ interface NumberFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "onChange" | "type"> {
122
+ }
123
+ declare function NumberField({ name, label, rules, className, style, ...inputProps }: NumberFieldProps): react.JSX.Element;
124
+ interface CheckboxFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "checked" | "onChange"> {
125
+ }
126
+ declare function CheckboxField({ name, label, rules, className, style, ...inputProps }: CheckboxFieldProps): react.JSX.Element;
127
+ interface SelectOption {
128
+ value: string;
129
+ label: string;
130
+ }
131
+ interface SelectFieldProps extends BaseFieldProps {
132
+ options: SelectOption[];
133
+ placeholder?: string;
134
+ }
135
+ declare function SelectField({ name, label, rules, options, placeholder, className, style }: SelectFieldProps): react.JSX.Element;
136
+ interface TextareaFieldProps extends BaseFieldProps, Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "name" | "value" | "onChange"> {
137
+ }
138
+ declare function TextareaField({ name, label, rules, className, style, ...textareaProps }: TextareaFieldProps): react.JSX.Element;
139
+
140
+ export { CheckboxField, type FieldError, type FieldHistory, 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 +1 @@
1
- 'use strict';var react=require('react');var j=100,V=300,X=500,Y=1;function q(e){return !e||typeof e=="boolean"?null:{key:e.key,debounceMs:e.debounceMs??X,version:e.version??Y}}function U(){return typeof window<"u"&&!!window.localStorage}function G(e,g){try{if(!U())return null;let f=window.localStorage.getItem(e);if(!f)return null;let E=JSON.parse(f);return E.__v!==g?(window.localStorage.removeItem(e),null):E.__s}catch{return null}}function K(e,g,f){try{if(!U())return;window.localStorage.setItem(e,JSON.stringify({__s:g,__v:f}));}catch{}}function Q(e){try{if(!U())return;window.localStorage.removeItem(e);}catch{}}function W(e,g={}){let{maxHistory:f=j,debounceMs:E=V,persist:C,keyboard:P=false,onUndo:k,onRedo:I,onSnapshot:M}=g,n=q(C),[R,h]=react.useState(()=>{if(n){let t=G(n.key,n.version);if(t!==null)return t}return e}),a=react.useRef(R);a.current=R;let u=react.useRef([]),c=react.useRef([]),[x,H]=react.useState([]),[L,S]=react.useState([]),o=react.useRef(null),m=react.useRef(null),i=react.useRef(null),y=react.useRef(null),T=react.useCallback(()=>{c.current=[],S([]);},[]),p=react.useCallback(t=>{let r=[...u.current,t];r.length>f&&r.splice(0,r.length-f),u.current=r,H(r);},[f]);react.useEffect(()=>()=>{o.current!==null&&clearTimeout(o.current),y.current!==null&&clearTimeout(y.current);},[]);let N=react.useCallback(t=>{n&&(y.current!==null&&clearTimeout(y.current),y.current=setTimeout(()=>{K(n.key,t,n.version),y.current=null;},n.debounceMs));},[n]),v=react.useCallback(t=>{n&&(y.current!==null&&(clearTimeout(y.current),y.current=null),K(n.key,t,n.version));},[n]),z=react.useCallback((t,r)=>{let s=a.current,d=typeof t=="function"?t(s):t;if(!Object.is(s,d)){if(h(d),a.current=d,N(d),E<=0){let F={state:s,timestamp:Date.now(),label:r};p(F),T();return}m.current===null&&(i.current=s),m.current=d,o.current!==null&&clearTimeout(o.current),o.current=setTimeout(()=>{let F={state:i.current,timestamp:Date.now(),label:r};p(F),T(),o.current=null,m.current=null,i.current=null;},E);}},[E,p,N,T]),D=react.useCallback(()=>{if(o.current!==null&&(clearTimeout(o.current),o.current=null),m.current!==null&&i.current!==null){let d={state:i.current,timestamp:Date.now()};p(d),T(),m.current=null,i.current=null;}if(u.current.length===0)return;let t=u.current[u.current.length-1],r=u.current.slice(0,-1);u.current=r,H(r);let s={state:a.current,timestamp:Date.now()};c.current=[...c.current,s],S(c.current),h(t.state),a.current=t.state,v(t.state),k?.(t.state);},[p,k,v,T]),_=react.useCallback(()=>{if(c.current.length===0)return;let t=c.current[c.current.length-1],r=c.current.slice(0,-1);c.current=r,S(r);let s={state:a.current,timestamp:Date.now()};u.current=[...u.current,s],H(u.current),h(t.state),a.current=t.state,v(t.state),I?.(t.state);},[I,v]);react.useEffect(()=>{if(!P)return;let t=r=>{!(r.metaKey||r.ctrlKey)||r.key!=="z"||(r.preventDefault(),r.shiftKey?_():D());};return window.addEventListener("keydown",t),()=>window.removeEventListener("keydown",t)},[P,D,_]);let b=react.useCallback(()=>{o.current!==null&&(clearTimeout(o.current),o.current=null),m.current=null,i.current=null,u.current=[],c.current=[],H([]),S([]);},[]),B=react.useCallback(t=>{let r=false;if(o.current!==null&&(clearTimeout(o.current),o.current=null),m.current!==null&&i.current!==null){let s={state:i.current,timestamp:Date.now()};p(s),T(),m.current=null,i.current=null,r=true;}if(r)t&&u.current.length>0&&(u.current[u.current.length-1].label=t,H([...u.current]));else {let s={state:a.current,timestamp:Date.now(),label:t};p(s),T(),M?.(s);}},[p,M,T]),J=react.useCallback(()=>{b(),n&&Q(n.key),h(e),a.current=e;},[b,e,n]);return {state:R,setState:z,undo:D,redo:_,canUndo:x.length>0,canRedo:L.length>0,clearHistory:b,clearDraft:J,snapshot:B,past:x,future:L}}exports.useFormHistory=W;
1
+ 'use strict';var react=require('react'),jsxRuntime=require('react/jsx-runtime');var ye=100,Fe=300,he=500,Te=1;function xe(e){return !e||typeof e=="boolean"?null:{key:e.key,debounceMs:e.debounceMs??he,version:e.version??Te}}function te(){return typeof window<"u"&&!!window.localStorage}function Re(e,n){try{if(!te())return null;let u=window.localStorage.getItem(e);if(!u)return null;let l=JSON.parse(u);return l.__v!==n?(window.localStorage.removeItem(e),null):l.__s}catch{return null}}function ie(e,n,u){try{if(!te())return;window.localStorage.setItem(e,JSON.stringify({__s:n,__v:u}));}catch{}}function ve(e){try{if(!te())return;window.localStorage.removeItem(e);}catch{}}function be(e,n={}){let{maxHistory:u=ye,debounceMs:l=Fe,persist:i,keyboard:a=false,onUndo:o,onRedo:p,onSnapshot:T}=n,r=xe(i),[m,F]=react.useState(()=>{if(r){let c=Re(r.key,r.version);if(c!==null)return c}return e}),g=react.useRef(m);g.current=m;let x=react.useRef([]),h=react.useRef([]),[H,S]=react.useState([]),[L,w]=react.useState([]),y=react.useRef(null),k=react.useRef(null),R=react.useRef(null),P=react.useRef(null),M=react.useCallback(()=>{h.current=[],w([]);},[]),N=react.useCallback(c=>{let d=[...x.current,c];d.length>u&&d.splice(0,d.length-u),x.current=d,S(d);},[u]);react.useEffect(()=>()=>{y.current!==null&&clearTimeout(y.current),P.current!==null&&clearTimeout(P.current);},[]);let Y=react.useCallback(c=>{r&&(P.current!==null&&clearTimeout(P.current),P.current=setTimeout(()=>{ie(r.key,c,r.version),P.current=null;},r.debounceMs));},[r]),$=react.useCallback(c=>{r&&(P.current!==null&&(clearTimeout(P.current),P.current=null),ie(r.key,c,r.version));},[r]),I=react.useCallback((c,d)=>{let v=g.current,O=typeof c=="function"?c(v):c;if(!Object.is(v,O)){if(F(O),g.current=O,Y(O),l<=0){let q={state:v,timestamp:Date.now(),label:d};N(q),M();return}k.current===null&&(R.current=v),k.current=O,y.current!==null&&clearTimeout(y.current),y.current=setTimeout(()=>{let q={state:R.current,timestamp:Date.now(),label:d};N(q),M(),y.current=null,k.current=null,R.current=null;},l);}},[l,N,Y,M]),j=react.useCallback(()=>{if(y.current!==null&&(clearTimeout(y.current),y.current=null),k.current!==null&&R.current!==null){let O={state:R.current,timestamp:Date.now()};N(O),M(),k.current=null,R.current=null;}if(x.current.length===0)return;let c=x.current[x.current.length-1],d=x.current.slice(0,-1);x.current=d,S(d);let v={state:g.current,timestamp:Date.now()};h.current=[...h.current,v],w(h.current),F(c.state),g.current=c.state,$(c.state),o?.(c.state);},[N,o,$,M]),K=react.useCallback(()=>{if(h.current.length===0)return;let c=h.current[h.current.length-1],d=h.current.slice(0,-1);h.current=d,w(d);let v={state:g.current,timestamp:Date.now()};x.current=[...x.current,v],S(x.current),F(c.state),g.current=c.state,$(c.state),p?.(c.state);},[p,$]);react.useEffect(()=>{if(!a)return;let c=d=>{!(d.metaKey||d.ctrlKey)||d.key!=="z"||(d.preventDefault(),d.shiftKey?K():j());};return window.addEventListener("keydown",c),()=>window.removeEventListener("keydown",c)},[a,j,K]);let V=react.useCallback(()=>{y.current!==null&&(clearTimeout(y.current),y.current=null),k.current=null,R.current=null,x.current=[],h.current=[],S([]),w([]);},[]),W=react.useCallback(c=>{let d=false;if(y.current!==null&&(clearTimeout(y.current),y.current=null),k.current!==null&&R.current!==null){let v={state:R.current,timestamp:Date.now()};N(v),M(),k.current=null,R.current=null,d=true;}if(d)c&&x.current.length>0&&(x.current[x.current.length-1].label=c,S([...x.current]));else {let v={state:g.current,timestamp:Date.now(),label:c};N(v),M(),T?.(v);}},[N,T,M]),z=react.useCallback(()=>{V(),r&&ve(r.key),F(e),g.current=e;},[V,e,r]);return {state:m,setState:I,undo:j,redo:K,canUndo:H.length>0,canRedo:L.length>0,clearHistory:V,clearDraft:z,snapshot:W,past:H,future:L}}function J(e,n){return e&&typeof e=="object"&&"value"in e?e:{value:e,message:n}}function G(e,n){let l=typeof e=="number"?e:Number(e);if(n.required&&(typeof e=="string"?e.trim()==="":e==null||e===false))return {message:typeof n.required=="string"?n.required:"Required",type:"required"};if(n.pattern!=null){let{value:i,message:a}=J(n.pattern,"Invalid format");if(typeof e=="string"&&!i.test(e))return {message:a,type:"pattern"}}if(n.minLength!=null&&typeof e=="string"){let{value:i,message:a}=J(n.minLength,`Min ${n.minLength} characters`);if(e.length<i)return {message:a,type:"minLength"}}if(n.maxLength!=null&&typeof e=="string"){let{value:i,message:a}=J(n.maxLength,`Max ${n.maxLength} characters`);if(e.length>i)return {message:a,type:"maxLength"}}if(n.min!=null&&!isNaN(l)){let{value:i,message:a}=J(n.min,`Min ${n.min}`);if(l<i)return {message:a,type:"min"}}if(n.max!=null&&!isNaN(l)){let{value:i,message:a}=J(n.max,`Max ${n.max}`);if(l>i)return {message:a,type:"max"}}if(n.validate){let i=n.validate(e);if(i)return {message:i,type:"validate"}}return null}function re(e,n){let u={};for(let l in n){let i=G(e[l],n[l]);i&&(u[l]=i);}return u}var ce=react.createContext(null);function A(){let e=react.useContext(ce);if(!e)throw new Error("useFormRewindContext must be used within <FormRewind>");return e}var Se=300;function le({initialState:e,children:n,onSubmit:u,maxHistory:l=100,debounceMs:i=Se,persist:a,keyboard:o=false}){let[p,T]=react.useState(e),r=react.useRef(p);r.current=p;let[m,F]=react.useState({}),[g,x]=react.useState({}),h=react.useRef({}),H=react.useRef({}),S=react.useRef({}),L=react.useRef({}),w=react.useRef({}),y=a&&typeof a=="object"?a:null,k=typeof window<"u"&&!!window.localStorage,R=react.useCallback(t=>{if(!(!y||!k))try{window.localStorage.setItem(y.key,JSON.stringify({__s:t,__v:y.version??1}));}catch{}},[y,k]),P=react.useCallback((t,s)=>{w.current[t]=s??{},h.current[t]||(h.current[t]={past:[],future:[]});},[]),M=react.useCallback(t=>{delete w.current[t],delete h.current[t],delete H.current[t],delete S.current[t],delete L.current[t];},[]),N=react.useCallback((t,s)=>{F(f=>({...f,[t]:s}));},[]),Y=react.useCallback(t=>{F(s=>{let f={...s};return delete f[t],f});},[]),$=react.useCallback(t=>{x(s=>({...s,[t]:true}));},[]),I=react.useCallback((t,s)=>{let f=h.current[t]??{past:[],future:[]};f.past.push(s),f.past.length>l&&f.past.shift(),f.future=[],h.current[t]=f;},[l]),j=react.useCallback((t,s)=>{let f=r.current[t];if(Object.is(f,s))return;let E={...r.current,[t]:s};if(T(E),r.current=E,R(E),i<=0){I(t,f);return}if(S.current[t]===null&&(L.current[t]=f),S.current[t]=s,H.current[t]!==null&&clearTimeout(H.current[t]),H.current[t]=setTimeout(()=>{I(t,L.current[t]),H.current[t]=null,S.current[t]=null,L.current[t]=null;},i),g[t]&&w.current[t]){let ne=G(s,w.current[t]);F(ne?Z=>({...Z,[t]:ne}):Z=>{let oe={...Z};return delete oe[t],oe});}},[i,I,R,g]),K=react.useCallback(t=>{H.current[t]!==null&&(clearTimeout(H.current[t]),H.current[t]=null,S.current[t]!==null&&L.current[t]!==null&&(I(t,L.current[t]),S.current[t]=null,L.current[t]=null));let s=h.current[t];if(!s||s.past.length===0)return;let f=s.past.pop();s.future.push(r.current[t]);let E={...r.current,[t]:f};T(E),r.current=E,R(E);},[I,R]),V=react.useCallback(t=>{let s=h.current[t];if(!s||s.future.length===0)return;let f=s.future.pop();s.past.push(r.current[t]);let E={...r.current,[t]:f};T(E),r.current=E,R(E);},[R]),W=react.useCallback(()=>{let t=document.activeElement;if(!t||!(t instanceof HTMLElement))return null;let s=t.getAttribute("name")||t.getAttribute("data-field");return s&&w.current[s]?s:null},[]),z=react.useRef(null);z.current=t=>{if(!(t.metaKey||t.ctrlKey)||t.key!=="z")return;let f=W();f&&(t.preventDefault(),t.shiftKey?V(f):K(f));};let c=react.useRef(o);c.current=o;let[d,v]=react.useState(false);o&&!d&&(window.addEventListener("keydown",t=>z.current?.(t)),v(true)),!o&&d&&(window.removeEventListener("keydown",t=>z.current?.(t)),v(false));let O=react.useCallback(t=>{t&&t.preventDefault();let s={};for(let E in w.current)s[E]=true;x(s);let f=re(r.current,w.current);return F(f),Object.keys(f).length===0&&u&&u(r.current),Object.keys(f).length===0},[u]),q={};for(let t in w.current){let s=h.current[t];q[t]={name:t,rules:w.current[t],touched:!!g[t],canUndo:s?s.past.length>0:false,canRedo:s?s.future.length>0:false};}let me={state:p,setState:j,errors:m,setError:N,clearError:Y,touched:g,touch:$,fields:q,registerField:P,unregisterField:M,undoField:K,redoField:V};return jsxRuntime.jsx(ce.Provider,{value:me,children:jsxRuntime.jsx("form",{onSubmit:O,noValidate:true,children:n})})}function ae({name:e,label:n,rules:u,className:l,style:i,...a}){let o=A();react.useEffect(()=>(o.registerField(e,u),()=>o.unregisterField(e)),[e,u,o.registerField,o.unregisterField]);let p=F=>{o.setState(e,F.target.value);},T=()=>{o.touch(e);},r=o.errors[e],m=String(o.state[e]??"");return jsxRuntime.jsxs("div",{className:l,style:i,children:[n&&jsxRuntime.jsx("label",{htmlFor:e,children:n}),jsxRuntime.jsx("input",{id:e,type:"text",value:m,onChange:p,onBlur:T,"aria-invalid":!!r,"aria-describedby":r?`${e}-error`:void 0,...a}),r&&jsxRuntime.jsx("span",{id:`${e}-error`,role:"alert",children:r.message})]})}function de({name:e,label:n,rules:u,className:l,style:i,...a}){let o=A();react.useEffect(()=>(o.registerField(e,u),()=>o.unregisterField(e)),[e,u,o.registerField,o.unregisterField]);let p=F=>{let g=F.target.value===""?"":Number(F.target.value);o.setState(e,g);},T=()=>{o.touch(e);},r=o.errors[e],m=o.state[e];return jsxRuntime.jsxs("div",{className:l,style:i,children:[n&&jsxRuntime.jsx("label",{htmlFor:e,children:n}),jsxRuntime.jsx("input",{id:e,type:"number",value:m===""||m==null?"":Number(m),onChange:p,onBlur:T,"aria-invalid":!!r,"aria-describedby":r?`${e}-error`:void 0,...a}),r&&jsxRuntime.jsx("span",{id:`${e}-error`,role:"alert",children:r.message})]})}function fe({name:e,label:n,rules:u,className:l,style:i,...a}){let o=A();react.useEffect(()=>(o.registerField(e,u),()=>o.unregisterField(e)),[e,u,o.registerField,o.unregisterField]);let p=m=>{o.setState(e,m.target.checked);},T=o.errors[e],r=!!o.state[e];return jsxRuntime.jsxs("div",{className:l,style:i,children:[jsxRuntime.jsxs("label",{children:[jsxRuntime.jsx("input",{type:"checkbox",checked:r,onChange:p,"aria-invalid":!!T,...a}),n]}),T&&jsxRuntime.jsx("span",{role:"alert",children:T.message})]})}function pe({name:e,label:n,rules:u,options:l,placeholder:i,className:a,style:o}){let p=A();react.useEffect(()=>(p.registerField(e,u),()=>p.unregisterField(e)),[e,u,p.registerField,p.unregisterField]);let T=g=>{p.setState(e,g.target.value);},r=()=>{p.touch(e);},m=p.errors[e],F=String(p.state[e]??"");return jsxRuntime.jsxs("div",{className:a,style:o,children:[n&&jsxRuntime.jsx("label",{htmlFor:e,children:n}),jsxRuntime.jsxs("select",{id:e,value:F,onChange:T,onBlur:r,"aria-invalid":!!m,"aria-describedby":m?`${e}-error`:void 0,children:[i&&jsxRuntime.jsx("option",{value:"",disabled:true,children:i}),l.map(g=>jsxRuntime.jsx("option",{value:g.value,children:g.label},g.value))]}),m&&jsxRuntime.jsx("span",{id:`${e}-error`,role:"alert",children:m.message})]})}function ge({name:e,label:n,rules:u,className:l,style:i,...a}){let o=A();react.useEffect(()=>(o.registerField(e,u),()=>o.unregisterField(e)),[e,u,o.registerField,o.unregisterField]);let p=F=>{o.setState(e,F.target.value);},T=()=>{o.touch(e);},r=o.errors[e],m=String(o.state[e]??"");return jsxRuntime.jsxs("div",{className:l,style:i,children:[n&&jsxRuntime.jsx("label",{htmlFor:e,children:n}),jsxRuntime.jsx("textarea",{id:e,value:m,onChange:p,onBlur:T,"aria-invalid":!!r,"aria-describedby":r?`${e}-error`:void 0,...a}),r&&jsxRuntime.jsx("span",{id:`${e}-error`,role:"alert",children:r.message})]})}exports.CheckboxField=fe;exports.FormRewind=le;exports.NumberField=de;exports.SelectField=pe;exports.TextField=ae;exports.TextareaField=ge;exports.useFormHistory=be;exports.useFormRewindContext=A;exports.validateAll=re;exports.validateField=G;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import {useState,useRef,useCallback,useEffect}from'react';var j=100,V=300,X=500,Y=1;function q(e){return !e||typeof e=="boolean"?null:{key:e.key,debounceMs:e.debounceMs??X,version:e.version??Y}}function U(){return typeof window<"u"&&!!window.localStorage}function G(e,g){try{if(!U())return null;let f=window.localStorage.getItem(e);if(!f)return null;let E=JSON.parse(f);return E.__v!==g?(window.localStorage.removeItem(e),null):E.__s}catch{return null}}function K(e,g,f){try{if(!U())return;window.localStorage.setItem(e,JSON.stringify({__s:g,__v:f}));}catch{}}function Q(e){try{if(!U())return;window.localStorage.removeItem(e);}catch{}}function W(e,g={}){let{maxHistory:f=j,debounceMs:E=V,persist:C,keyboard:P=false,onUndo:k,onRedo:I,onSnapshot:M}=g,n=q(C),[R,h]=useState(()=>{if(n){let t=G(n.key,n.version);if(t!==null)return t}return e}),a=useRef(R);a.current=R;let u=useRef([]),c=useRef([]),[x,H]=useState([]),[L,S]=useState([]),o=useRef(null),m=useRef(null),i=useRef(null),y=useRef(null),T=useCallback(()=>{c.current=[],S([]);},[]),p=useCallback(t=>{let r=[...u.current,t];r.length>f&&r.splice(0,r.length-f),u.current=r,H(r);},[f]);useEffect(()=>()=>{o.current!==null&&clearTimeout(o.current),y.current!==null&&clearTimeout(y.current);},[]);let N=useCallback(t=>{n&&(y.current!==null&&clearTimeout(y.current),y.current=setTimeout(()=>{K(n.key,t,n.version),y.current=null;},n.debounceMs));},[n]),v=useCallback(t=>{n&&(y.current!==null&&(clearTimeout(y.current),y.current=null),K(n.key,t,n.version));},[n]),z=useCallback((t,r)=>{let s=a.current,d=typeof t=="function"?t(s):t;if(!Object.is(s,d)){if(h(d),a.current=d,N(d),E<=0){let F={state:s,timestamp:Date.now(),label:r};p(F),T();return}m.current===null&&(i.current=s),m.current=d,o.current!==null&&clearTimeout(o.current),o.current=setTimeout(()=>{let F={state:i.current,timestamp:Date.now(),label:r};p(F),T(),o.current=null,m.current=null,i.current=null;},E);}},[E,p,N,T]),D=useCallback(()=>{if(o.current!==null&&(clearTimeout(o.current),o.current=null),m.current!==null&&i.current!==null){let d={state:i.current,timestamp:Date.now()};p(d),T(),m.current=null,i.current=null;}if(u.current.length===0)return;let t=u.current[u.current.length-1],r=u.current.slice(0,-1);u.current=r,H(r);let s={state:a.current,timestamp:Date.now()};c.current=[...c.current,s],S(c.current),h(t.state),a.current=t.state,v(t.state),k?.(t.state);},[p,k,v,T]),_=useCallback(()=>{if(c.current.length===0)return;let t=c.current[c.current.length-1],r=c.current.slice(0,-1);c.current=r,S(r);let s={state:a.current,timestamp:Date.now()};u.current=[...u.current,s],H(u.current),h(t.state),a.current=t.state,v(t.state),I?.(t.state);},[I,v]);useEffect(()=>{if(!P)return;let t=r=>{!(r.metaKey||r.ctrlKey)||r.key!=="z"||(r.preventDefault(),r.shiftKey?_():D());};return window.addEventListener("keydown",t),()=>window.removeEventListener("keydown",t)},[P,D,_]);let b=useCallback(()=>{o.current!==null&&(clearTimeout(o.current),o.current=null),m.current=null,i.current=null,u.current=[],c.current=[],H([]),S([]);},[]),B=useCallback(t=>{let r=false;if(o.current!==null&&(clearTimeout(o.current),o.current=null),m.current!==null&&i.current!==null){let s={state:i.current,timestamp:Date.now()};p(s),T(),m.current=null,i.current=null,r=true;}if(r)t&&u.current.length>0&&(u.current[u.current.length-1].label=t,H([...u.current]));else {let s={state:a.current,timestamp:Date.now(),label:t};p(s),T(),M?.(s);}},[p,M,T]),J=useCallback(()=>{b(),n&&Q(n.key),h(e),a.current=e;},[b,e,n]);return {state:R,setState:z,undo:D,redo:_,canUndo:x.length>0,canRedo:L.length>0,clearHistory:b,clearDraft:J,snapshot:B,past:x,future:L}}export{W as useFormHistory};
1
+ import {createContext,useState,useRef,useCallback,useEffect,useContext}from'react';import {jsx,jsxs}from'react/jsx-runtime';var ye=100,Fe=300,he=500,Te=1;function xe(e){return !e||typeof e=="boolean"?null:{key:e.key,debounceMs:e.debounceMs??he,version:e.version??Te}}function te(){return typeof window<"u"&&!!window.localStorage}function Re(e,n){try{if(!te())return null;let u=window.localStorage.getItem(e);if(!u)return null;let l=JSON.parse(u);return l.__v!==n?(window.localStorage.removeItem(e),null):l.__s}catch{return null}}function ie(e,n,u){try{if(!te())return;window.localStorage.setItem(e,JSON.stringify({__s:n,__v:u}));}catch{}}function ve(e){try{if(!te())return;window.localStorage.removeItem(e);}catch{}}function be(e,n={}){let{maxHistory:u=ye,debounceMs:l=Fe,persist:i,keyboard:a=false,onUndo:o,onRedo:p,onSnapshot:T}=n,r=xe(i),[m,F]=useState(()=>{if(r){let c=Re(r.key,r.version);if(c!==null)return c}return e}),g=useRef(m);g.current=m;let x=useRef([]),h=useRef([]),[H,S]=useState([]),[L,w]=useState([]),y=useRef(null),k=useRef(null),R=useRef(null),P=useRef(null),M=useCallback(()=>{h.current=[],w([]);},[]),N=useCallback(c=>{let d=[...x.current,c];d.length>u&&d.splice(0,d.length-u),x.current=d,S(d);},[u]);useEffect(()=>()=>{y.current!==null&&clearTimeout(y.current),P.current!==null&&clearTimeout(P.current);},[]);let Y=useCallback(c=>{r&&(P.current!==null&&clearTimeout(P.current),P.current=setTimeout(()=>{ie(r.key,c,r.version),P.current=null;},r.debounceMs));},[r]),$=useCallback(c=>{r&&(P.current!==null&&(clearTimeout(P.current),P.current=null),ie(r.key,c,r.version));},[r]),I=useCallback((c,d)=>{let v=g.current,O=typeof c=="function"?c(v):c;if(!Object.is(v,O)){if(F(O),g.current=O,Y(O),l<=0){let q={state:v,timestamp:Date.now(),label:d};N(q),M();return}k.current===null&&(R.current=v),k.current=O,y.current!==null&&clearTimeout(y.current),y.current=setTimeout(()=>{let q={state:R.current,timestamp:Date.now(),label:d};N(q),M(),y.current=null,k.current=null,R.current=null;},l);}},[l,N,Y,M]),j=useCallback(()=>{if(y.current!==null&&(clearTimeout(y.current),y.current=null),k.current!==null&&R.current!==null){let O={state:R.current,timestamp:Date.now()};N(O),M(),k.current=null,R.current=null;}if(x.current.length===0)return;let c=x.current[x.current.length-1],d=x.current.slice(0,-1);x.current=d,S(d);let v={state:g.current,timestamp:Date.now()};h.current=[...h.current,v],w(h.current),F(c.state),g.current=c.state,$(c.state),o?.(c.state);},[N,o,$,M]),K=useCallback(()=>{if(h.current.length===0)return;let c=h.current[h.current.length-1],d=h.current.slice(0,-1);h.current=d,w(d);let v={state:g.current,timestamp:Date.now()};x.current=[...x.current,v],S(x.current),F(c.state),g.current=c.state,$(c.state),p?.(c.state);},[p,$]);useEffect(()=>{if(!a)return;let c=d=>{!(d.metaKey||d.ctrlKey)||d.key!=="z"||(d.preventDefault(),d.shiftKey?K():j());};return window.addEventListener("keydown",c),()=>window.removeEventListener("keydown",c)},[a,j,K]);let V=useCallback(()=>{y.current!==null&&(clearTimeout(y.current),y.current=null),k.current=null,R.current=null,x.current=[],h.current=[],S([]),w([]);},[]),W=useCallback(c=>{let d=false;if(y.current!==null&&(clearTimeout(y.current),y.current=null),k.current!==null&&R.current!==null){let v={state:R.current,timestamp:Date.now()};N(v),M(),k.current=null,R.current=null,d=true;}if(d)c&&x.current.length>0&&(x.current[x.current.length-1].label=c,S([...x.current]));else {let v={state:g.current,timestamp:Date.now(),label:c};N(v),M(),T?.(v);}},[N,T,M]),z=useCallback(()=>{V(),r&&ve(r.key),F(e),g.current=e;},[V,e,r]);return {state:m,setState:I,undo:j,redo:K,canUndo:H.length>0,canRedo:L.length>0,clearHistory:V,clearDraft:z,snapshot:W,past:H,future:L}}function J(e,n){return e&&typeof e=="object"&&"value"in e?e:{value:e,message:n}}function G(e,n){let l=typeof e=="number"?e:Number(e);if(n.required&&(typeof e=="string"?e.trim()==="":e==null||e===false))return {message:typeof n.required=="string"?n.required:"Required",type:"required"};if(n.pattern!=null){let{value:i,message:a}=J(n.pattern,"Invalid format");if(typeof e=="string"&&!i.test(e))return {message:a,type:"pattern"}}if(n.minLength!=null&&typeof e=="string"){let{value:i,message:a}=J(n.minLength,`Min ${n.minLength} characters`);if(e.length<i)return {message:a,type:"minLength"}}if(n.maxLength!=null&&typeof e=="string"){let{value:i,message:a}=J(n.maxLength,`Max ${n.maxLength} characters`);if(e.length>i)return {message:a,type:"maxLength"}}if(n.min!=null&&!isNaN(l)){let{value:i,message:a}=J(n.min,`Min ${n.min}`);if(l<i)return {message:a,type:"min"}}if(n.max!=null&&!isNaN(l)){let{value:i,message:a}=J(n.max,`Max ${n.max}`);if(l>i)return {message:a,type:"max"}}if(n.validate){let i=n.validate(e);if(i)return {message:i,type:"validate"}}return null}function re(e,n){let u={};for(let l in n){let i=G(e[l],n[l]);i&&(u[l]=i);}return u}var ce=createContext(null);function A(){let e=useContext(ce);if(!e)throw new Error("useFormRewindContext must be used within <FormRewind>");return e}var Se=300;function le({initialState:e,children:n,onSubmit:u,maxHistory:l=100,debounceMs:i=Se,persist:a,keyboard:o=false}){let[p,T]=useState(e),r=useRef(p);r.current=p;let[m,F]=useState({}),[g,x]=useState({}),h=useRef({}),H=useRef({}),S=useRef({}),L=useRef({}),w=useRef({}),y=a&&typeof a=="object"?a:null,k=typeof window<"u"&&!!window.localStorage,R=useCallback(t=>{if(!(!y||!k))try{window.localStorage.setItem(y.key,JSON.stringify({__s:t,__v:y.version??1}));}catch{}},[y,k]),P=useCallback((t,s)=>{w.current[t]=s??{},h.current[t]||(h.current[t]={past:[],future:[]});},[]),M=useCallback(t=>{delete w.current[t],delete h.current[t],delete H.current[t],delete S.current[t],delete L.current[t];},[]),N=useCallback((t,s)=>{F(f=>({...f,[t]:s}));},[]),Y=useCallback(t=>{F(s=>{let f={...s};return delete f[t],f});},[]),$=useCallback(t=>{x(s=>({...s,[t]:true}));},[]),I=useCallback((t,s)=>{let f=h.current[t]??{past:[],future:[]};f.past.push(s),f.past.length>l&&f.past.shift(),f.future=[],h.current[t]=f;},[l]),j=useCallback((t,s)=>{let f=r.current[t];if(Object.is(f,s))return;let E={...r.current,[t]:s};if(T(E),r.current=E,R(E),i<=0){I(t,f);return}if(S.current[t]===null&&(L.current[t]=f),S.current[t]=s,H.current[t]!==null&&clearTimeout(H.current[t]),H.current[t]=setTimeout(()=>{I(t,L.current[t]),H.current[t]=null,S.current[t]=null,L.current[t]=null;},i),g[t]&&w.current[t]){let ne=G(s,w.current[t]);F(ne?Z=>({...Z,[t]:ne}):Z=>{let oe={...Z};return delete oe[t],oe});}},[i,I,R,g]),K=useCallback(t=>{H.current[t]!==null&&(clearTimeout(H.current[t]),H.current[t]=null,S.current[t]!==null&&L.current[t]!==null&&(I(t,L.current[t]),S.current[t]=null,L.current[t]=null));let s=h.current[t];if(!s||s.past.length===0)return;let f=s.past.pop();s.future.push(r.current[t]);let E={...r.current,[t]:f};T(E),r.current=E,R(E);},[I,R]),V=useCallback(t=>{let s=h.current[t];if(!s||s.future.length===0)return;let f=s.future.pop();s.past.push(r.current[t]);let E={...r.current,[t]:f};T(E),r.current=E,R(E);},[R]),W=useCallback(()=>{let t=document.activeElement;if(!t||!(t instanceof HTMLElement))return null;let s=t.getAttribute("name")||t.getAttribute("data-field");return s&&w.current[s]?s:null},[]),z=useRef(null);z.current=t=>{if(!(t.metaKey||t.ctrlKey)||t.key!=="z")return;let f=W();f&&(t.preventDefault(),t.shiftKey?V(f):K(f));};let c=useRef(o);c.current=o;let[d,v]=useState(false);o&&!d&&(window.addEventListener("keydown",t=>z.current?.(t)),v(true)),!o&&d&&(window.removeEventListener("keydown",t=>z.current?.(t)),v(false));let O=useCallback(t=>{t&&t.preventDefault();let s={};for(let E in w.current)s[E]=true;x(s);let f=re(r.current,w.current);return F(f),Object.keys(f).length===0&&u&&u(r.current),Object.keys(f).length===0},[u]),q={};for(let t in w.current){let s=h.current[t];q[t]={name:t,rules:w.current[t],touched:!!g[t],canUndo:s?s.past.length>0:false,canRedo:s?s.future.length>0:false};}let me={state:p,setState:j,errors:m,setError:N,clearError:Y,touched:g,touch:$,fields:q,registerField:P,unregisterField:M,undoField:K,redoField:V};return jsx(ce.Provider,{value:me,children:jsx("form",{onSubmit:O,noValidate:true,children:n})})}function ae({name:e,label:n,rules:u,className:l,style:i,...a}){let o=A();useEffect(()=>(o.registerField(e,u),()=>o.unregisterField(e)),[e,u,o.registerField,o.unregisterField]);let p=F=>{o.setState(e,F.target.value);},T=()=>{o.touch(e);},r=o.errors[e],m=String(o.state[e]??"");return jsxs("div",{className:l,style:i,children:[n&&jsx("label",{htmlFor:e,children:n}),jsx("input",{id:e,type:"text",value:m,onChange:p,onBlur:T,"aria-invalid":!!r,"aria-describedby":r?`${e}-error`:void 0,...a}),r&&jsx("span",{id:`${e}-error`,role:"alert",children:r.message})]})}function de({name:e,label:n,rules:u,className:l,style:i,...a}){let o=A();useEffect(()=>(o.registerField(e,u),()=>o.unregisterField(e)),[e,u,o.registerField,o.unregisterField]);let p=F=>{let g=F.target.value===""?"":Number(F.target.value);o.setState(e,g);},T=()=>{o.touch(e);},r=o.errors[e],m=o.state[e];return jsxs("div",{className:l,style:i,children:[n&&jsx("label",{htmlFor:e,children:n}),jsx("input",{id:e,type:"number",value:m===""||m==null?"":Number(m),onChange:p,onBlur:T,"aria-invalid":!!r,"aria-describedby":r?`${e}-error`:void 0,...a}),r&&jsx("span",{id:`${e}-error`,role:"alert",children:r.message})]})}function fe({name:e,label:n,rules:u,className:l,style:i,...a}){let o=A();useEffect(()=>(o.registerField(e,u),()=>o.unregisterField(e)),[e,u,o.registerField,o.unregisterField]);let p=m=>{o.setState(e,m.target.checked);},T=o.errors[e],r=!!o.state[e];return jsxs("div",{className:l,style:i,children:[jsxs("label",{children:[jsx("input",{type:"checkbox",checked:r,onChange:p,"aria-invalid":!!T,...a}),n]}),T&&jsx("span",{role:"alert",children:T.message})]})}function pe({name:e,label:n,rules:u,options:l,placeholder:i,className:a,style:o}){let p=A();useEffect(()=>(p.registerField(e,u),()=>p.unregisterField(e)),[e,u,p.registerField,p.unregisterField]);let T=g=>{p.setState(e,g.target.value);},r=()=>{p.touch(e);},m=p.errors[e],F=String(p.state[e]??"");return jsxs("div",{className:a,style:o,children:[n&&jsx("label",{htmlFor:e,children:n}),jsxs("select",{id:e,value:F,onChange:T,onBlur:r,"aria-invalid":!!m,"aria-describedby":m?`${e}-error`:void 0,children:[i&&jsx("option",{value:"",disabled:true,children:i}),l.map(g=>jsx("option",{value:g.value,children:g.label},g.value))]}),m&&jsx("span",{id:`${e}-error`,role:"alert",children:m.message})]})}function ge({name:e,label:n,rules:u,className:l,style:i,...a}){let o=A();useEffect(()=>(o.registerField(e,u),()=>o.unregisterField(e)),[e,u,o.registerField,o.unregisterField]);let p=F=>{o.setState(e,F.target.value);},T=()=>{o.touch(e);},r=o.errors[e],m=String(o.state[e]??"");return jsxs("div",{className:l,style:i,children:[n&&jsx("label",{htmlFor:e,children:n}),jsx("textarea",{id:e,value:m,onChange:p,onBlur:T,"aria-invalid":!!r,"aria-describedby":r?`${e}-error`:void 0,...a}),r&&jsx("span",{id:`${e}-error`,role:"alert",children:r.message})]})}export{fe as CheckboxField,le as FormRewind,de as NumberField,pe as SelectField,ae as TextField,ge as TextareaField,be as useFormHistory,A as useFormRewindContext,re as validateAll,G as validateField};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-form-rewind",
3
- "version": "0.5.0",
3
+ "version": "1.1.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",