Skip to content

Commit 55c2967

Browse files
committed
feat: show mouse cursor and click ripples in recorded demos
Injects a lightweight CSS+JS overlay into every recorded page via context.addInitScript(), rendering a visible cursor dot that tracks mousemove and a ripple animation on each click. Enabled by default via a new `showMouseClicks` config option (set to false to opt out). https://claude.ai/code/session_01VLEqaM8NsNZ5Zh3ya7QSme
1 parent 5ac3c4d commit 55c2967

3 files changed

Lines changed: 55 additions & 1 deletion

File tree

packages/core/src/config/defaults.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const DEFAULT_RECORDING: RecordingConfig = {
55
format: 'gif',
66
maxDuration: 30,
77
deviceScaleFactor: 2,
8+
showMouseClicks: true,
89
};
910

1011
export const DEFAULT_LLM: LLMConfig = {

packages/core/src/config/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const RecordingConfigSchema = z.object({
2323
format: z.enum(['gif', 'mp4', 'webm']).default('gif'),
2424
maxDuration: z.number().default(30),
2525
deviceScaleFactor: z.number().default(2),
26+
showMouseClicks: z.boolean().default(true),
2627
});
2728

2829
export const LLMConfigSchema = z.object({

packages/core/src/recorder/playwright-runner.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,66 @@ async function createContext(
5656
recording: RecordingConfig,
5757
outputDir: string
5858
): Promise<BrowserContext> {
59-
return browser.newContext({
59+
const context = await browser.newContext({
6060
recordVideo: {
6161
dir: outputDir,
6262
size: recording.viewport,
6363
},
6464
viewport: recording.viewport,
6565
deviceScaleFactor: recording.deviceScaleFactor,
6666
});
67+
68+
if (recording.showMouseClicks !== false) {
69+
await context.addInitScript(buildMouseClickOverlayScript());
70+
}
71+
72+
return context;
73+
}
74+
75+
function buildMouseClickOverlayScript(): string {
76+
return `(() => {
77+
const style = document.createElement('style');
78+
style.textContent = \`
79+
.gg-cursor {
80+
width: 16px; height: 16px; border-radius: 50%;
81+
background: rgba(255, 80, 0, 0.85);
82+
border: 2px solid white;
83+
position: fixed; pointer-events: none; z-index: 999999;
84+
transform: translate(-50%, -50%);
85+
transition: left 30ms linear, top 30ms linear;
86+
box-shadow: 0 0 4px rgba(0,0,0,0.4);
87+
}
88+
@keyframes gg-ripple {
89+
from { transform: translate(-50%, -50%) scale(1); opacity: 0.8; }
90+
to { transform: translate(-50%, -50%) scale(3.5); opacity: 0; }
91+
}
92+
.gg-ripple {
93+
width: 24px; height: 24px; border-radius: 50%;
94+
border: 3px solid rgba(255, 80, 0, 0.9);
95+
position: fixed; pointer-events: none; z-index: 999998;
96+
animation: gg-ripple 500ms ease-out forwards;
97+
}
98+
\`;
99+
document.head.appendChild(style);
100+
101+
const cursor = document.createElement('div');
102+
cursor.className = 'gg-cursor';
103+
document.body.appendChild(cursor);
104+
105+
document.addEventListener('mousemove', (e) => {
106+
cursor.style.left = e.clientX + 'px';
107+
cursor.style.top = e.clientY + 'px';
108+
});
109+
110+
document.addEventListener('click', (e) => {
111+
const ripple = document.createElement('div');
112+
ripple.className = 'gg-ripple';
113+
ripple.style.left = e.clientX + 'px';
114+
ripple.style.top = e.clientY + 'px';
115+
document.body.appendChild(ripple);
116+
setTimeout(() => ripple.remove(), 500);
117+
});
118+
})();`;
67119
}
68120

69121
async function executeScript(script: string, page: Page, _baseUrl: string): Promise<void> {

0 commit comments

Comments
 (0)