@patch-kit/history 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 +197 -0
- package/dist/index.d.mts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +124 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +106 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# History Manager
|
|
2
|
+
|
|
3
|
+
A self-contained undo/redo system for using the Command Pattern.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Each user action is recorded as a command with `undo()` and `redo()` functions, enabling full history traversal. The factory pattern ensures each history instance is independently scoped and fully typed.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
Call `createHistory()` once at module level and export the bound pair. Both `HistoryProvider` and `useHistory` must come from the same call — they share the same context instance.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { createHistory } from '@patch-kit/history';
|
|
19
|
+
|
|
20
|
+
export const { HistoryProvider, useHistory } = createHistory();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For multiple independent history stacks, each call creates its own isolated instance:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
export const { HistoryProvider: HistoryA, useHistory: useHistoryA } = createHistory();
|
|
27
|
+
export const { HistoryProvider: HistoryB, useHistory: useHistoryB } = createHistory();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Basic Usage
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
<HistoryProvider>
|
|
36
|
+
<App />
|
|
37
|
+
</HistoryProvider>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
const { addHistory, undo, redo, canUndo, canRedo } = useHistory();
|
|
42
|
+
|
|
43
|
+
addHistory({
|
|
44
|
+
name: 'History Command',
|
|
45
|
+
undo() {
|
|
46
|
+
// Undoes the action (called on undo)
|
|
47
|
+
}
|
|
48
|
+
redo() {
|
|
49
|
+
// Re-applies the action (called on redo)
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `immediate`
|
|
55
|
+
|
|
56
|
+
Pass `true` as the second argument to apply the action immediately on record, instead of applying it manually beforehand:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// without immediate — apply manually first, then record
|
|
60
|
+
applyChange(next);
|
|
61
|
+
addHistory({ undo: () => applyChange(prev), redo: () => applyChange(next) });
|
|
62
|
+
|
|
63
|
+
// with immediate — record and apply in one call
|
|
64
|
+
addHistory({ undo: () => applyChange(prev), redo: () => applyChange(next) }, true);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Typed Return Values
|
|
70
|
+
|
|
71
|
+
Commands can return a value from `undo()` and `redo()`. When they do, the Provider's `onUndo`/`onRedo` callbacks receive that value — letting you centralise any shared logic that would otherwise be repeated at every call site.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
type HistoryValue = { type: string; payload: unknown };
|
|
75
|
+
|
|
76
|
+
export const { HistoryProvider, useHistory } = createHistory<HistoryValue>();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<HistoryProvider
|
|
81
|
+
onUndo={(value) => { /* handle value centrally */ }}
|
|
82
|
+
onRedo={(value) => { /* handle value centrally */ }}
|
|
83
|
+
>
|
|
84
|
+
<App />
|
|
85
|
+
</HistoryProvider>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
addHistory({
|
|
90
|
+
name: 'Update',
|
|
91
|
+
undo() { return { type: 'update', payload: previous }; },
|
|
92
|
+
redo() { return { type: 'update', payload: next }; }
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
If no return value is needed, `undo()` and `redo()` can be void — `onUndo`/`onRedo` are optional and only relevant when commands return data.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## API
|
|
101
|
+
|
|
102
|
+
### `createHistory<T = any>()`
|
|
103
|
+
|
|
104
|
+
Factory function. Call at module level, outside any component.
|
|
105
|
+
|
|
106
|
+
Returns `{ HistoryProvider, useHistory }` bound to the same context instance.
|
|
107
|
+
|
|
108
|
+
### `<HistoryProvider>`
|
|
109
|
+
|
|
110
|
+
| Prop | Type | Default | Description |
|
|
111
|
+
|------|------|---------|-------------|
|
|
112
|
+
| `limit` | `number` | `64` | Max number of history entries |
|
|
113
|
+
| `onUndo` | `(value: T) => void` | — | Called after `undo()` with its return value |
|
|
114
|
+
| `onRedo` | `(value: T) => void` | — | Called after `redo()` with its return value |
|
|
115
|
+
|
|
116
|
+
### `useHistory()`
|
|
117
|
+
|
|
118
|
+
Must be called inside the matching `HistoryProvider`.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
const {
|
|
122
|
+
addHistory, // (command: Command<T>, immediate?: boolean) => void
|
|
123
|
+
undo, // () => void
|
|
124
|
+
redo, // () => void
|
|
125
|
+
resetHistory, // () => void
|
|
126
|
+
canUndo, // boolean
|
|
127
|
+
canRedo, // boolean
|
|
128
|
+
} = useHistory();
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `Command<T>`
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
type Command<T = any> = {
|
|
135
|
+
name: string;
|
|
136
|
+
undo: () => T; // return value passed to onUndo
|
|
137
|
+
redo: () => T; // return value passed to onRedo
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Patterns
|
|
144
|
+
|
|
145
|
+
### Capture state before mutating
|
|
146
|
+
|
|
147
|
+
Always snapshot the previous state before applying the change:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
const previous = structuredClone(item);
|
|
151
|
+
applyChange(next);
|
|
152
|
+
|
|
153
|
+
addHistory({
|
|
154
|
+
name: 'Update',
|
|
155
|
+
undo() { applyChange(previous); },
|
|
156
|
+
redo() { applyChange(next); }
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Batch related operations
|
|
161
|
+
|
|
162
|
+
Multiple related changes should be a single history entry:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// ✅ one entry, one undo
|
|
166
|
+
addHistory({
|
|
167
|
+
name: `Move ${ids.length} items`,
|
|
168
|
+
undo() { ids.forEach(id => move(id, prevPos[id])); },
|
|
169
|
+
redo() { ids.forEach(id => move(id, nextPos[id])); }
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ❌ N entries, N undos
|
|
173
|
+
ids.forEach(id => addHistory({ ... }));
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Delayed commit (drag, resize, etc.)
|
|
177
|
+
|
|
178
|
+
For continuous interactions, accumulate changes and commit once on release:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
const pending = useRef({ initial: null, final: null });
|
|
182
|
+
|
|
183
|
+
onDragStart(() => { pending.current.initial = getPosition(); });
|
|
184
|
+
onDragMove((pos) => { pending.current.final = pos; });
|
|
185
|
+
onDragEnd(() => {
|
|
186
|
+
const { initial, final } = pending.current;
|
|
187
|
+
addHistory({
|
|
188
|
+
name: 'Move',
|
|
189
|
+
undo() { setPosition(initial); },
|
|
190
|
+
redo() { setPosition(final); }
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Updating state on every move event causes re-renders that feed back into the drag — commit once at the end instead.
|
|
196
|
+
|
|
197
|
+
---
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Defines an action that can be executed and reverted.
|
|
6
|
+
* Tells the action **WHAT** happends.
|
|
7
|
+
*/
|
|
8
|
+
type Command<T = any> = {
|
|
9
|
+
name: string;
|
|
10
|
+
/**
|
|
11
|
+
* Undo the action attached to the command.
|
|
12
|
+
*/
|
|
13
|
+
undo: () => T;
|
|
14
|
+
/**
|
|
15
|
+
* Redo the action attached to the command.
|
|
16
|
+
*/
|
|
17
|
+
redo: () => T;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
interface HistoryContextType<T> {
|
|
21
|
+
addHistory: (command: Command<T>, immediate?: boolean) => void;
|
|
22
|
+
undo: () => void;
|
|
23
|
+
redo: () => void;
|
|
24
|
+
canUndo: boolean;
|
|
25
|
+
canRedo: boolean;
|
|
26
|
+
resetHistory: () => void;
|
|
27
|
+
}
|
|
28
|
+
interface HistoryProviderProps<T> {
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
limit?: number;
|
|
31
|
+
onUndo?: (value: T) => void;
|
|
32
|
+
onRedo?: (value: T) => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Manages a history of commands to provide undo and redo functionality.
|
|
36
|
+
*
|
|
37
|
+
* Implements the Command design pattern's history tracking. It stores
|
|
38
|
+
* a list of executed commands in a buffer with a limit.
|
|
39
|
+
* When a new command is added after an `undo` operation, any existing `redo`
|
|
40
|
+
* history is cleared. If the history limit is exceeded, the oldest command is
|
|
41
|
+
* discarded.
|
|
42
|
+
*/
|
|
43
|
+
declare function createHistory<T = any>(): {
|
|
44
|
+
HistoryProvider: ({ children, limit, onUndo, onRedo, }: HistoryProviderProps<T>) => react_jsx_runtime.JSX.Element;
|
|
45
|
+
useHistory: () => HistoryContextType<T>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export { createHistory };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Defines an action that can be executed and reverted.
|
|
6
|
+
* Tells the action **WHAT** happends.
|
|
7
|
+
*/
|
|
8
|
+
type Command<T = any> = {
|
|
9
|
+
name: string;
|
|
10
|
+
/**
|
|
11
|
+
* Undo the action attached to the command.
|
|
12
|
+
*/
|
|
13
|
+
undo: () => T;
|
|
14
|
+
/**
|
|
15
|
+
* Redo the action attached to the command.
|
|
16
|
+
*/
|
|
17
|
+
redo: () => T;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
interface HistoryContextType<T> {
|
|
21
|
+
addHistory: (command: Command<T>, immediate?: boolean) => void;
|
|
22
|
+
undo: () => void;
|
|
23
|
+
redo: () => void;
|
|
24
|
+
canUndo: boolean;
|
|
25
|
+
canRedo: boolean;
|
|
26
|
+
resetHistory: () => void;
|
|
27
|
+
}
|
|
28
|
+
interface HistoryProviderProps<T> {
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
limit?: number;
|
|
31
|
+
onUndo?: (value: T) => void;
|
|
32
|
+
onRedo?: (value: T) => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Manages a history of commands to provide undo and redo functionality.
|
|
36
|
+
*
|
|
37
|
+
* Implements the Command design pattern's history tracking. It stores
|
|
38
|
+
* a list of executed commands in a buffer with a limit.
|
|
39
|
+
* When a new command is added after an `undo` operation, any existing `redo`
|
|
40
|
+
* history is cleared. If the history limit is exceeded, the oldest command is
|
|
41
|
+
* discarded.
|
|
42
|
+
*/
|
|
43
|
+
declare function createHistory<T = any>(): {
|
|
44
|
+
HistoryProvider: ({ children, limit, onUndo, onRedo, }: HistoryProviderProps<T>) => react_jsx_runtime.JSX.Element;
|
|
45
|
+
useHistory: () => HistoryContextType<T>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export { createHistory };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
"use client";
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// index.tsx
|
|
23
|
+
var index_exports = {};
|
|
24
|
+
__export(index_exports, {
|
|
25
|
+
createHistory: () => createHistory
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
30
|
+
var initialState = {
|
|
31
|
+
commands: [],
|
|
32
|
+
step: 0
|
|
33
|
+
};
|
|
34
|
+
function historyReducer(state, action) {
|
|
35
|
+
switch (action.type) {
|
|
36
|
+
case "ADD": {
|
|
37
|
+
try {
|
|
38
|
+
const newHistory = state.commands.slice(0, state.step);
|
|
39
|
+
newHistory.push(action.command);
|
|
40
|
+
if (newHistory.length > action.limit) {
|
|
41
|
+
newHistory.shift();
|
|
42
|
+
}
|
|
43
|
+
return { commands: newHistory, step: newHistory.length };
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(`Failed to add command ${action.command.name}: ${error}`);
|
|
46
|
+
return state;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
case "UNDO": {
|
|
50
|
+
if (state.step <= 0) return state;
|
|
51
|
+
return { ...state, step: state.step - 1 };
|
|
52
|
+
}
|
|
53
|
+
case "REDO": {
|
|
54
|
+
if (state.step >= state.commands.length) return state;
|
|
55
|
+
return { ...state, step: state.step + 1 };
|
|
56
|
+
}
|
|
57
|
+
case "RESET":
|
|
58
|
+
return initialState;
|
|
59
|
+
default:
|
|
60
|
+
return state;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function createHistory() {
|
|
64
|
+
const Context = (0, import_react.createContext)(null);
|
|
65
|
+
function HistoryProvider({
|
|
66
|
+
children,
|
|
67
|
+
limit = 64,
|
|
68
|
+
onUndo,
|
|
69
|
+
onRedo
|
|
70
|
+
}) {
|
|
71
|
+
const [state, dispatch] = (0, import_react.useReducer)(historyReducer, initialState);
|
|
72
|
+
const canUndo = state.step > 0;
|
|
73
|
+
const canRedo = state.step < state.commands.length;
|
|
74
|
+
const addHistory = (0, import_react.useCallback)(
|
|
75
|
+
(command, immediate = false) => {
|
|
76
|
+
if (immediate) command.redo();
|
|
77
|
+
dispatch({ type: "ADD", command, limit });
|
|
78
|
+
},
|
|
79
|
+
[limit]
|
|
80
|
+
);
|
|
81
|
+
const undo = (0, import_react.useCallback)(() => {
|
|
82
|
+
const { commands, step } = state;
|
|
83
|
+
if (step <= 0) return;
|
|
84
|
+
const command = commands[step - 1];
|
|
85
|
+
const value2 = command.undo();
|
|
86
|
+
onUndo?.(value2);
|
|
87
|
+
dispatch({ type: "UNDO" });
|
|
88
|
+
}, [state, onUndo]);
|
|
89
|
+
const redo = (0, import_react.useCallback)(() => {
|
|
90
|
+
const { commands, step } = state;
|
|
91
|
+
if (step >= commands.length) return;
|
|
92
|
+
const command = commands[step];
|
|
93
|
+
const value2 = command.redo();
|
|
94
|
+
onRedo?.(value2);
|
|
95
|
+
dispatch({ type: "REDO" });
|
|
96
|
+
}, [state, onRedo]);
|
|
97
|
+
const resetHistory = (0, import_react.useCallback)(() => {
|
|
98
|
+
dispatch({ type: "RESET" });
|
|
99
|
+
}, []);
|
|
100
|
+
const value = (0, import_react.useMemo)(
|
|
101
|
+
() => ({
|
|
102
|
+
addHistory,
|
|
103
|
+
undo,
|
|
104
|
+
redo,
|
|
105
|
+
canUndo,
|
|
106
|
+
canRedo,
|
|
107
|
+
resetHistory
|
|
108
|
+
}),
|
|
109
|
+
[addHistory, undo, redo, canUndo, canRedo, resetHistory]
|
|
110
|
+
);
|
|
111
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Context.Provider, { value, children });
|
|
112
|
+
}
|
|
113
|
+
function useHistory() {
|
|
114
|
+
const context = (0, import_react.useContext)(Context);
|
|
115
|
+
if (!context) throw new Error("useHistory must be used within a HistoryProvider");
|
|
116
|
+
return context;
|
|
117
|
+
}
|
|
118
|
+
return { HistoryProvider, useHistory };
|
|
119
|
+
}
|
|
120
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
121
|
+
0 && (module.exports = {
|
|
122
|
+
createHistory
|
|
123
|
+
});
|
|
124
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../index.tsx"],"sourcesContent":["\"use client\";\n\nimport React, {\n createContext,\n useContext,\n useReducer,\n useCallback,\n useMemo,\n} from \"react\";\nimport { Command } from \"./types\";\n\ninterface HistoryContextType<T> {\n addHistory: (command: Command<T>, immediate?: boolean) => void;\n undo: () => void;\n redo: () => void;\n canUndo: boolean;\n canRedo: boolean;\n resetHistory: () => void;\n}\n\n// State and action types for the reducer\ninterface HistoryState {\n commands: Command<any>[];\n step: number;\n}\n\ntype HistoryAction =\n | { type: \"ADD\"; command: Command<any>; limit: number }\n | { type: \"UNDO\" }\n | { type: \"REDO\" }\n | { type: \"RESET\" };\n\nconst initialState: HistoryState = {\n commands: [],\n step: 0,\n};\n\n/**\n * Pure reducer function that handles all history state transitions atomically.\n * Side effects (undo/redo) are handled outside the reducer to avoid\n * state updates during render.\n */\nfunction historyReducer(state: HistoryState, action: HistoryAction): HistoryState {\n switch (action.type) {\n case \"ADD\": {\n try {\n const newHistory = state.commands.slice(0, state.step);\n newHistory.push(action.command);\n\n if (newHistory.length > action.limit) {\n newHistory.shift();\n }\n\n return { commands: newHistory, step: newHistory.length };\n } catch (error) {\n console.error(`Failed to add command ${action.command.name}: ${error}`);\n return state;\n }\n }\n case \"UNDO\": {\n if (state.step <= 0) return state;\n return { ...state, step: state.step - 1 };\n }\n case \"REDO\": {\n if (state.step >= state.commands.length) return state;\n return { ...state, step: state.step + 1 };\n }\n case \"RESET\":\n return initialState;\n default:\n return state;\n }\n}\n\ninterface HistoryProviderProps<T> {\n children: React.ReactNode;\n limit?: number;\n onUndo?: (value: T) => void;\n onRedo?: (value: T) => void;\n}\n\n/**\n * Manages a history of commands to provide undo and redo functionality.\n *\n * Implements the Command design pattern's history tracking. It stores\n * a list of executed commands in a buffer with a limit.\n * When a new command is added after an `undo` operation, any existing `redo`\n * history is cleared. If the history limit is exceeded, the oldest command is\n * discarded.\n */\nexport function createHistory<T = any>() {\n const Context = createContext<HistoryContextType<T> | null>(null);\n\n function HistoryProvider({\n children,\n limit = 64,\n onUndo,\n onRedo,\n }: HistoryProviderProps<T>) {\n const [state, dispatch] = useReducer(historyReducer, initialState);\n\n const canUndo = state.step > 0;\n const canRedo = state.step < state.commands.length;\n\n const addHistory = useCallback(\n (command: Command<T>, immediate = false) => {\n if (immediate) command.redo();\n dispatch({ type: \"ADD\", command, limit });\n },\n [limit]\n );\n\n const undo = useCallback(() => {\n const { commands, step } = state;\n if (step <= 0) return;\n\n const command = commands[step - 1];\n const value = command.undo();\n onUndo?.(value as T);\n dispatch({ type: \"UNDO\" });\n }, [state, onUndo]);\n\n const redo = useCallback(() => {\n const { commands, step } = state;\n if (step >= commands.length) return;\n\n const command = commands[step];\n const value = command.redo();\n onRedo?.(value as T);\n dispatch({ type: \"REDO\" });\n }, [state, onRedo]);\n\n const resetHistory = useCallback(() => {\n dispatch({ type: \"RESET\" });\n }, []);\n\n const value = useMemo(\n () => ({\n addHistory,\n undo,\n redo,\n canUndo,\n canRedo,\n resetHistory,\n }),\n [addHistory, undo, redo, canUndo, canRedo, resetHistory]\n );\n\n return <Context.Provider value={value}>{children}</Context.Provider>;\n }\n\n function useHistory() {\n const context = useContext(Context);\n if (!context) throw new Error(\"useHistory must be used within a HistoryProvider\");\n return context;\n }\n\n return { HistoryProvider, useHistory };\n}"],"mappings":"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","names":["value"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// index.tsx
|
|
5
|
+
import {
|
|
6
|
+
createContext,
|
|
7
|
+
useContext,
|
|
8
|
+
useReducer,
|
|
9
|
+
useCallback,
|
|
10
|
+
useMemo
|
|
11
|
+
} from "react";
|
|
12
|
+
import { jsx } from "react/jsx-runtime";
|
|
13
|
+
var initialState = {
|
|
14
|
+
commands: [],
|
|
15
|
+
step: 0
|
|
16
|
+
};
|
|
17
|
+
function historyReducer(state, action) {
|
|
18
|
+
switch (action.type) {
|
|
19
|
+
case "ADD": {
|
|
20
|
+
try {
|
|
21
|
+
const newHistory = state.commands.slice(0, state.step);
|
|
22
|
+
newHistory.push(action.command);
|
|
23
|
+
if (newHistory.length > action.limit) {
|
|
24
|
+
newHistory.shift();
|
|
25
|
+
}
|
|
26
|
+
return { commands: newHistory, step: newHistory.length };
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(`Failed to add command ${action.command.name}: ${error}`);
|
|
29
|
+
return state;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
case "UNDO": {
|
|
33
|
+
if (state.step <= 0) return state;
|
|
34
|
+
return { ...state, step: state.step - 1 };
|
|
35
|
+
}
|
|
36
|
+
case "REDO": {
|
|
37
|
+
if (state.step >= state.commands.length) return state;
|
|
38
|
+
return { ...state, step: state.step + 1 };
|
|
39
|
+
}
|
|
40
|
+
case "RESET":
|
|
41
|
+
return initialState;
|
|
42
|
+
default:
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function createHistory() {
|
|
47
|
+
const Context = createContext(null);
|
|
48
|
+
function HistoryProvider({
|
|
49
|
+
children,
|
|
50
|
+
limit = 64,
|
|
51
|
+
onUndo,
|
|
52
|
+
onRedo
|
|
53
|
+
}) {
|
|
54
|
+
const [state, dispatch] = useReducer(historyReducer, initialState);
|
|
55
|
+
const canUndo = state.step > 0;
|
|
56
|
+
const canRedo = state.step < state.commands.length;
|
|
57
|
+
const addHistory = useCallback(
|
|
58
|
+
(command, immediate = false) => {
|
|
59
|
+
if (immediate) command.redo();
|
|
60
|
+
dispatch({ type: "ADD", command, limit });
|
|
61
|
+
},
|
|
62
|
+
[limit]
|
|
63
|
+
);
|
|
64
|
+
const undo = useCallback(() => {
|
|
65
|
+
const { commands, step } = state;
|
|
66
|
+
if (step <= 0) return;
|
|
67
|
+
const command = commands[step - 1];
|
|
68
|
+
const value2 = command.undo();
|
|
69
|
+
onUndo?.(value2);
|
|
70
|
+
dispatch({ type: "UNDO" });
|
|
71
|
+
}, [state, onUndo]);
|
|
72
|
+
const redo = useCallback(() => {
|
|
73
|
+
const { commands, step } = state;
|
|
74
|
+
if (step >= commands.length) return;
|
|
75
|
+
const command = commands[step];
|
|
76
|
+
const value2 = command.redo();
|
|
77
|
+
onRedo?.(value2);
|
|
78
|
+
dispatch({ type: "REDO" });
|
|
79
|
+
}, [state, onRedo]);
|
|
80
|
+
const resetHistory = useCallback(() => {
|
|
81
|
+
dispatch({ type: "RESET" });
|
|
82
|
+
}, []);
|
|
83
|
+
const value = useMemo(
|
|
84
|
+
() => ({
|
|
85
|
+
addHistory,
|
|
86
|
+
undo,
|
|
87
|
+
redo,
|
|
88
|
+
canUndo,
|
|
89
|
+
canRedo,
|
|
90
|
+
resetHistory
|
|
91
|
+
}),
|
|
92
|
+
[addHistory, undo, redo, canUndo, canRedo, resetHistory]
|
|
93
|
+
);
|
|
94
|
+
return /* @__PURE__ */ jsx(Context.Provider, { value, children });
|
|
95
|
+
}
|
|
96
|
+
function useHistory() {
|
|
97
|
+
const context = useContext(Context);
|
|
98
|
+
if (!context) throw new Error("useHistory must be used within a HistoryProvider");
|
|
99
|
+
return context;
|
|
100
|
+
}
|
|
101
|
+
return { HistoryProvider, useHistory };
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
createHistory
|
|
105
|
+
};
|
|
106
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../index.tsx"],"sourcesContent":["\"use client\";\n\nimport React, {\n createContext,\n useContext,\n useReducer,\n useCallback,\n useMemo,\n} from \"react\";\nimport { Command } from \"./types\";\n\ninterface HistoryContextType<T> {\n addHistory: (command: Command<T>, immediate?: boolean) => void;\n undo: () => void;\n redo: () => void;\n canUndo: boolean;\n canRedo: boolean;\n resetHistory: () => void;\n}\n\n// State and action types for the reducer\ninterface HistoryState {\n commands: Command<any>[];\n step: number;\n}\n\ntype HistoryAction =\n | { type: \"ADD\"; command: Command<any>; limit: number }\n | { type: \"UNDO\" }\n | { type: \"REDO\" }\n | { type: \"RESET\" };\n\nconst initialState: HistoryState = {\n commands: [],\n step: 0,\n};\n\n/**\n * Pure reducer function that handles all history state transitions atomically.\n * Side effects (undo/redo) are handled outside the reducer to avoid\n * state updates during render.\n */\nfunction historyReducer(state: HistoryState, action: HistoryAction): HistoryState {\n switch (action.type) {\n case \"ADD\": {\n try {\n const newHistory = state.commands.slice(0, state.step);\n newHistory.push(action.command);\n\n if (newHistory.length > action.limit) {\n newHistory.shift();\n }\n\n return { commands: newHistory, step: newHistory.length };\n } catch (error) {\n console.error(`Failed to add command ${action.command.name}: ${error}`);\n return state;\n }\n }\n case \"UNDO\": {\n if (state.step <= 0) return state;\n return { ...state, step: state.step - 1 };\n }\n case \"REDO\": {\n if (state.step >= state.commands.length) return state;\n return { ...state, step: state.step + 1 };\n }\n case \"RESET\":\n return initialState;\n default:\n return state;\n }\n}\n\ninterface HistoryProviderProps<T> {\n children: React.ReactNode;\n limit?: number;\n onUndo?: (value: T) => void;\n onRedo?: (value: T) => void;\n}\n\n/**\n * Manages a history of commands to provide undo and redo functionality.\n *\n * Implements the Command design pattern's history tracking. It stores\n * a list of executed commands in a buffer with a limit.\n * When a new command is added after an `undo` operation, any existing `redo`\n * history is cleared. If the history limit is exceeded, the oldest command is\n * discarded.\n */\nexport function createHistory<T = any>() {\n const Context = createContext<HistoryContextType<T> | null>(null);\n\n function HistoryProvider({\n children,\n limit = 64,\n onUndo,\n onRedo,\n }: HistoryProviderProps<T>) {\n const [state, dispatch] = useReducer(historyReducer, initialState);\n\n const canUndo = state.step > 0;\n const canRedo = state.step < state.commands.length;\n\n const addHistory = useCallback(\n (command: Command<T>, immediate = false) => {\n if (immediate) command.redo();\n dispatch({ type: \"ADD\", command, limit });\n },\n [limit]\n );\n\n const undo = useCallback(() => {\n const { commands, step } = state;\n if (step <= 0) return;\n\n const command = commands[step - 1];\n const value = command.undo();\n onUndo?.(value as T);\n dispatch({ type: \"UNDO\" });\n }, [state, onUndo]);\n\n const redo = useCallback(() => {\n const { commands, step } = state;\n if (step >= commands.length) return;\n\n const command = commands[step];\n const value = command.redo();\n onRedo?.(value as T);\n dispatch({ type: \"REDO\" });\n }, [state, onRedo]);\n\n const resetHistory = useCallback(() => {\n dispatch({ type: \"RESET\" });\n }, []);\n\n const value = useMemo(\n () => ({\n addHistory,\n undo,\n redo,\n canUndo,\n canRedo,\n resetHistory,\n }),\n [addHistory, undo, redo, canUndo, canRedo, resetHistory]\n );\n\n return <Context.Provider value={value}>{children}</Context.Provider>;\n }\n\n function useHistory() {\n const context = useContext(Context);\n if (!context) throw new Error(\"useHistory must be used within a HistoryProvider\");\n return context;\n }\n\n return { HistoryProvider, useHistory };\n}"],"mappings":"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","names":["value"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@patch-kit/history",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "History Manager for React",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/XKeeXE/patchkit",
|
|
9
|
+
"directory": "packages/history"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.cjs",
|
|
12
|
+
"module": "./dist/index.mjs",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.mjs",
|
|
18
|
+
"require": "./dist/index.cjs"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": ["dist"],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup",
|
|
24
|
+
"dev": "tsup --watch",
|
|
25
|
+
"type-check": "tsc --noEmit"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": ">=18.0.0",
|
|
29
|
+
"react-dom": ">=18.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/react": "^18.3.0",
|
|
33
|
+
"@types/react-dom": "^18.3.0",
|
|
34
|
+
"react": "^18.3.0",
|
|
35
|
+
"react-dom": "^18.3.0",
|
|
36
|
+
"tsup": "^8.1.0",
|
|
37
|
+
"typescript": "^5.4.0"
|
|
38
|
+
}
|
|
39
|
+
}
|