@voidhash/mimic-react 0.0.1-alpha.4 → 0.0.1-alpha.6

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.
@@ -0,0 +1,259 @@
1
+ /**
2
+ * @voidhash/mimic-react/zustand-commander
3
+ *
4
+ * React hooks for zustand-commander.
5
+ *
6
+ * @since 0.0.1
7
+ */
8
+
9
+ import { useCallback, useEffect, useMemo } from "react";
10
+ import { useStore, type StoreApi, type UseBoundStore } from "zustand";
11
+ import { performRedo, performUndo, clearUndoHistory } from "./commander";
12
+ import {
13
+ isUndoableCommand,
14
+ type Command,
15
+ type CommandContext,
16
+ type CommandDispatch,
17
+ type CommanderSlice,
18
+ type ExtractState,
19
+ } from "./types.js";
20
+
21
+ // =============================================================================
22
+ // useCommander Hook
23
+ // =============================================================================
24
+
25
+ /**
26
+ * Creates a dispatch function for commands.
27
+ * This is for use outside of React components (e.g., in command handlers).
28
+ */
29
+ function createDispatchFromApi<TStore extends CommanderSlice>(
30
+ storeApi: StoreApi<TStore>,
31
+ maxUndoStackSize = 100
32
+ ): CommandDispatch<TStore> {
33
+ const dispatch: CommandDispatch<TStore> = <TParams, TReturn>(
34
+ command: Command<TStore, TParams, TReturn>
35
+ ) => {
36
+ return (params: TParams): TReturn => {
37
+ // Create context for the command
38
+ const ctx: CommandContext<TStore> = {
39
+ getState: () => storeApi.getState(),
40
+ setState: (partial) => storeApi.setState(partial as Partial<TStore>),
41
+ dispatch,
42
+ };
43
+
44
+ // Execute the command
45
+ const result = command.fn(ctx, params);
46
+
47
+ // If it's an undoable command, add to undo stack
48
+ if (isUndoableCommand(command)) {
49
+ storeApi.setState((state: TStore) => {
50
+ const { undoStack } = state._commander;
51
+
52
+ // Add to undo stack, respecting max size
53
+ const newUndoStack = [
54
+ ...undoStack,
55
+ {
56
+ command,
57
+ params,
58
+ result,
59
+ timestamp: Date.now(),
60
+ },
61
+ ].slice(-maxUndoStackSize);
62
+
63
+ // Clear redo stack when a new command is executed
64
+ return {
65
+ ...state,
66
+ _commander: {
67
+ undoStack: newUndoStack,
68
+ redoStack: [],
69
+ },
70
+ } as TStore;
71
+ });
72
+ }
73
+
74
+ return result;
75
+ };
76
+ };
77
+
78
+ return dispatch;
79
+ }
80
+
81
+ /**
82
+ * React hook to get a dispatch function for commands.
83
+ * The dispatch function executes commands and manages undo/redo state.
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * const dispatch = useCommander(useStore);
88
+ *
89
+ * const handleClick = () => {
90
+ * dispatch(addCard)({ columnId: "col-1", title: "New Card" });
91
+ * };
92
+ * ```
93
+ */
94
+ export function useCommander<TStore extends CommanderSlice>(
95
+ store: UseBoundStore<StoreApi<TStore>>
96
+ ): CommandDispatch<TStore> {
97
+ // Get the store API
98
+ const storeApi = useMemo(() => {
99
+ // UseBoundStore has the StoreApi attached
100
+ return store as unknown as StoreApi<TStore>;
101
+ }, [store]);
102
+
103
+ // Create a stable dispatch function
104
+ const dispatch = useMemo(
105
+ () => createDispatchFromApi<TStore>(storeApi),
106
+ [storeApi]
107
+ );
108
+
109
+ return dispatch as CommandDispatch<TStore>;
110
+ }
111
+
112
+ // =============================================================================
113
+ // useUndoRedo Hook
114
+ // =============================================================================
115
+
116
+ /**
117
+ * State and actions for undo/redo functionality.
118
+ */
119
+ export interface UndoRedoState {
120
+ /** Whether there are actions that can be undone */
121
+ readonly canUndo: boolean;
122
+ /** Whether there are actions that can be redone */
123
+ readonly canRedo: boolean;
124
+ /** Number of items in the undo stack */
125
+ readonly undoCount: number;
126
+ /** Number of items in the redo stack */
127
+ readonly redoCount: number;
128
+ /** Undo the last action */
129
+ readonly undo: () => boolean;
130
+ /** Redo the last undone action */
131
+ readonly redo: () => boolean;
132
+ /** Clear the undo/redo history */
133
+ readonly clear: () => void;
134
+ }
135
+
136
+ /**
137
+ * React hook for undo/redo functionality.
138
+ * Provides state (canUndo, canRedo) and actions (undo, redo, clear).
139
+ *
140
+ * @example
141
+ * ```tsx
142
+ * const { canUndo, canRedo, undo, redo } = useUndoRedo(useStore);
143
+ *
144
+ * return (
145
+ * <>
146
+ * <button onClick={undo} disabled={!canUndo}>Undo</button>
147
+ * <button onClick={redo} disabled={!canRedo}>Redo</button>
148
+ * </>
149
+ * );
150
+ * ```
151
+ */
152
+ export function useUndoRedo<TStore extends CommanderSlice>(
153
+ store: UseBoundStore<StoreApi<TStore>>
154
+ ): UndoRedoState {
155
+ // Get the store API
156
+ const storeApi = useMemo(() => {
157
+ return store as unknown as StoreApi<TStore>;
158
+ }, [store]);
159
+
160
+ // Subscribe to commander state
161
+ const commanderState = useStore(
162
+ store,
163
+ (state: TStore) => state._commander
164
+ );
165
+
166
+ const canUndo = commanderState.undoStack.length > 0;
167
+ const canRedo = commanderState.redoStack.length > 0;
168
+ const undoCount = commanderState.undoStack.length;
169
+ const redoCount = commanderState.redoStack.length;
170
+
171
+ const undo = useCallback(() => {
172
+ return performUndo(storeApi);
173
+ }, [storeApi]);
174
+
175
+ const redo = useCallback(() => {
176
+ return performRedo(storeApi);
177
+ }, [storeApi]);
178
+
179
+ const clear = useCallback(() => {
180
+ clearUndoHistory(storeApi);
181
+ }, [storeApi]);
182
+
183
+ return {
184
+ canUndo,
185
+ canRedo,
186
+ undoCount,
187
+ redoCount,
188
+ undo,
189
+ redo,
190
+ clear,
191
+ };
192
+ }
193
+
194
+ // =============================================================================
195
+ // Keyboard Shortcut Hook
196
+ // =============================================================================
197
+
198
+ /**
199
+ * Options for the keyboard shortcut hook.
200
+ */
201
+ export interface UseUndoRedoKeyboardOptions {
202
+ /** Enable Ctrl/Cmd+Z for undo (default: true) */
203
+ readonly enableUndo?: boolean;
204
+ /** Enable Ctrl/Cmd+Shift+Z or Ctrl+Y for redo (default: true) */
205
+ readonly enableRedo?: boolean;
206
+ }
207
+
208
+ /**
209
+ * React hook that adds keyboard shortcuts for undo/redo.
210
+ * Listens for Ctrl/Cmd+Z (undo) and Ctrl/Cmd+Shift+Z or Ctrl+Y (redo).
211
+ *
212
+ * @example
213
+ * ```tsx
214
+ * // In your app component
215
+ * useUndoRedoKeyboard(useStore);
216
+ * ```
217
+ */
218
+ export function useUndoRedoKeyboard<TStore extends CommanderSlice>(
219
+ store: UseBoundStore<StoreApi<TStore>>,
220
+ options: UseUndoRedoKeyboardOptions = {}
221
+ ): void {
222
+ const { enableUndo = true, enableRedo = true } = options;
223
+
224
+ const storeApi = useMemo(() => {
225
+ return store as unknown as StoreApi<TStore>;
226
+ }, [store]);
227
+
228
+ // Set up keyboard listener
229
+ useEffect(() => {
230
+ if (typeof window === "undefined") {
231
+ return;
232
+ }
233
+
234
+ const handleKeyDown = (event: KeyboardEvent) => {
235
+ const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
236
+ const modKey = isMac ? event.metaKey : event.ctrlKey;
237
+
238
+ if (!modKey) return;
239
+
240
+ // Undo: Ctrl/Cmd + Z (without Shift)
241
+ if (enableUndo && event.key === "z" && !event.shiftKey) {
242
+ event.preventDefault();
243
+ performUndo(storeApi);
244
+ return;
245
+ }
246
+
247
+ // Redo: Ctrl/Cmd + Shift + Z or Ctrl + Y
248
+ if (enableRedo) {
249
+ if ((event.key === "z" && event.shiftKey) || event.key === "y") {
250
+ event.preventDefault();
251
+ performRedo(storeApi);
252
+ }
253
+ }
254
+ };
255
+
256
+ window.addEventListener("keydown", handleKeyDown);
257
+ return () => window.removeEventListener("keydown", handleKeyDown);
258
+ }, [storeApi, enableUndo, enableRedo]);
259
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @voidhash/mimic-react/zustand-commander
3
+ *
4
+ * A typesafe command system for zustand + mimic that enables business logic
5
+ * encapsulation and client-side undo/redo capabilities.
6
+ *
7
+ * @since 0.0.1
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { createCommander, useCommander, useUndoRedo } from "@voidhash/mimic-react/zustand-commander";
12
+ * import { Schema } from "effect";
13
+ *
14
+ * // 1. Create commander bound to your store type
15
+ * const commander = createCommander<StoreState>();
16
+ *
17
+ * // 2. Define regular actions
18
+ * const selectCard = commander.action(
19
+ * Schema.Struct({ cardId: Schema.String }),
20
+ * (ctx, params) => {
21
+ * ctx.setState({ selectedCardId: params.cardId });
22
+ * }
23
+ * );
24
+ *
25
+ * // 3. Define undoable actions
26
+ * const moveCard = commander.undoableAction(
27
+ * Schema.Struct({ cardId: Schema.String, toColumnId: Schema.String }),
28
+ * (ctx, params) => {
29
+ * const { mimic } = ctx.getState();
30
+ * const fromColumnId = // get current column
31
+ *
32
+ * mimic.document.transaction(root => {
33
+ * // move card to new column
34
+ * });
35
+ *
36
+ * return { fromColumnId }; // Return data needed for revert
37
+ * },
38
+ * (ctx, params, result) => {
39
+ * // Revert: move card back to original column
40
+ * ctx.dispatch(moveCard)({
41
+ * cardId: params.cardId,
42
+ * toColumnId: result.fromColumnId
43
+ * });
44
+ * }
45
+ * );
46
+ *
47
+ * // 4. Create store with commander middleware
48
+ * const useStore = create(
49
+ * commander.middleware(
50
+ * mimic(document, (set, get) => ({
51
+ * selectedCardId: null as string | null,
52
+ * }))
53
+ * )
54
+ * );
55
+ *
56
+ * // 5. Use in components
57
+ * function MyComponent() {
58
+ * const dispatch = useCommander(useStore);
59
+ * const { canUndo, canRedo, undo, redo } = useUndoRedo(useStore);
60
+ *
61
+ * return (
62
+ * <>
63
+ * <button onClick={() => dispatch(moveCard)({ cardId: "1", toColumnId: "2" })}>
64
+ * Move Card
65
+ * </button>
66
+ * <button onClick={undo} disabled={!canUndo}>Undo</button>
67
+ * <button onClick={redo} disabled={!canRedo}>Redo</button>
68
+ * </>
69
+ * );
70
+ * }
71
+ * ```
72
+ */
73
+
74
+ // =============================================================================
75
+ // Commander
76
+ // =============================================================================
77
+
78
+ export {
79
+ createCommander,
80
+ performUndo,
81
+ performRedo,
82
+ clearUndoHistory,
83
+ } from "./commander.js";
84
+
85
+ // =============================================================================
86
+ // Hooks
87
+ // =============================================================================
88
+
89
+ export {
90
+ useCommander,
91
+ useUndoRedo,
92
+ useUndoRedoKeyboard,
93
+ type UndoRedoState,
94
+ type UseUndoRedoKeyboardOptions,
95
+ } from "./hooks.js";
96
+
97
+ // =============================================================================
98
+ // Types
99
+ // =============================================================================
100
+
101
+ export type {
102
+ // Schema types
103
+ AnyEffectSchema,
104
+ InferSchemaType,
105
+ // Command types
106
+ Command,
107
+ UndoableCommand,
108
+ AnyCommand,
109
+ AnyUndoableCommand,
110
+ // Context & functions
111
+ CommandContext,
112
+ CommandFn,
113
+ RevertFn,
114
+ CommandDispatch,
115
+ // Undo/Redo
116
+ UndoEntry,
117
+ CommanderSlice,
118
+ // Commander
119
+ Commander,
120
+ CommanderOptions,
121
+ CommanderMiddleware,
122
+ // Helpers
123
+ CommandParams,
124
+ CommandReturn,
125
+ CommandStore,
126
+ ExtractState,
127
+ } from "./types.js";
128
+
129
+ // =============================================================================
130
+ // Symbols & Type Guards
131
+ // =============================================================================
132
+
133
+ export {
134
+ COMMAND_SYMBOL,
135
+ UNDOABLE_COMMAND_SYMBOL,
136
+ isCommand,
137
+ isUndoableCommand,
138
+ } from "./types.js";
139
+