feat(record-mode): unify mouse tool and enhance visuals

This commit is contained in:
2026-02-15 18:25:52 +01:00
parent b32db4b277
commit 9274807427
3 changed files with 273 additions and 221 deletions

View File

@@ -1,55 +1,90 @@
'use client';
import React, { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { motion, AnimatePresence } from 'framer-motion';
import { useRecordMode } from './RecordModeContext';
export function PlaybackCursor() {
const { isPlaying, cursorPosition } = useRecordMode();
const [scrollOffset, setScrollOffset] = useState({ x: 0, y: 0 });
const { isPlaying, cursorPosition, isClicking } = useRecordMode();
const [scrollOffset, setScrollOffset] = useState({ x: 0, y: 0 });
// Track scroll so cursor stays locked to the correct element
useEffect(() => {
if (!isPlaying) return;
// Track scroll so cursor stays locked to the correct element
useEffect(() => {
if (!isPlaying) return;
const handleScroll = () => {
setScrollOffset({ x: window.scrollX, y: window.scrollY });
};
const handleScroll = () => {
setScrollOffset({ x: window.scrollX, y: window.scrollY });
};
handleScroll(); // Init
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, [isPlaying]);
handleScroll(); // Init
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, [isPlaying]);
if (!isPlaying) return null;
if (!isPlaying) return null;
return (
<motion.div
className="fixed z-[10000] pointer-events-none"
animate={{
x: cursorPosition.x,
y: cursorPosition.y,
}}
transition={{
type: 'spring',
damping: 30,
stiffness: 250,
mass: 0.5,
}}
return (
<motion.div
className="fixed z-[10000] pointer-events-none"
animate={{
x: cursorPosition.x,
y: cursorPosition.y,
scale: isClicking ? 0.8 : 1,
rotateX: isClicking ? 15 : 0,
rotateY: isClicking ? -15 : 0,
}}
transition={{
x: { type: 'spring', damping: 30, stiffness: 250, mass: 0.5 },
y: { type: 'spring', damping: 30, stiffness: 250, mass: 0.5 },
scale: { type: 'spring', damping: 15, stiffness: 400 },
rotateX: { type: 'spring', damping: 15, stiffness: 400 },
rotateY: { type: 'spring', damping: 15, stiffness: 400 },
}}
style={{ perspective: '1000px' }}
>
<AnimatePresence>
{isClicking && (
<motion.div
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 2.5, opacity: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.4, ease: 'easeOut' }}
className="absolute inset-0 rounded-full border-2 border-[#82ed20] blur-[1px]"
/>
)}
</AnimatePresence>
{/* Outer Pulse Ring */}
<div
className={`absolute -inset-3 rounded-full bg-[#82ed20]/10 ${isClicking ? 'scale-150 opacity-0' : 'animate-ping'} transition-all duration-300`}
/>
{/* Visual Cursor */}
<div className="relative">
{/* Soft Glow */}
<div
className={`absolute -inset-2 bg-[#82ed20]/20 rounded-full blur-md transition-all ${isClicking ? 'bg-[#82ed20]/50 blur-xl' : ''}`}
/>
{/* Pointer Arrow */}
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={`drop-shadow-[0_2px_8px_rgba(130,237,32,0.5)] transition-transform ${isClicking ? 'translate-y-0.5' : ''}`}
>
{/* Outer Pulse Ring */}
<div className="absolute -inset-3 rounded-full bg-[#82ed20]/10 animate-ping" />
{/* Visual Cursor */}
<div className="relative">
{/* Soft Glow */}
<div className="absolute -inset-2 bg-[#82ed20]/20 rounded-full blur-md" />
{/* Pointer Arrow */}
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="drop-shadow-[0_2px_8px_rgba(130,237,32,0.5)]">
<path d="M3 3L10.07 19.97L12.58 12.58L19.97 10.07L3 3Z" fill="white" stroke="black" strokeWidth="1.5" strokeLinejoin="round" />
</svg>
</div>
</motion.div>
);
<path
d="M3 3L10.07 19.97L12.58 12.58L19.97 10.07L3 3Z"
fill={isClicking ? '#82ed20' : 'white'}
stroke="black"
strokeWidth="1.5"
strokeLinejoin="round"
className="transition-colors duration-150"
/>
</svg>
</div>
</motion.div>
);
}

View File

@@ -25,6 +25,7 @@ interface RecordModeContextType {
reorderEvents: (startIndex: number, endIndex: number) => void;
hoveredEventId: string | null;
setHoveredEventId: (id: string | null) => void;
isClicking: boolean;
}
const RecordModeContext = createContext<RecordModeContextType | null>(null);
@@ -32,29 +33,29 @@ const RecordModeContext = createContext<RecordModeContextType | null>(null);
export function useRecordMode(): RecordModeContextType {
const context = useContext(RecordModeContext);
if (!context) {
// Return a fail-safe fallback for SSR/Static generation where provider might be missing
return {
isActive: false,
setIsActive: () => { },
setIsActive: () => {},
events: [],
addEvent: () => { },
updateEvent: () => { },
removeEvent: () => { },
clearEvents: () => { },
addEvent: () => {},
updateEvent: () => {},
removeEvent: () => {},
clearEvents: () => {},
isPlaying: false,
playEvents: () => { },
stopPlayback: () => { },
playEvents: () => {},
stopPlayback: () => {},
cursorPosition: { x: 0, y: 0 },
zoomLevel: 1,
isBlurry: false,
currentSession: null,
isFeedbackActive: false,
setIsFeedbackActive: () => { },
saveSession: () => { },
reorderEvents: () => { },
setIsFeedbackActive: () => {},
saveSession: () => {},
reorderEvents: () => {},
hoveredEventId: null,
setHoveredEventId: () => { },
setEvents: () => { },
setHoveredEventId: () => {},
setEvents: () => {},
isClicking: false,
};
}
return context;
@@ -69,17 +70,18 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
const [isBlurry, setIsBlurry] = useState(false);
const [isFeedbackActive, setIsFeedbackActiveState] = useState(false);
const [hoveredEventId, setHoveredEventId] = useState<string | null>(null);
const [isClicking, setIsClicking] = useState(false);
const [isEmbedded, setIsEmbedded] = useState(false);
useEffect(() => {
const embedded = typeof window !== 'undefined' &&
const embedded =
typeof window !== 'undefined' &&
(window.location.search.includes('embedded=true') ||
window.name === 'record-mode-iframe' ||
window.self !== window.top);
setIsEmbedded(embedded);
}, []);
// Synchronous mutually exclusive setters
const setIsActive = (active: boolean) => {
setIsActiveState(active);
if (active) setIsFeedbackActiveState(false);
@@ -90,52 +92,39 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
if (active) setIsActiveState(false);
};
const playbackRequestRef = useRef<number>(0);
const isPlayingRef = useRef(false);
const isLoadedRef = useRef(false);
// Load draft from localStorage on mount
useEffect(() => {
const savedEvents = localStorage.getItem('klz-record-events');
const savedActive = localStorage.getItem('klz-record-active');
if (savedEvents) setEvents(JSON.parse(savedEvents));
if (savedActive) setIsActive(JSON.parse(savedActive));
isLoadedRef.current = true;
}, []);
// Sync events to localStorage
useEffect(() => {
if (!isLoadedRef.current) return;
localStorage.setItem('klz-record-events', JSON.stringify(events));
}, [events]);
// Sync active state to localStorage
useEffect(() => {
localStorage.setItem('klz-record-active', JSON.stringify(isActive));
if (isActive && isFeedbackActive) {
setIsFeedbackActive(false);
}
}, [isActive, isFeedbackActive]);
}, [isActive]);
useEffect(() => {
if (isFeedbackActive && isActive) {
setIsActive(false);
}
}, [isFeedbackActive, isActive]);
// GUEST LISTENERS: Execute events coming from host
useEffect(() => {
if (!isEmbedded) return;
const handlePlaybackMessage = (e: MessageEvent) => {
if (e.data.type === 'PLAY_EVENT') {
const { event } = e.data;
if (event.selector) {
const el = document.querySelector(event.selector) as HTMLElement;
if (isEmbedded) {
const handlePlaybackMessage = (e: MessageEvent) => {
if (e.data.type === 'PLAY_EVENT') {
const { event } = e.data;
const el = event.selector
? (document.querySelector(event.selector) as HTMLElement)
: null;
if (el) {
if (event.type === 'scroll') {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else if (event.type === 'mouse') {
const currentRect = el.getBoundingClientRect();
// Calculate Point based on Origin
let targetX = currentRect.left + currentRect.width / 2;
let targetY = currentRect.top + currentRect.height / 2;
@@ -153,30 +142,30 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
targetY = currentRect.bottom - 5;
}
const eventCoords = {
clientX: targetX,
clientY: targetY
};
const eventCoords = { clientX: targetX, clientY: targetY };
const dispatchMouse = (type: string) => {
el.dispatchEvent(new MouseEvent(type, {
view: window,
bubbles: true,
cancelable: true,
...eventCoords
}));
el.dispatchEvent(
new MouseEvent(type, {
view: window,
bubbles: true,
cancelable: true,
...eventCoords,
}),
);
};
if (event.interactionType === 'click') {
setIsClicking(true);
dispatchMouse('mousedown');
dispatchMouse('mouseup');
dispatchMouse('click');
if (event.realClick) {
el.click();
}
setTimeout(() => {
dispatchMouse('mouseup');
if (event.realClick) {
dispatchMouse('click');
el.click();
}
setIsClicking(false);
}, 150);
} else {
// Hover Interaction
dispatchMouse('mousemove');
dispatchMouse('mouseover');
dispatchMouse('mouseenter');
@@ -184,36 +173,27 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
}
}
}
}
};
window.addEventListener('message', handlePlaybackMessage);
return () => window.removeEventListener('message', handlePlaybackMessage);
};
window.addEventListener('message', handlePlaybackMessage);
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 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
}, '*');
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 newEvent: RecordEvent = {
realClick: false,
...event,
id: Math.random().toString(36).substr(2, 9),
timestamp: Date.now(),
@@ -223,74 +203,72 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
const updateEvent = (id: string, updatedFields: Partial<RecordEvent>) => {
setEvents((prev) =>
prev.map((event) => (event.id === id ? { ...event, ...updatedFields } : event))
prev.map((event) => (event.id === id ? { ...event, ...updatedFields } : event)),
);
};
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 removeEvent = (id: string) => {
setEvents((prev) => prev.filter((event) => event.id !== id));
};
const clearEvents = () => {
if (confirm('Clear all recorded events?')) {
setEvents([]);
}
if (confirm('Clear all recorded events?')) setEvents([]);
};
const currentSession: RecordingSession | null = events.length > 0 ? {
id: 'draft',
name: 'Draft Session',
events,
createdAt: new Date().toISOString(),
} : null;
const currentSession: RecordingSession | null =
events.length > 0
? {
id: 'draft',
name: 'Draft Session',
events,
createdAt: new Date().toISOString(),
}
: null;
const saveSession = (name: string) => {
// In a real app, this would be an API call
console.log('Saving session:', name, events);
};
const playEvents = async () => {
if (events.length === 0 || isPlayingRef.current) return;
setIsPlaying(true);
isPlayingRef.current = true;
// Sort events by timestamp just in case
const sortedEvents = [...events].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
for (const event of sortedEvents) {
if (!isPlayingRef.current) break;
// 1. Move Cursor (Host logic)
if (event.rect && !isEmbedded) {
const iframe = document.querySelector('iframe[name="record-mode-iframe"]') as HTMLIFrameElement;
const iframe = document.querySelector(
'iframe[name="record-mode-iframe"]',
) as HTMLIFrameElement;
const iframeRect = iframe?.getBoundingClientRect();
// Add iframe offset to guest-relative coordinates
setCursorPosition({
x: (iframeRect?.left || 0) + event.rect.x + event.rect.width / 2,
y: (iframeRect?.top || 0) + event.rect.y + event.rect.height / 2,
});
}
// 2. Handle Action
if (event.selector) {
if (!isEmbedded) {
// HOST: Delegate to Iframe
const iframe = document.querySelector('iframe[name="record-mode-iframe"]') as HTMLIFrameElement;
if (iframe?.contentWindow) {
const iframe = document.querySelector(
'iframe[name="record-mode-iframe"]',
) as HTMLIFrameElement;
if (iframe?.contentWindow)
iframe.contentWindow.postMessage({ type: 'PLAY_EVENT', event }, '*');
}
} else {
// GUEST (Self-execution failsafe)
// Self-execution logic for guest
const el = document.querySelector(event.selector) as HTMLElement;
if (el) {
if (event.type === 'scroll') {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else if (event.type === 'mouse') {
if (event.type === 'scroll') el.scrollIntoView({ behavior: 'smooth', block: 'center' });
else if (event.type === 'mouse') {
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;
@@ -308,30 +286,30 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
targetY = currentRect.bottom - 5;
}
const eventCoords = {
clientX: targetX,
clientY: targetY
};
const eventCoords = { clientX: targetX, clientY: targetY };
const dispatchMouse = (type: string) => {
el.dispatchEvent(new MouseEvent(type, {
view: window,
bubbles: true,
cancelable: true,
...eventCoords
}));
el.dispatchEvent(
new MouseEvent(type, {
view: window,
bubbles: true,
cancelable: true,
...eventCoords,
}),
);
};
if (event.interactionType === 'click') {
setIsClicking(true);
dispatchMouse('mousedown');
dispatchMouse('mouseup');
dispatchMouse('click');
if (event.realClick) {
el.click();
}
setTimeout(() => {
dispatchMouse('mouseup');
if (event.realClick) {
dispatchMouse('click');
el.click();
}
setIsClicking(false);
}, 150);
} else {
// Hover Interaction
dispatchMouse('mousemove');
dispatchMouse('mouseover');
dispatchMouse('mouseenter');
@@ -341,15 +319,11 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
}
}
// 3. Zoom/Blur
if (event.zoom) setZoomLevel(event.zoom);
if (event.motionBlur) setIsBlurry(true);
// 4. Wait
await new Promise((resolve) => setTimeout(resolve, event.duration || 1000));
setIsBlurry(false);
}
setIsPlaying(false);
isPlayingRef.current = false;
setZoomLevel(1);
@@ -386,6 +360,7 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
reorderEvents,
hoveredEventId,
setHoveredEventId,
isClicking,
}}
>
{children}

View File

@@ -70,8 +70,9 @@ export function RecordModeOverlay() {
selector,
duration: lastInteractionType === 'click' ? 1000 : 1500,
zoom: 1,
description: `${lastInteractionType === 'click' ? 'Click' : 'Hover'} on ${tagName}`,
description: `Mouse ${lastInteractionType === 'click' ? '(Click)' : '(Hover)'} on ${tagName}`,
motionBlur: false,
realClick: false,
rect,
});
} else if (pickingMode === 'scroll') {
@@ -125,7 +126,12 @@ export function RecordModeOverlay() {
if (!isActive) {
// Failsafe: Never render host toggle in embedded mode
if (typeof window !== 'undefined' && (window.self !== window.top || window.name === 'record-mode-iframe' || window.location.search.includes('embedded=true'))) {
if (
typeof window !== 'undefined' &&
(window.self !== window.top ||
window.name === 'record-mode-iframe' ||
window.location.search.includes('embedded=true'))
) {
return null;
}
@@ -144,13 +150,16 @@ export function RecordModeOverlay() {
{/* 1. Global Toolbar - Slim Industrial Bar */}
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[10000] pointer-events-auto">
<div className="bg-black/80 backdrop-blur-2xl border border-white/10 p-2 rounded-[24px] shadow-[0_32px_80px_rgba(0,0,0,0.8)] flex items-center gap-2">
{/* Identity Tag */}
<div className="flex items-center gap-3 px-4 py-2 bg-white/5 rounded-[16px] border border-white/5 mx-1">
<div className="w-2 h-2 rounded-full bg-accent animate-pulse" />
<div className="flex flex-col">
<span className="text-[10px] font-bold text-white uppercase tracking-wider leading-none">Event Builder</span>
<span className="text-[8px] text-white/30 uppercase tracking-widest mt-1">Manual Mode</span>
<span className="text-[10px] font-bold text-white uppercase tracking-wider leading-none">
Event Builder
</span>
<span className="text-[8px] text-white/30 uppercase tracking-widest mt-1">
Manual Mode
</span>
</div>
</div>
@@ -163,21 +172,10 @@ export function RecordModeOverlay() {
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'}`}
className={`flex items-center gap-2 px-4 py-2.5 rounded-[16px] transition-all text-xs font-bold uppercase tracking-wide ${pickingMode === 'mouse' ? '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>
<span>Mouse</span>
</button>
<button
@@ -189,13 +187,15 @@ export function RecordModeOverlay() {
</button>
<button
onClick={() => addEvent({
type: 'wait',
duration: 2000,
zoom: 1,
description: 'Wait for 2s',
motionBlur: false
})}
onClick={() =>
addEvent({
type: 'wait',
duration: 2000,
zoom: 1,
description: 'Wait for 2s',
motionBlur: false,
})
}
className="flex items-center gap-2 px-4 py-2.5 rounded-[16px] text-white/40 hover:text-white hover:bg-white/5 transition-all text-xs font-bold uppercase tracking-wide"
>
<Plus size={16} />
@@ -258,7 +258,11 @@ export function RecordModeOverlay() {
<button
onClick={() => {
const data = JSON.stringify({ events, name: 'Recording', createdAt: new Date().toISOString() }, null, 2);
const data = JSON.stringify(
{ events, name: 'Recording', createdAt: new Date().toISOString() },
null,
2,
);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
@@ -294,7 +298,9 @@ export function RecordModeOverlay() {
<div className="flex items-center justify-between mb-6">
<div>
<h4 className="text-white font-bold text-lg leading-none">Recording Track</h4>
<p className="text-[10px] text-white/30 uppercase tracking-widest mt-2">{events.length} Actions Recorded</p>
<p className="text-[10px] text-white/30 uppercase tracking-widest mt-2">
{events.length} Actions Recorded
</p>
</div>
<button
onClick={clearEvents}
@@ -332,12 +338,18 @@ 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 === 'mouse' ? event.interactionType : event.type}
{event.type === 'mouse' ? `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>
{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>
</div>
<p className="text-[9px] text-white/30 truncate font-mono mt-1 opacity-60">
{event.selector || 'system:wait'}
@@ -377,7 +389,9 @@ export function RecordModeOverlay() {
<div className="bg-accent text-primary-dark px-6 py-3 rounded-full flex items-center gap-4 shadow-[0_20px_40px_rgba(130,237,32,0.4)] animate-reveal border border-primary-dark/10">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-primary-dark animate-pulse" />
<span className="font-black uppercase tracking-widest text-xs">Assigning {pickingMode}</span>
<span className="font-black uppercase tracking-widest text-xs">
Assigning {pickingMode}
</span>
</div>
<div className="w-px h-6 bg-primary-dark/20" />
<button
@@ -400,8 +414,13 @@ export function RecordModeOverlay() {
{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">
<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>
@@ -409,31 +428,37 @@ 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">Interaction Type</label>
<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' }))}
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' }))}
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' }))}
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' }))}
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} />
@@ -445,15 +470,22 @@ export function RecordModeOverlay() {
{/* Precise Click Origin */}
{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>
<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' }
{ 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 }))}
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}
@@ -475,7 +507,9 @@ export function RecordModeOverlay() {
max="5000"
step="100"
value={editForm.duration || 1000}
onChange={(e) => setEditForm(prev => ({ ...prev, duration: parseInt(e.target.value) }))}
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>
@@ -485,7 +519,9 @@ export function RecordModeOverlay() {
<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>
<span className="text-xs font-bold text-white uppercase tracking-wider">
Zoom Shift
</span>
</div>
<input
type="number"
@@ -493,13 +529,15 @@ export function RecordModeOverlay() {
min="1"
max="3"
value={editForm.zoom || 1}
onChange={(e) => setEditForm(prev => ({ ...prev, zoom: parseFloat(e.target.value) }))}
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 }))}
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">
@@ -511,14 +549,18 @@ export function RecordModeOverlay() {
{editForm.type === 'mouse' && editForm.interactionType === 'click' && (
<button
onClick={() => setEditForm(prev => ({ ...prev, realClick: !prev.realClick }))}
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>
<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]" />}