Skip to content

Commit 315bc3b

Browse files
committed
Enhance theme transition effects and accessibility for language switcher
1 parent 2ab5333 commit 315bc3b

3 files changed

Lines changed: 183 additions & 33 deletions

File tree

pcd-website/src/components/LanguageSwitcher.vue

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<template>
2-
<div class="lang-switcher" ref="wrapRef">
2+
<div class="lang-switcher" ref="wrapRef" @keydown.escape="closeAndRefocus">
33
<button
4+
ref="btnRef"
45
class="lang-btn"
56
:aria-label="t('language_switcher.label')"
67
:title="t('language_switcher.label')"
@@ -47,6 +48,7 @@ import { currentLocale, setLocale } from '../i18n/localeState';
4748
const { t } = useI18n();
4849
const open = ref(false);
4950
const wrapRef = ref<HTMLElement | null>(null);
51+
const btnRef = ref<HTMLButtonElement | null>(null);
5052
5153
const LANGUAGE_NAMES: Record<SupportedLocale, string> = {
5254
en: 'English',
@@ -65,6 +67,11 @@ function select(locale: SupportedLocale) {
6567
open.value = false;
6668
}
6769
70+
function closeAndRefocus() {
71+
open.value = false;
72+
btnRef.value?.focus();
73+
}
74+
6875
function handleOutsideClick(e: MouseEvent) {
6976
if (open.value && !wrapRef.value?.contains(e.target as Node)) {
7077
open.value = false;
@@ -77,10 +84,7 @@ onUnmounted(() => document.removeEventListener('click', handleOutsideClick));
7784

7885
<style scoped>
7986
.lang-switcher {
80-
position: fixed;
81-
top: 1rem;
82-
right: calc(1rem + 40px + 0.5rem);
83-
z-index: var(--z-controls);
87+
position: relative;
8488
}
8589
8690
.lang-btn {
@@ -129,7 +133,7 @@ onUnmounted(() => document.removeEventListener('click', handleOutsideClick));
129133
130134
.lang-dropdown {
131135
position: absolute;
132-
right: 0;
136+
left: 0;
133137
top: calc(100% + 4px);
134138
list-style: none;
135139
margin: 0;

pcd-website/src/components/MapView.vue

Lines changed: 149 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const MAP_STYLES: MapStyle[] = [
6363
6464
const STORAGE_KEY = 'pcd-map-style';
6565
let activeTileLayers: import('leaflet').TileLayer[] = [];
66+
let themeTransitionTimer: number | null = null;
6667
6768
const 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+
90104
function 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
95112
function setActiveMarker(nodeId: string | null) {
@@ -420,6 +437,10 @@ onMounted(async () => {
420437
});
421438
422439
onUnmounted(() => {
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: 8px;
539+
border-radius: 999px;
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>

pcd-website/src/styles/global.css

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,28 @@ body {
6464
line-height: 1.5;
6565
}
6666

67+
html.theme-transition,
68+
html.theme-transition *,
69+
html.theme-transition *::before,
70+
html.theme-transition *::after {
71+
transition:
72+
background-color 0.28s ease,
73+
border-color 0.28s ease,
74+
color 0.28s ease,
75+
fill 0.28s ease,
76+
stroke 0.28s ease,
77+
box-shadow 0.28s ease;
78+
}
79+
80+
@media (prefers-reduced-motion: reduce) {
81+
html.theme-transition,
82+
html.theme-transition *,
83+
html.theme-transition *::before,
84+
html.theme-transition *::after {
85+
transition: none;
86+
}
87+
}
88+
6789
/* ─── Skip Link ─────────────────────────────────────────────── */
6890
.skip-link {
6991
position: absolute;
@@ -142,8 +164,8 @@ body {
142164
top: var(--spacing-md);
143165
left: var(--spacing-md);
144166
z-index: var(--z-controls);
145-
width: 44px;
146-
height: 44px;
167+
width: 40px;
168+
height: 40px;
147169
background: var(--color-bg-popup);
148170
border: 1px solid var(--color-border);
149171
border-radius: 8px;

0 commit comments

Comments
 (0)