@patch-kit/history 1.0.2 → 2.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/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +33 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +35 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -19,8 +19,8 @@ type Command<T = any> = {
|
|
|
19
19
|
|
|
20
20
|
interface HistoryContextType<T> {
|
|
21
21
|
addHistory: (command: Command<T>, immediate?: boolean) => void;
|
|
22
|
-
undo: () => void;
|
|
23
|
-
redo: () => void;
|
|
22
|
+
undo: (steps?: number) => void;
|
|
23
|
+
redo: (steps?: number) => void;
|
|
24
24
|
canUndo: boolean;
|
|
25
25
|
canRedo: boolean;
|
|
26
26
|
resetHistory: () => void;
|
package/dist/index.d.ts
CHANGED
|
@@ -19,8 +19,8 @@ type Command<T = any> = {
|
|
|
19
19
|
|
|
20
20
|
interface HistoryContextType<T> {
|
|
21
21
|
addHistory: (command: Command<T>, immediate?: boolean) => void;
|
|
22
|
-
undo: () => void;
|
|
23
|
-
redo: () => void;
|
|
22
|
+
undo: (steps?: number) => void;
|
|
23
|
+
redo: (steps?: number) => void;
|
|
24
24
|
canUndo: boolean;
|
|
25
25
|
canRedo: boolean;
|
|
26
26
|
resetHistory: () => void;
|
package/dist/index.js
CHANGED
|
@@ -48,11 +48,11 @@ function historyReducer(state, action) {
|
|
|
48
48
|
}
|
|
49
49
|
case "UNDO": {
|
|
50
50
|
if (state.step <= 0) return state;
|
|
51
|
-
return { ...state, step: state.step -
|
|
51
|
+
return { ...state, step: Math.max(0, state.step - action.steps) };
|
|
52
52
|
}
|
|
53
53
|
case "REDO": {
|
|
54
54
|
if (state.step >= state.commands.length) return state;
|
|
55
|
-
return { ...state, step: state.step +
|
|
55
|
+
return { ...state, step: Math.min(state.commands.length, state.step + action.steps) };
|
|
56
56
|
}
|
|
57
57
|
case "RESET":
|
|
58
58
|
return initialState;
|
|
@@ -71,6 +71,7 @@ function createHistory() {
|
|
|
71
71
|
const [state, dispatch] = (0, import_react.useReducer)(historyReducer, initialState);
|
|
72
72
|
const canUndo = state.step > 0;
|
|
73
73
|
const canRedo = state.step < state.commands.length;
|
|
74
|
+
const busy = (0, import_react.useRef)(false);
|
|
74
75
|
const addHistory = (0, import_react.useCallback)(
|
|
75
76
|
(command, immediate = false) => {
|
|
76
77
|
if (immediate) command.redo();
|
|
@@ -78,21 +79,39 @@ function createHistory() {
|
|
|
78
79
|
},
|
|
79
80
|
[limit]
|
|
80
81
|
);
|
|
81
|
-
const undo = (0, import_react.useCallback)(() => {
|
|
82
|
+
const undo = (0, import_react.useCallback)(async (steps = 1) => {
|
|
83
|
+
if (steps <= 0) throw new Error(`undo() requires a positive number of steps, got ${steps}`);
|
|
84
|
+
if (busy.current) return;
|
|
82
85
|
const { commands, step } = state;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
const actual = Math.min(steps, step);
|
|
87
|
+
if (actual === 0) return;
|
|
88
|
+
busy.current = true;
|
|
89
|
+
try {
|
|
90
|
+
for (let index = 0; index < actual; index++) {
|
|
91
|
+
const value2 = await commands[step - 1 - index].undo();
|
|
92
|
+
onUndo?.(value2);
|
|
93
|
+
}
|
|
94
|
+
dispatch({ type: "UNDO", steps: actual });
|
|
95
|
+
} finally {
|
|
96
|
+
busy.current = false;
|
|
97
|
+
}
|
|
88
98
|
}, [state, onUndo]);
|
|
89
|
-
const redo = (0, import_react.useCallback)(() => {
|
|
99
|
+
const redo = (0, import_react.useCallback)(async (steps = 1) => {
|
|
100
|
+
if (steps <= 0) throw new Error(`redo() requires a positive number of steps, got ${steps}`);
|
|
101
|
+
if (busy.current) return;
|
|
90
102
|
const { commands, step } = state;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
103
|
+
const actual = Math.min(steps, commands.length - step);
|
|
104
|
+
if (actual === 0) return;
|
|
105
|
+
busy.current = true;
|
|
106
|
+
try {
|
|
107
|
+
for (let index = 0; index < actual; index++) {
|
|
108
|
+
const value2 = await commands[step + index].redo();
|
|
109
|
+
onRedo?.(value2);
|
|
110
|
+
}
|
|
111
|
+
dispatch({ type: "REDO", steps: actual });
|
|
112
|
+
} finally {
|
|
113
|
+
busy.current = false;
|
|
114
|
+
}
|
|
96
115
|
}, [state, onRedo]);
|
|
97
116
|
const resetHistory = (0, import_react.useCallback)(() => {
|
|
98
117
|
dispatch({ type: "RESET" });
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/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
|
+
{"version":3,"sources":["../src/index.tsx"],"sourcesContent":["\"use client\";\n\nimport React, {\n createContext,\n useContext,\n useReducer,\n useCallback,\n useMemo,\n useRef,\n} from \"react\";\nimport { Command } from \"./types\";\n\ninterface HistoryContextType<T> {\n addHistory: (command: Command<T>, immediate?: boolean) => void;\n undo: (steps?: number) => void;\n redo: (steps?: number) => 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\"; steps: number }\n | { type: \"REDO\"; steps: number }\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: Math.max(0, state.step - action.steps) };\n }\n case \"REDO\": {\n if (state.step >= state.commands.length) return state;\n return { ...state, step: Math.min(state.commands.length, state.step + action.steps) };\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 const busy = useRef(false);\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(async (steps = 1) => {\n if (steps <= 0) throw new Error(`undo() requires a positive number of steps, got ${steps}`);\n if (busy.current) return;\n const { commands, step } = state;\n const actual = Math.min(steps, step);\n if (actual === 0) return;\n\n busy.current = true;\n try {\n for (let index = 0; index < actual; index++) {\n const value = await commands[step - 1 - index].undo();\n onUndo?.(value as T);\n }\n dispatch({ type: \"UNDO\", steps: actual });\n } finally {\n busy.current = false;\n }\n }, [state, onUndo]);\n\n const redo = useCallback(async (steps = 1) => {\n if (steps <= 0) throw new Error(`redo() requires a positive number of steps, got ${steps}`);\n if (busy.current) return;\n const { commands, step } = state;\n const actual = Math.min(steps, commands.length - step);\n if (actual === 0) return;\n\n busy.current = true;\n try {\n for (let index = 0; index < actual; index++) {\n const value = await commands[step + index].redo();\n onRedo?.(value as T);\n }\n dispatch({ type: \"REDO\", steps: actual });\n } finally {\n busy.current = false;\n }\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
CHANGED
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
useContext,
|
|
8
8
|
useReducer,
|
|
9
9
|
useCallback,
|
|
10
|
-
useMemo
|
|
10
|
+
useMemo,
|
|
11
|
+
useRef
|
|
11
12
|
} from "react";
|
|
12
13
|
import { jsx } from "react/jsx-runtime";
|
|
13
14
|
var initialState = {
|
|
@@ -31,11 +32,11 @@ function historyReducer(state, action) {
|
|
|
31
32
|
}
|
|
32
33
|
case "UNDO": {
|
|
33
34
|
if (state.step <= 0) return state;
|
|
34
|
-
return { ...state, step: state.step -
|
|
35
|
+
return { ...state, step: Math.max(0, state.step - action.steps) };
|
|
35
36
|
}
|
|
36
37
|
case "REDO": {
|
|
37
38
|
if (state.step >= state.commands.length) return state;
|
|
38
|
-
return { ...state, step: state.step +
|
|
39
|
+
return { ...state, step: Math.min(state.commands.length, state.step + action.steps) };
|
|
39
40
|
}
|
|
40
41
|
case "RESET":
|
|
41
42
|
return initialState;
|
|
@@ -54,6 +55,7 @@ function createHistory() {
|
|
|
54
55
|
const [state, dispatch] = useReducer(historyReducer, initialState);
|
|
55
56
|
const canUndo = state.step > 0;
|
|
56
57
|
const canRedo = state.step < state.commands.length;
|
|
58
|
+
const busy = useRef(false);
|
|
57
59
|
const addHistory = useCallback(
|
|
58
60
|
(command, immediate = false) => {
|
|
59
61
|
if (immediate) command.redo();
|
|
@@ -61,21 +63,39 @@ function createHistory() {
|
|
|
61
63
|
},
|
|
62
64
|
[limit]
|
|
63
65
|
);
|
|
64
|
-
const undo = useCallback(() => {
|
|
66
|
+
const undo = useCallback(async (steps = 1) => {
|
|
67
|
+
if (steps <= 0) throw new Error(`undo() requires a positive number of steps, got ${steps}`);
|
|
68
|
+
if (busy.current) return;
|
|
65
69
|
const { commands, step } = state;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
const actual = Math.min(steps, step);
|
|
71
|
+
if (actual === 0) return;
|
|
72
|
+
busy.current = true;
|
|
73
|
+
try {
|
|
74
|
+
for (let index = 0; index < actual; index++) {
|
|
75
|
+
const value2 = await commands[step - 1 - index].undo();
|
|
76
|
+
onUndo?.(value2);
|
|
77
|
+
}
|
|
78
|
+
dispatch({ type: "UNDO", steps: actual });
|
|
79
|
+
} finally {
|
|
80
|
+
busy.current = false;
|
|
81
|
+
}
|
|
71
82
|
}, [state, onUndo]);
|
|
72
|
-
const redo = useCallback(() => {
|
|
83
|
+
const redo = useCallback(async (steps = 1) => {
|
|
84
|
+
if (steps <= 0) throw new Error(`redo() requires a positive number of steps, got ${steps}`);
|
|
85
|
+
if (busy.current) return;
|
|
73
86
|
const { commands, step } = state;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
const actual = Math.min(steps, commands.length - step);
|
|
88
|
+
if (actual === 0) return;
|
|
89
|
+
busy.current = true;
|
|
90
|
+
try {
|
|
91
|
+
for (let index = 0; index < actual; index++) {
|
|
92
|
+
const value2 = await commands[step + index].redo();
|
|
93
|
+
onRedo?.(value2);
|
|
94
|
+
}
|
|
95
|
+
dispatch({ type: "REDO", steps: actual });
|
|
96
|
+
} finally {
|
|
97
|
+
busy.current = false;
|
|
98
|
+
}
|
|
79
99
|
}, [state, onRedo]);
|
|
80
100
|
const resetHistory = useCallback(() => {
|
|
81
101
|
dispatch({ type: "RESET" });
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/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
|
+
{"version":3,"sources":["../src/index.tsx"],"sourcesContent":["\"use client\";\n\nimport React, {\n createContext,\n useContext,\n useReducer,\n useCallback,\n useMemo,\n useRef,\n} from \"react\";\nimport { Command } from \"./types\";\n\ninterface HistoryContextType<T> {\n addHistory: (command: Command<T>, immediate?: boolean) => void;\n undo: (steps?: number) => void;\n redo: (steps?: number) => 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\"; steps: number }\n | { type: \"REDO\"; steps: number }\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: Math.max(0, state.step - action.steps) };\n }\n case \"REDO\": {\n if (state.step >= state.commands.length) return state;\n return { ...state, step: Math.min(state.commands.length, state.step + action.steps) };\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 const busy = useRef(false);\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(async (steps = 1) => {\n if (steps <= 0) throw new Error(`undo() requires a positive number of steps, got ${steps}`);\n if (busy.current) return;\n const { commands, step } = state;\n const actual = Math.min(steps, step);\n if (actual === 0) return;\n\n busy.current = true;\n try {\n for (let index = 0; index < actual; index++) {\n const value = await commands[step - 1 - index].undo();\n onUndo?.(value as T);\n }\n dispatch({ type: \"UNDO\", steps: actual });\n } finally {\n busy.current = false;\n }\n }, [state, onUndo]);\n\n const redo = useCallback(async (steps = 1) => {\n if (steps <= 0) throw new Error(`redo() requires a positive number of steps, got ${steps}`);\n if (busy.current) return;\n const { commands, step } = state;\n const actual = Math.min(steps, commands.length - step);\n if (actual === 0) return;\n\n busy.current = true;\n try {\n for (let index = 0; index < actual; index++) {\n const value = await commands[step + index].redo();\n onRedo?.(value as T);\n }\n dispatch({ type: \"REDO\", steps: actual });\n } finally {\n busy.current = false;\n }\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"]}
|