@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 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
+ ---
@@ -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 };
@@ -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
+ }