@@ -4,6 +4,14 @@ const scene = document.getElementById('scene-container');
44const motionBtn = document . getElementById ( 'toggle-motion' ) ;
55const searchInput = document . getElementById ( 'search-input' ) ;
66
7+ // --- Preview Pane Elements ---
8+ const previewSidebar = document . getElementById ( 'preview-sidebar' ) ;
9+ const previewFrame = document . getElementById ( 'preview-frame' ) ;
10+ const previewTitle = document . getElementById ( 'preview-title' ) ;
11+ const previewLink = document . getElementById ( 'preview-link' ) ;
12+ const closePreviewBtn = document . getElementById ( 'close-preview' ) ;
13+ let previewDebounceTimer = null ;
14+
715// --- Configuration ---
816const config = {
917 sphereRadius : 280 ,
@@ -57,24 +65,39 @@ class Point {
5765 this . element = document . createElement ( 'a' ) ;
5866 this . element . href = data . url ;
5967 this . element . className = 'node-link' ;
60- this . element . target = "_blank" ;
68+ // We remove target="_blank" behavior in JS, but keep it in HTML for fallback
69+ this . element . target = "_blank" ;
6170 this . element . setAttribute ( 'aria-label' , `Visit ${ data . text } ` ) ;
6271
6372 this . element . innerHTML = `
6473 <div class="node-dot"></div>
6574 <span class="node-text">${ data . text } </span>
6675 ` ;
6776
77+ // --- Event Listeners ---
78+
79+ // CLICK: Open immediately
80+ this . element . addEventListener ( 'click' , ( e ) => {
81+ e . preventDefault ( ) ; // Stop normal navigation
82+ focusedPoint = this ;
83+ config . isPaused = true ;
84+ loadPreviewNow ( this . data ) ;
85+ } ) ;
86+
87+ // FOCUS: Open with delay (Debounce) to prevent lag while tabbing
6888 this . element . addEventListener ( 'focus' , ( ) => {
6989 focusedPoint = this ;
7090 config . isPaused = true ;
91+ schedulePreview ( this . data ) ;
7192 } ) ;
7293
7394 this . element . addEventListener ( 'blur' , ( ) => {
7495 focusedPoint = null ;
7596 if ( ! mediaQuery . matches && motionBtn . innerText === "PAUSE MOTION" ) {
7697 config . isPaused = false ;
7798 }
99+ // Cancel any pending preview load if user tabs away quickly
100+ clearTimeout ( previewDebounceTimer ) ;
78101 } ) ;
79102
80103 scene . appendChild ( this . element ) ;
@@ -157,6 +180,61 @@ function drawConnections() {
157180 ctx . restore ( ) ;
158181}
159182
183+ // --- Preview Logic ---
184+
185+ function schedulePreview ( data ) {
186+ // Cancel any existing timer
187+ clearTimeout ( previewDebounceTimer ) ;
188+ // Wait 800ms before loading. If user tabs away, 'blur' will clear this.
189+ previewDebounceTimer = setTimeout ( ( ) => {
190+ loadPreviewNow ( data ) ;
191+ } , 800 ) ;
192+ }
193+
194+ function loadPreviewNow ( data ) {
195+ clearTimeout ( previewDebounceTimer ) ;
196+
197+ // Update Sidebar UI
198+ previewTitle . textContent = data . text ;
199+ previewLink . href = data . url ;
200+ previewSidebar . classList . add ( 'active' ) ;
201+
202+ // Only load if it's a new URL (prevents reloading on repeated clicks)
203+ if ( previewFrame . getAttribute ( 'src' ) !== data . url ) {
204+ previewFrame . classList . remove ( 'loaded' ) ; // Hide until loaded
205+ previewFrame . src = data . url ;
206+
207+ previewFrame . onload = ( ) => {
208+ previewFrame . classList . add ( 'loaded' ) ;
209+ } ;
210+ }
211+ }
212+
213+ function closePreview ( ) {
214+ previewSidebar . classList . remove ( 'active' ) ;
215+
216+ // Clear iframe source after animation to free memory/CPU
217+ setTimeout ( ( ) => {
218+ previewFrame . src = '' ;
219+ previewFrame . classList . remove ( 'loaded' ) ;
220+ } , 500 ) ;
221+ }
222+
223+ // Close preview when clicking X
224+ closePreviewBtn . addEventListener ( 'click' , closePreview ) ;
225+
226+ // Close preview when clicking outside sidebar and nodes
227+ window . addEventListener ( 'click' , ( e ) => {
228+ const isNode = e . target . closest ( '.node-link' ) ;
229+ const isSidebar = e . target . closest ( '#preview-sidebar' ) ;
230+ if ( ! isNode && ! isSidebar ) {
231+ closePreview ( ) ;
232+ }
233+ } ) ;
234+
235+
236+ // --- Existing Event Listeners ---
237+
160238searchInput . addEventListener ( 'mouseenter' , ( ) => { isSearchHovering = true ; } ) ;
161239searchInput . addEventListener ( 'mouseleave' , ( ) => { isSearchHovering = false ; } ) ;
162240searchInput . addEventListener ( 'touchstart' , ( ) => { isSearchHovering = true ; } , { passive : true } ) ;
@@ -242,5 +320,7 @@ window.addEventListener('keydown', (e) => {
242320 } else {
243321 if ( document . activeElement === last ) { e . preventDefault ( ) ; first . focus ( ) ; }
244322 }
323+ } else if ( e . key === 'Escape' ) {
324+ closePreview ( ) ;
245325 }
246- } ) ;
326+ } ) ;
0 commit comments