diff --git a/components/record-mode/PickingHelper.tsx b/components/record-mode/PickingHelper.tsx index 5ca4d33f..31074e37 100644 --- a/components/record-mode/PickingHelper.tsx +++ b/components/record-mode/PickingHelper.tsx @@ -14,6 +14,14 @@ export function PickingHelper() { } else if (e.data.type === 'STOP_PICKING') { setPickingMode(null); setHoveredElement(null); + } else if (e.data.type === 'SET_HOVER_SELECTOR') { + const selector = e.data.selector; + if (selector) { + const el = document.querySelector(selector) as HTMLElement; + setHoveredElement(el || null); + } else { + setHoveredElement(null); + } } }; @@ -86,7 +94,9 @@ export function PickingHelper() { }; }, [pickingMode, hoveredElement]); - if (!pickingMode || !hoveredElement) return null; + if (!hoveredElement) return null; + // Don't show highlight if we are in picking mode but NOT hovering anything (handled by logic above) + // but DO show if we have a hoveredElement (from message or mouseover) const rect = hoveredElement.getBoundingClientRect(); diff --git a/components/record-mode/RecordModeContext.tsx b/components/record-mode/RecordModeContext.tsx index c99926a0..52a3fb2c 100644 --- a/components/record-mode/RecordModeContext.tsx +++ b/components/record-mode/RecordModeContext.tsx @@ -11,6 +11,7 @@ interface RecordModeContextType { updateEvent: (id: string, event: Partial) => void; removeEvent: (id: string) => void; clearEvents: () => void; + setEvents: (events: RecordEvent[]) => void; isPlaying: boolean; playEvents: () => void; stopPlayback: () => void; @@ -21,6 +22,9 @@ interface RecordModeContextType { saveSession: (name: string) => void; isFeedbackActive: boolean; setIsFeedbackActive: (active: boolean) => void; + reorderEvents: (startIndex: number, endIndex: number) => void; + hoveredEventId: string | null; + setHoveredEventId: (id: string | null) => void; } const RecordModeContext = createContext(null); @@ -47,6 +51,10 @@ export function useRecordMode(): RecordModeContextType { isFeedbackActive: false, setIsFeedbackActive: () => { }, saveSession: () => { }, + reorderEvents: () => { }, + hoveredEventId: null, + setHoveredEventId: () => { }, + setEvents: () => { }, }; } return context; @@ -60,6 +68,7 @@ export function RecordModeProvider({ children }: { children: React.ReactNode }) const [zoomLevel, setZoomLevel] = useState(1); const [isBlurry, setIsBlurry] = useState(false); const [isFeedbackActive, setIsFeedbackActiveState] = useState(false); + const [hoveredEventId, setHoveredEventId] = useState(null); const [isEmbedded, setIsEmbedded] = useState(false); useEffect(() => { @@ -124,13 +133,30 @@ export function RecordModeProvider({ children }: { children: React.ReactNode }) if (event.type === 'scroll') { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); } else if (event.type === 'click') { - // Get CURRENT rect for industrial precision const currentRect = el.getBoundingClientRect(); - const eventCoords = { - clientX: currentRect.left + currentRect.width / 2, - clientY: currentRect.top + currentRect.height / 2 - }; + // Calculate Point based on Origin + let targetX = currentRect.left + currentRect.width / 2; + let targetY = currentRect.top + currentRect.height / 2; + + if (event.clickOrigin === 'top-left') { + targetX = currentRect.left + 5; + targetY = currentRect.top + 5; + } else if (event.clickOrigin === 'top-right') { + targetX = currentRect.right - 5; + targetY = currentRect.top + 5; + } else if (event.clickOrigin === 'bottom-left') { + targetX = currentRect.left + 5; + targetY = currentRect.bottom - 5; + } else if (event.clickOrigin === 'bottom-right') { + targetX = currentRect.right - 5; + targetY = currentRect.bottom - 5; + } + + const eventCoords = { + clientX: targetX, + clientY: targetY + }; const dispatchMouse = (type: string) => { el.dispatchEvent(new MouseEvent(type, { view: window, @@ -153,6 +179,27 @@ export function RecordModeProvider({ children }: { children: React.ReactNode }) return () => window.removeEventListener('message', handlePlaybackMessage); }, [isEmbedded]); + // Sync Hover Preview to Iframe + useEffect(() => { + if (isEmbedded || !isActive) return; + + const event = events.find(e => e.id === hoveredEventId); + const iframe = document.querySelector('iframe[name="record-mode-iframe"]') as HTMLIFrameElement; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage({ + type: 'SET_HOVER_SELECTOR', + selector: event?.selector || null + }, '*'); + } + }, [hoveredEventId, events, isActive, isEmbedded]); + + const reorderEvents = (startIndex: number, endIndex: number) => { + const result = Array.from(events); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + setEvents(result); + }; + const addEvent = (event: Omit) => { const newEvent: RecordEvent = { ...event, @@ -230,9 +277,28 @@ export function RecordModeProvider({ children }: { children: React.ReactNode }) el.scrollIntoView({ behavior: 'smooth', block: 'center' }); } else if (event.type === 'click') { const currentRect = el.getBoundingClientRect(); + + // Calculate Point based on Origin (same as above for parity) + let targetX = currentRect.left + currentRect.width / 2; + let targetY = currentRect.top + currentRect.height / 2; + + if (event.clickOrigin === 'top-left') { + targetX = currentRect.left + 5; + targetY = currentRect.top + 5; + } else if (event.clickOrigin === 'top-right') { + targetX = currentRect.right - 5; + targetY = currentRect.top + 5; + } else if (event.clickOrigin === 'bottom-left') { + targetX = currentRect.left + 5; + targetY = currentRect.bottom - 5; + } else if (event.clickOrigin === 'bottom-right') { + targetX = currentRect.right - 5; + targetY = currentRect.bottom - 5; + } + const eventCoords = { - clientX: currentRect.left + currentRect.width / 2, - clientY: currentRect.top + currentRect.height / 2 + clientX: targetX, + clientY: targetY }; const dispatchMouse = (type: string) => { el.dispatchEvent(new MouseEvent(type, { @@ -282,6 +348,7 @@ export function RecordModeProvider({ children }: { children: React.ReactNode }) updateEvent, removeEvent, clearEvents, + setEvents, isPlaying, playEvents, stopPlayback, @@ -292,6 +359,9 @@ export function RecordModeProvider({ children }: { children: React.ReactNode }) saveSession, isFeedbackActive, setIsFeedbackActive, + reorderEvents, + hoveredEventId, + setHoveredEventId, }} > {children} diff --git a/components/record-mode/RecordModeOverlay.tsx b/components/record-mode/RecordModeOverlay.tsx index 44d02424..46665498 100644 --- a/components/record-mode/RecordModeOverlay.tsx +++ b/components/record-mode/RecordModeOverlay.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { useRecordMode } from './RecordModeContext'; -import { finder } from '@medv/finder'; +import { Reorder, AnimatePresence } from 'framer-motion'; import { Play, Square, @@ -16,6 +16,11 @@ import { X, Check, Download, + Settings2, + GripVertical, + Clock, + Maximize2, + Box, } from 'lucide-react'; import { RecordEvent } from '@/types/record-mode'; import { PlaybackCursor } from './PlaybackCursor'; @@ -32,6 +37,9 @@ export function RecordModeOverlay() { playEvents, saveSession, clearEvents, + reorderEvents, + setHoveredEventId, + setEvents, // Added setEvents here } = useRecordMode(); const [pickingMode, setPickingMode] = useState<'click' | 'scroll' | null>(null); @@ -99,9 +107,16 @@ export function RecordModeOverlay() { return () => { window.removeEventListener('message', handleMessage); }; - }, [isActive, pickingMode, addEvent]); + }, [isActive, pickingMode, addEvent, mounted]); - const [showEvents, setShowEvents] = useState(false); + const saveEdit = () => { + if (editingEventId) { + updateEvent(editingEventId, editForm); + setEditingEventId(null); + } + }; + + const [showEvents, setShowEvents] = useState(true); if (!mounted) return null; @@ -273,7 +288,12 @@ export function RecordModeOverlay() { -
+ {events.length === 0 ? (
@@ -281,34 +301,51 @@ export function RecordModeOverlay() {
) : ( events.map((event, index) => ( -
setHoveredEventId(event.id)} + onMouseLeave={() => setHoveredEventId(null)} > -
- {index + 1} +
+
- {event.type} + {event.type} + {event.clickOrigin && event.clickOrigin !== 'center' && ( + {event.clickOrigin} + )} {event.duration}ms
-

+

{event.selector || 'system:wait'}

- -
+
+ + +
+ )) )} -
+
)} @@ -338,6 +375,108 @@ export function RecordModeOverlay() { )} + + {/* 3. Event Options Panel (Sidebar-like) */} + + {editingEventId && ( +
+
+

Event Options

+ +
+ +
+ {/* Type Display */} +
+ +
+
+ {editForm.type === 'click' ? : editForm.type === 'scroll' ? : } +
+ {editForm.type} +
+
+ + {/* Precise Click Origin */} + {editForm.type === 'click' && ( +
+ +
+ {[ + { id: 'top-left', label: 'TL' }, { id: 'top-right', label: 'TR' }, { id: 'center', label: 'CTR' }, + { id: 'bottom-left', label: 'BL' }, { id: 'bottom-right', label: 'BR' } + ].map((origin) => ( + + ))} +
+
+ )} + + {/* Timing */} +
+ + setEditForm(prev => ({ ...prev, duration: parseInt(e.target.value) }))} + className="w-full h-1 bg-white/10 rounded-lg appearance-none cursor-pointer accent-accent" + /> +
+ + {/* Zoom & Effects */} +
+
+
+ + Zoom Shift +
+ setEditForm(prev => ({ ...prev, zoom: parseFloat(e.target.value) }))} + className="w-16 bg-white/10 border border-white/10 rounded-lg px-2 py-1 text-xs text-white text-center font-mono" + /> +
+ + +
+
+ + +
+ )} +
); } diff --git a/types/record-mode.ts b/types/record-mode.ts index ed8a01dd..4332fdcc 100644 --- a/types/record-mode.ts +++ b/types/record-mode.ts @@ -3,11 +3,12 @@ export interface RecordEvent { type: 'click' | 'scroll' | 'wait' | 'hover'; selector?: string; // CSS selector timestamp: number; // Time in ms since start of recording - duration: number; // Duration of the action (e.g. scroll duration) + duration: number; // Duration allocated for this action in playback zoom?: number; // Zoom level during event description?: string; // Optional label motionBlur?: boolean; // Enable motion blur effect rect?: { x: number; y: number; width: number; height: number }; // Element position for rendering + clickOrigin?: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; } export interface RecordingSession {