@@ -63,6 +63,7 @@ const MAP_STYLES: MapStyle[] = [
6363
6464const STORAGE_KEY = ' pcd-map-style' ;
6565let activeTileLayers: import (' leaflet' ).TileLayer [] = [];
66+ let themeTransitionTimer: number | null = null ;
6667
6768const currentStyle = ref <string >(' ' );
6869
@@ -87,9 +88,25 @@ function setMapStyle(styleId: string, map: import('leaflet').Map, L: typeof impo
8788 document .documentElement .dataset .theme = styleId === ' dark' ? ' dark' : ' light' ;
8889}
8990
91+ function animateThemeTransition() {
92+ if (window .matchMedia (' (prefers-reduced-motion: reduce)' ).matches ) return ;
93+ const root = document .documentElement ;
94+ root .classList .add (' theme-transition' );
95+ if (themeTransitionTimer !== null ) {
96+ window .clearTimeout (themeTransitionTimer );
97+ }
98+ themeTransitionTimer = window .setTimeout (() => {
99+ root .classList .remove (' theme-transition' );
100+ themeTransitionTimer = null ;
101+ }, 360 );
102+ }
103+
90104function toggleTheme() {
91105 const next = currentStyle .value === ' dark' ? ' light' : ' dark' ;
92- if (mapInstance && leafletRef ) setMapStyle (next , mapInstance , leafletRef );
106+ if (mapInstance && leafletRef ) {
107+ animateThemeTransition ();
108+ setMapStyle (next , mapInstance , leafletRef );
109+ }
93110}
94111
95112function setActiveMarker(nodeId : string | null ) {
@@ -420,6 +437,10 @@ onMounted(async () => {
420437});
421438
422439onUnmounted (() => {
440+ if (themeTransitionTimer !== null ) {
441+ window .clearTimeout (themeTransitionTimer );
442+ }
443+ document .documentElement .classList .remove (' theme-transition' );
423444 document .removeEventListener (' keydown' , handleKeydown );
424445 mapInstance ?.remove ();
425446});
@@ -435,25 +456,38 @@ onUnmounted(() => {
435456 >
436457 ☰
437458 </button >
459+ <div class =" banner-controls-left" >
460+ <LanguageSwitcher />
461+ </div >
438462 <a
439463 id =" host-btn"
440464 :href =" SUBMIT_EVENT_URL"
441465 >{{ t('nav.submit_event') }}</a >
442- <LanguageSwitcher />
443- <button
444- id =" theme-toggle"
445- :aria-label =" currentStyle === 'dark' ? t('nav.switch_to_light') : t('nav.switch_to_dark')"
446- :title =" currentStyle === 'dark' ? t('nav.switch_to_light') : t('nav.switch_to_dark')"
447- @click =" toggleTheme"
448- >
449- <svg v-if =" currentStyle === 'dark'" xmlns =" http://www.w3.org/2000/svg" width =" 20" height =" 20" viewBox =" 0 0 24 24" fill =" none" stroke =" currentColor" stroke-width =" 2" stroke-linecap =" round" stroke-linejoin =" round" aria-hidden =" true" >
450- <circle cx =" 12" cy =" 12" r =" 4" />
451- <path d =" M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
452- </svg >
453- <svg v-else xmlns =" http://www.w3.org/2000/svg" width =" 20" height =" 20" viewBox =" 0 0 24 24" fill =" none" stroke =" currentColor" stroke-width =" 2" stroke-linecap =" round" stroke-linejoin =" round" aria-hidden =" true" >
454- <path d =" M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
455- </svg >
456- </button >
466+ <div class =" banner-controls-right" >
467+ <button
468+ id =" theme-toggle"
469+ role =" switch"
470+ :aria-checked =" currentStyle === 'dark'"
471+ :aria-label =" currentStyle === 'dark' ? t('nav.switch_to_light') : t('nav.switch_to_dark')"
472+ :title =" currentStyle === 'dark' ? t('nav.switch_to_light') : t('nav.switch_to_dark')"
473+ @click =" toggleTheme"
474+ >
475+ <span class =" theme-toggle__track" aria-hidden =" true" >
476+ <span class =" theme-toggle__thumb" ></span >
477+ <span class =" theme-toggle__icon theme-toggle__icon--sun" >
478+ <svg xmlns =" http://www.w3.org/2000/svg" width =" 18" height =" 18" viewBox =" 0 0 24 24" fill =" none" stroke =" currentColor" stroke-width =" 1.8" stroke-linecap =" round" stroke-linejoin =" round" >
479+ <circle cx =" 12" cy =" 12" r =" 4" />
480+ <path d =" M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
481+ </svg >
482+ </span >
483+ <span class =" theme-toggle__icon theme-toggle__icon--moon" >
484+ <svg xmlns =" http://www.w3.org/2000/svg" width =" 18" height =" 18" viewBox =" 0 0 24 24" fill =" none" stroke =" currentColor" stroke-width =" 1.8" stroke-linecap =" round" stroke-linejoin =" round" >
485+ <path d =" M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
486+ </svg >
487+ </span >
488+ </span >
489+ </button >
490+ </div >
457491 </div >
458492 <NodePanel :node =" selectedNode" @close =" closePanel" />
459493 <NodeList
@@ -475,35 +509,125 @@ onUnmounted(() => {
475509 z-index : 0 ;
476510}
477511
478- #theme-toggle {
512+ .banner-controls-right {
479513 position : fixed ;
480514 top : 1rem ;
481515 right : 1rem ;
482516 z-index : var (--z-controls );
483- width : 40px ;
517+ display : flex ;
518+ align-items : center ;
519+ }
520+
521+ .banner-controls-left {
522+ position : fixed ;
523+ top : 1rem ;
524+ left : calc (1rem + 44px + 0.5rem );
525+ z-index : var (--z-controls );
526+ display : flex ;
527+ align-items : center ;
528+ }
529+
530+ #theme-toggle {
531+ width : 75px ;
484532 height : 40px ;
533+ padding : 0 ;
485534 display : flex ;
486535 align-items : center ;
487536 justify-content : center ;
488- background : var (--color-bg-popup );
537+ background : color-mix(in srgb , var (--color-bg-popup ) 86 % , transparent );
489538 border : 1px solid var (--color-border );
490- border-radius : 8 px ;
539+ border-radius : 999 px ;
491540 cursor : pointer ;
492541 color : var (--color-text );
493- transition : background-color 0.12s ease , color 0.12s ease , border-color 0.12s ease ;
494- box-shadow : 0 1px 4px rgba (0 , 0 , 0 , 0.15 );
542+ transition : background-color 0.28s ease , color 0.28s ease , border-color 0.28s ease , box-shadow 0.28s ease ;
543+ box-shadow : 0 10px 28px rgba (18 , 19 , 33 , 0.18 );
544+ backdrop-filter : blur (14px );
495545}
496546
497547#theme-toggle :hover {
498- background : var (--color-primary );
499- color : #fff ;
500- border-color : var (--color-primary );
548+ background : var (--color-bg-popup-hover );
501549}
502550
503551#theme-toggle :focus-visible {
504552 outline : 2px solid var (--color-focus );
505553 outline-offset : 2px ;
506554}
555+
556+ .theme-toggle__track {
557+ position : relative ;
558+ width : 100% ;
559+ height : 100% ;
560+ display : grid ;
561+ grid-template-columns : 1fr 1fr ;
562+ align-items : center ;
563+ padding : 4px ;
564+ }
565+
566+ .theme-toggle__thumb {
567+ position : absolute ;
568+ top : 4px ;
569+ left : 4px ;
570+ width : 30px ;
571+ height : 30px ;
572+ border-radius : 999px ;
573+ background : linear-gradient (135deg , #f7d76d 0% , #f3c84d 48% , #ecaa2a 100% );
574+ box-shadow : 0 8px 18px rgba (124 , 79 , 10 , 0.32 );
575+ transition : transform 0.32s cubic-bezier (0.22 , 1 , 0.36 , 1 ), background 0.32s ease , box-shadow 0.32s ease ;
576+ }
577+
578+ .theme-toggle__icon {
579+ position : relative ;
580+ z-index : 1 ;
581+ display : inline-flex ;
582+ align-items : center ;
583+ justify-content : center ;
584+ width : 30px ;
585+ height : 30px ;
586+ justify-self : center ;
587+ color : var (--color-text-muted );
588+ transition : color 0.24s ease , opacity 0.24s ease ;
589+ }
590+
591+ .theme-toggle__icon--sun {
592+ transform : translateX (-1px );
593+ }
594+
595+ .theme-toggle__icon--moon {
596+ transform : translateX (3px );
597+ }
598+
599+ #theme-toggle [aria-checked = " false" ] .theme-toggle__icon--sun ,
600+ #theme-toggle [aria-checked = " true" ] .theme-toggle__icon--moon {
601+ color : #241336 ;
602+ opacity : 1 ;
603+ }
604+
605+ #theme-toggle [aria-checked = " false" ] .theme-toggle__icon--moon ,
606+ #theme-toggle [aria-checked = " true" ] .theme-toggle__icon--sun {
607+ opacity : 0.68 ;
608+ }
609+
610+ #theme-toggle [aria-checked = " true" ] .theme-toggle__thumb {
611+ transform : translateX (37px );
612+ background : linear-gradient (135deg , #d8b4fe 0% , #c084fc 44% , #9d4edd 100% );
613+ box-shadow : 0 8px 18px rgba (88 , 28 , 135 , 0.34 );
614+ }
615+
616+ #theme-toggle [aria-checked = " true" ] {
617+ background : color-mix(in srgb , var (--color-bg-popup ) 78% , #0b1024 22% );
618+ }
619+
620+ #theme-toggle [aria-checked = " true" ]:hover {
621+ background : color-mix(in srgb , var (--color-bg-popup-hover ) 76% , #0b1024 24% );
622+ }
623+
624+ @media (prefers-reduced-motion: reduce) {
625+ #theme-toggle ,
626+ .theme-toggle__thumb ,
627+ .theme-toggle__icon {
628+ transition : none ;
629+ }
630+ }
507631 </style >
508632
509633<style >
0 commit comments