8.0 KiB
Detailed Chat Summary
Phase 1: Audit & Analysis
Started with a 1800-line Zustand timeline store doing EVERYTHING (core logic + UI state + history). Performed comprehensive audit identifying:
- ~40 methods mixing core logic with UI orchestration
- Inconsistent parameter patterns (some destructured, some not)
- Redundant methods (
updateElementStartTimevsupdateElementStartTimeWithRipple) - Three separate split operations that should be unified
- Delete operations conflating UI state with core logic
- Methods referencing
selectedElements(UI state) that shouldn't exist in core
Phase 2: Architecture Decision
Decided on Command Pattern + separation of concerns:
- Core Logic → TimelineManager + Commands
- UI State → Zustand store
- Ephemeral Interaction → Component state (dragState)
- History Tracking → HistoryManager
Phase 3: HistoryManager Creation
Created generic HistoryManager<T>:
execute(command)returns the command (enables getting return values)- Maintains
historyandredoStackarrays of Command objects undo()pops from history, callscommand.undo()redo()pops from redoStack, callscommand.redo()- Methods:
canUndo(),canRedo(),clear()
Phase 4: Command Architecture
Created base Command abstract class in src/lib/commands/base-command.ts:
- Abstract
execute()method (must implement) undo()throws by default (implement if reversible)redo()defaults to callingexecute()again
Built 15 command classes organized in folders:
- Track operations (3):
AddTrackCommand,RemoveTrackCommand,ToggleTrackMuteCommand - Element CRUD (5):
AddElementToTrackCommand,DeleteElementsCommand,DuplicateElementsCommand,UpdateElementTrimCommand,UpdateElementDurationCommand - Element properties (4):
UpdateTextElementCommand,ToggleElementsHiddenCommand,ToggleElementsMutedCommand,SplitElementsCommand - Unified position (1):
UpdateElementStartTimeCommand(replaced 3 old methods) - Clipboard (1):
PasteCommand
Each command:
- Takes minimal parameters in constructor
- Saves state before execution for undo
- Implements
execute()andundo() - Has getters for return values (e.g.,
getTrackId()) - Uses
EditorCore.getInstance()to access timeline manager
Phase 5: AddTrackCommand Details
Updated AddTrackCommand to support optional index parameter (unified addTrack and insertTrackAt):
- Constructor:
new AddTrackCommand(type, index?) - Splices into array at index if provided, appends if not
- Returns trackId via
getTrackId()getter - Saves full state for undo
Phase 6: TimelineManager Refactor - Part 1
Removed ALL UI state fields:
- ❌
clipboard→ UI state - ❌
snappingEnabled→ UI preference - ❌
rippleEditingEnabled→ UI preference - ❌
selectedElements→ UI selection - ❌
dragState→ ephemeral interaction - ❌
DragDataimport (only used for UI state)
Phase 7: TimelineManager Refactor - Part 2
Updated method implementations to use commands:
addTrack()→new AddTrackCommand(type, index).execute()removeTrack()→new RemoveTrackCommand(trackId).execute()updateElementTrim()→ creates command, conditionally pushes historyupdateElementDuration()→ creates command, conditionally pushes historyupdateElementStartTime()→ simplified (always through history)deleteElements()→ simplified (always through history)splitElements()→new SplitElementsCommand(elements, splitTime, retainSide).execute()duplicateElements(),toggleElementsHidden(),toggleElementsMuted()→ via commands
Phase 8: TimelineManager Cleanup
Removed methods that referenced UI state:
- ❌
getSortedTracks()(stub, redundant) - ❌
toggleSnapping()(UI preference) - ❌
toggleRippleEditing()(UI preference) - ❌
selectElement(),deselectElement(), etc. (UI selection) - ❌
setDragState(),startDrag(),updateDragTime(),endDrag()(UI interaction) - ❌
copySelected()(orchestrates UI state + core logic) - ❌
deleteSelectedElements()(uses UI state) - ❌
splitSelected()(uses UI state) - ❌
toggleSelectedHidden(),toggleSelectedMuted()(use UI state) - ❌
revealElementInMedia()(UI navigation) - ❌
getContextMenuState()(UI query) - ❌
addElementAtTime(),addElementToNewTrack()(convenience wrappers)
Result: TimelineManager reduced from 1800 → 315 lines, now pure core logic only
Phase 9: Timeline Store Refactor
Completely rewrote store (1800 → 104 lines) to contain ONLY UI state:
interface TimelineStore {
// Selection
selectedElements: { trackId: string; elementId: string }[]
selectElement()
deselectElement()
clearSelectedElements()
setSelectedElements()
// Preferences
snappingEnabled: boolean
toggleSnapping()
rippleEditingEnabled: boolean
toggleRippleEditing()
// Clipboard
clipboard: { items: Array<...> } | null
setClipboard()
}
Removed all methods that:
- Operated on
_tracks - Managed history
- Used commands
- Implemented core timeline logic
Phase 10: Identified dragState Problem
Analyzed dragState usage across three files:
- timeline-track.tsx (producer): Sets/updates drag state on mouse events
- timeline-element.tsx (consumer): Reads
dragState.elementIdanddragState.currentTimefor visual feedback during drag - index.tsx (consumer): Reads
dragState.isDraggingto show snap indicator
Problem: In global Zustand store (wrong scope)
- Only used during active drag operations
- Ephemeral state (cleared after drag ends)
- Causes unnecessary re-renders across entire app
- Only needs to exist in Timeline component tree
Solution: Move to Timeline component state:
- Keep
dragStateas local state inTimelinecomponent - Pass down via props to
TimelineTrackandTimelineElement - Timeline component manages
handleMouseDown,handleMouseMove,handleMouseUpin TimelineTrack - This keeps scope correct and re-renders localized
Phase 11: Architectural Alignment
Confirmed final architecture:
-
Editor Core (singleton):
history: HistoryManager(manages undo/redo)timeline: TimelineManager(pure data + commands)- Other managers (project, media, scene, playback)
-
Timeline Component (React):
dragState(local state via useState)- Props down to children
-
Timeline Store (Zustand):
- Selection state + methods
- Preferences (snapping, ripple editing)
- Clipboard state
Phase 12: Command Pattern Benefits Identified
- Self-contained operations - Each command knows how to undo itself
- Discoverable API - All timeline actions are in
/lib/commands/timeline/ - Type-safe - Commands are strongly typed
- Testable - Each command can be unit tested independently
- Scalable - Adding new operations = creating new command class
- Clean manager - TimelineManager becomes orchestrator, not logic holder
- Reusable - Commands can be executed from anywhere (UI, scripts, etc.)
Summary of Changes Made
Files Created:
src/lib/commands/base-command.ts(12 lines)src/lib/commands/index.ts(3 lines)src/lib/commands/timeline/index.ts(4 lines)src/lib/commands/timeline/track/(3 commands)src/lib/commands/timeline/element/(9 commands)src/lib/commands/timeline/clipboard/(1 command)
Files Modified:
src/core/managers/history-manager.ts- Refactored to work with Commands, now returns commandsrc/core/managers/timeline-manager.ts- Removed UI state, implemented all methods as command-drivensrc/stores/timeline-store.ts- From 1800 → 104 lines, now UI state only
Files Deleted:
- Old history logic removed (consolidated into HistoryManager)
- Removed unused imports from components (cascading changes needed)
Remaining Work
- Move
dragStatefrom store to Timeline component - Update all component imports and method calls
- Implement remaining stub methods (
checkElementOverlap,findOrCreateTrack, etc.) - Wire up UI to call new manager methods instead of store methods