diff --git a/components/editor/action-bar/components/action-bar-buttons.tsx b/components/editor/action-bar/components/action-bar-buttons.tsx
index 4b75c123..7d7db87a 100644
--- a/components/editor/action-bar/components/action-bar-buttons.tsx
+++ b/components/editor/action-bar/components/action-bar-buttons.tsx
@@ -10,6 +10,7 @@ import { EditButton } from "./edit-button";
import { MoreOptions } from "./more-options";
import { AIGenerateButton } from "./ai-generate-button";
import { ShareButton } from "./share-button";
+import { UndoRedoButtons } from "./undo-redo-buttons";
interface ActionBarButtonsProps {
onImportClick: () => void;
@@ -28,7 +29,7 @@ export function ActionBarButtons({
onShareClick,
isSaving,
}: ActionBarButtonsProps) {
- const { themeState, resetToCurrentPreset, hasThemeChangedFromCheckpoint } = useEditorStore();
+ const { themeState, resetToCurrentPreset, hasUnsavedChanges } = useEditorStore();
const { getPreset } = useThemePresetStore();
const currentPreset = themeState?.preset ? getPreset(themeState?.preset) : undefined;
@@ -40,7 +41,9 @@ export function ActionBarButtons({
-
+
+
+
diff --git a/components/editor/action-bar/components/undo-redo-buttons.tsx b/components/editor/action-bar/components/undo-redo-buttons.tsx
new file mode 100644
index 00000000..89e0fa81
--- /dev/null
+++ b/components/editor/action-bar/components/undo-redo-buttons.tsx
@@ -0,0 +1,35 @@
+import { Button } from "@/components/ui/button";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+import { useDialogActions } from "@/hooks/use-dialog-actions";
+import { Redo, Undo } from "lucide-react";
+
+export function UndoRedoButtons() {
+ const { handleUndo, handleRedo, canUndo, canRedo } = useDialogActions();
+
+ return (
+
+
+
+
+
+
+
+ Undo
+
+
+
+
+
+
+
+ Redo
+
+
+
+
+ );
+}
diff --git a/hooks/use-dialog-actions.tsx b/hooks/use-dialog-actions.tsx
index 5a40a0e8..3f491de7 100644
--- a/hooks/use-dialog-actions.tsx
+++ b/hooks/use-dialog-actions.tsx
@@ -42,6 +42,10 @@ interface DialogActionsContextType {
handleGenerateTheme: (promptText: string, jsonPromptText: string) => Promise;
handleShareClick: (id?: string) => Promise;
saveTheme: (themeName: string) => Promise;
+ handleUndo: () => void;
+ handleRedo: () => void;
+ canUndo: boolean;
+ canRedo: boolean;
}
function useDialogActionsStore(): DialogActionsContextType {
@@ -54,8 +58,16 @@ function useDialogActionsStore(): DialogActionsContextType {
const [shareUrl, setShareUrl] = useState("");
const [dialogKey, setDialogKey] = useState(0);
- const { themeState, setThemeState, applyThemePreset, hasThemeChangedFromCheckpoint } =
- useEditorStore();
+ const {
+ themeState,
+ setThemeState,
+ applyThemePreset,
+ hasThemeChangedFromCheckpoint,
+ undo,
+ redo,
+ canUndo: editorCanUndo,
+ canRedo: editorCanRedo,
+ } = useEditorStore();
const { getPreset } = useThemePresetStore();
const { data: session } = authClient.useSession();
const { openAuthDialog } = useAuthStore();
@@ -76,6 +88,14 @@ function useDialogActionsStore(): DialogActionsContextType {
setShareAfterSave(true);
});
+ const handleUndo = () => {
+ undo();
+ };
+
+ const handleRedo = () => {
+ redo();
+ };
+
const handleGenerateTheme = async (promptText: string, jsonPromptText: string) => {
if (!promptText.trim()) return;
await generateTheme({
@@ -215,6 +235,10 @@ function useDialogActionsStore(): DialogActionsContextType {
handleGenerateTheme,
handleShareClick,
saveTheme,
+ handleUndo,
+ handleRedo,
+ canUndo: editorCanUndo(),
+ canRedo: editorCanRedo(),
};
return value;
diff --git a/store/editor-store.ts b/store/editor-store.ts
index d4c251f8..afc4541e 100644
--- a/store/editor-store.ts
+++ b/store/editor-store.ts
@@ -5,9 +5,19 @@ import { defaultThemeState } from "@/config/theme";
import { getPresetThemeStyles } from "@/utils/theme-preset-helper";
import { isDeepEqual } from "@/lib/utils";
+const MAX_HISTORY_COUNT = 30;
+const HISTORY_OVERRIDE_THRESHOLD_MS = 500; // 0.5 seconds
+
+interface ThemeHistoryEntry {
+ state: ThemeEditorState;
+ timestamp: number;
+}
+
interface EditorStore {
themeState: ThemeEditorState;
themeCheckpoint: ThemeEditorState | null;
+ history: ThemeHistoryEntry[];
+ future: ThemeHistoryEntry[];
setThemeState: (state: ThemeEditorState) => void;
applyThemePreset: (preset: string) => void;
saveThemeCheckpoint: () => void;
@@ -15,6 +25,10 @@ interface EditorStore {
resetToCurrentPreset: () => void;
hasThemeChangedFromCheckpoint: () => boolean;
hasUnsavedChanges: () => boolean;
+ undo: () => void;
+ redo: () => void;
+ canUndo: () => boolean;
+ canRedo: () => boolean;
}
export const useEditorStore = create()(
@@ -22,23 +36,86 @@ export const useEditorStore = create()(
(set, get) => ({
themeState: defaultThemeState,
themeCheckpoint: null,
- setThemeState: (state: ThemeEditorState) => {
- set({ themeState: state });
+ history: [],
+ future: [],
+ setThemeState: (newState: ThemeEditorState) => {
+ const oldThemeState = get().themeState;
+ let currentHistory = get().history;
+
+ // Check if only currentMode changed
+ const oldStateWithoutMode = { ...oldThemeState, currentMode: undefined };
+ const newStateWithoutMode = { ...newState, currentMode: undefined };
+
+ if (
+ isDeepEqual(oldStateWithoutMode, newStateWithoutMode) &&
+ oldThemeState.currentMode !== newState.currentMode
+ ) {
+ // Only currentMode changed, or nothing changed but currentMode (which is fine)
+ // Just update themeState without affecting history or future
+ set({ themeState: newState });
+ return;
+ }
+
+ const currentTime = Date.now();
+
+ // If other things changed, or if it's an actual identical state set (though less likely here)
+ // Proceed with history logic
+ const lastHistoryEntry =
+ currentHistory.length > 0 ? currentHistory[currentHistory.length - 1] : null;
+
+ if (
+ lastHistoryEntry &&
+ currentTime - lastHistoryEntry.timestamp < HISTORY_OVERRIDE_THRESHOLD_MS
+ ) {
+ // Override the last history entry
+ currentHistory = [
+ ...currentHistory.slice(0, -1),
+ { state: newState, timestamp: currentTime },
+ ];
+ } else {
+ // Add a new history entry
+ currentHistory = [...currentHistory, { state: newState, timestamp: currentTime }];
+ }
+
+ if (currentHistory.length > MAX_HISTORY_COUNT) {
+ currentHistory.shift(); // Remove the oldest entry
+ }
+
+ console.log(
+ "currentHistory",
+ currentHistory?.map((h) => h.timestamp)
+ );
+ set({
+ themeState: newState,
+ history: currentHistory,
+ future: [], // Clear future on new state change
+ });
},
applyThemePreset: (preset: string) => {
- const themeState = get().themeState;
+ const currentThemeState = get().themeState;
+ const oldHistory = get().history;
+ const currentTime = Date.now();
+
const newStyles = getPresetThemeStyles(preset);
const newThemeState: ThemeEditorState = {
- ...themeState,
+ ...currentThemeState,
preset,
styles: newStyles,
hslAdjustments: defaultThemeState.hslAdjustments,
};
- const updates: Partial & { themeState: ThemeEditorState } = {
+
+ const newHistoryEntry = { state: currentThemeState, timestamp: currentTime };
+ let updatedHistory = [...oldHistory, newHistoryEntry];
+ if (updatedHistory.length > MAX_HISTORY_COUNT) {
+ updatedHistory.shift();
+ }
+
+ set({
themeState: newThemeState,
- themeCheckpoint: newThemeState,
- };
- set(updates);
+ themeCheckpoint: newThemeState, // Applying a preset also updates the checkpoint
+ history: updatedHistory,
+ future: [],
+ });
},
saveThemeCheckpoint: () => {
set({ themeCheckpoint: get().themeState });
@@ -46,11 +123,23 @@ export const useEditorStore = create()(
restoreThemeCheckpoint: () => {
const checkpoint = get().themeCheckpoint;
if (checkpoint) {
+ const oldThemeState = get().themeState;
+ const oldHistory = get().history;
+ const currentTime = Date.now();
+
+ const newHistoryEntry = { state: oldThemeState, timestamp: currentTime };
+ let updatedHistory = [...oldHistory, newHistoryEntry];
+ if (updatedHistory.length > MAX_HISTORY_COUNT) {
+ updatedHistory.shift();
+ }
+
set({
themeState: {
...checkpoint,
currentMode: get().themeState.currentMode,
},
+ history: updatedHistory,
+ future: [],
});
} else {
console.warn("No theme checkpoint available to restore to.");
@@ -71,19 +160,77 @@ export const useEditorStore = create()(
return stylesChanged || hslChanged;
},
resetToCurrentPreset: () => {
- const themeState = get().themeState;
- const presetThemeStyles = getPresetThemeStyles(themeState.preset ?? "default");
+ const currentThemeState = get().themeState;
+
+ const presetThemeStyles = getPresetThemeStyles(currentThemeState.preset ?? "default");
const newThemeState: ThemeEditorState = {
- ...themeState,
+ ...currentThemeState,
styles: presetThemeStyles,
- preset: themeState.preset,
hslAdjustments: defaultThemeState.hslAdjustments,
};
+
set({
themeState: newThemeState,
themeCheckpoint: newThemeState,
+ history: [],
+ future: [],
});
},
+ undo: () => {
+ const history = get().history;
+ if (history.length === 0) {
+ return;
+ }
+
+ const currentThemeState = get().themeState;
+ const future = get().future;
+
+ const newFutureEntry = { state: currentThemeState, timestamp: Date.now() };
+ const newFuture = [newFutureEntry, ...future];
+
+ if (history.length === 1) {
+ get().resetToCurrentPreset();
+ set({
+ future: newFuture,
+ });
+ return;
+ }
+
+ const lastHistoryEntry = history[history.length - 2];
+ const newHistory = history.slice(0, -1);
+
+ set({
+ themeState: lastHistoryEntry.state,
+ themeCheckpoint: lastHistoryEntry.state,
+ history: newHistory,
+ future: newFuture,
+ });
+ },
+ redo: () => {
+ const future = get().future;
+ if (future.length === 0) {
+ return;
+ }
+ const history = get().history;
+
+ const firstFutureEntry = future[0];
+ const newFuture = future.slice(1);
+
+ const newHistoryEntry = { state: firstFutureEntry.state, timestamp: Date.now() };
+ let updatedHistory = [...history, newHistoryEntry];
+ if (updatedHistory.length > MAX_HISTORY_COUNT) {
+ updatedHistory.shift();
+ }
+
+ set({
+ themeState: firstFutureEntry.state,
+ themeCheckpoint: firstFutureEntry.state,
+ history: updatedHistory,
+ future: newFuture,
+ });
+ },
+ canUndo: () => get().history.length > 0,
+ canRedo: () => get().future.length > 0,
}),
{
name: "editor-storage",