feat: refactor clicks to generic mouse interactions with click/hover subtypes

This commit is contained in:
2026-02-15 18:17:10 +01:00
parent d8a4ffe230
commit b32db4b277
3 changed files with 112 additions and 27 deletions

View File

@@ -132,7 +132,7 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
if (el) {
if (event.type === 'scroll') {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else if (event.type === 'click') {
} else if (event.type === 'mouse') {
const currentRect = el.getBoundingClientRect();
// Calculate Point based on Origin
@@ -157,6 +157,7 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
clientX: targetX,
clientY: targetY
};
const dispatchMouse = (type: string) => {
el.dispatchEvent(new MouseEvent(type, {
view: window,
@@ -165,10 +166,21 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
...eventCoords
}));
};
dispatchMouse('mousedown');
dispatchMouse('mouseup');
dispatchMouse('click');
el.click();
if (event.interactionType === 'click') {
dispatchMouse('mousedown');
dispatchMouse('mouseup');
dispatchMouse('click');
if (event.realClick) {
el.click();
}
} else {
// Hover Interaction
dispatchMouse('mousemove');
dispatchMouse('mouseover');
dispatchMouse('mouseenter');
}
}
}
}
@@ -275,7 +287,7 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
if (el) {
if (event.type === 'scroll') {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else if (event.type === 'click') {
} else if (event.type === 'mouse') {
const currentRect = el.getBoundingClientRect();
// Calculate Point based on Origin (same as above for parity)
@@ -300,6 +312,7 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
clientX: targetX,
clientY: targetY
};
const dispatchMouse = (type: string) => {
el.dispatchEvent(new MouseEvent(type, {
view: window,
@@ -308,10 +321,21 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
...eventCoords
}));
};
dispatchMouse('mousedown');
dispatchMouse('mouseup');
dispatchMouse('click');
el.click();
if (event.interactionType === 'click') {
dispatchMouse('mousedown');
dispatchMouse('mouseup');
dispatchMouse('click');
if (event.realClick) {
el.click();
}
} else {
// Hover Interaction
dispatchMouse('mousemove');
dispatchMouse('mouseover');
dispatchMouse('mouseenter');
}
}
}
}

View File

