Phases 7-10: Complete remaining features and optimizations

Phase 7.2 - Debounce undo/redo history:
- Add 300ms debounce timer for rapid drag/transform changes
- Commit history on dragEnd/transformEnd events only
- Prevents history bloat during continuous interactions

Phase 8.3 - Template-aware export:
- Render template background layer first
- Apply slot crop regions for image elements
- Render template overlay layer last
- Support nonPrintable flag for guides/watermarks

Phase 9 - PWA icons:
- Add pwa-192x192.svg and pwa-512x512.svg icons
- Update vite.config.js manifest configuration

Phase 10.3 - Performance optimizations:
- Add React.memo to canvas components (ImageElement, TextElement, DesignCanvas)
- Add React.memo to panel components (LayersPanel, PropertiesPanel)
- Prevent unnecessary re-renders during canvas interactions

Phase 10.6 - Template documentation:
- Document template JSON schema in docs/template-schema.md
- Include element properties, slot definitions, and examples
- Describe background/overlay layer structure
This commit is contained in:
Khalid A
2026-04-21 21:50:33 -05:00
parent a02f020d4c
commit 4ca7910465
14 changed files with 465 additions and 93 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useCallback, useRef } from 'react';
import { useState, useCallback, useRef, useEffect } from 'react';
const MAX_HISTORY = 50;
const DEBOUNCE_DELAY_MS = 300;
export function useDesignEditor() {
const [elements, setElements] = useState([]);
@@ -10,6 +11,10 @@ export function useDesignEditor() {
const historyRef = useRef([]);
const historyIndexRef = useRef(-1);
// Debounce timer for rapid changes (drag/transform)
const historyTimerRef = useRef(null);
const pendingChangesRef = useRef(null);
const saveToHistory = useCallback((newElements) => {
// Remove any future history if we're in the middle of the stack
if (historyIndexRef.current < historyRef.current.length - 1) {
@@ -27,10 +32,34 @@ export function useDesignEditor() {
}
}, []);
// Flush pending changes to history
const flushPendingChanges = useCallback(() => {
if (pendingChangesRef.current) {
saveToHistory(pendingChangesRef.current);
pendingChangesRef.current = null;
}
if (historyTimerRef.current) {
clearTimeout(historyTimerRef.current);
historyTimerRef.current = null;
}
}, [saveToHistory]);
// Cleanup timer on unmount
useEffect(() => {
return () => {
if (historyTimerRef.current) {
clearTimeout(historyTimerRef.current);
}
};
}, []);
const canUndo = historyIndexRef.current > 0;
const canRedo = historyIndexRef.current < historyRef.current.length - 1;
const addElement = useCallback((element) => {
// Flush any pending debounced changes first
flushPendingChanges();
const newElement = {
...element,
id: `element-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
@@ -44,17 +73,34 @@ export function useDesignEditor() {
setSelectedId(newElement.id);
return newElement.id;
}, [saveToHistory]);
}, [flushPendingChanges, saveToHistory]);
const updateElement = useCallback((id, attrs) => {
setElements((prev) => {
const newElements = prev.map((el) => (el.id === id ? { ...el, ...attrs } : el));
saveToHistory(newElements);
// Debounce history commits for rapid changes (drag/transform)
// Store pending changes but don't commit yet
pendingChangesRef.current = newElements;
// Clear existing timer
if (historyTimerRef.current) {
clearTimeout(historyTimerRef.current);
}
// Set timer to commit changes after delay
historyTimerRef.current = setTimeout(() => {
flushPendingChanges();
}, DEBOUNCE_DELAY_MS);
return newElements;
});
}, [saveToHistory]);
}, [flushPendingChanges]);
const deleteElement = useCallback((id) => {
// Flush any pending debounced changes first
flushPendingChanges();
setElements((prev) => {
const newElements = prev.filter((el) => el.id !== id);
saveToHistory(newElements);
@@ -64,7 +110,7 @@ export function useDesignEditor() {
if (selectedId === id) {
setSelectedId(null);
}
}, [selectedId, saveToHistory]);
}, [selectedId, flushPendingChanges, saveToHistory]);
const selectElement = useCallback((id) => {
setSelectedId(id);
@@ -75,6 +121,9 @@ export function useDesignEditor() {
}, []);
const reorderElement = useCallback((id, newOrder) => {
// Flush any pending debounced changes first
flushPendingChanges();
setElements((prev) => {
const index = prev.findIndex((el) => el.id === id);
if (index === -1 || index === newOrder) return prev;
@@ -86,7 +135,12 @@ export function useDesignEditor() {
saveToHistory(newElements);
return newElements;
});
}, [saveToHistory]);
}, [flushPendingChanges, saveToHistory]);
// Commit history immediately (called on dragEnd/transformEnd)
const commitHistory = useCallback(() => {
flushPendingChanges();
}, [flushPendingChanges]);
const undo = useCallback(() => {
if (historyIndexRef.current > 0) {
@@ -121,6 +175,7 @@ export function useDesignEditor() {
selectElement,
deselectAll,
reorderElement,
commitHistory, // Call this on dragEnd/transformEnd to commit debounced changes
undo,
redo,
canUndo,