diff --git a/src/assets/icons/link.svg b/src/assets/icons/link.svg new file mode 100644 index 00000000..09c80683 --- /dev/null +++ b/src/assets/icons/link.svg @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts new file mode 100644 index 00000000..b3a750a6 --- /dev/null +++ b/src/hooks/useTheme.ts @@ -0,0 +1,104 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ref, computed } from "vue"; +import { Themes } from "@/constants/data"; +import { useAppStoreWithOut } from "@/store/modules/app"; + +export function useTheme() { + const appStore = useAppStoreWithOut(); + const theme = ref(true); + const themeSwitchRef = ref(); + + // Initialize theme from localStorage or system preference + function initializeTheme() { + const savedTheme = window.localStorage.getItem("theme-is-dark"); + let isDark = true; // default to dark theme + + if (savedTheme === "false") { + isDark = false; + } else if (savedTheme === "") { + // read the theme preference from system setting if there is no user setting + isDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; + } + + theme.value = isDark; + applyTheme(); + } + + // Apply theme to DOM and store + function applyTheme() { + const root = document.documentElement; + + if (theme.value) { + root.classList.add(Themes.Dark); + root.classList.remove(Themes.Light); + appStore.setTheme(Themes.Dark); + } else { + root.classList.add(Themes.Light); + root.classList.remove(Themes.Dark); + appStore.setTheme(Themes.Light); + } + + window.localStorage.setItem("theme-is-dark", String(theme.value)); + } + + // Handle theme change with transition animation + function handleChangeTheme() { + const x = themeSwitchRef.value?.offsetLeft ?? 0; + const y = themeSwitchRef.value?.offsetTop ?? 0; + const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y)); + + // compatibility handling + if (!document.startViewTransition) { + applyTheme(); + return; + } + + // api: https://developer.chrome.com/docs/web-platform/view-transitions + const transition = document.startViewTransition(() => { + applyTheme(); + }); + + transition.ready.then(() => { + const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`]; + document.documentElement.animate( + { + clipPath: !theme.value ? clipPath.reverse() : clipPath, + }, + { + duration: 500, + easing: "ease-in", + pseudoElement: !theme.value ? "::view-transition-old(root)" : "::view-transition-new(root)", + }, + ); + }); + } + + // Computed properties + const isDark = computed(() => theme.value); + const isLight = computed(() => !theme.value); + + return { + theme, + themeSwitchRef, + isDark, + isLight, + initializeTheme, + applyTheme, + handleChangeTheme, + }; +} diff --git a/src/layout/Index.vue b/src/layout/Index.vue index 902d95e4..ef8f27f9 100644 --- a/src/layout/Index.vue +++ b/src/layout/Index.vue @@ -14,15 +14,31 @@ See the License for the specific language governing permissions and limitations under the License. -->