Browse Source

Persistent color theme

master
Riyyi 2 weeks ago
parent
commit
64a8aecf25
  1. 1
      nuxt.config.ts
  2. BIN
      public/favicon.ico
  3. 3
      src/app.vue
  4. 124
      src/app/spa-loading-template.html
  5. 2
      src/components.d.ts
  6. 56
      src/components/ColorMode.vue
  7. 23
      src/components/ToggleColorMode.vue
  8. 2
      src/layouts/default.vue
  9. 38
      src/stores/stateStore.ts

1
nuxt.config.ts

@ -47,7 +47,6 @@ export default defineNuxtConfig({
}, },
piniaPluginPersistedstate: { piniaPluginPersistedstate: {
debug: process.env.NODE_ENV === "development", // log error to console debug: process.env.NODE_ENV === "development", // log error to console
storage: "cookies",
cookieOptions: { cookieOptions: {
sameSite: "lax", // prevent CSRF sameSite: "lax", // prevent CSRF
secure: process.env.NODE_ENV !== "development" // only send over HTTPS secure: process.env.NODE_ENV !== "development" // only send over HTTPS

BIN
public/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 12 KiB

3
src/app.vue

@ -12,6 +12,9 @@ import { useStateStore } from "@/stores/stateStore";
const router = useRouter(); const router = useRouter();
const store = useStateStore(); const store = useStateStore();
// Set dark theme
store.applyColorMode();
useHead({ useHead({
titleTemplate: (titleChunk: string | undefined): string | null => { titleTemplate: (titleChunk: string | undefined): string | null => {
return titleChunk ? `${titleChunk} - website-vue` : 'website-vue'; return titleChunk ? `${titleChunk} - website-vue` : 'website-vue';

124
src/app/spa-loading-template.html

@ -1,41 +1,85 @@
<!-- https://github.com/barelyhuman/snips/blob/dev/pages/css-loader.md --> <!-- https://github.com/barelyhuman/snips/blob/dev/pages/css-loader.md -->
<div class="loader"></div> <!DOCTYPE html>
<style> <html lang="en">
.loader { <head>
display: block; <style>
position: fixed; body {
z-index: 1031; background-color: #fff;
top: 50%; }
left: 50%;
transform: translate(-50%, -50%); .dark {
width: 36px; background-color: #1a1d20;
height: 36px; }
box-sizing: border-box;
border: solid 2px transparent; .loader {
border-top-color: #000; display: block;
border-left-color: #000; position: fixed;
border-bottom-color: #efefef; z-index: 1031;
border-right-color: #efefef; top: 50%;
border-radius: 50%; left: 50%;
-webkit-animation: loader 400ms linear infinite; transform: translate(-50%, -50%);
animation: loader 400ms linear infinite; width: 36px;
} height: 36px;
box-sizing: border-box;
@-webkit-keyframes loader { border: solid 2px transparent;
0% { border-top-color: #000;
-webkit-transform: translate(-50%, -50%) rotate(0deg); border-left-color: #000;
} border-bottom-color: #efefef;
100% { border-right-color: #efefef;
-webkit-transform: translate(-50%, -50%) rotate(360deg); border-radius: 50%;
} -webkit-animation: loader 400ms linear infinite;
} animation: loader 400ms linear infinite;
}
@keyframes loader {
0% { .dark .loader {
transform: translate(-50%, -50%) rotate(0deg); border-top-color: #fff !important;
} border-left-color: #fff !important;
100% { border-bottom-color: #444 !important;
transform: translate(-50%, -50%) rotate(360deg); border-right-color: #444 !important;
} }
}
</style> @-webkit-keyframes loader {
0% {
-webkit-transform: translate(-50%, -50%) rotate(0deg);
}
100% {
-webkit-transform: translate(-50%, -50%) rotate(360deg);
}
}
@keyframes loader {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
</style>
</head>
<body>
<div class="loader"></div>
<script>
let css = "light";
let state = null;
const json = localStorage.getItem("state");
if (json !== null) {
state = JSON.parse(json).colorMode;
}
if (state === null || state === "auto") {
const preferDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (preferDark) {
css = "dark";
}
}
else if (state === "dark") {
css = "dark";
}
const element = document.querySelector("html");
element.classList.add(css);
</script>
</body>
</html>

2
src/components.d.ts vendored

@ -10,6 +10,7 @@ declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
IBi0Circle: typeof import('~icons/bi/0-circle')['default'] IBi0Circle: typeof import('~icons/bi/0-circle')['default']
IBiActivity: typeof import('~icons/bi/activity')['default'] IBiActivity: typeof import('~icons/bi/activity')['default']
IFaAdjust: typeof import('~icons/fa/adjust')['default']
IFaAngellist: typeof import('~icons/fa/angellist')['default'] IFaAngellist: typeof import('~icons/fa/angellist')['default']
IFaCheck: typeof import('~icons/fa/check')['default'] IFaCheck: typeof import('~icons/fa/check')['default']
IFaClone: typeof import('~icons/fa/clone')['default'] IFaClone: typeof import('~icons/fa/clone')['default']
@ -22,6 +23,7 @@ declare module 'vue' {
IFaLink: typeof import('~icons/fa/link')['default'] IFaLink: typeof import('~icons/fa/link')['default']
IFaLinkedinSquare: typeof import('~icons/fa/linkedin-square')['default'] IFaLinkedinSquare: typeof import('~icons/fa/linkedin-square')['default']
IFaMoonO: typeof import('~icons/fa/moon-o')['default'] IFaMoonO: typeof import('~icons/fa/moon-o')['default']
IFaSolidMoon: typeof import('~icons/fa-solid/moon')['default']
IFaSolidSun: typeof import('~icons/fa-solid/sun')['default'] IFaSolidSun: typeof import('~icons/fa-solid/sun')['default']
IMdiAccountBox: typeof import('~icons/mdi/account-box')['default'] IMdiAccountBox: typeof import('~icons/mdi/account-box')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']

56
src/components/ColorMode.vue

@ -0,0 +1,56 @@
<template>
<div class="dropup position-fixed corner">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<template v-if="store.colorMode === 'light'">
<IFaSolidSun />
</template>
<template v-else-if="store.colorMode === 'dark'">
<IFaSolidMoon />
</template>
<template v-else>
<IFaAdjust />
</template>
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" :class="store.colorMode === 'light' ? 'active' : ''" @click="store.setColorMode('light')">
<IFaSolidSun /> Light &nbsp;
<IFaCheck v-if="store.colorMode === 'light'" class="font-smaller float-right" />
</a>
</li>
<li>
<a class="dropdown-item" :class="store.colorMode === 'dark' ? 'active' : ''" @click="store.setColorMode('dark')">
<IFaSolidMoon /> Dark
<IFaCheck v-if="store.colorMode === 'dark'" class="font-smaller float-right" />
</a>
</li>
<li>
<a class="dropdown-item" :class="store.colorMode === 'auto' ? 'active' : ''" @click="store.setColorMode('auto')">
<IFaAdjust width="1.3em" /> Auto
<IFaCheck v-if="store.colorMode === 'auto'" class="font-smaller float-right" />
</a>
</li>
</ul>
</div>
</template>
<style scoped>
.corner {
right: 10px;
bottom: 10px;
}
.dropdown-menu {
min-width: 125px;
}
.font-smaller {
font-size: .6rem;
}
</style>
<script setup lang="ts">
import { useStateStore } from "@/stores/stateStore";
const store = useStateStore();
</script>

23
src/components/ToggleColorMode.vue

@ -1,23 +0,0 @@
<template>
<div class="position-fixed corner">
<template v-if="store.colorMode === 'dark'">
<IFaSolidSun @click="store.toggleColorMode" />
</template>
<template v-else>
<IFaMoonO @click="store.toggleColorMode" />
</template>
</div>
</template>
<style scoped>
.corner {
right: 10px;
bottom: 10px;
}
</style>
<script setup lang="ts">
import { useStateStore } from "@/stores/stateStore";
const store = useStateStore();
</script>

2
src/layouts/default.vue

@ -10,7 +10,7 @@
</div> </div>
<SharedFooter /> <SharedFooter />
</div> </div>
<ToggleColorMode /> <ColorMode />
</template> </template>
<style scoped> <style scoped>

38
src/stores/stateStore.ts

@ -6,21 +6,34 @@ import bootstrap from "bootstrap/dist/js/bootstrap.bundle.min";
export const useStateStore = defineStore("state", () => { export const useStateStore = defineStore("state", () => {
const colorMode = ref<string>("light"); const colorMode = ref<string>("auto");
const toggleColorMode = (): void => { const setColorMode = (newMode: string): void => {
colorMode.value = colorMode.value === "dark" ? "light" : "dark"; colorMode.value = newMode;
const html = document.querySelector("html") as HTMLElement; applyColorMode();
};
const applyColorMode = (): void => {
let css = "light";
if (colorMode.value === "auto") {
const preferDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (preferDark) {
css = "dark";
}
}
else if (colorMode.value == "dark") {
css = "dark";
}
const html = document.documentElement as HTMLElement;
// Theme used by Bootstrap // Theme used by Bootstrap
html.setAttribute('data-bs-theme', colorMode.value); html.setAttribute('data-bs-theme', css);
// Theme class used by shiki syntax highlighting // Theme class used by shiki syntax highlighting
if (colorMode.value === "dark") { html.classList.remove("dark");
if (css === "dark") {
html.classList.add("dark"); html.classList.add("dark");
} else {
html.classList.remove("dark");
} }
}; }
const popoverList = ref<any[]>([]); const popoverList = ref<any[]>([]);
const tooltipList = ref<any[]>([]); const tooltipList = ref<any[]>([]);
@ -37,7 +50,10 @@ export const useStateStore = defineStore("state", () => {
tooltipList.value = [...tooltipTriggerList].map(tooltip => new bootstrap.Tooltip(tooltip)); tooltipList.value = [...tooltipTriggerList].map(tooltip => new bootstrap.Tooltip(tooltip));
}; };
return { colorMode, toggleColorMode, initBootstrap } return { colorMode, setColorMode, applyColorMode, initBootstrap }
}, { }, {
persist: process.env.NODE_ENV === 'development' ? true : false, persist: {
pick: ["colorMode"],
storage: localStorage
}
}) })

Loading…
Cancel
Save