feat(record-mode): unify mouse tool and enhance visuals
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Failing after 1m6s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Failing after 1m6s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
This commit is contained in:
@@ -1,55 +1,90 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { useRecordMode } from './RecordModeContext';
|
import { useRecordMode } from './RecordModeContext';
|
||||||
|
|
||||||
export function PlaybackCursor() {
|
export function PlaybackCursor() {
|
||||||
const { isPlaying, cursorPosition } = useRecordMode();
|
const { isPlaying, cursorPosition, isClicking } = useRecordMode();
|
||||||
const [scrollOffset, setScrollOffset] = useState({ x: 0, y: 0 });
|
const [scrollOffset, setScrollOffset] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
// Track scroll so cursor stays locked to the correct element
|
// Track scroll so cursor stays locked to the correct element
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isPlaying) return;
|
if (!isPlaying) return;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
setScrollOffset({ x: window.scrollX, y: window.scrollY });
|
setScrollOffset({ x: window.scrollX, y: window.scrollY });
|
||||||
};
|
};
|
||||||
|
|
||||||
handleScroll(); // Init
|
handleScroll(); // Init
|
||||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
}, [isPlaying]);
|
}, [isPlaying]);
|
||||||
|
|
||||||
if (!isPlaying) return null;
|
if (!isPlaying) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="fixed z-[10000] pointer-events-none"
|
className="fixed z-[10000] pointer-events-none"
|
||||||
animate={{
|
animate={{
|
||||||
x: cursorPosition.x,
|
x: cursorPosition.x,
|
||||||
y: cursorPosition.y,
|
y: cursorPosition.y,
|
||||||
}}
|
scale: isClicking ? 0.8 : 1,
|
||||||
transition={{
|
rotateX: isClicking ? 15 : 0,
|
||||||
type: 'spring',
|
rotateY: isClicking ? -15 : 0,
|
||||||
damping: 30,
|
}}
|
||||||
stiffness: 250,
|
transition={{
|
||||||
mass: 0.5,
|
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 */}
|
<path
|
||||||
<div className="absolute -inset-3 rounded-full bg-[#82ed20]/10 animate-ping" />
|
d="M3 3L10.07 19.97L12.58 12.58L19.97 10.07L3 3Z"
|
||||||
|
fill={isClicking ? '#82ed20' : 'white'}
|
||||||
{/* Visual Cursor */}
|
stroke="black"
|
||||||
<div className="relative">
|
strokeWidth="1.5"
|
||||||
{/* Soft Glow */}
|
strokeLinejoin="round"
|
||||||
<div className="absolute -inset-2 bg-[#82ed20]/20 rounded-full blur-md" />
|
className="transition-colors duration-150"
|
||||||
|
/>
|
||||||
{/* Pointer Arrow */}
|
</svg>
|
||||||
<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)]">
|
</div>
|
||||||
<path d="M3 3L10.07 19.97L12.58 12.58L19.97 10.07L3 3Z" fill="white" stroke="black" strokeWidth="1.5" strokeLinejoin="round" />
|
</motion.div>
|
||||||
</svg>
|
);
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ interface RecordModeContextType {
|
|||||||
reorderEvents: (startIndex: number, endIndex: number) => void;
|
reorderEvents: (startIndex: number, endIndex: number) => void;
|
||||||
hoveredEventId: string | null;
|
hoveredEventId: string | null;
|
||||||
setHoveredEventId: (id: string | null) => void;
|
setHoveredEventId: (id: string | null) => void;
|
||||||
|
isClicking: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RecordModeContext = createContext<RecordModeContextType | null>(null);
|
const RecordModeContext = createContext<RecordModeContextType | null>(null);
|
||||||
@@ -32,29 +33,29 @@ const RecordModeContext = createContext<RecordModeContextType | null>(null);
|
|||||||
export function useRecordMode(): RecordModeContextType {
|
export function useRecordMode(): RecordModeContextType {
|
||||||
const context = useContext(RecordModeContext);
|
const context = useContext(RecordModeContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
// Return a fail-safe fallback for SSR/Static generation where provider might be missing
|
|
||||||
return {
|
return {
|
||||||
isActive: false,
|
isActive: false,
|
||||||
setIsActive: () => { },
|
setIsActive: () => {},
|
||||||
events: [],
|
events: [],
|
||||||
addEvent: () => { },
|
addEvent: () => {},
|
||||||
updateEvent: () => { },
|
updateEvent: () => {},
|
||||||
removeEvent: () => { },
|
removeEvent: () => {},
|
||||||
clearEvents: () => { },
|
clearEvents: () => {},
|
||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
playEvents: () => { },
|
playEvents: () => {},
|
||||||
stopPlayback: () => { },
|
stopPlayback: () => {},
|
||||||
cursorPosition: { x: 0, y: 0 },
|
cursorPosition: { x: 0, y: 0 },
|
||||||
zoomLevel: 1,
|
zoomLevel: 1,
|
||||||
isBlurry: false,
|
isBlurry: false,
|
||||||
currentSession: null,
|
currentSession: null,
|
||||||
isFeedbackActive: false,
|
isFeedbackActive: false,
|
||||||
setIsFeedbackActive: () => { },
|
setIsFeedbackActive: () => {},
|
||||||
saveSession: () => { },
|
saveSession: () => {},
|
||||||
reorderEvents: () => { },
|
reorderEvents: () => {},
|
||||||
hoveredEventId: null,
|
hoveredEventId: null,
|
||||||
setHoveredEventId: () => { },
|
setHoveredEventId: () => {},
|
||||||
setEvents: () => { },
|
setEvents: () => {},
|
||||||
|
isClicking: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
@@ -69,17 +70,18 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
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 [hoveredEventId, setHoveredEventId] = useState<string | null>(null);
|
||||||
|
const [isClicking, setIsClicking] = useState(false);
|
||||||
const [isEmbedded, setIsEmbedded] = useState(false);
|
const [isEmbedded, setIsEmbedded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const embedded = typeof window !== 'undefined' &&
|
const embedded =
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
(window.location.search.includes('embedded=true') ||
|
(window.location.search.includes('embedded=true') ||
|
||||||
window.name === 'record-mode-iframe' ||
|
window.name === 'record-mode-iframe' ||
|
||||||
window.self !== window.top);
|
window.self !== window.top);
|
||||||
setIsEmbedded(embedded);
|
setIsEmbedded(embedded);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Synchronous mutually exclusive setters
|
|
||||||
const setIsActive = (active: boolean) => {
|
const setIsActive = (active: boolean) => {
|
||||||
setIsActiveState(active);
|
setIsActiveState(active);
|
||||||
if (active) setIsFeedbackActiveState(false);
|
if (active) setIsFeedbackActiveState(false);
|
||||||
@@ -90,52 +92,39 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
if (active) setIsActiveState(false);
|
if (active) setIsActiveState(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const playbackRequestRef = useRef<number>(0);
|
|
||||||
const isPlayingRef = useRef(false);
|
const isPlayingRef = useRef(false);
|
||||||
|
const isLoadedRef = useRef(false);
|
||||||
|
|
||||||
// Load draft from localStorage on mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedEvents = localStorage.getItem('klz-record-events');
|
const savedEvents = localStorage.getItem('klz-record-events');
|
||||||
const savedActive = localStorage.getItem('klz-record-active');
|
const savedActive = localStorage.getItem('klz-record-active');
|
||||||
if (savedEvents) setEvents(JSON.parse(savedEvents));
|
if (savedEvents) setEvents(JSON.parse(savedEvents));
|
||||||
if (savedActive) setIsActive(JSON.parse(savedActive));
|
if (savedActive) setIsActive(JSON.parse(savedActive));
|
||||||
|
isLoadedRef.current = true;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Sync events to localStorage
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isLoadedRef.current) return;
|
||||||
localStorage.setItem('klz-record-events', JSON.stringify(events));
|
localStorage.setItem('klz-record-events', JSON.stringify(events));
|
||||||
}, [events]);
|
}, [events]);
|
||||||
|
|
||||||
// Sync active state to localStorage
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('klz-record-active', JSON.stringify(isActive));
|
localStorage.setItem('klz-record-active', JSON.stringify(isActive));
|
||||||
if (isActive && isFeedbackActive) {
|
}, [isActive]);
|
||||||
setIsFeedbackActive(false);
|
|
||||||
}
|
|
||||||
}, [isActive, isFeedbackActive]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFeedbackActive && isActive) {
|
if (isEmbedded) {
|
||||||
setIsActive(false);
|
const handlePlaybackMessage = (e: MessageEvent) => {
|
||||||
}
|
if (e.data.type === 'PLAY_EVENT') {
|
||||||
}, [isFeedbackActive, isActive]);
|
const { event } = e.data;
|
||||||
|
const el = event.selector
|
||||||
// GUEST LISTENERS: Execute events coming from host
|
? (document.querySelector(event.selector) as HTMLElement)
|
||||||
useEffect(() => {
|
: null;
|
||||||
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 (el) {
|
if (el) {
|
||||||
if (event.type === 'scroll') {
|
if (event.type === 'scroll') {
|
||||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
} else if (event.type === 'mouse') {
|
} else if (event.type === 'mouse') {
|
||||||
const currentRect = el.getBoundingClientRect();
|
const currentRect = el.getBoundingClientRect();
|
||||||
|
|
||||||
// Calculate Point based on Origin
|
|
||||||
let targetX = currentRect.left + currentRect.width / 2;
|
let targetX = currentRect.left + currentRect.width / 2;
|
||||||
let targetY = currentRect.top + currentRect.height / 2;
|
let targetY = currentRect.top + currentRect.height / 2;
|
||||||
|
|
||||||
@@ -153,30 +142,30 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
targetY = currentRect.bottom - 5;
|
targetY = currentRect.bottom - 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventCoords = {
|
const eventCoords = { clientX: targetX, clientY: targetY };
|
||||||
clientX: targetX,
|
|
||||||
clientY: targetY
|
|
||||||
};
|
|
||||||
|
|
||||||
const dispatchMouse = (type: string) => {
|
const dispatchMouse = (type: string) => {
|
||||||
el.dispatchEvent(new MouseEvent(type, {
|
el.dispatchEvent(
|
||||||
view: window,
|
new MouseEvent(type, {
|
||||||
bubbles: true,
|
view: window,
|
||||||
cancelable: true,
|
bubbles: true,
|
||||||
...eventCoords
|
cancelable: true,
|
||||||
}));
|
...eventCoords,
|
||||||
|
}),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (event.interactionType === 'click') {
|
if (event.interactionType === 'click') {
|
||||||
|
setIsClicking(true);
|
||||||
dispatchMouse('mousedown');
|
dispatchMouse('mousedown');
|
||||||
dispatchMouse('mouseup');
|
setTimeout(() => {
|
||||||
dispatchMouse('click');
|
dispatchMouse('mouseup');
|
||||||
|
if (event.realClick) {
|
||||||
if (event.realClick) {
|
dispatchMouse('click');
|
||||||
el.click();
|
el.click();
|
||||||
}
|
}
|
||||||
|
setIsClicking(false);
|
||||||
|
}, 150);
|
||||||
} else {
|
} else {
|
||||||
// Hover Interaction
|
|
||||||
dispatchMouse('mousemove');
|
dispatchMouse('mousemove');
|
||||||
dispatchMouse('mouseover');
|
dispatchMouse('mouseover');
|
||||||
dispatchMouse('mouseenter');
|
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]);
|
}, [isEmbedded]);
|
||||||
|
|
||||||
// Sync Hover Preview to Iframe
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEmbedded || !isActive) return;
|
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;
|
const iframe = document.querySelector('iframe[name="record-mode-iframe"]') as HTMLIFrameElement;
|
||||||
if (iframe?.contentWindow) {
|
if (iframe?.contentWindow) {
|
||||||
iframe.contentWindow.postMessage({
|
iframe.contentWindow.postMessage(
|
||||||
type: 'SET_HOVER_SELECTOR',
|
{ type: 'SET_HOVER_SELECTOR', selector: event?.selector || null },
|
||||||
selector: event?.selector || null
|
'*',
|
||||||
}, '*');
|
);
|
||||||
}
|
}
|
||||||
}, [hoveredEventId, events, isActive, isEmbedded]);
|
}, [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 = {
|
||||||
|
realClick: false,
|
||||||
...event,
|
...event,
|
||||||
id: Math.random().toString(36).substr(2, 9),
|
id: Math.random().toString(36).substr(2, 9),
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@@ -223,74 +203,72 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
|
|
||||||
const updateEvent = (id: string, updatedFields: Partial<RecordEvent>) => {
|
const updateEvent = (id: string, updatedFields: Partial<RecordEvent>) => {
|
||||||
setEvents((prev) =>
|
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) => {
|
const removeEvent = (id: string) => {
|
||||||
setEvents((prev) => prev.filter((event) => event.id !== id));
|
setEvents((prev) => prev.filter((event) => event.id !== id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearEvents = () => {
|
const clearEvents = () => {
|
||||||
if (confirm('Clear all recorded events?')) {
|
if (confirm('Clear all recorded events?')) setEvents([]);
|
||||||
setEvents([]);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentSession: RecordingSession | null = events.length > 0 ? {
|
const currentSession: RecordingSession | null =
|
||||||
id: 'draft',
|
events.length > 0
|
||||||
name: 'Draft Session',
|
? {
|
||||||
events,
|
id: 'draft',
|
||||||
createdAt: new Date().toISOString(),
|
name: 'Draft Session',
|
||||||
} : null;
|
events,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
const saveSession = (name: string) => {
|
const saveSession = (name: string) => {
|
||||||
// In a real app, this would be an API call
|
|
||||||
console.log('Saving session:', name, events);
|
console.log('Saving session:', name, events);
|
||||||
};
|
};
|
||||||
|
|
||||||
const playEvents = async () => {
|
const playEvents = async () => {
|
||||||
if (events.length === 0 || isPlayingRef.current) return;
|
if (events.length === 0 || isPlayingRef.current) return;
|
||||||
|
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
isPlayingRef.current = true;
|
isPlayingRef.current = true;
|
||||||
|
|
||||||
// Sort events by timestamp just in case
|
|
||||||
const sortedEvents = [...events].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
const sortedEvents = [...events].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
||||||
|
|
||||||
for (const event of sortedEvents) {
|
for (const event of sortedEvents) {
|
||||||
if (!isPlayingRef.current) break;
|
if (!isPlayingRef.current) break;
|
||||||
|
|
||||||
// 1. Move Cursor (Host logic)
|
|
||||||
if (event.rect && !isEmbedded) {
|
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();
|
const iframeRect = iframe?.getBoundingClientRect();
|
||||||
|
|
||||||
// Add iframe offset to guest-relative coordinates
|
|
||||||
setCursorPosition({
|
setCursorPosition({
|
||||||
x: (iframeRect?.left || 0) + event.rect.x + event.rect.width / 2,
|
x: (iframeRect?.left || 0) + event.rect.x + event.rect.width / 2,
|
||||||
y: (iframeRect?.top || 0) + event.rect.y + event.rect.height / 2,
|
y: (iframeRect?.top || 0) + event.rect.y + event.rect.height / 2,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Handle Action
|
|
||||||
if (event.selector) {
|
if (event.selector) {
|
||||||
if (!isEmbedded) {
|
if (!isEmbedded) {
|
||||||
// HOST: Delegate to Iframe
|
const iframe = document.querySelector(
|
||||||
const iframe = document.querySelector('iframe[name="record-mode-iframe"]') as HTMLIFrameElement;
|
'iframe[name="record-mode-iframe"]',
|
||||||
if (iframe?.contentWindow) {
|
) as HTMLIFrameElement;
|
||||||
|
if (iframe?.contentWindow)
|
||||||
iframe.contentWindow.postMessage({ type: 'PLAY_EVENT', event }, '*');
|
iframe.contentWindow.postMessage({ type: 'PLAY_EVENT', event }, '*');
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// GUEST (Self-execution failsafe)
|
// Self-execution logic for guest
|
||||||
const el = document.querySelector(event.selector) as HTMLElement;
|
const el = document.querySelector(event.selector) as HTMLElement;
|
||||||
if (el) {
|
if (el) {
|
||||||
if (event.type === 'scroll') {
|
if (event.type === 'scroll') el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
else if (event.type === 'mouse') {
|
||||||
} else if (event.type === 'mouse') {
|
|
||||||
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 targetX = currentRect.left + currentRect.width / 2;
|
||||||
let targetY = currentRect.top + currentRect.height / 2;
|
let targetY = currentRect.top + currentRect.height / 2;
|
||||||
|
|
||||||
@@ -308,30 +286,30 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
targetY = currentRect.bottom - 5;
|
targetY = currentRect.bottom - 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventCoords = {
|
const eventCoords = { clientX: targetX, clientY: targetY };
|
||||||
clientX: targetX,
|
|
||||||
clientY: targetY
|
|
||||||
};
|
|
||||||
|
|
||||||
const dispatchMouse = (type: string) => {
|
const dispatchMouse = (type: string) => {
|
||||||
el.dispatchEvent(new MouseEvent(type, {
|
el.dispatchEvent(
|
||||||
view: window,
|
new MouseEvent(type, {
|
||||||
bubbles: true,
|
view: window,
|
||||||
cancelable: true,
|
bubbles: true,
|
||||||
...eventCoords
|
cancelable: true,
|
||||||
}));
|
...eventCoords,
|
||||||
|
}),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (event.interactionType === 'click') {
|
if (event.interactionType === 'click') {
|
||||||
|
setIsClicking(true);
|
||||||
dispatchMouse('mousedown');
|
dispatchMouse('mousedown');
|
||||||
dispatchMouse('mouseup');
|
setTimeout(() => {
|
||||||
dispatchMouse('click');
|
dispatchMouse('mouseup');
|
||||||
|
if (event.realClick) {
|
||||||
if (event.realClick) {
|
dispatchMouse('click');
|
||||||
el.click();
|
el.click();
|
||||||
}
|
}
|
||||||
|
setIsClicking(false);
|
||||||
|
}, 150);
|
||||||
} else {
|
} else {
|
||||||
// Hover Interaction
|
|
||||||
dispatchMouse('mousemove');
|
dispatchMouse('mousemove');
|
||||||
dispatchMouse('mouseover');
|
dispatchMouse('mouseover');
|
||||||
dispatchMouse('mouseenter');
|
dispatchMouse('mouseenter');
|
||||||
@@ -341,15 +319,11 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Zoom/Blur
|
|
||||||
if (event.zoom) setZoomLevel(event.zoom);
|
if (event.zoom) setZoomLevel(event.zoom);
|
||||||
if (event.motionBlur) setIsBlurry(true);
|
if (event.motionBlur) setIsBlurry(true);
|
||||||
|
|
||||||
// 4. Wait
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, event.duration || 1000));
|
await new Promise((resolve) => setTimeout(resolve, event.duration || 1000));
|
||||||
setIsBlurry(false);
|
setIsBlurry(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
isPlayingRef.current = false;
|
isPlayingRef.current = false;
|
||||||
setZoomLevel(1);
|
setZoomLevel(1);
|
||||||
@@ -386,6 +360,7 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
reorderEvents,
|
reorderEvents,
|
||||||
hoveredEventId,
|
hoveredEventId,
|
||||||
setHoveredEventId,
|
setHoveredEventId,
|
||||||
|
isClicking,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -70,8 +70,9 @@ export function RecordModeOverlay() {
|
|||||||
selector,
|
selector,
|
||||||
duration: lastInteractionType === 'click' ? 1000 : 1500,
|
duration: lastInteractionType === 'click' ? 1000 : 1500,
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
description: `${lastInteractionType === 'click' ? 'Click' : 'Hover'} on ${tagName}`,
|
description: `Mouse ${lastInteractionType === 'click' ? '(Click)' : '(Hover)'} on ${tagName}`,
|
||||||
motionBlur: false,
|
motionBlur: false,
|
||||||
|
realClick: false,
|
||||||
rect,
|
rect,
|
||||||
});
|
});
|
||||||
} else if (pickingMode === 'scroll') {
|
} else if (pickingMode === 'scroll') {
|
||||||
@@ -125,7 +126,12 @@ export function RecordModeOverlay() {
|
|||||||
|
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
// Failsafe: Never render host toggle in embedded mode
|
// 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,13 +150,16 @@ export function RecordModeOverlay() {
|
|||||||
{/* 1. Global Toolbar - Slim Industrial Bar */}
|
{/* 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="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">
|
<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 */}
|
{/* 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="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="w-2 h-2 rounded-full bg-accent animate-pulse" />
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-[10px] font-bold text-white uppercase tracking-wider leading-none">Event Builder</span>
|
<span className="text-[10px] font-bold text-white uppercase tracking-wider leading-none">
|
||||||
<span className="text-[8px] text-white/30 uppercase tracking-widest mt-1">Manual Mode</span>
|
Event Builder
|
||||||
|
</span>
|
||||||
|
<span className="text-[8px] text-white/30 uppercase tracking-widest mt-1">
|
||||||
|
Manual Mode
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -163,21 +172,10 @@ export function RecordModeOverlay() {
|
|||||||
setPickingMode('mouse');
|
setPickingMode('mouse');
|
||||||
setLastInteractionType('click');
|
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} />
|
<MousePointer2 size={16} />
|
||||||
<span>Click</span>
|
<span>Mouse</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>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -189,13 +187,15 @@ export function RecordModeOverlay() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => addEvent({
|
onClick={() =>
|
||||||
type: 'wait',
|
addEvent({
|
||||||
duration: 2000,
|
type: 'wait',
|
||||||
zoom: 1,
|
duration: 2000,
|
||||||
description: 'Wait for 2s',
|
zoom: 1,
|
||||||
motionBlur: false
|
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"
|
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} />
|
<Plus size={16} />
|
||||||
@@ -258,7 +258,11 @@ export function RecordModeOverlay() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
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 blob = new Blob([data], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
@@ -294,7 +298,9 @@ export function RecordModeOverlay() {
|
|||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-white font-bold text-lg leading-none">Recording Track</h4>
|
<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>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={clearEvents}
|
onClick={clearEvents}
|
||||||
@@ -332,12 +338,18 @@ export function RecordModeOverlay() {
|
|||||||
<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-[10px] font-black uppercase tracking-widest">
|
<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>
|
</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>
|
</div>
|
||||||
<p className="text-[9px] 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'}
|
||||||
@@ -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="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="flex items-center gap-2">
|
||||||
<div className="w-2 h-2 rounded-full bg-primary-dark animate-pulse" />
|
<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>
|
||||||
<div className="w-px h-6 bg-primary-dark/20" />
|
<div className="w-px h-6 bg-primary-dark/20" />
|
||||||
<button
|
<button
|
||||||
@@ -400,8 +414,13 @@ export function RecordModeOverlay() {
|
|||||||
{editingEventId && (
|
{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="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">
|
<div className="flex items-center justify-between mb-8">
|
||||||
<h3 className="text-white font-black uppercase tracking-tighter text-xl">Event Options</h3>
|
<h3 className="text-white font-black uppercase tracking-tighter text-xl">
|
||||||
<button onClick={() => setEditingEventId(null)} className="p-2 text-white/40 hover:text-white transition-colors">
|
Event Options
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setEditingEventId(null)}
|
||||||
|
className="p-2 text-white/40 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -409,31 +428,37 @@ export function RecordModeOverlay() {
|
|||||||
<div className="flex-1 space-y-8 overflow-y-auto pr-2 scrollbar-hide">
|
<div className="flex-1 space-y-8 overflow-y-auto pr-2 scrollbar-hide">
|
||||||
{/* Type Display */}
|
{/* Type Display */}
|
||||||
<div className="space-y-3">
|
<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">
|
<div className="flex gap-2 p-1 bg-white/5 rounded-2xl border border-white/5">
|
||||||
<button
|
<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'}`}
|
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} />
|
<MousePointer2 size={14} />
|
||||||
<span className="text-[10px] font-black uppercase">Click</span>
|
<span className="text-[10px] font-black uppercase">Click</span>
|
||||||
</button>
|
</button>
|
||||||
<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'}`}
|
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} />
|
<Eye size={14} />
|
||||||
<span className="text-[10px] font-black uppercase">Hover</span>
|
<span className="text-[10px] font-black uppercase">Hover</span>
|
||||||
</button>
|
</button>
|
||||||
<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'}`}
|
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} />
|
<Scroll size={14} />
|
||||||
<span className="text-[10px] font-black uppercase">Scroll</span>
|
<span className="text-[10px] font-black uppercase">Scroll</span>
|
||||||
</button>
|
</button>
|
||||||
<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'}`}
|
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} />
|
<Clock size={14} />
|
||||||
@@ -445,15 +470,22 @@ export function RecordModeOverlay() {
|
|||||||
{/* Precise Click Origin */}
|
{/* Precise Click Origin */}
|
||||||
{editForm.type === 'mouse' && editForm.interactionType === 'click' && (
|
{editForm.type === 'mouse' && editForm.interactionType === 'click' && (
|
||||||
<div className="space-y-4">
|
<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">
|
<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: 'top-left', label: 'TL' },
|
||||||
{ id: 'bottom-left', label: 'BL' }, { id: 'bottom-right', label: 'BR' }
|
{ id: 'top-right', label: 'TR' },
|
||||||
|
{ id: 'center', label: 'CTR' },
|
||||||
|
{ id: 'bottom-left', label: 'BL' },
|
||||||
|
{ id: 'bottom-right', label: 'BR' },
|
||||||
].map((origin) => (
|
].map((origin) => (
|
||||||
<button
|
<button
|
||||||
key={origin.id}
|
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'}`}
|
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}
|
{origin.label}
|
||||||
@@ -475,7 +507,9 @@ export function RecordModeOverlay() {
|
|||||||
max="5000"
|
max="5000"
|
||||||
step="100"
|
step="100"
|
||||||
value={editForm.duration || 1000}
|
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"
|
className="w-full h-1 bg-white/10 rounded-lg appearance-none cursor-pointer accent-accent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 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">
|
<div className="flex items-center gap-3">
|
||||||
<Maximize2 size={18} className="text-white/40" />
|
<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>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -493,13 +529,15 @@ export function RecordModeOverlay() {
|
|||||||
min="1"
|
min="1"
|
||||||
max="3"
|
max="3"
|
||||||
value={editForm.zoom || 1}
|
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"
|
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>
|
</div>
|
||||||
|
|
||||||
<button
|
<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'}`}
|
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">
|
<div className="flex items-center gap-3">
|
||||||
@@ -511,14 +549,18 @@ export function RecordModeOverlay() {
|
|||||||
|
|
||||||
{editForm.type === 'mouse' && editForm.interactionType === 'click' && (
|
{editForm.type === 'mouse' && editForm.interactionType === 'click' && (
|
||||||
<button
|
<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'}`}
|
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">
|
<div className="flex items-center gap-3">
|
||||||
<ExternalLink size={18} />
|
<ExternalLink size={18} />
|
||||||
<div className="flex flex-col items-start">
|
<div className="flex flex-col items-start">
|
||||||
<span className="text-xs font-bold uppercase tracking-wider">Trigger Navigation</span>
|
<span className="text-xs font-bold uppercase tracking-wider">
|
||||||
<span className="text-[8px] opacity-60">Allows URL transitions in Studio</span>
|
Trigger Navigation
|
||||||
|
</span>
|
||||||
|
<span className="text-[8px] opacity-60">
|
||||||
|
Allows URL transitions in Studio
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{editForm.realClick ? <Check size={18} /> : <div className="w-[18px]" />}
|
{editForm.realClick ? <Check size={18} /> : <div className="w-[18px]" />}
|
||||||
|
|||||||
Reference in New Issue
Block a user