@@ -21,6 +21,7 @@ import {
Clock,
Maximize2,
Box,
ExternalLink,
} from 'lucide-react';
import { RecordEvent } from '@/types/record-mode';
import { PlaybackCursor } from './PlaybackCursor';
@@ -42,7 +43,8 @@ export function RecordModeOverlay() {
setEvents, // Added setEvents here
} = useRecordMode();
const [pickingMode, setPickingMode] = useState<'click' | 'scroll' | null>(null);
const [pickingMode, setPickingMode] = useState<'mouse' | 'scroll' | null>(null);
const [lastInteractionType, setLastInteractionType] = useState<'click' | 'hover'>('click');
const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(null);
const [editingEventId, setEditingEventId] = useState<string | null>(null);
@@ -61,13 +63,14 @@ export function RecordModeOverlay() {
if (e.data.type === 'ELEMENT_SELECTED') {
const { selector, rect, tagName } = e.data;
if (pickingMode === 'click') {
if (pickingMode === 'mouse') {
addEvent({
type: 'click',
type: 'mouse',
interactionType: lastInteractionType,
selector,
duration: 1000,
duration: lastInteractionType === 'click' ? 1000 : 1500,
zoom: 1,
description: `Click on ${tagName}`,
description: `${lastInteractionType === 'click' ? 'Click' : 'Hover'} on ${tagName}`,
motionBlur: false,
rect,
});
@@ -156,13 +159,27 @@ export function RecordModeOverlay() {
{/* Action Tools */}
<div className="flex items-center gap-1">
<button
onClick={() => setPickingMode('click')}
className={`flex items-center gap-2 px-4 py-2.5 rounded-[16px] transition-all text-xs font-bold uppercase tracking-wide ${pickingMode === 'click' ? 'bg-accent text-primary-dark shadow-lg shadow-accent/20' : 'text-white/40 hover:text-white hover:bg-white/5'}`}
onClick={() => {
setPickingMode('mouse');
setLastInteractionType('click');
}}
className={`flex items-center gap-2 px-4 py-2.5 rounded-[16px] transition-all text-xs font-bold uppercase tracking-wide ${pickingMode === 'mouse' && lastInteractionType === 'click' ? 'bg-accent text-primary-dark shadow-lg shadow-accent/20' : 'text-white/40 hover:text-white hover:bg-white/5'}`}
>
<MousePointer2 size={16} />
<span>Click</span>
</button>
<button
onClick={() => {
setPickingMode('mouse');
setLastInteractionType('hover');
}}
className={`flex items-center gap-2 px-4 py-2.5 rounded-[16px] transition-all text-xs font-bold uppercase tracking-wide ${pickingMode === 'mouse' && lastInteractionType === 'hover' ? 'bg-accent text-primary-dark shadow-lg shadow-accent/20' : 'text-white/40 hover:text-white hover:bg-white/5'}`}
>
<Eye size={16} />
<span>Hover</span>
</button>
<button
onClick={() => setPickingMode('scroll')}
className={`flex items-center gap-2 px-4 py-2.5 rounded-[16px] transition-all text-xs font-bold uppercase tracking-wide ${pickingMode === 'scroll' ? 'bg-accent text-primary-dark shadow-lg shadow-accent/20' : 'text-white/40 hover:text-white hover:bg-white/5'}`}
@@ -314,8 +331,10 @@ export function RecordModeOverlay() {
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-white text-[10px] font-black uppercase tracking-widest">{event.type}</span>
{event.clickOrigin && event.clickOrigin !== 'center' && (
<span className="text-white text-[10px] font-black uppercase tracking-widest">
{event.type === 'mouse' ? event.interactionType : event.type}
</span>
{event.clickOrigin && event.clickOrigin !== 'center' && event.interactionType === 'click' && (
<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>
@@ -390,17 +409,41 @@ export function RecordModeOverlay() {
<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>
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-white/30 leading-none">Interaction Type</label>
<div className="flex gap-2 p-1 bg-white/5 rounded-2xl border border-white/5">
<button
onClick={() => setEditForm(prev => ({ ...prev, type: 'mouse', interactionType: 'click' }))}
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl transition-all border ${editForm.type === 'mouse' && editForm.interactionType === 'click' ? 'bg-accent text-primary-dark border-accent' : 'text-white/40 border-transparent hover:border-white/10'}`}
>
<MousePointer2 size={14} />
<span className="text-[10px] font-black uppercase">Click</span>
</button>
<button
onClick={() => setEditForm(prev => ({ ...prev, type: 'mouse', interactionType: 'hover' }))}
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl transition-all border ${editForm.type === 'mouse' && editForm.interactionType === 'hover' ? 'bg-accent text-primary-dark border-accent' : 'text-white/40 border-transparent hover:border-white/10'}`}
>
<Eye size={14} />
<span className="text-[10px] font-black uppercase">Hover</span>
</button>
<button
onClick={() => setEditForm(prev => ({ ...prev, type: 'scroll' }))}
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl transition-all border ${editForm.type === 'scroll' ? 'bg-accent text-primary-dark border-accent' : 'text-white/40 border-transparent hover:border-white/10'}`}
>
<Scroll size={14} />
<span className="text-[10px] font-black uppercase">Scroll</span>
</button>
<button
onClick={() => setEditForm(prev => ({ ...prev, type: 'wait' }))}
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl transition-all border ${editForm.type === 'wait' ? 'bg-accent text-primary-dark border-accent' : 'text-white/40 border-transparent hover:border-white/10'}`}
>
<Clock size={14} />
<span className="text-[10px] font-black uppercase">Wait</span>
</button>
</div>
</div>
{/* Precise Click Origin */}
{editForm.type === 'click' && (
{editForm.type === 'mouse' && editForm.interactionType === '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">
@@ -465,6 +508,22 @@ export function RecordModeOverlay() {
</div>
{editForm.motionBlur ? <Check size={18} /> : <div className="w-[18px]" />}
</button>
{editForm.type === 'mouse' && editForm.interactionType === 'click' && (
<button
onClick={() => setEditForm(prev => ({ ...prev, realClick: !prev.realClick }))}
className={`flex items-center justify-between w-full p-4 rounded-2xl border transition-all ${editForm.realClick ? 'bg-orange-500/10 border-orange-500/30 text-orange-400' : 'bg-white/5 border-white/5 text-white/40'}`}
>
<div className="flex items-center gap-3">
<ExternalLink size={18} />
<div className="flex flex-col items-start">
<span className="text-xs font-bold uppercase tracking-wider">Trigger Navigation</span>
<span className="text-[8px] opacity-60">Allows URL transitions in Studio</span>
</div>
</div>
{editForm.realClick ? <Check size={18} /> : <div className="w-[18px]" />}
</button>
)}
</div>
</div>

View File

@@ -1,6 +1,7 @@
export interface RecordEvent {
id: string;
type: 'click' | 'scroll' | 'wait' | 'hover';
type: 'mouse' | 'scroll' | 'wait';
interactionType?: 'click' | 'hover';
selector?: string; // CSS selector
timestamp: number; // Time in ms since start of recording
duration: number; // Duration allocated for this action in playback
@@ -9,6 +10,7 @@ export interface RecordEvent {
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';
realClick?: boolean; // Trigger real browser action (navigation)
}
export interface RecordingSession {