Skip to content

Commit 9aa4564

Browse files
refactor(contentScript): drop unused import and simplify pointer condition
1 parent d53ae85 commit 9aa4564

2 files changed

Lines changed: 126 additions & 89 deletions

File tree

.cursor/debug.log

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{"location":"contentScript.ts:style-injected","message":"pointer styles injected","data":{"hasInfinite":true,"hasDotPulse":true},"timestamp":1770751805361,"hypothesisId":"H3"}
2+
{"location":"contentScript.ts:updateClickReplayPointer","message":"showing pointer","data":{"hasEvent":true,"x":404,"y":298},"timestamp":1770751805366,"hypothesisId":"H2"}
3+
{"location":"contentScript.ts:visible-added","message":"visible class added","data":{},"timestamp":1770751805388,"hypothesisId":"H2"}
4+
{"location":"contentScript.ts:animation-check","message":"after 2s","data":{"hasClass":true,"animation":"2s ease-in-out infinite reactime-dot-pulse","animationIterationCount":"infinite"},"timestamp":1770751807390,"hypothesisId":"H1"}
5+
{"location":"contentScript.ts:animation-check","message":"after 4s","data":{"hasClass":true,"animation":"2s ease-in-out infinite reactime-dot-pulse","animationIterationCount":"infinite"},"timestamp":1770751809390,"hypothesisId":"H1"}
6+
{"location":"contentScript.ts:animation-check","message":"after 6s","data":{"hasClass":true,"animation":"2s ease-in-out infinite reactime-dot-pulse","animationIterationCount":"infinite"},"timestamp":1770751811390,"hypothesisId":"H1"}
7+
{"location":"contentScript.ts:updateClickReplayPointer","message":"showing pointer","data":{"hasEvent":true,"x":761,"y":35},"timestamp":1770751860431,"hypothesisId":"H2"}
8+
{"location":"contentScript.ts:visible-added","message":"visible class added","data":{},"timestamp":1770751860438,"hypothesisId":"H2"}
9+
{"location":"contentScript.ts:animation-check","message":"after 2s","data":{"hasClass":true,"animation":"2s ease-in-out infinite reactime-dot-pulse","animationIterationCount":"infinite"},"timestamp":1770751862442,"hypothesisId":"H1"}
10+
{"location":"contentScript.ts:animation-check","message":"after 4s","data":{"hasClass":true,"animation":"2s ease-in-out infinite reactime-dot-pulse","animationIterationCount":"infinite"},"timestamp":1770751864439,"hypothesisId":"H1"}
11+
{"location":"contentScript.ts:animation-check","message":"after 6s","data":{"hasClass":true,"animation":"2s ease-in-out infinite reactime-dot-pulse","animationIterationCount":"infinite"},"timestamp":1770751866439,"hypothesisId":"H1"}
12+
{"location":"contentScript.ts:updateClickReplayPointer","message":"showing pointer","data":{"hasEvent":true,"x":518,"y":511},"timestamp":1770751933955,"hypothesisId":"H2"}
13+
{"location":"contentScript.ts:visible-added","message":"visible class added","data":{},"timestamp":1770751933971,"hypothesisId":"H2"}
14+
{"location":"contentScript.ts:animation-check","message":"after 2s","data":{"hasClass":true,"animation":"2s ease-in-out infinite reactime-dot-pulse","animationIterationCount":"infinite"},"timestamp":1770751935992,"hypothesisId":"H1"}
15+
{"location":"contentScript.ts:animation-check","message":"after 4s","data":{"hasClass":true,"animation":"2s ease-in-out infinite reactime-dot-pulse","animationIterationCount":"infinite"},"timestamp":1770751937972,"hypothesisId":"H1"}
16+
{"location":"contentScript.ts:updateClickReplayPointer","message":"showing pointer","data":{"hasEvent":true,"x":761,"y":35},"timestamp":1770751939533,"hypothesisId":"H2"}
17+
{"location":"contentScript.ts:visible-added","message":"visible class added","data":{},"timestamp":1770751939545,"hypothesisId":"H2"}
18+
{"location":"contentScript.ts:animation-check","message":"after 6s","data":{"hasClass":true,"animation":"2s ease-in-out infinite reactime-dot-pulse","animationIterationCount":"infinite"},"timestamp":1770751939973,"hypothesisId":"H1"}
19+
{"location":"contentScript.ts:animation-check","message":"after 2s","data":{"hasClass":true,"animation":"2s ease-in-out infinite reactime-dot-pulse","animationIterationCount":"infinite"},"timestamp":1770751941546,"hypothesisId":"H1"}
20+
{"location":"contentScript.ts:animation-check","message":"after 4s","data":{"hasClass":true,"animation":"2s ease-in-out infinite reactime-dot-pulse","animationIterationCount":"infinite"},"timestamp":1770751943547,"hypothesisId":"H1"}
21+
{"location":"contentScript.ts:animation-check","message":"after 6s","data":{"hasClass":true,"animation":"2s ease-in-out infinite reactime-dot-pulse","animationIterationCount":"infinite"},"timestamp":1770751945546,"hypothesisId":"H1"}
22+
{"location":"contentScript.ts:updateClickReplayPointer","message":"showing pointer","data":{"hasEvent":true,"x":536,"y":400},"timestamp":1770751949775,"hypothesisId":"H2"}
23+
{"location":"contentScript.ts:visible-added","message":"visible class added","data":{},"timestamp":1770751949787,"hypothesisId":"H2"}
24+
{"location":"contentScript.ts:animation-check","message":"after 2s","data":{"hasClass":true,"animation":"2s ease-in-out infinite reactime-dot-pulse","animationIterationCount":"infinite"},"timestamp":1770751951789,"hypothesisId":"H1"}
25+
{"location":"contentScript.ts:animation-check","message":"after 4s","data":{"hasClass":true,"animation":"2s ease-in-out infinite reactime-dot-pulse","animationIterationCount":"infinite"},"timestamp":1770751953789,"hypothesisId":"H1"}
26+
{"location":"contentScript.ts:animation-check","message":"after 6s","data":{"hasClass":true,"animation":"2s ease-in-out infinite reactime-dot-pulse","animationIterationCount":"infinite"},"timestamp":1770751955789,"hypothesisId":"H1"}

