@yogiswara/honcho-editor-ui 1.3.7 → 1.3.9

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,91 @@
1
+ import { AdjustmentState } from './editor/useHonchoEditor';
2
+ /**
3
+ * Configuration options for the adjustment history hook
4
+ */
5
+ interface HistoryOptions {
6
+ /** Maximum number of history entries to keep. Use 'unlimited' for no limit */
7
+ maxSize?: number | 'unlimited';
8
+ /** Whether to enable batch mode for grouping multiple changes */
9
+ enableBatching?: boolean;
10
+ /** Enable development warnings for performance issues */
11
+ devWarnings?: boolean;
12
+ }
13
+ /**
14
+ * Information about the current history state
15
+ */
16
+ export interface HistoryInfo {
17
+ /** Whether undo operation is available */
18
+ canUndo: boolean;
19
+ /** Whether redo operation is available */
20
+ canRedo: boolean;
21
+ /** Current position in history (0-based index) */
22
+ currentIndex: number;
23
+ /** Total number of states in history */
24
+ totalStates: number;
25
+ /** Current size of history in memory */
26
+ historySize: number;
27
+ /** Whether batch mode is currently active */
28
+ isBatchMode: boolean;
29
+ }
30
+ /**
31
+ * Actions available for history management
32
+ */
33
+ export interface HistoryActions {
34
+ /** Add a new adjustment state to history */
35
+ pushState: (state: AdjustmentState) => void;
36
+ /** Undo to previous adjustment state */
37
+ undo: () => void;
38
+ /** Redo to next adjustment state */
39
+ redo: () => void;
40
+ /** Reset history with new initial adjustment state */
41
+ reset: (newInitialState: AdjustmentState) => void;
42
+ /** Jump to specific index in history */
43
+ jumpToIndex: (index: number) => void;
44
+ /** Clear all history and start fresh */
45
+ clearHistory: () => void;
46
+ /** Get a copy of the entire history array */
47
+ getHistory: () => AdjustmentState[];
48
+ /** Trim history to specified size, keeping most recent entries */
49
+ trimHistory: (keepLast: number) => void;
50
+ }
51
+ /**
52
+ * Configuration actions for runtime adjustment
53
+ */
54
+ export interface HistoryConfig {
55
+ /** Set maximum history size */
56
+ setMaxSize: (size: number | 'unlimited') => void;
57
+ /** Enable or disable batch mode */
58
+ setBatchMode: (enabled: boolean) => void;
59
+ /** Get current memory usage estimate */
60
+ getMemoryUsage: () => number;
61
+ }
62
+ /**
63
+ * Return type for the useAdjustmentHistory hook
64
+ */
65
+ export interface UseAdjustmentHistoryReturn {
66
+ /** Current adjustment state value */
67
+ currentState: AdjustmentState;
68
+ /** Information about history state */
69
+ historyInfo: HistoryInfo;
70
+ /** Available history actions */
71
+ actions: HistoryActions;
72
+ /** Configuration options */
73
+ config: HistoryConfig;
74
+ }
75
+ /**
76
+ * Advanced hook for managing AdjustmentState history with undo/redo functionality.
77
+ *
78
+ * Features:
79
+ * - Unlimited or configurable history size
80
+ * - Batch mode for grouping multiple changes into single undo operations
81
+ * - Memory usage monitoring and optimization
82
+ * - Internal stabilization to prevent re-render issues
83
+ * - Jump to any point in history
84
+ * - Automatic AdjustmentState comparison
85
+ *
86
+ * @param initialState - The initial AdjustmentState value
87
+ * @param options - Configuration options for history behavior
88
+ * @returns Object with current state, history info, actions, and config
89
+ */
90
+ export declare function useAdjustmentHistory(initialState: AdjustmentState, options?: HistoryOptions): UseAdjustmentHistoryReturn;
91
+ export {};
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Demo component showing batch mode behavior
3
+ */
4
+ export declare function BatchModeDemo(): import("react/jsx-runtime").JSX.Element;
5
+ /**
6
+ * Example showing smooth preset application
7
+ */
8
+ export declare function SmoothPresetDemo(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,106 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useState } from 'react';
3
+ import { useAdjustmentHistory } from './useAdjustmentHistory';
4
+ const initialAdjustments = {
5
+ tempScore: 0, tintScore: 0, vibranceScore: 0, saturationScore: 0,
6
+ exposureScore: 0, highlightsScore: 0, shadowsScore: 0, whitesScore: 0,
7
+ blacksScore: 0, contrastScore: 0, clarityScore: 0, sharpnessScore: 0,
8
+ };
9
+ /**
10
+ * Demo component showing batch mode behavior
11
+ */
12
+ export function BatchModeDemo() {
13
+ const { currentState, historyInfo, actions, config } = useAdjustmentHistory(initialAdjustments);
14
+ const [updateCount, setUpdateCount] = useState(0);
15
+ // Track UI updates
16
+ React.useEffect(() => {
17
+ setUpdateCount(prev => prev + 1);
18
+ }, [currentState]);
19
+ const handleBatchUpdates = async () => {
20
+ setUpdateCount(0); // Reset counter
21
+ console.log('Starting batch mode...');
22
+ config.setBatchMode(true);
23
+ // These will update currentState 4 times (UI updates)
24
+ console.log('Push state 1...');
25
+ actions.pushState({ ...currentState, tempScore: 25 });
26
+ console.log('Push state 2...');
27
+ actions.pushState({ ...currentState, tempScore: 50 });
28
+ console.log('Push state 3...');
29
+ actions.pushState({ ...currentState, tempScore: 75 });
30
+ console.log('Push state 4...');
31
+ actions.pushState({ ...currentState, tempScore: 100 });
32
+ console.log('Ending batch mode...');
33
+ config.setBatchMode(false); // Only now will 1 history entry be created
34
+ console.log('Batch complete!');
35
+ };
36
+ const handleInstantUpdates = () => {
37
+ setUpdateCount(0); // Reset counter
38
+ // These will create 4 history entries (normal mode)
39
+ actions.pushState({ ...currentState, tintScore: 25 });
40
+ actions.pushState({ ...currentState, tintScore: 50 });
41
+ actions.pushState({ ...currentState, tintScore: 75 });
42
+ actions.pushState({ ...currentState, tintScore: 100 });
43
+ };
44
+ return (_jsxs("div", { style: { padding: '20px', fontFamily: 'monospace' }, children: [_jsx("h2", { children: "Batch Mode Demo" }), _jsxs("div", { style: { marginBottom: '20px', padding: '10px', backgroundColor: '#f5f5f5' }, children: [_jsx("strong", { children: "Current State:" }), _jsxs("div", { children: ["Temperature: ", currentState.tempScore] }), _jsxs("div", { children: ["Tint: ", currentState.tintScore] }), _jsxs("div", { children: ["UI Updates: ", updateCount] }), _jsxs("div", { children: ["History Size: ", historyInfo.totalStates] }), _jsxs("div", { children: ["Can Undo: ", historyInfo.canUndo ? 'Yes' : 'No'] }), _jsxs("div", { children: ["Batch Mode: ", historyInfo.isBatchMode ? 'Active' : 'Inactive'] })] }), _jsxs("div", { style: { display: 'flex', gap: '10px', marginBottom: '20px' }, children: [_jsx("button", { onClick: handleBatchUpdates, style: { padding: '10px', backgroundColor: '#4CAF50', color: 'white', border: 'none' }, children: "Batch Mode (4 UI updates, 1 history entry)" }), _jsx("button", { onClick: handleInstantUpdates, style: { padding: '10px', backgroundColor: '#2196F3', color: 'white', border: 'none' }, children: "Normal Mode (4 UI updates, 4 history entries)" })] }), _jsxs("div", { style: { display: 'flex', gap: '10px' }, children: [_jsx("button", { onClick: actions.undo, disabled: !historyInfo.canUndo, style: {
45
+ padding: '10px',
46
+ backgroundColor: historyInfo.canUndo ? '#FF9800' : '#ccc',
47
+ color: 'white',
48
+ border: 'none'
49
+ }, children: "Undo" }), _jsx("button", { onClick: actions.redo, disabled: !historyInfo.canRedo, style: {
50
+ padding: '10px',
51
+ backgroundColor: historyInfo.canRedo ? '#9C27B0' : '#ccc',
52
+ color: 'white',
53
+ border: 'none'
54
+ }, children: "Redo" }), _jsx("button", { onClick: () => actions.reset(initialAdjustments), style: { padding: '10px', backgroundColor: '#f44336', color: 'white', border: 'none' }, children: "Reset" })] }), _jsxs("div", { style: { marginTop: '20px', fontSize: '12px', color: '#666' }, children: [_jsx("h3", { children: "How it works:" }), _jsxs("ul", { children: [_jsxs("li", { children: [_jsx("strong", { children: "Batch Mode:" }), " UI updates immediately on each pushState, but only final state is saved to history when batch ends"] }), _jsxs("li", { children: [_jsx("strong", { children: "Normal Mode:" }), " Each pushState creates both UI update and history entry"] }), _jsxs("li", { children: [_jsx("strong", { children: "Result:" }), " Smooth UI animations with clean undo/redo history"] })] })] })] }));
55
+ }
56
+ /**
57
+ * Example showing smooth preset application
58
+ */
59
+ export function SmoothPresetDemo() {
60
+ const { currentState, historyInfo, actions, config } = useAdjustmentHistory(initialAdjustments);
61
+ const dramaticPreset = {
62
+ tempScore: 50, tintScore: -20, vibranceScore: 80, saturationScore: 60,
63
+ exposureScore: 30, highlightsScore: -40, shadowsScore: 40, whitesScore: 20,
64
+ blacksScore: -30, contrastScore: 70, clarityScore: 50, sharpnessScore: 40,
65
+ };
66
+ const handleSmoothPreset = async () => {
67
+ config.setBatchMode(true);
68
+ // Create smooth transition with multiple steps
69
+ const steps = 8;
70
+ for (let i = 1; i <= steps; i++) {
71
+ const progress = i / steps;
72
+ const intermediateState = {
73
+ tempScore: Math.round(currentState.tempScore + (dramaticPreset.tempScore - currentState.tempScore) * progress),
74
+ tintScore: Math.round(currentState.tintScore + (dramaticPreset.tintScore - currentState.tintScore) * progress),
75
+ vibranceScore: Math.round(currentState.vibranceScore + (dramaticPreset.vibranceScore - currentState.vibranceScore) * progress),
76
+ saturationScore: Math.round(currentState.saturationScore + (dramaticPreset.saturationScore - currentState.saturationScore) * progress),
77
+ exposureScore: Math.round(currentState.exposureScore + (dramaticPreset.exposureScore - currentState.exposureScore) * progress),
78
+ highlightsScore: Math.round(currentState.highlightsScore + (dramaticPreset.highlightsScore - currentState.highlightsScore) * progress),
79
+ shadowsScore: Math.round(currentState.shadowsScore + (dramaticPreset.shadowsScore - currentState.shadowsScore) * progress),
80
+ whitesScore: Math.round(currentState.whitesScore + (dramaticPreset.whitesScore - currentState.whitesScore) * progress),
81
+ blacksScore: Math.round(currentState.blacksScore + (dramaticPreset.blacksScore - currentState.blacksScore) * progress),
82
+ contrastScore: Math.round(currentState.contrastScore + (dramaticPreset.contrastScore - currentState.contrastScore) * progress),
83
+ clarityScore: Math.round(currentState.clarityScore + (dramaticPreset.clarityScore - currentState.clarityScore) * progress),
84
+ sharpnessScore: Math.round(currentState.sharpnessScore + (dramaticPreset.sharpnessScore - currentState.sharpnessScore) * progress),
85
+ };
86
+ actions.pushState(intermediateState);
87
+ // Small delay for smooth animation
88
+ await new Promise(resolve => setTimeout(resolve, 100));
89
+ }
90
+ config.setBatchMode(false); // Commit final state
91
+ };
92
+ return (_jsxs("div", { style: { padding: '20px', fontFamily: 'monospace' }, children: [_jsx("h2", { children: "Smooth Preset Application" }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '10px', marginBottom: '20px', fontSize: '12px' }, children: [_jsxs("div", { children: ["Temp: ", currentState.tempScore] }), _jsxs("div", { children: ["Tint: ", currentState.tintScore] }), _jsxs("div", { children: ["Vibrance: ", currentState.vibranceScore] }), _jsxs("div", { children: ["Saturation: ", currentState.saturationScore] }), _jsxs("div", { children: ["Exposure: ", currentState.exposureScore] }), _jsxs("div", { children: ["Highlights: ", currentState.highlightsScore] }), _jsxs("div", { children: ["Shadows: ", currentState.shadowsScore] }), _jsxs("div", { children: ["Whites: ", currentState.whitesScore] }), _jsxs("div", { children: ["Blacks: ", currentState.blacksScore] }), _jsxs("div", { children: ["Contrast: ", currentState.contrastScore] }), _jsxs("div", { children: ["Clarity: ", currentState.clarityScore] }), _jsxs("div", { children: ["Sharpness: ", currentState.sharpnessScore] })] }), _jsx("button", { onClick: handleSmoothPreset, style: {
93
+ padding: '15px 30px',
94
+ backgroundColor: '#4CAF50',
95
+ color: 'white',
96
+ border: 'none',
97
+ fontSize: '16px',
98
+ marginRight: '10px'
99
+ }, children: "Apply Dramatic Preset (Smooth)" }), _jsx("button", { onClick: actions.undo, disabled: !historyInfo.canUndo, style: {
100
+ padding: '15px 30px',
101
+ backgroundColor: historyInfo.canUndo ? '#FF9800' : '#ccc',
102
+ color: 'white',
103
+ border: 'none',
104
+ fontSize: '16px'
105
+ }, children: "Undo Preset" }), _jsx("div", { style: { marginTop: '20px', fontSize: '12px', color: '#666' }, children: _jsx("p", { children: "This demo shows smooth preset application with 8 intermediate steps, but only 1 undo operation!" }) })] }));
106
+ }
@@ -0,0 +1,33 @@
1
+ import { type HistoryInfo } from './useAdjustmentHistory';
2
+ import { AdjustmentState } from './editor/useHonchoEditor';
3
+ export declare function useEditorWithHistory(): {
4
+ adjustments: AdjustmentState;
5
+ canUndo: boolean;
6
+ canRedo: boolean;
7
+ undo: () => void;
8
+ redo: () => void;
9
+ updateTemperature: (newTemp: number) => void;
10
+ applyPresetWithSmoothUI: (presetState: AdjustmentState) => void;
11
+ updateMultipleAdjustments: (updates: Partial<AdjustmentState>) => void;
12
+ resetAdjustments: () => void;
13
+ jumpToIndex: (index: number) => void;
14
+ getHistory: () => AdjustmentState[];
15
+ clearHistory: () => void;
16
+ setBatchMode: (enabled: boolean) => void;
17
+ setMaxSize: (size: number | "unlimited") => void;
18
+ getMemoryUsage: () => number;
19
+ historyInfo: HistoryInfo;
20
+ };
21
+ /**
22
+ * Example integration with your existing useHonchoEditor pattern
23
+ */
24
+ export declare function integrateWithHonchoEditor(): {
25
+ currentAdjustments: AdjustmentState;
26
+ canUndo: boolean;
27
+ canRedo: boolean;
28
+ handleSliderChange: (key: keyof AdjustmentState, value: number) => void;
29
+ handlePresetWithAnimation: (presetState: AdjustmentState) => Promise<void>;
30
+ handleUndo: () => void;
31
+ handleRedo: () => void;
32
+ handleRevert: () => void;
33
+ };
@@ -0,0 +1,150 @@
1
+ import { useAdjustmentHistory } from './useAdjustmentHistory';
2
+ /**
3
+ * Example usage of the simplified useAdjustmentHistory hook
4
+ * This shows how to integrate it with your existing useHonchoEditor
5
+ */
6
+ // Example initial adjustment state
7
+ const initialAdjustments = {
8
+ tempScore: 0,
9
+ tintScore: 0,
10
+ vibranceScore: 0,
11
+ saturationScore: 0,
12
+ exposureScore: 0,
13
+ highlightsScore: 0,
14
+ shadowsScore: 0,
15
+ whitesScore: 0,
16
+ blacksScore: 0,
17
+ contrastScore: 0,
18
+ clarityScore: 0,
19
+ sharpnessScore: 0,
20
+ };
21
+ export function useEditorWithHistory() {
22
+ // Initialize the adjustment history hook
23
+ const { currentState: currentAdjustments, historyInfo, actions, config } = useAdjustmentHistory(initialAdjustments, {
24
+ maxSize: 100, // Keep last 100 states
25
+ enableBatching: false, // Start with batching disabled
26
+ devWarnings: true // Show performance warnings in development
27
+ });
28
+ // Example: Update a single adjustment value
29
+ const updateTemperature = (newTemp) => {
30
+ const newState = {
31
+ ...currentAdjustments,
32
+ tempScore: newTemp
33
+ };
34
+ actions.pushState(newState);
35
+ };
36
+ // Example: Apply preset with batch mode for smooth UI updates
37
+ const applyPresetWithSmoothUI = (presetState) => {
38
+ // Enable batch mode to group all changes into one undo operation
39
+ config.setBatchMode(true);
40
+ // These will update the UI immediately but not create history entries
41
+ actions.pushState({ ...currentAdjustments, tempScore: presetState.tempScore });
42
+ actions.pushState({ ...currentAdjustments, tempScore: presetState.tempScore, tintScore: presetState.tintScore });
43
+ actions.pushState({ ...currentAdjustments, tempScore: presetState.tempScore, tintScore: presetState.tintScore, exposureScore: presetState.exposureScore });
44
+ actions.pushState(presetState); // Final complete state
45
+ // Disable batch mode to commit only the final state to history
46
+ config.setBatchMode(false);
47
+ // Result: UI updated 4 times (smooth animation), but only 1 undo operation
48
+ };
49
+ // Example: Bulk update multiple adjustments
50
+ const updateMultipleAdjustments = (updates) => {
51
+ config.setBatchMode(true); // Start batching
52
+ const newState = {
53
+ ...currentAdjustments,
54
+ ...updates
55
+ };
56
+ actions.pushState(newState);
57
+ config.setBatchMode(false); // Commit batch
58
+ };
59
+ // Example: Reset to initial state
60
+ const resetAdjustments = () => {
61
+ actions.reset(initialAdjustments);
62
+ };
63
+ return {
64
+ // Current adjustment values
65
+ adjustments: currentAdjustments,
66
+ // History controls
67
+ canUndo: historyInfo.canUndo,
68
+ canRedo: historyInfo.canRedo,
69
+ undo: actions.undo,
70
+ redo: actions.redo,
71
+ // Adjustment functions
72
+ updateTemperature,
73
+ applyPresetWithSmoothUI,
74
+ updateMultipleAdjustments,
75
+ resetAdjustments,
76
+ // Advanced features
77
+ jumpToIndex: actions.jumpToIndex,
78
+ getHistory: actions.getHistory,
79
+ clearHistory: actions.clearHistory,
80
+ // Configuration
81
+ setBatchMode: config.setBatchMode,
82
+ setMaxSize: config.setMaxSize,
83
+ getMemoryUsage: config.getMemoryUsage,
84
+ // History info
85
+ historyInfo
86
+ };
87
+ }
88
+ /**
89
+ * Example integration with your existing useHonchoEditor pattern
90
+ */
91
+ export function integrateWithHonchoEditor() {
92
+ const history = useAdjustmentHistory(initialAdjustments);
93
+ // Replace your current manual history management with this:
94
+ // Instead of:
95
+ // const [history, setHistory] = useState<AdjustmentState[]>([initialAdjustments]);
96
+ // const [historyIndex, setHistoryIndex] = useState(0);
97
+ // Use:
98
+ const currentAdjustments = history.currentState;
99
+ const canUndo = history.historyInfo.canUndo;
100
+ const canRedo = history.historyInfo.canRedo;
101
+ // When any adjustment changes (e.g., slider value):
102
+ const handleSliderChange = (key, value) => {
103
+ const newState = {
104
+ ...currentAdjustments,
105
+ [key]: value
106
+ };
107
+ history.actions.pushState(newState);
108
+ };
109
+ // Smooth preset application with multiple UI updates
110
+ const handlePresetWithAnimation = async (presetState) => {
111
+ history.config.setBatchMode(true);
112
+ // Animate through intermediate states for smooth UI
113
+ const steps = 4;
114
+ for (let i = 1; i <= steps; i++) {
115
+ const progress = i / steps;
116
+ const intermediateState = {
117
+ tempScore: Math.round(currentAdjustments.tempScore + (presetState.tempScore - currentAdjustments.tempScore) * progress),
118
+ tintScore: Math.round(currentAdjustments.tintScore + (presetState.tintScore - currentAdjustments.tintScore) * progress),
119
+ vibranceScore: Math.round(currentAdjustments.vibranceScore + (presetState.vibranceScore - currentAdjustments.vibranceScore) * progress),
120
+ saturationScore: Math.round(currentAdjustments.saturationScore + (presetState.saturationScore - currentAdjustments.saturationScore) * progress),
121
+ exposureScore: Math.round(currentAdjustments.exposureScore + (presetState.exposureScore - currentAdjustments.exposureScore) * progress),
122
+ highlightsScore: Math.round(currentAdjustments.highlightsScore + (presetState.highlightsScore - currentAdjustments.highlightsScore) * progress),
123
+ shadowsScore: Math.round(currentAdjustments.shadowsScore + (presetState.shadowsScore - currentAdjustments.shadowsScore) * progress),
124
+ whitesScore: Math.round(currentAdjustments.whitesScore + (presetState.whitesScore - currentAdjustments.whitesScore) * progress),
125
+ blacksScore: Math.round(currentAdjustments.blacksScore + (presetState.blacksScore - currentAdjustments.blacksScore) * progress),
126
+ contrastScore: Math.round(currentAdjustments.contrastScore + (presetState.contrastScore - currentAdjustments.contrastScore) * progress),
127
+ clarityScore: Math.round(currentAdjustments.clarityScore + (presetState.clarityScore - currentAdjustments.clarityScore) * progress),
128
+ sharpnessScore: Math.round(currentAdjustments.sharpnessScore + (presetState.sharpnessScore - currentAdjustments.sharpnessScore) * progress),
129
+ };
130
+ history.actions.pushState(intermediateState);
131
+ // Small delay for smooth animation
132
+ await new Promise(resolve => setTimeout(resolve, 50));
133
+ }
134
+ history.config.setBatchMode(false); // Commit only final state to history
135
+ };
136
+ // Your existing undo/redo handlers become:
137
+ const handleUndo = history.actions.undo;
138
+ const handleRedo = history.actions.redo;
139
+ const handleRevert = () => history.actions.reset(initialAdjustments);
140
+ return {
141
+ currentAdjustments,
142
+ canUndo,
143
+ canRedo,
144
+ handleSliderChange,
145
+ handlePresetWithAnimation,
146
+ handleUndo,
147
+ handleRedo,
148
+ handleRevert
149
+ };
150
+ }
@@ -0,0 +1,277 @@
1
+ import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
2
+ /**
3
+ * Compare two AdjustmentState objects for equality
4
+ * Uses JSON.stringify for deep comparison of all adjustment values
5
+ */
6
+ const compareAdjustmentStates = (a, b) => {
7
+ try {
8
+ return JSON.stringify(a) === JSON.stringify(b);
9
+ }
10
+ catch (error) {
11
+ // Fallback to manual comparison if JSON.stringify fails
12
+ console.warn('Failed to compare adjustment states with JSON.stringify, falling back to manual comparison:', error);
13
+ return (a.tempScore === b.tempScore &&
14
+ a.tintScore === b.tintScore &&
15
+ a.vibranceScore === b.vibranceScore &&
16
+ a.saturationScore === b.saturationScore &&
17
+ a.exposureScore === b.exposureScore &&
18
+ a.highlightsScore === b.highlightsScore &&
19
+ a.shadowsScore === b.shadowsScore &&
20
+ a.whitesScore === b.whitesScore &&
21
+ a.blacksScore === b.blacksScore &&
22
+ a.contrastScore === b.contrastScore &&
23
+ a.clarityScore === b.clarityScore &&
24
+ a.sharpnessScore === b.sharpnessScore);
25
+ }
26
+ };
27
+ /**
28
+ * Advanced hook for managing AdjustmentState history with undo/redo functionality.
29
+ *
30
+ * Features:
31
+ * - Unlimited or configurable history size
32
+ * - Batch mode for grouping multiple changes into single undo operations
33
+ * - Memory usage monitoring and optimization
34
+ * - Internal stabilization to prevent re-render issues
35
+ * - Jump to any point in history
36
+ * - Automatic AdjustmentState comparison
37
+ *
38
+ * @param initialState - The initial AdjustmentState value
39
+ * @param options - Configuration options for history behavior
40
+ * @returns Object with current state, history info, actions, and config
41
+ */
42
+ export function useAdjustmentHistory(initialState, options = {}) {
43
+ // Internal stabilization - prevent re-renders from options object recreation
44
+ const internalOptions = useMemo(() => ({
45
+ maxSize: options.maxSize ?? 'unlimited',
46
+ enableBatching: options.enableBatching ?? false,
47
+ devWarnings: options.devWarnings ?? false
48
+ }), [
49
+ options.maxSize,
50
+ options.enableBatching,
51
+ options.devWarnings
52
+ ]);
53
+ // Core state management
54
+ const [history, setHistory] = useState([initialState]);
55
+ const [currentIndex, setCurrentIndex] = useState(0);
56
+ const [currentState, setCurrentState] = useState(initialState);
57
+ // Batch mode state - ref to avoid triggering effects
58
+ const batchModeRef = useRef(internalOptions.enableBatching);
59
+ const batchStartIndexRef = useRef(null);
60
+ const batchStartStateRef = useRef(null);
61
+ // Configuration refs - prevent re-renders when config changes
62
+ const maxSizeRef = useRef(internalOptions.maxSize);
63
+ const devWarningsRef = useRef(internalOptions.devWarnings);
64
+ // Performance monitoring
65
+ const performanceRef = useRef({
66
+ lastHistorySize: 1,
67
+ lastUpdateTime: Date.now(),
68
+ largeHistoryWarningShown: false
69
+ });
70
+ // Sync currentState with history when not in batch mode
71
+ useEffect(() => {
72
+ if (!batchModeRef.current) {
73
+ setCurrentState(history[currentIndex]);
74
+ }
75
+ }, [history, currentIndex]);
76
+ const getMemoryUsage = useCallback(() => {
77
+ try {
78
+ const historyString = JSON.stringify(history);
79
+ return historyString.length * 2; // Rough estimate: 2 bytes per character
80
+ }
81
+ catch (error) {
82
+ console.warn('Failed to estimate memory usage:', error);
83
+ return history.length * 1000; // Fallback estimate
84
+ }
85
+ }, [history]);
86
+ // Development warnings for performance
87
+ const checkPerformance = useCallback(() => {
88
+ if (!devWarningsRef.current)
89
+ return;
90
+ const now = Date.now();
91
+ const perfData = performanceRef.current;
92
+ // Warn about large history sizes
93
+ if (history.length > 1000 && !perfData.largeHistoryWarningShown) {
94
+ console.warn(`useAdjustmentHistory: Large history size detected (${history.length} entries). Consider setting a maxSize limit.`);
95
+ perfData.largeHistoryWarningShown = true;
96
+ }
97
+ // Update performance tracking
98
+ perfData.lastHistorySize = history.length;
99
+ perfData.lastUpdateTime = now;
100
+ }, [history.length]);
101
+ // Trim history to specified size, keeping most recent entries
102
+ const trimHistoryToSize = useCallback((size) => {
103
+ if (size <= 0)
104
+ return;
105
+ setHistory(prevHistory => {
106
+ if (prevHistory.length <= size)
107
+ return prevHistory;
108
+ const startIndex = Math.max(0, prevHistory.length - size);
109
+ const trimmedHistory = prevHistory.slice(startIndex);
110
+ // Adjust current index to maintain relative position
111
+ setCurrentIndex(prevIndex => {
112
+ const adjustedIndex = prevIndex - startIndex;
113
+ return Math.max(0, Math.min(adjustedIndex, trimmedHistory.length - 1));
114
+ });
115
+ return trimmedHistory;
116
+ });
117
+ }, []);
118
+ // Apply max size limit when history grows
119
+ const enforceMaxSize = useCallback(() => {
120
+ if (maxSizeRef.current === 'unlimited')
121
+ return;
122
+ const maxSize = maxSizeRef.current;
123
+ if (history.length > maxSize) {
124
+ trimHistoryToSize(maxSize);
125
+ }
126
+ }, [history.length, trimHistoryToSize]);
127
+ // Push new state to history
128
+ const pushState = useCallback((newState) => {
129
+ // Skip if state hasn't changed
130
+ if (compareAdjustmentStates(newState, currentState)) {
131
+ return;
132
+ }
133
+ // Always update currentState immediately for smooth UI
134
+ setCurrentState(newState);
135
+ if (batchModeRef.current) {
136
+ // In batch mode: Don't update history yet, just update UI state
137
+ // History will be updated when batch mode ends
138
+ return;
139
+ }
140
+ // Normal mode: Update history immediately
141
+ setHistory(prevHistory => {
142
+ const truncatedHistory = prevHistory.slice(0, currentIndex + 1);
143
+ const newHistory = [...truncatedHistory, newState];
144
+ setCurrentIndex(newHistory.length - 1);
145
+ return newHistory;
146
+ });
147
+ }, [currentState, currentIndex]);
148
+ // Undo to previous state
149
+ const undo = useCallback(() => {
150
+ if (currentIndex > 0) {
151
+ const newIndex = currentIndex - 1;
152
+ setCurrentIndex(newIndex);
153
+ setCurrentState(history[newIndex]);
154
+ // Exit batch mode when undoing
155
+ if (batchModeRef.current) {
156
+ batchModeRef.current = false;
157
+ batchStartIndexRef.current = null;
158
+ batchStartStateRef.current = null;
159
+ }
160
+ }
161
+ }, [currentIndex, history]);
162
+ // Redo to next state
163
+ const redo = useCallback(() => {
164
+ if (currentIndex < history.length - 1) {
165
+ const newIndex = currentIndex + 1;
166
+ setCurrentIndex(newIndex);
167
+ setCurrentState(history[newIndex]);
168
+ }
169
+ }, [currentIndex, history]);
170
+ // Reset history with new initial state
171
+ const reset = useCallback((newInitialState) => {
172
+ setHistory([newInitialState]);
173
+ setCurrentIndex(0);
174
+ setCurrentState(newInitialState);
175
+ batchModeRef.current = internalOptions.enableBatching;
176
+ batchStartIndexRef.current = null;
177
+ batchStartStateRef.current = null;
178
+ }, [internalOptions.enableBatching]);
179
+ // Jump to specific index in history
180
+ const jumpToIndex = useCallback((index) => {
181
+ if (index >= 0 && index < history.length) {
182
+ setCurrentIndex(index);
183
+ setCurrentState(history[index]);
184
+ // Exit batch mode when jumping
185
+ if (batchModeRef.current) {
186
+ batchModeRef.current = false;
187
+ batchStartIndexRef.current = null;
188
+ batchStartStateRef.current = null;
189
+ }
190
+ }
191
+ }, [history]);
192
+ // Clear all history and start fresh
193
+ const clearHistory = useCallback(() => {
194
+ setHistory([currentState]);
195
+ setCurrentIndex(0);
196
+ batchModeRef.current = internalOptions.enableBatching;
197
+ batchStartIndexRef.current = null;
198
+ batchStartStateRef.current = null;
199
+ }, [currentState, internalOptions.enableBatching]);
200
+ // Get copy of entire history
201
+ const getHistory = useCallback(() => {
202
+ return [...history];
203
+ }, [history]);
204
+ // Manually trim history
205
+ const trimHistory = useCallback((keepLast) => {
206
+ trimHistoryToSize(keepLast);
207
+ }, [trimHistoryToSize]);
208
+ // Configuration setters
209
+ const setMaxSize = useCallback((size) => {
210
+ maxSizeRef.current = size;
211
+ if (size !== 'unlimited') {
212
+ enforceMaxSize();
213
+ }
214
+ }, [enforceMaxSize]);
215
+ const setBatchMode = useCallback((enabled) => {
216
+ const wasInBatch = batchModeRef.current;
217
+ if (enabled && !wasInBatch) {
218
+ // Starting batch mode - save current state as batch start
219
+ batchModeRef.current = true;
220
+ batchStartIndexRef.current = currentIndex;
221
+ batchStartStateRef.current = currentState;
222
+ }
223
+ else if (!enabled && wasInBatch) {
224
+ // Ending batch mode - commit final state to history
225
+ batchModeRef.current = false;
226
+ // Only add to history if state actually changed from batch start
227
+ if (batchStartStateRef.current &&
228
+ !compareAdjustmentStates(currentState, batchStartStateRef.current)) {
229
+ setHistory(prevHistory => {
230
+ const truncatedHistory = prevHistory.slice(0, batchStartIndexRef.current + 1);
231
+ const newHistory = [...truncatedHistory, currentState];
232
+ setCurrentIndex(newHistory.length - 1);
233
+ return newHistory;
234
+ });
235
+ }
236
+ batchStartIndexRef.current = null;
237
+ batchStartStateRef.current = null;
238
+ }
239
+ }, [currentIndex, currentState]);
240
+ // History info object
241
+ const historyInfo = useMemo(() => ({
242
+ canUndo: currentIndex > 0,
243
+ canRedo: currentIndex < history.length - 1,
244
+ currentIndex,
245
+ totalStates: history.length,
246
+ historySize: getMemoryUsage(),
247
+ isBatchMode: batchModeRef.current
248
+ }), [currentIndex, history.length, getMemoryUsage]);
249
+ // Actions object - stabilized with useMemo
250
+ const actions = useMemo(() => ({
251
+ pushState,
252
+ undo,
253
+ redo,
254
+ reset,
255
+ jumpToIndex,
256
+ clearHistory,
257
+ getHistory,
258
+ trimHistory
259
+ }), [pushState, undo, redo, reset, jumpToIndex, clearHistory, getHistory, trimHistory]);
260
+ // Config object - stabilized with useMemo
261
+ const config = useMemo(() => ({
262
+ setMaxSize,
263
+ setBatchMode,
264
+ getMemoryUsage
265
+ }), [setMaxSize, setBatchMode, getMemoryUsage]);
266
+ // Apply max size enforcement when history changes
267
+ useEffect(() => {
268
+ enforceMaxSize();
269
+ checkPerformance();
270
+ }, [enforceMaxSize, checkPerformance]);
271
+ return {
272
+ currentState,
273
+ historyInfo,
274
+ actions,
275
+ config
276
+ };
277
+ }