https://github.com/user-attachments/assets/fc4d15f9-2b54-4627-940f-94772ec739b1 --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
137 lines
3.8 KiB
Vue
137 lines
3.8 KiB
Vue
<script>
|
|
import { directive as onClickaway } from 'vue3-click-away';
|
|
|
|
const OPTION_DEFS = [
|
|
{ value: 'system', icon: 'i-lucide-monitor', fallback: 'System' },
|
|
{ value: 'light', icon: 'i-lucide-sun', fallback: 'Light' },
|
|
{ value: 'dark', icon: 'i-lucide-moon', fallback: 'Dark' },
|
|
];
|
|
|
|
const readLabels = () => {
|
|
const el = document.querySelector('#sidebar-theme-toggle');
|
|
const data = (el && el.dataset) || {};
|
|
return {
|
|
trigger: data.labelTrigger || 'Appearance',
|
|
options: OPTION_DEFS.map(opt => ({
|
|
...opt,
|
|
label:
|
|
data[
|
|
`label${opt.value.charAt(0).toUpperCase()}${opt.value.slice(1)}`
|
|
] || opt.fallback,
|
|
})),
|
|
};
|
|
};
|
|
|
|
export default {
|
|
directives: { onClickaway },
|
|
data() {
|
|
const labels = readLabels();
|
|
return {
|
|
mode: 'system',
|
|
systemPrefersDark: false,
|
|
open: false,
|
|
options: labels.options,
|
|
triggerLabel: labels.trigger,
|
|
};
|
|
},
|
|
computed: {
|
|
triggerIcon() {
|
|
const opt = this.options.find(o => o.value === this.mode);
|
|
return opt ? opt.icon : 'i-lucide-monitor';
|
|
},
|
|
isDarkActive() {
|
|
if (this.mode === 'dark') return true;
|
|
if (this.mode === 'light') return false;
|
|
return this.systemPrefersDark;
|
|
},
|
|
},
|
|
mounted() {
|
|
try {
|
|
const stored = localStorage.theme;
|
|
this.mode = stored === 'dark' || stored === 'light' ? stored : 'system';
|
|
} catch (e) {
|
|
this.mode = 'system';
|
|
}
|
|
if (window.matchMedia) {
|
|
this.media = window.matchMedia('(prefers-color-scheme: dark)');
|
|
this.systemPrefersDark = this.media.matches;
|
|
this.media.addEventListener('change', this.onSystemChange);
|
|
}
|
|
this.applyTheme();
|
|
},
|
|
unmounted() {
|
|
if (this.media)
|
|
this.media.removeEventListener('change', this.onSystemChange);
|
|
},
|
|
methods: {
|
|
onSystemChange(e) {
|
|
this.systemPrefersDark = e.matches;
|
|
if (this.mode === 'system') this.applyTheme();
|
|
},
|
|
applyTheme() {
|
|
const dark = this.isDarkActive;
|
|
document.documentElement.classList.remove('dark', 'light');
|
|
document.documentElement.classList.add(dark ? 'dark' : 'light');
|
|
},
|
|
select(value) {
|
|
this.mode = value;
|
|
try {
|
|
if (value === 'system') localStorage.removeItem('theme');
|
|
else localStorage.theme = value;
|
|
} catch (e) {
|
|
/* no-op */
|
|
}
|
|
this.applyTheme();
|
|
this.open = false;
|
|
},
|
|
toggle() {
|
|
this.open = !this.open;
|
|
},
|
|
close() {
|
|
this.open = false;
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div v-on-clickaway="close" class="relative">
|
|
<button
|
|
type="button"
|
|
class="flex items-center justify-center w-9 h-9 text-n-slate-11 hover:text-n-slate-12 hover:bg-n-alpha-2 rounded-lg transition"
|
|
:aria-label="triggerLabel"
|
|
:aria-expanded="open"
|
|
@click="toggle"
|
|
>
|
|
<span class="size-4" :class="[triggerIcon]" aria-hidden="true" />
|
|
</button>
|
|
<div
|
|
v-if="open"
|
|
class="absolute end-0 top-full mt-2 bg-n-slate-1 border border-solid border-n-weak rounded-lg shadow-lg p-1 min-w-36 z-50 flex flex-col gap-0.5"
|
|
>
|
|
<button
|
|
v-for="opt in options"
|
|
:key="opt.value"
|
|
type="button"
|
|
class="w-full flex items-center justify-between gap-2.5 px-2.5 py-2 text-sm rounded-md transition"
|
|
:class="[
|
|
mode === opt.value
|
|
? 'bg-n-portal-soft text-n-portal'
|
|
: 'text-n-slate-11 hover:bg-n-alpha-2 hover:text-n-slate-12',
|
|
]"
|
|
@click="select(opt.value)"
|
|
>
|
|
<span class="flex items-center gap-2">
|
|
<span class="size-4" :class="[opt.icon]" aria-hidden="true" />
|
|
{{ opt.label }}
|
|
</span>
|
|
<span
|
|
v-if="mode === opt.value"
|
|
class="i-lucide-check size-3.5"
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|