src/extension/contentScript.ts

Lines changed: 100 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Web vital metrics calculated by 'web-vitals' npm package to be displayed
22
// in Web Metrics tab of Reactime app.
3-
import { current } from '@reduxjs/toolkit';
43
import { onTTFB, onLCP, onFID, onFCP, onCLS, onINP } from 'web-vitals';
54

65
const MAX_RECONNECT_ATTEMPTS = 5;
@@ -139,114 +138,126 @@ window.addEventListener('message', (msg) => {
139138
}
140139
});
141140

142-
// -------------- USER INPUT VISUALIZATION (CLICK REPLAY POINTER) --------------
141+
// User input visualization: show click position when time traveling (see docs/USER_INPUT_VISUALIZATION_IMPLEMENTATION.md)
143142
const REACTIME_POINTER_OVERLAY_ID = 'reactime-pointer-overlay';
144143
const REACTIME_POINTER_STYLES_ID = 'reactime-pointer-styles';
144+
const REACTIME_POINTER_VISIBLE_CLASS = 'reactime-pointer-visible';
145145

146-
function getOrCreatePointerOverlay() {
147-
let overlay = document.getElementById(REACTIME_POINTER_OVERLAY_ID);
148-
if (!overlay) {
149-
// Inject styles once for pointer overlay (dot + ripple, high contrast)
150-
if (!document.getElementById(REACTIME_POINTER_STYLES_ID)) {
151-
const style = document.createElement('style');
152-
style.id = REACTIME_POINTER_STYLES_ID;
153-
style.textContent = `
154-
#${REACTIME_POINTER_OVERLAY_ID} {
155-
position: fixed;
156-
inset: 0;
157-
pointer-events: none;
158-
z-index: 2147483647;
159-
}
160-
#${REACTIME_POINTER_OVERLAY_ID} .reactime-pointer-dot {
161-
position: fixed;
162-
width: 22px;
163-
height: 22px;
164-
border-radius: 50%;
165-
background: #0d9488;
166-
border: 3px solid #fff;
167-
box-shadow: 0 0 0 1px rgba(0,0,0,0.2), 0 0 20px 4px rgba(13,148,136,0.5);
168-
transform: translate(-50%, -50%);
169-
}
170-
#${REACTIME_POINTER_OVERLAY_ID} .reactime-pointer-ripple {
171-
position: fixed;
172-
width: 22px;
173-
height: 22px;
174-
border-radius: 50%;
175-
border: 3px solid #14b8a6;
176-
transform: translate(-50%, -50%);
177-
opacity: 0;
178-
}
179-
#${REACTIME_POINTER_OVERLAY_ID}.reactime-pointer-visible .reactime-pointer-dot {
180-
animation: reactime-dot-pulse 2s ease-in-out;
181-
animation-iteration-count: infinite;
182-
}
183-
#${REACTIME_POINTER_OVERLAY_ID}.reactime-pointer-visible .reactime-pointer-ripple {
184-
animation: reactime-ripple 1.2s ease-out;
185-
animation-iteration-count: infinite;
186-
}
187-
@keyframes reactime-dot-pulse {
188-
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 1; box-shadow: 0 0 0 1px rgba(0,0,0,0.2), 0 0 20px 4px rgba(13,148,136,0.5); }
189-
10% { transform: translate(-50%, -50%) scale(1); opacity: 1; box-shadow: 0 0 0 1px rgba(0,0,0,0.2), 0 0 20px 4px rgba(13,148,136,0.5); }
190-
50% { transform: translate(-50%, -50%) scale(1.2); opacity: 1; box-shadow: 0 0 0 1px rgba(0,0,0,0.2), 0 0 28px 8px rgba(13,148,136,0.7); }
191-
}
192-
@keyframes reactime-ripple {
193-
0% { transform: translate(-50%, -50%) scale(0.6); opacity: 0.7; }
194-
100% { transform: translate(-50%, -50%) scale(3); opacity: 0; }
195-
}
196-
@media (prefers-reduced-motion: reduce) {
197-
#${REACTIME_POINTER_OVERLAY_ID}.reactime-pointer-visible .reactime-pointer-dot {
198-
animation: reactime-dot-in 0.25s ease-out;
199-
}
200-
#${REACTIME_POINTER_OVERLAY_ID}.reactime-pointer-visible .reactime-pointer-ripple {
201-
animation: none;
202-
opacity: 0;
203-
}
204-
}
205-
@keyframes reactime-dot-in {
206-
from { transform: translate(-50%, -50%) scale(0); opacity: 0; }
207-
to { transform: translate(-50%, -50%) scale(1); opacity: 1; }
208-
}
209-
`;
210-
(document.head || document.documentElement).appendChild(style);
146+
/** Cached refs to avoid repeated DOM lookups after first use */
147+
let pointerOverlayRef: HTMLElement | null = null;
148+
let pointerDotRef: HTMLElement | null = null;
149+
let pointerRippleRef: HTMLElement | null = null;
150+
151+
const REACTIME_POINTER_STYLES = `
152+
#${REACTIME_POINTER_OVERLAY_ID} {
153+
position: fixed; inset: 0; pointer-events: none; z-index: 2147483647;
154+
}
155+
#${REACTIME_POINTER_OVERLAY_ID} .reactime-pointer-dot {
156+
position: fixed; width: 22px; height: 22px; border-radius: 50%;
157+
background: #0d9488; border: 3px solid #fff;
158+
box-shadow: 0 0 0 1px rgba(0,0,0,0.2), 0 0 20px 4px rgba(13,148,136,0.5);
159+
transform: translate(-50%, -50%);
160+
}
161+
#${REACTIME_POINTER_OVERLAY_ID} .reactime-pointer-ripple {
162+
position: fixed; width: 22px; height: 22px; border-radius: 50%;
163+
border: 3px solid #14b8a6; transform: translate(-50%, -50%); opacity: 0;
164+
}
165+
#${REACTIME_POINTER_OVERLAY_ID}.${REACTIME_POINTER_VISIBLE_CLASS} .reactime-pointer-dot {
166+
animation: reactime-dot-pulse 2s ease-in-out; animation-iteration-count: infinite;
167+
}
168+
#${REACTIME_POINTER_OVERLAY_ID}.${REACTIME_POINTER_VISIBLE_CLASS} .reactime-pointer-ripple {
169+
animation: reactime-ripple 1.2s ease-out; animation-iteration-count: infinite;
170+
}
171+
@keyframes reactime-dot-pulse {
172+
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 1; box-shadow: 0 0 0 1px rgba(0,0,0,0.2), 0 0 20px 4px rgba(13,148,136,0.5); }
173+
10% { transform: translate(-50%, -50%) scale(1); opacity: 1; box-shadow: 0 0 0 1px rgba(0,0,0,0.2), 0 0 20px 4px rgba(13,148,136,0.5); }
174+
50% { transform: translate(-50%, -50%) scale(1.2); opacity: 1; box-shadow: 0 0 0 1px rgba(0,0,0,0.2), 0 0 28px 8px rgba(13,148,136,0.7); }
175+
}
176+
@keyframes reactime-ripple {
177+
0% { transform: translate(-50%, -50%) scale(0.6); opacity: 0.7; }
178+
100% { transform: translate(-50%, -50%) scale(3); opacity: 0; }
179+
}
180+
@media (prefers-reduced-motion: reduce) {
181+
#${REACTIME_POINTER_OVERLAY_ID}.${REACTIME_POINTER_VISIBLE_CLASS} .reactime-pointer-dot {
182+
animation: reactime-dot-in 0.25s ease-out;
211183
}
212-
overlay = document.createElement('div');
213-
overlay.id = REACTIME_POINTER_OVERLAY_ID;
214-
overlay.setAttribute('aria-hidden', 'true');
215-
const ripple = document.createElement('div');
216-
ripple.className = 'reactime-pointer-ripple';
217-
const dot = document.createElement('div');
218-
dot.className = 'reactime-pointer-dot';
219-
overlay.appendChild(ripple);
220-
overlay.appendChild(dot);
221-
overlay.style.display = 'none';
222-
(document.body || document.documentElement).appendChild(overlay);
184+
#${REACTIME_POINTER_OVERLAY_ID}.${REACTIME_POINTER_VISIBLE_CLASS} .reactime-pointer-ripple {
185+
animation: none; opacity: 0;
186+
}
187+
}
188+
@keyframes reactime-dot-in {
189+
from { transform: translate(-50%, -50%) scale(0); opacity: 0; }
190+
to { transform: translate(-50%, -50%) scale(1); opacity: 1; }
223191
}
192+
`;
193+
194+
/**
195+
* Returns the pointer overlay element, creating it (and injecting styles) only on first use.
196+
* Reuses cached refs to avoid repeated DOM lookups.
197+
*/
198+
function getOrCreatePointerOverlay(): HTMLElement {
199+
if (pointerOverlayRef) return pointerOverlayRef;
200+
201+
if (!document.getElementById(REACTIME_POINTER_STYLES_ID)) {
202+
const style = document.createElement('style');
203+
style.id = REACTIME_POINTER_STYLES_ID;
204+
style.textContent = REACTIME_POINTER_STYLES;
205+
(document.head || document.documentElement).appendChild(style);
206+
}
207+
208+
const overlay = document.createElement('div');
209+
overlay.id = REACTIME_POINTER_OVERLAY_ID;
210+
overlay.setAttribute('aria-hidden', 'true');
211+
const ripple = document.createElement('div');
212+
ripple.className = 'reactime-pointer-ripple';
213+
const dot = document.createElement('div');
214+
dot.className = 'reactime-pointer-dot';
215+
overlay.appendChild(ripple);
216+
overlay.appendChild(dot);
217+
overlay.style.display = 'none';
218+
(document.body || document.documentElement).appendChild(overlay);
219+
220+
pointerOverlayRef = overlay;
221+
pointerDotRef = dot;
222+
pointerRippleRef = ripple;
224223
return overlay;
225224
}
226225

227-
function updateClickReplayPointer(payload) {
228-
const event = payload?.lastUserEvent;
226+
/** Payload shape we use for click replay (snapshot may include lastUserEvent from backend). */
227+
interface ClickReplayPayload {
228+
lastUserEvent?: { x: number; y: number } | null;
229+
}
230+
231+
/**
232+
* Shows or hides the click-replay pointer on the page based on snapshot payload.
233+
* Uses cached overlay/dot/ripple refs after first run to avoid repeated DOM queries.
234+
*/
235+
function updateClickReplayPointer(payload: ClickReplayPayload | undefined): void {
229236
const overlay = getOrCreatePointerOverlay();
230-
const dot = overlay.querySelector('.reactime-pointer-dot');
231-
const ripple = overlay.querySelector('.reactime-pointer-ripple');
232-
if (!dot || !(dot instanceof HTMLElement)) return;
233-
if (event && typeof event.x === 'number' && typeof event.y === 'number') {
237+
const dot = pointerDotRef;
238+
const ripple = pointerRippleRef;
239+
if (!dot) return;
240+
241+
const event = payload?.lastUserEvent;
242+
const hasValidEvent =
243+
event != null && typeof event.x === 'number' && typeof event.y === 'number';
244+
245+
if (hasValidEvent) {
234246
const left = `${event.x}px`;
235247
const top = `${event.y}px`;
236248
dot.style.left = left;
237249
dot.style.top = top;
238-
if (ripple && ripple instanceof HTMLElement) {
250+
if (ripple) {
239251
ripple.style.left = left;
240252
ripple.style.top = top;
241253
}
242254
overlay.style.display = '';
243-
// Remove then re-add visible class so ripple animation plays every time we jump
244-
overlay.classList.remove('reactime-pointer-visible');
255+
overlay.classList.remove(REACTIME_POINTER_VISIBLE_CLASS);
245256
requestAnimationFrame(() => {
246-
overlay.classList.add('reactime-pointer-visible');
257+
overlay.classList.add(REACTIME_POINTER_VISIBLE_CLASS);
247258
});
248259
} else {
249-
overlay.classList.remove('reactime-pointer-visible');
260+
overlay.classList.remove(REACTIME_POINTER_VISIBLE_CLASS);
250261
overlay.style.display = 'none';
251262
}
252263
}

0 commit comments

Comments
 (0)