@@ -98,84 +98,137 @@ document.getElementById("goBackBtn").addEventListener("click", () => {
9898
9999// Universal Dark Mode Toggle
100100( function ( ) {
101- const STORAGE_KEY = "darkMode" , body = document . body ;
102-
103- // Dark mode CSS
104- const sheet = document . createElement ( "style" ) ;
105- sheet . id = "universal-dark-mode-sheet" ;
106- sheet . textContent = `
107- .dark-mode {
108- --dm-bg:#0f0f10;--dm-surface:#1e1e1e;--dm-text:#eaeaea;--dm-muted:#bdbdbd;--dm-accent:#eaeaea;
109- }
110- .dark-mode, .dark-mode body {background-color:var(--dm-bg)!important;color:var(--dm-text)!important;}
111- .dark-mode .dm-btn {background:#2a2a2a!important;color:var(--dm-text)!important;border:1px solid rgba(200,200,200,0.45)!important;border-radius:8px!important;box-shadow:0 0 10px rgba(200,200,200,0.18)!important;}
112- .dark-mode .dm-title {color:var(--dm-accent)!important;}
113- .dark-mode .dm-note {color:var(--dm-muted)!important;font-style:italic;}
114- .dark-mode .dm-panel {background:var(--dm-surface)!important;color:var(--dm-text)!important;border:1px solid rgba(255,255,255,0.03)!important;box-shadow:0 6px 18px rgba(0,0,0,0.35) inset!important;}
115- .dark-mode * {transition:color 0.25s ease,background-color 0.25s ease,border-color 0.25s ease,box-shadow 0.25s ease!important;}
116- ` ;
117- document . head . appendChild ( sheet ) ;
118-
119- // Toggle button
120- const toggle = Object . assign ( document . createElement ( "button" ) , {
121- id : "dark-mode-toggle" ,
122- textContent : "🌙" ,
123- onclick ( ) {
124- const enabled = body . classList . toggle ( "dark-mode" ) ;
125- toggle . textContent = enabled ? "☀️" : "🌙" ;
126- localStorage . setItem ( STORAGE_KEY , enabled ? "true" : "false" ) ;
127- enabled ? applyDark ( ) : restoreStyles ( ) ;
128- }
129- } ) ;
130- Object . assign ( toggle . style , {
131- position :"fixed" , bottom :"15px" , right :"15px" , background :"transparent" ,
132- border :"2px solid rgba(200,200,200,0.22)" , borderRadius :"8px" ,
133- fontSize :"1.4rem" , cursor :"pointer" , zIndex :2147483647 ,
134- padding :"6px" , lineHeight :"1" , display :"flex" ,
135- alignItems :"center" , justifyContent :"center" ,
136- transition :"transform 0.18s ease,border-color 0.18s ease,opacity 0.18s ease"
137- } ) ;
138- toggle . onmouseenter = ( ) => { toggle . style . transform = "scale(1.15)" ; toggle . style . borderColor = "rgba(200,200,200,0.7)" ; } ;
139- toggle . onmouseleave = ( ) => { toggle . style . transform = "scale(1)" ; toggle . style . borderColor = "rgba(200,200,200,0.22)" ; } ;
140- document . body . appendChild ( toggle ) ;
141-
142- // Load preference
143- const saved = localStorage . getItem ( STORAGE_KEY ) ;
144- body . classList . toggle ( "dark-mode" , saved === "true" ) ;
145- toggle . textContent = saved === "true" ? "☀️" : "🌙" ;
146- if ( saved === "true" ) applyDark ( ) ;
147-
148- const brightness = rgb => ( rgb [ 0 ] * 299 + rgb [ 1 ] * 587 + rgb [ 2 ] * 114 ) / 1000 ;
101+ const STORAGE_KEY = "darkMode" , BODY = document . body ;
102+ const TOGGLE_ID = "dark-mode-toggle" , STYLE_ID = "universal-dark-mode-sheet" ;
103+ const SESSION_INIT = "dmInitialized" , SESSION_USER_TOGGLED = "dmUserToggled" ;
104+ let applyTimer = 0 , observer = null ;
105+
106+ // insert stylesheet once
107+ if ( ! document . getElementById ( STYLE_ID ) ) {
108+ const s = document . createElement ( "style" ) ;
109+ s . id = STYLE_ID ;
110+ s . textContent = `
111+ .dark-mode {
112+ --dm-bg:#0f0f10;--dm-surface:#1e1e1e;--dm-text:#eaeaea;--dm-muted:#bdbdbd;--dm-accent:#eaeaea;
113+ }
114+ .dark-mode, .dark-mode body {background-color:var(--dm-bg) !important; color:var(--dm-text) !important;}
115+ .dark-mode .dm-btn {background:#2a2a2a !important; color:var(--dm-text) !important; border:1px solid rgba(200,200,200,0.45) !important; border-radius:8px !important; box-shadow:0 0 10px rgba(200,200,200,0.18) !important;}
116+ .dark-mode .dm-title {color:var(--dm-accent) !important;}
117+ .dark-mode .dm-note {color:var(--dm-muted) !important; font-style:italic;}
118+ .dark-mode .dm-panel {background:var(--dm-surface) !important; color:var(--dm-text) !important; border:1px solid rgba(255,255,255,0.03) !important; box-shadow:0 6px 18px rgba(0,0,0,0.35) inset !important;}
119+ .dm-transition * {transition: color 0.25s ease, background-color 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease !important;}
120+ ` ;
121+ document . head . appendChild ( s ) ;
122+ }
123+
124+ function parseRgb ( str ) {
125+ if ( ! str ) return null ;
126+ const m = str . match ( / r g b a ? \( ( \d + ) [ ^ \d ] + ( \d + ) [ ^ \d ] + ( \d + ) / i) ;
127+ if ( m ) return [ + m [ 1 ] , + m [ 2 ] , + m [ 3 ] ] ;
128+ const h = ( str . match ( / ^ # ( [ 0 - 9 a - f ] { 6 } ) $ / i) || [ ] ) [ 1 ] ;
129+ return h ? [ parseInt ( h . slice ( 0 , 2 ) , 16 ) , parseInt ( h . slice ( 2 , 4 ) , 16 ) , parseInt ( h . slice ( 4 , 6 ) , 16 ) ] : null ;
130+ }
131+ const brightness = rgb => rgb ? ( rgb [ 0 ] * 299 + rgb [ 1 ] * 587 + rgb [ 2 ] * 114 ) / 1000 : 255 ;
149132
150133 function applyDark ( ) {
151- document . querySelectorAll ( "*" ) . forEach ( el => {
152- if ( el . dataset . dmProcessed ) return ;
153- el . dataset . dmOriginalStyle = el . getAttribute ( "style" ) || "" ;
154- const added = [ ] ;
155- try {
156- const cs = getComputedStyle ( el ) , bg = cs . backgroundColor || "" , color = cs . color || "" , fs = parseFloat ( cs . fontSize || 0 ) ;
157- if ( el . tagName === "BUTTON" || el . getAttribute ( "role" ) === "button" ) added . push ( "dm-btn" , el . classList . add ( "dm-btn" ) ) ;
158- else {
159- if ( / [ \p{ Emoji} ] / u. test ( el . textContent || "" ) || fs >= 18 ) added . push ( "dm-title" , el . classList . add ( "dm-title" ) ) ;
160- else if ( fs <= 12 || cs . fontStyle === "italic" ) added . push ( "dm-note" , el . classList . add ( "dm-note" ) ) ;
161- if ( bg && bg !== "rgba(0, 0, 0, 0)" && bg . match ( / \d + / ) ) {
162- const rgb = bg . match ( / \d + / g) . map ( Number ) ;
163- if ( brightness ( rgb ) > 150 ) added . push ( "dm-panel" , el . classList . add ( "dm-panel" ) ) ;
134+ clearTimeout ( applyTimer ) ;
135+ applyTimer = setTimeout ( ( ) => requestAnimationFrame ( ( ) => {
136+ document . querySelectorAll ( "body *" ) . forEach ( el => {
137+ if ( el . dataset . dmProcessed ) return ;
138+ try {
139+ el . dataset . dmOriginalStyle = el . getAttribute ( "style" ) || "" ;
140+ const cs = getComputedStyle ( el ) , bg = cs . backgroundColor || "" , fs = parseFloat ( cs . fontSize || 0 ) ;
141+ const added = [ ] ;
142+ if ( el . tagName === "BUTTON" || el . getAttribute ( "role" ) === "button" ||
143+ ( el . tagName === "INPUT" && [ "button" , "submit" , "reset" ] . includes ( ( el . type || "" ) . toLowerCase ( ) ) ) ) {
144+ added . push ( "dm-btn" ) ; el . classList . add ( "dm-btn" ) ;
145+ } else {
146+ const txt = ( el . textContent || "" ) . trim ( ) ;
147+ if ( / [ \p{ Emoji} ] / u. test ( txt ) || fs >= 18 ) { added . push ( "dm-title" ) ; el . classList . add ( "dm-title" ) ; }
148+ else if ( fs <= 12 || cs . fontStyle === "italic" ) { added . push ( "dm-note" ) ; el . classList . add ( "dm-note" ) ; }
149+ const rgb = parseRgb ( bg ) ;
150+ if ( ( rgb && brightness ( rgb ) > 150 ) || ( cs . backgroundImage && cs . backgroundImage !== "none" ) ) {
151+ added . push ( "dm-panel" ) ; el . classList . add ( "dm-panel" ) ;
152+ }
164153 }
165- if ( el . tagName === "INPUT" && [ "button" , "submit" , "reset" ] . includes ( ( el . type || "" ) . toLowerCase ( ) ) ) added . push ( "dm-btn" , el . classList . add ( "dm-btn" ) ) ;
166- }
167- if ( added . length ) el . dataset . dmAddedClasses = added . join ( " " ) ;
168- } catch ( e ) { }
169- el . dataset . dmProcessed = "1" ;
170- } ) ;
154+ if ( added . length ) el . dataset . dmAddedClasses = added . join ( " " ) ;
155+ el . dataset . dmProcessed = "1" ;
156+ } catch ( e ) { }
157+ } ) ;
158+ } ) , 50 ) ;
171159 }
172160
173161 function restoreStyles ( ) {
174- document . querySelectorAll ( "[data-dm-processed='1']" ) . forEach ( el => {
175- const orig = el . dataset . dmOriginalStyle || "" ; orig ? el . setAttribute ( "style" , orig ) : el . removeAttribute ( "style" ) ;
176- ( el . dataset . dmAddedClasses || "" ) . split ( / \s + / ) . forEach ( c => c && el . classList . remove ( c ) ) ;
177- delete el . dataset . dmProcessed ; delete el . dataset . dmOriginalStyle ; delete el . dataset . dmAddedClasses ;
162+ clearTimeout ( applyTimer ) ;
163+ if ( observer ) observer . disconnect ( ) ;
164+ document . querySelectorAll ( "[data-dm-processed='1']" ) . forEach ( el => {
165+ try {
166+ const orig = el . dataset . dmOriginalStyle || "" ;
167+ orig ? el . setAttribute ( "style" , orig ) : el . removeAttribute ( "style" ) ;
168+ ( el . dataset . dmAddedClasses || "" ) . split ( / \s + / ) . forEach ( c => c && el . classList . remove ( c ) ) ;
169+ delete el . dataset . dmProcessed ; delete el . dataset . dmOriginalStyle ; delete el . dataset . dmAddedClasses ;
170+ } catch ( e ) { }
171+ } ) ;
172+ BODY . classList . remove ( "dark-mode" ) ;
173+ }
174+
175+ function ensureToggle ( ) {
176+ let t = document . getElementById ( TOGGLE_ID ) ;
177+ if ( ! t ) {
178+ t = Object . assign ( document . createElement ( "button" ) , {
179+ id : TOGGLE_ID ,
180+ textContent : "🌙" ,
181+ onclick ( ) {
182+ const enabled = BODY . classList . toggle ( "dark-mode" ) ;
183+ this . textContent = enabled ? "☀️" : "🌙" ;
184+ sessionStorage . setItem ( SESSION_USER_TOGGLED , "1" ) ;
185+ localStorage . setItem ( STORAGE_KEY , enabled ? "true" : "false" ) ;
186+ if ( enabled ) {
187+ // one-time transition on first toggle
188+ BODY . classList . add ( "dm-transition" ) ;
189+ applyDark ( ) ;
190+ startObserver ( ) ;
191+ setTimeout ( ( ) => BODY . classList . remove ( "dm-transition" ) , 260 ) ;
192+ } else restoreStyles ( ) ;
193+ }
194+ } ) ;
195+ Object . assign ( t . style , { position :"fixed" , bottom :"15px" , right :"15px" , background :"transparent" , border :"2px solid rgba(200,200,200,0.22)" , borderRadius :"8px" , fontSize :"1.4rem" , cursor :"pointer" , zIndex :2147483647 , padding :"6px" , lineHeight :"1" , display :"flex" , alignItems :"center" , justifyContent :"center" , transition :"transform 0.18s ease,border-color 0.18s ease,opacity 0.18s ease" } ) ;
196+ t . onmouseenter = ( ) => { t . style . transform = "scale(1.15)" ; t . style . borderColor = "rgba(200,200,200,0.7)" ; } ;
197+ t . onmouseleave = ( ) => { t . style . transform = "scale(1)" ; t . style . borderColor = "rgba(200,200,200,0.22)" ; } ;
198+ document . body . appendChild ( t ) ;
199+ }
200+ return t ;
201+ }
202+
203+ function startObserver ( ) {
204+ observer && observer . disconnect ( ) ;
205+ observer = new MutationObserver ( muts => {
206+ if ( ! isEnabled ( ) ) return ;
207+ for ( const m of muts ) if ( m . addedNodes && m . addedNodes . length ) { applyDark ( ) ; break ; }
178208 } ) ;
179- body . classList . remove ( "dark-mode" ) ;
209+ observer . observe ( document . body , { childList : true , subtree : true } ) ;
180210 }
211+
212+ const isEnabled = ( ) => BODY . classList . contains ( "dark-mode" ) ;
213+
214+ function init ( ) {
215+ const toggle = ensureToggle ( ) ;
216+ const saved = localStorage . getItem ( STORAGE_KEY ) , inited = sessionStorage . getItem ( SESSION_INIT ) ;
217+ if ( ! inited ) {
218+ sessionStorage . setItem ( SESSION_INIT , "1" ) ;
219+ toggle . textContent = saved === "true" ? "☀️" : "🌙" ;
220+ return ;
221+ }
222+ const userToggled = sessionStorage . getItem ( SESSION_USER_TOGGLED ) ;
223+ if ( userToggled === "1" || saved === "true" ) {
224+ BODY . classList . toggle ( "dark-mode" , saved === "true" ) ;
225+ toggle . textContent = saved === "true" ? "☀️" : "🌙" ;
226+ if ( saved === "true" ) { applyDark ( ) ; startObserver ( ) ; }
227+ } else toggle . textContent = "🌙" ;
228+ }
229+
230+ if ( document . readyState === "loading" ) document . addEventListener ( "DOMContentLoaded" , init , { once : true } ) ;
231+ else init ( ) ;
232+
233+ window . addEventListener ( "popstate" , ( ) => { clearTimeout ( applyTimer ) ; applyTimer = setTimeout ( init , 40 ) ; } ) ;
181234} ) ( ) ;
0 commit comments