feat: enhance Recording Studio with reorderable events, origin options, and hover previews
This commit is contained in:
@@ -14,6 +14,14 @@ export function PickingHelper() {
|
|||||||
} else if (e.data.type === 'STOP_PICKING') {
|
} else if (e.data.type === 'STOP_PICKING') {
|
||||||
setPickingMode(null);
|
setPickingMode(null);
|
||||||
setHoveredElement(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]);
|
}, [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();
|
const rect = hoveredElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface RecordModeContextType {
|
|||||||
updateEvent: (id: string, event: Partial<RecordEvent>) => void;
|
updateEvent: (id: string, event: Partial<RecordEvent>) => void;
|
||||||
removeEvent: (id: string) => void;
|
removeEvent: (id: string) => void;
|
||||||
clearEvents: () => void;
|
clearEvents: () => void;
|
||||||
|
setEvents: (events: RecordEvent[]) => void;
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
playEvents: () => void;
|
playEvents: () => void;
|
||||||
stopPlayback: () => void;
|
stopPlayback: () => void;
|
||||||
@@ -21,6 +22,9 @@ interface RecordModeContextType {
|
|||||||
saveSession: (name: string) => void;
|
saveSession: (name: string) => void;
|
||||||
isFeedbackActive: boolean;
|
isFeedbackActive: boolean;
|
||||||
setIsFeedbackActive: (active: boolean) => void;
|
setIsFeedbackActive: (active: boolean) => void;
|
||||||
|
reorderEvents: (startIndex: number, endIndex: number) => void;
|
||||||
|
hoveredEventId: string | null;
|
||||||
|
setHoveredEventId: (id: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RecordModeContext = createContext<RecordModeContextType | null>(null);
|
const RecordModeContext = createContext<RecordModeContextType | null>(null);
|
||||||
@@ -47,6 +51,10 @@ export function useRecordMode(): RecordModeContextType {
|
|||||||
isFeedbackActive: false,
|
isFeedbackActive: false,
|
||||||
setIsFeedbackActive: () => { },
|
setIsFeedbackActive: () => { },
|
||||||
saveSession: () => { },
|
saveSession: () => { },
|
||||||
|
reorderEvents: () => { },
|
||||||
|
hoveredEventId: null,
|
||||||
|
setHoveredEventId: () => { },
|
||||||
|
setEvents: () => { },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
@@ -60,6 +68,7 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
const [zoomLevel, setZoomLevel] = useState(1);
|
const [zoomLevel, setZoomLevel] = useState(1);
|
||||||
const [isBlurry, setIsBlurry] = useState(false);
|
const [isBlurry, setIsBlurry] = useState(false);
|
||||||
const [isFeedbackActive, setIsFeedbackActiveState] = useState(false);
|
const [isFeedbackActive, setIsFeedbackActiveState] = useState(false);
|
||||||
|
const [hoveredEventId, setHoveredEventId] = useState<string | null>(null);
|
||||||
const [isEmbedded, setIsEmbedded] = useState(false);
|
const [isEmbedded, setIsEmbedded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -124,13 +133,30 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
if (event.type === 'scroll') {
|
if (event.type === 'scroll') {
|
||||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
} else if (event.type === 'click') {
|
} else if (event.type === 'click') {
|
||||||
// Get CURRENT rect for industrial precision
|
|
||||||
const currentRect = el.getBoundingClientRect();
|
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) => {
|
const dispatchMouse = (type: string) => {
|
||||||
el.dispatchEvent(new MouseEvent(type, {
|
el.dispatchEvent(new MouseEvent(type, {
|
||||||
view: window,
|
view: window,
|
||||||
@@ -153,6 +179,27 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
return () => window.removeEventListener('message', handlePlaybackMessage);
|
return () => window.removeEventListener('message', handlePlaybackMessage);
|
||||||
}, [isEmbedded]);
|
}, [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<RecordEvent, 'id' | 'timestamp'>) => {
|
const addEvent = (event: Omit<RecordEvent, 'id' | 'timestamp'>) => {
|
||||||
const newEvent: RecordEvent = {
|
const newEvent: RecordEvent = {
|
||||||
...event,
|
...event,
|
||||||
@@ -230,9 +277,28 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
} else if (event.type === 'click') {
|
} else if (event.type === 'click') {
|
||||||
const currentRect = el.getBoundingClientRect();
|
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 = {
|
const eventCoords = {
|
||||||
clientX: currentRect.left + currentRect.width / 2,
|
clientX: targetX,
|
||||||
clientY: currentRect.top + currentRect.height / 2
|
clientY: targetY
|
||||||
};
|
};
|
||||||
const dispatchMouse = (type: string) => {
|
const dispatchMouse = (type: string) => {
|
||||||
el.dispatchEvent(new MouseEvent(type, {
|
el.dispatchEvent(new MouseEvent(type, {
|
||||||
@@ -282,6 +348,7 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
updateEvent,
|
updateEvent,
|
||||||
removeEvent,
|
removeEvent,
|
||||||
clearEvents,
|
clearEvents,
|
||||||
|
setEvents,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
playEvents,
|
playEvents,
|
||||||
stopPlayback,
|
stopPlayback,
|
||||||
@@ -292,6 +359,9 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
saveSession,
|
saveSession,
|
||||||
isFeedbackActive,
|
isFeedbackActive,
|
||||||
setIsFeedbackActive,
|
setIsFeedbackActive,
|
||||||
|
reorderEvents,
|
||||||
|
hoveredEventId,
|
||||||
|
setHoveredEventId,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useRecordMode } from './RecordModeContext';
|
import { useRecordMode } from './RecordModeContext';
|
||||||
import { finder } from '@medv/finder';
|
import { Reorder, AnimatePresence } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
Play,
|
Play,
|
||||||
Square,
|
Square,
|
||||||
@@ -16,6 +16,11 @@ import {
|
|||||||
X,
|
X,
|
||||||
Check,
|
Check,
|
||||||
Download,
|
Download,
|
||||||
|
Settings2,
|
||||||
|
GripVertical,
|
||||||
|
Clock,
|
||||||
|
Maximize2,
|
||||||
|
Box,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { RecordEvent } from '@/types/record-mode';
|
import { RecordEvent } from '@/types/record-mode';
|
||||||
import { PlaybackCursor } from './PlaybackCursor';
|
import { PlaybackCursor } from './PlaybackCursor';
|
||||||
@@ -32,6 +37,9 @@ export function RecordModeOverlay() {
|
|||||||
playEvents,
|
playEvents,
|
||||||
saveSession,
|
saveSession,
|
||||||
clearEvents,
|
clearEvents,
|
||||||
|
reorderEvents,
|
||||||
|
setHoveredEventId,
|
||||||
|
setEvents, // Added setEvents here
|
||||||
} = useRecordMode();
|
} = useRecordMode();
|
||||||
|
|
||||||
const [pickingMode, setPickingMode] = useState<'click' | 'scroll' | null>(null);
|
const [pickingMode, setPickingMode] = useState<'click' | 'scroll' | null>(null);
|
||||||
@@ -99,9 +107,16 @@ export function RecordModeOverlay() {
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('message', handleMessage);
|
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;
|
if (!mounted) return null;
|
||||||
|
|
||||||
@@ -273,7 +288,12 @@ export function RecordModeOverlay() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto space-y-2 pr-2 scrollbar-hide">
|
<Reorder.Group
|
||||||
|
axis="y"
|
||||||
|
values={events}
|
||||||
|
onReorder={setEvents}
|
||||||
|
className="flex-1 overflow-y-auto space-y-2 pr-2 scrollbar-hide"
|
||||||
|
>
|
||||||
{events.length === 0 ? (
|
{events.length === 0 ? (
|
||||||
<div className="py-12 flex flex-col items-center justify-center text-white/10">
|
<div className="py-12 flex flex-col items-center justify-center text-white/10">
|
||||||
<Plus size={40} strokeWidth={1} />
|
<Plus size={40} strokeWidth={1} />
|
||||||
@@ -281,34 +301,51 @@ export function RecordModeOverlay() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
events.map((event, index) => (
|
events.map((event, index) => (
|
||||||
<div
|
<Reorder.Item
|
||||||
key={event.id}
|
key={event.id}
|
||||||
className="group flex items-center gap-4 bg-white/[0.03] border border-white/5 p-3 rounded-[20px] transition-all hover:bg-white/[0.06] hover:border-white/10"
|
value={event}
|
||||||
|
className="group flex items-center gap-3 bg-white/[0.03] border border-white/5 p-3 rounded-[20px] transition-all hover:bg-white/[0.06] hover:border-white/10"
|
||||||
|
onMouseEnter={() => setHoveredEventId(event.id)}
|
||||||
|
onMouseLeave={() => setHoveredEventId(null)}
|
||||||
>
|
>
|
||||||
<div className="w-6 h-6 rounded-full bg-white/5 border border-white/5 flex items-center justify-center text-[9px] font-bold text-white/30">
|
<div className="cursor-grab active:cursor-grabbing text-white/10 hover:text-white/30 transition-colors">
|
||||||
{index + 1}
|
<GripVertical size={16} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-white text-xs font-bold uppercase tracking-widest">{event.type}</span>
|
<span className="text-white text-[10px] font-black uppercase tracking-widest">{event.type}</span>
|
||||||
|
{event.clickOrigin && event.clickOrigin !== 'center' && (
|
||||||
|
<span className="text-[8px] bg-accent/20 text-accent px-1.5 py-0.5 rounded uppercase font-bold">{event.clickOrigin}</span>
|
||||||
|
)}
|
||||||
<span className="text-[8px] bg-white/10 px-1.5 py-0.5 rounded text-white/40 font-mono italic">{event.duration}ms</span>
|
<span className="text-[8px] bg-white/10 px-1.5 py-0.5 rounded text-white/40 font-mono italic">{event.duration}ms</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-white/30 truncate font-mono mt-1 opacity-60">
|
<p className="text-[9px] text-white/30 truncate font-mono mt-1 opacity-60">
|
||||||
{event.selector || 'system:wait'}
|
{event.selector || 'system:wait'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div className="flex items-center gap-1">
|
||||||
onClick={() => removeEvent(event.id)}
|
<button
|
||||||
className="p-2 text-white/0 group-hover:text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded-xl transition-all"
|
onClick={() => {
|
||||||
>
|
setEditingEventId(event.id);
|
||||||
<Trash2 size={14} />
|
setEditForm(event);
|
||||||
</button>
|
}}
|
||||||
</div>
|
className="p-2 text-white/0 group-hover:text-white/40 hover:text-white hover:bg-white/10 rounded-xl transition-all"
|
||||||
|
>
|
||||||
|
<Settings2 size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => removeEvent(event.id)}
|
||||||
|
className="p-2 text-white/0 group-hover:text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded-xl transition-all"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Reorder.Item>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</Reorder.Group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -338,6 +375,108 @@ export function RecordModeOverlay() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<PlaybackCursor />
|
<PlaybackCursor />
|
||||||
|
|
||||||
|
{/* 3. Event Options Panel (Sidebar-like) */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{editingEventId && (
|
||||||
|
<div className="fixed inset-y-0 right-0 w-[350px] bg-black/95 backdrop-blur-3xl border-l border-white/10 z-[11000] pointer-events-auto p-8 shadow-[-40px_0_100px_rgba(0,0,0,0.9)] flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-8">
|
||||||
|
<h3 className="text-white font-black uppercase tracking-tighter text-xl">Event Options</h3>
|
||||||
|
<button onClick={() => setEditingEventId(null)} className="p-2 text-white/40 hover:text-white transition-colors">
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 space-y-8 overflow-y-auto pr-2 scrollbar-hide">
|
||||||
|
{/* Type Display */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-white/30 leading-none">Action Type</label>
|
||||||
|
<div className="bg-white/5 p-4 rounded-2xl border border-white/5 flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-accent/20 text-accent flex items-center justify-center">
|
||||||
|
{editForm.type === 'click' ? <MousePointer2 size={20} /> : editForm.type === 'scroll' ? <Scroll size={20} /> : <Clock size={20} />}
|
||||||
|
</div>
|
||||||
|
<span className="text-white font-black uppercase tracking-widest text-sm">{editForm.type}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Precise Click Origin */}
|
||||||
|
{editForm.type === 'click' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-white/30 leading-none">Click Origin</label>
|
||||||
|
<div className="grid grid-cols-3 gap-2 p-2 bg-white/5 rounded-2xl border border-white/5">
|
||||||
|
{[
|
||||||
|
{ 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) => (
|
||||||
|
<button
|
||||||
|
key={origin.id}
|
||||||
|
onClick={() => setEditForm(prev => ({ ...prev, clickOrigin: origin.id as any }))}
|
||||||
|
className={`py-3 rounded-xl text-[10px] font-black uppercase tracking-tighter transition-all border ${editForm.clickOrigin === origin.id ? 'bg-accent text-primary-dark border-accent' : 'bg-transparent text-white/40 border-white/5 hover:border-white/20'}`}
|
||||||
|
>
|
||||||
|
{origin.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Timing */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-white/30 leading-none flex items-center justify-between">
|
||||||
|
<span>Timeline Allocation</span>
|
||||||
|
<span className="text-accent">{editForm.duration}ms</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="5000"
|
||||||
|
step="100"
|
||||||
|
value={editForm.duration || 1000}
|
||||||
|
onChange={(e) => setEditForm(prev => ({ ...prev, duration: parseInt(e.target.value) }))}
|
||||||
|
className="w-full h-1 bg-white/10 rounded-lg appearance-none cursor-pointer accent-accent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Zoom & Effects */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between p-4 bg-white/5 rounded-2xl border border-white/5 group hover:border-white/20 transition-all">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Maximize2 size={18} className="text-white/40" />
|
||||||
|
<span className="text-xs font-bold text-white uppercase tracking-wider">Zoom Shift</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
min="1"
|
||||||
|
max="3"
|
||||||
|
value={editForm.zoom || 1}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setEditForm(prev => ({ ...prev, motionBlur: !prev.motionBlur }))}
|
||||||
|
className={`flex items-center justify-between w-full p-4 rounded-2xl border transition-all ${editForm.motionBlur ? 'bg-accent/10 border-accent/30 text-accent' : 'bg-white/5 border-white/5 text-white/40'}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Box size={18} />
|
||||||
|
<span className="text-xs font-bold uppercase tracking-wider">Motion Blur</span>
|
||||||
|
</div>
|
||||||
|
{editForm.motionBlur ? <Check size={18} /> : <div className="w-[18px]" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={saveEdit}
|
||||||
|
className="mt-8 py-5 bg-accent text-primary-dark text-xs font-black uppercase tracking-[0.2em] rounded-2xl shadow-2xl shadow-accent/20 hover:scale-[1.02] transition-all"
|
||||||
|
>
|
||||||
|
Commit Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ export interface RecordEvent {
|
|||||||
type: 'click' | 'scroll' | 'wait' | 'hover';
|
type: 'click' | 'scroll' | 'wait' | 'hover';
|
||||||
selector?: string; // CSS selector
|
selector?: string; // CSS selector
|
||||||
timestamp: number; // Time in ms since start of recording
|
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
|
zoom?: number; // Zoom level during event
|
||||||
description?: string; // Optional label
|
description?: string; // Optional label
|
||||||
motionBlur?: boolean; // Enable motion blur effect
|
motionBlur?: boolean; // Enable motion blur effect
|
||||||
rect?: { x: number; y: number; width: number; height: number }; // Element position for rendering
|
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 {
|
export interface RecordingSession {
|
||||||
|
|||||||
Reference in New Issue
Block a user