@@ -139,6 +139,111 @@ window.addEventListener('message', (msg) => {
139139 }
140140} ) ;
141141
142+ // -------------- USER INPUT VISUALIZATION (CLICK REPLAY POINTER) --------------
143+ const REACTIME_POINTER_OVERLAY_ID = 'reactime-pointer-overlay' ;
144+ const REACTIME_POINTER_STYLES_ID = 'reactime-pointer-styles' ;
145+
146+ function getOrCreatePointerOverlay ( ) {
147+ let overlay = document . getElementById ( REACTIME_POINTER_OVERLAY_ID ) ;
148+ if ( ! overlay ) {
149+ // Inject styles once – pointer designed to draw attention (larger, ripple, glow)
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-in 0.25s ease-out;
181+ }
182+ #${ REACTIME_POINTER_OVERLAY_ID } .reactime-pointer-visible .reactime-pointer-ripple {
183+ animation: reactime-ripple 0.8s ease-out 1;
184+ }
185+ @keyframes reactime-dot-in {
186+ from { transform: translate(-50%, -50%) scale(0); opacity: 0; }
187+ to { transform: translate(-50%, -50%) scale(1); opacity: 1; }
188+ }
189+ @keyframes reactime-ripple {
190+ 0% { transform: translate(-50%, -50%) scale(0.5); opacity: 0.8; }
191+ 100% { transform: translate(-50%, -50%) scale(3); opacity: 0; }
192+ }
193+ @media (prefers-reduced-motion: reduce) {
194+ #${ REACTIME_POINTER_OVERLAY_ID } .reactime-pointer-visible .reactime-pointer-dot,
195+ #${ REACTIME_POINTER_OVERLAY_ID } .reactime-pointer-visible .reactime-pointer-ripple {
196+ animation: none;
197+ }
198+ #${ REACTIME_POINTER_OVERLAY_ID } .reactime-pointer-visible .reactime-pointer-ripple {
199+ opacity: 0;
200+ }
201+ }
202+ ` ;
203+ ( document . head || document . documentElement ) . appendChild ( style ) ;
204+ }
205+ overlay = document . createElement ( 'div' ) ;
206+ overlay . id = REACTIME_POINTER_OVERLAY_ID ;
207+ overlay . setAttribute ( 'aria-hidden' , 'true' ) ;
208+ const ripple = document . createElement ( 'div' ) ;
209+ ripple . className = 'reactime-pointer-ripple' ;
210+ const dot = document . createElement ( 'div' ) ;
211+ dot . className = 'reactime-pointer-dot' ;
212+ overlay . appendChild ( ripple ) ;
213+ overlay . appendChild ( dot ) ;
214+ overlay . style . display = 'none' ;
215+ ( document . body || document . documentElement ) . appendChild ( overlay ) ;
216+ }
217+ return overlay ;
218+ }
219+
220+ function updateClickReplayPointer ( payload ) {
221+ const event = payload ?. lastUserEvent ;
222+ const overlay = getOrCreatePointerOverlay ( ) ;
223+ const dot = overlay . querySelector ( '.reactime-pointer-dot' ) ;
224+ const ripple = overlay . querySelector ( '.reactime-pointer-ripple' ) ;
225+ if ( ! dot || ! ( dot instanceof HTMLElement ) ) return ;
226+ if ( event && typeof event . x === 'number' && typeof event . y === 'number' ) {
227+ const left = `${ event . x } px` ;
228+ const top = `${ event . y } px` ;
229+ dot . style . left = left ;
230+ dot . style . top = top ;
231+ if ( ripple && ripple instanceof HTMLElement ) {
232+ ripple . style . left = left ;
233+ ripple . style . top = top ;
234+ }
235+ overlay . style . display = '' ;
236+ // Remove then re-add visible class so ripple animation plays every time we jump
237+ overlay . classList . remove ( 'reactime-pointer-visible' ) ;
238+ requestAnimationFrame ( ( ) => {
239+ overlay . classList . add ( 'reactime-pointer-visible' ) ;
240+ } ) ;
241+ } else {
242+ overlay . classList . remove ( 'reactime-pointer-visible' ) ;
243+ overlay . style . display = 'none' ;
244+ }
245+ }
246+
142247// FROM BACKGROUND TO CONTENT SCRIPT
143248// Listening for messages from the UI of the Reactime extension.
144249chrome . runtime . onMessage . addListener ( ( request ) => {
@@ -151,6 +256,7 @@ chrome.runtime.onMessage.addListener((request) => {
151256 }
152257 // this is only listening for Jump toSnap
153258 if ( action === 'jumpToSnap' ) {
259+ updateClickReplayPointer ( request . payload ) ;
154260 chrome . runtime . sendMessage ( request ) ;
155261 // After the jumpToSnap action has been sent back to background js,
156262 // it will send the same action to backend files (index.ts) for it execute the jump feature
0 commit comments