mirror of
https://github.com/fmhy/edit.git
synced 2026-03-11 08:55:38 +00:00
UI fix (#4694)
* blur to social media tooltip * fix theme selector * better transitions when switching to and from amoled * fix mobile search scrolling down * scroll instead of instant when navigating to section * fix unocss in dev mode * fix unocss in dev mode, again * prevent scroll transition when changing pages
This commit is contained in:
parent
e7fe537a73
commit
a6114818b9
7 changed files with 225 additions and 86 deletions
|
|
@ -122,7 +122,9 @@ export default defineConfig({
|
|||
output: ['console', 'terminal']
|
||||
}),
|
||||
UnoCSS({
|
||||
configFile: '../unocss.config.ts'
|
||||
configFile: fileURLToPath(
|
||||
new URL('../../unocss.config.ts', import.meta.url)
|
||||
)
|
||||
}),
|
||||
AutoImport({
|
||||
dts: '../.cache/imports.d.ts',
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useTheme } from '../themes/themeHandler'
|
||||
import type { DisplayMode } from '../themes/types'
|
||||
|
||||
const { mode, setMode, state, amoledEnabled, setAmoledEnabled } = useTheme()
|
||||
const { mode, amoledEnabled, setAppearance } = useTheme()
|
||||
|
||||
const isOpen = ref(false)
|
||||
const dropdownRef = ref<HTMLElement | null>(null)
|
||||
const wrapperRef = ref<HTMLElement | null>(null)
|
||||
|
||||
interface ModeChoice {
|
||||
mode: DisplayMode
|
||||
|
|
@ -29,19 +28,12 @@ const currentChoice = computed(() => {
|
|||
return modeChoices.find(choice => choice.mode === current && !choice.isAmoled) || modeChoices[0]
|
||||
})
|
||||
|
||||
const toggleDropdown = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
|
||||
const selectMode = (choice: ModeChoice) => {
|
||||
if (choice.isAmoled) {
|
||||
setMode('dark')
|
||||
setAmoledEnabled(true)
|
||||
setAppearance('dark', true)
|
||||
} else {
|
||||
setMode(choice.mode)
|
||||
setAmoledEnabled(false)
|
||||
setAppearance(choice.mode, false)
|
||||
}
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
const isActiveChoice = (choice: ModeChoice) => {
|
||||
|
|
@ -52,56 +44,112 @@ const isActiveChoice = (choice: ModeChoice) => {
|
|||
return choice.mode === current && !choice.isAmoled && !amoledEnabled.value
|
||||
}
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
|
||||
isOpen.value = false
|
||||
// Logic to override the parent VPFlyout behavior to be click-based
|
||||
const setupParentFlyoutOverride = () => {
|
||||
if (!wrapperRef.value) return
|
||||
|
||||
const flyout = wrapperRef.value.closest('.VPFlyout')
|
||||
if (!flyout) return
|
||||
|
||||
// Add class to disable CSS hover via global style
|
||||
flyout.classList.add('click-based-flyout')
|
||||
|
||||
// Find the toggle button
|
||||
const button = flyout.querySelector('button')
|
||||
if (!button) return
|
||||
|
||||
// Click handler for toggle
|
||||
const toggleFlyout = (e: MouseEvent) => {
|
||||
flyout.classList.toggle('open')
|
||||
}
|
||||
|
||||
button.addEventListener('click', toggleFlyout)
|
||||
|
||||
// Global click listener to close when clicking outside
|
||||
const closeFlyout = (e: MouseEvent) => {
|
||||
if (!flyout.contains(e.target as Node)) {
|
||||
flyout.classList.remove('open')
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', closeFlyout)
|
||||
|
||||
;(wrapperRef.value as any)._cleanup = () => {
|
||||
flyout.classList.remove('click-based-flyout')
|
||||
button.removeEventListener('click', toggleFlyout)
|
||||
document.removeEventListener('click', closeFlyout)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
// defer slightly to ensuring DOM is ready
|
||||
setTimeout(setupParentFlyoutOverride, 100)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
if (wrapperRef.value && (wrapperRef.value as any)._cleanup) {
|
||||
;(wrapperRef.value as any)._cleanup()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="dropdownRef" class="theme-dropdown">
|
||||
<button
|
||||
type="button"
|
||||
class="theme-dropdown-toggle"
|
||||
:title="currentChoice.label"
|
||||
@click="toggleDropdown"
|
||||
<div ref="wrapperRef" class="theme-dropdown-wrapper">
|
||||
<VDropdown
|
||||
class="theme-dropdown"
|
||||
theme="theme-selector"
|
||||
:distance="12"
|
||||
placement="bottom-end"
|
||||
:triggers="['click']"
|
||||
:popper-triggers="['click']"
|
||||
:auto-hide="true"
|
||||
>
|
||||
<ClientOnly>
|
||||
<div :class="[currentChoice.icon, 'text-xl']" />
|
||||
</ClientOnly>
|
||||
</button>
|
||||
|
||||
<Transition name="dropdown">
|
||||
<div v-if="isOpen" class="theme-dropdown-menu">
|
||||
<button
|
||||
v-for="(choice, index) in modeChoices"
|
||||
:key="index"
|
||||
class="theme-dropdown-item"
|
||||
:class="{ active: isActiveChoice(choice) }"
|
||||
@click="selectMode(choice)"
|
||||
type="button"
|
||||
class="theme-dropdown-toggle"
|
||||
:title="currentChoice.label"
|
||||
>
|
||||
<div :class="[choice.icon, 'text-lg']" />
|
||||
<span>{{ choice.label }}</span>
|
||||
<div v-if="isActiveChoice(choice)" class="i-ph-check text-lg ml-auto" />
|
||||
<ClientOnly>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div :key="currentChoice.label" :class="[currentChoice.icon, 'text-xl']" />
|
||||
</Transition>
|
||||
</ClientOnly>
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<template #popper>
|
||||
<div class="theme-dropdown-content">
|
||||
<button
|
||||
v-for="(choice, index) in modeChoices"
|
||||
:key="index"
|
||||
class="theme-dropdown-item"
|
||||
:class="{ active: isActiveChoice(choice) }"
|
||||
@click="selectMode(choice)"
|
||||
v-close-popper
|
||||
>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div :key="choice.label" :class="[choice.icon, 'text-lg']" />
|
||||
</Transition>
|
||||
<span>{{ choice.label }}</span>
|
||||
<div v-if="isActiveChoice(choice)" class="i-ph-check text-lg ml-auto" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.theme-dropdown-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.theme-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.theme-dropdown-toggle {
|
||||
|
|
@ -121,25 +169,12 @@ onUnmounted(() => {
|
|||
color: var(--vp-c-text-1);
|
||||
background: var(--vp-c-bg-elv);
|
||||
transition: color 0.25s, background 0.25s;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
.theme-dropdown-content {
|
||||
min-width: 180px;
|
||||
background: var(--vp-c-bg-elv);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
padding: 6px;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(12px);
|
||||
|
||||
.dark & {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-dropdown-item {
|
||||
|
|
@ -171,14 +206,13 @@ onUnmounted(() => {
|
|||
}
|
||||
}
|
||||
|
||||
.dropdown-enter-active,
|
||||
.dropdown-leave-active {
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.dropdown-enter-from,
|
||||
.dropdown-leave-to {
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -317,11 +317,16 @@ debouncedWatch(
|
|||
await mergeNearbyMarks()
|
||||
}
|
||||
|
||||
const excerpts = Array.from(el.value?.querySelectorAll('.result .excerpt') ?? [])
|
||||
const excerpts = Array.from(el.value?.querySelectorAll('.result .excerpt') ?? []) as HTMLElement[]
|
||||
for (const excerpt of excerpts) {
|
||||
excerpt
|
||||
.querySelector('mark[data-markjs="true"]')
|
||||
?.scrollIntoView({ block: 'center' })
|
||||
const mark = excerpt.querySelector('mark[data-markjs="true"]') as HTMLElement | null
|
||||
if (mark) {
|
||||
const markTop = mark.offsetTop
|
||||
const markHeight = mark.offsetHeight
|
||||
const excerptHeight = excerpt.clientHeight
|
||||
|
||||
excerpt.scrollTop = markTop - excerptHeight / 2 + markHeight / 2
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -343,8 +348,10 @@ debouncedWatch(
|
|||
resultMarks.value = newResultMarks
|
||||
currentMarkIndex.value = newCurrentMarkIndex
|
||||
|
||||
// FIXME: without this whole page scrolls to the bottom
|
||||
resultsEl.value?.firstElementChild?.scrollIntoView({ block: 'start' })
|
||||
// Reset scroll position to top
|
||||
if (resultsEl.value) {
|
||||
resultsEl.value.scrollTop = 0
|
||||
}
|
||||
},
|
||||
{ debounce: 200, immediate: true }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,35 @@ export default {
|
|||
app.component('Feedback', Feedback)
|
||||
app.component('Tooltip', Tooltip)
|
||||
loadProgress(router)
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const originalBefore = router.onBeforeRouteChange
|
||||
const originalAfter = router.onAfterRouteChanged
|
||||
|
||||
router.onBeforeRouteChange = (to) => {
|
||||
try {
|
||||
// Force scroll-behavior: auto (instant) when changing pages (path),
|
||||
// preventing the "scroll to top" animation.
|
||||
// Smooth scrolling is preserved for same-page hash/anchor changes.
|
||||
const targetUrl = new URL(to, window.location.href)
|
||||
if (targetUrl.pathname !== window.location.pathname) {
|
||||
document.documentElement.style.scrollBehavior = 'auto'
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback if URL parsing fails
|
||||
}
|
||||
originalBefore?.(to)
|
||||
}
|
||||
|
||||
router.onAfterRouteChanged = (to) => {
|
||||
originalAfter?.(to)
|
||||
// Re-enable smooth scrolling shortly after navigation completes
|
||||
setTimeout(() => {
|
||||
document.documentElement.style.scrollBehavior = 'smooth'
|
||||
}, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize theme handler
|
||||
useThemeHandler()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
body {
|
||||
transition: background-color 0.5s cubic-bezier(0.25, 0.8, 0.25, 1), color 0.5s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
/* Colors: Brand */
|
||||
--vp-c-brand-1: theme('colors.swarm.500');
|
||||
--vp-c-brand-2: theme('colors.swarm.600');
|
||||
|
|
@ -400,3 +405,51 @@ .VPLocalSearchBox .backdrop {
|
|||
.dark .VPLocalSearchBox .backdrop {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* Glassy Tooltips */
|
||||
.v-popper--theme-tooltip .v-popper__inner {
|
||||
background: var(--vp-c-bg-elv) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
border: 1px solid var(--vp-c-divider) !important;
|
||||
color: var(--vp-c-text-1) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.v-popper--theme-tooltip .v-popper__arrow {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Global theme styles for the dropdown to ensure correct box appearance */
|
||||
.v-popper--theme-theme-selector .v-popper__inner {
|
||||
background: var(--vp-c-bg-elv);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
padding: 6px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.dark .v-popper--theme-theme-selector .v-popper__inner {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.v-popper--theme-theme-selector .v-popper__arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Override VPFlyout hover behavior when manually controlled */
|
||||
.VPFlyout.click-based-flyout:hover .menu {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
transform: translateY(0) !important;
|
||||
/* Reset transform if any */
|
||||
}
|
||||
|
||||
.VPFlyout.click-based-flyout.open .menu {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
|
@ -106,15 +106,21 @@ export class ThemeHandler {
|
|||
private applyDOMClasses(mode: DisplayMode) {
|
||||
const root = document.documentElement
|
||||
|
||||
// Remove all mode classes
|
||||
root.classList.remove('dark', 'light', 'amoled')
|
||||
const isDark = mode === 'dark'
|
||||
const isAmoled = isDark && this.amoledEnabled.value
|
||||
|
||||
// Add current mode class
|
||||
root.classList.add(mode)
|
||||
if (isDark) {
|
||||
if (!root.classList.contains('dark')) root.classList.add('dark')
|
||||
if (root.classList.contains('light')) root.classList.remove('light')
|
||||
} else {
|
||||
if (!root.classList.contains('light')) root.classList.add('light')
|
||||
if (root.classList.contains('dark')) root.classList.remove('dark')
|
||||
}
|
||||
|
||||
// Add amoled class if enabled in dark mode
|
||||
if (mode === 'dark' && this.amoledEnabled.value) {
|
||||
root.classList.add('amoled')
|
||||
if (isAmoled) {
|
||||
if (!root.classList.contains('amoled')) root.classList.add('amoled')
|
||||
} else {
|
||||
if (root.classList.contains('amoled')) root.classList.remove('amoled')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,12 +130,12 @@ export class ThemeHandler {
|
|||
const root = document.documentElement
|
||||
|
||||
// Clear ALL inline styles related to theming to ensure clean slate
|
||||
const allStyleProps = Array.from(root.style)
|
||||
allStyleProps.forEach(prop => {
|
||||
if (prop.startsWith('--vp-')) {
|
||||
root.style.removeProperty(prop)
|
||||
}
|
||||
})
|
||||
// const allStyleProps = Array.from(root.style)
|
||||
// allStyleProps.forEach(prop => {
|
||||
// if (prop.startsWith('--vp-')) {
|
||||
// root.style.removeProperty(prop)
|
||||
// }
|
||||
// })
|
||||
let bgColor = colors.bg
|
||||
let bgAltColor = colors.bgAlt
|
||||
let bgElvColor = colors.bgElv
|
||||
|
|
@ -296,6 +302,14 @@ export class ThemeHandler {
|
|||
this.setMode(newMode)
|
||||
}
|
||||
|
||||
public setAppearance(mode: DisplayMode, amoled: boolean) {
|
||||
this.state.value.currentMode = mode
|
||||
this.amoledEnabled.value = amoled
|
||||
localStorage.setItem(STORAGE_KEY_MODE, mode)
|
||||
localStorage.setItem(STORAGE_KEY_AMOLED, amoled.toString())
|
||||
this.applyTheme()
|
||||
}
|
||||
|
||||
public setAmoledEnabled(enabled: boolean) {
|
||||
this.amoledEnabled.value = enabled
|
||||
localStorage.setItem(STORAGE_KEY_AMOLED, enabled.toString())
|
||||
|
|
@ -332,7 +346,6 @@ export class ThemeHandler {
|
|||
public getState() {
|
||||
return this.state
|
||||
}
|
||||
|
||||
public getMode() {
|
||||
return this.state.value.currentMode
|
||||
}
|
||||
|
|
@ -395,6 +408,7 @@ export function useTheme() {
|
|||
amoledEnabled: handler.getAmoledEnabledRef(),
|
||||
setAmoledEnabled: (enabled: boolean) => handler.setAmoledEnabled(enabled),
|
||||
toggleAmoled: () => handler.toggleAmoled(),
|
||||
setAppearance: (mode: DisplayMode, amoled: boolean) => handler.setAppearance(mode, amoled),
|
||||
state
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ const createColorRules = (type: 'text' | 'bg' | 'border'): Rule[] => {
|
|||
|
||||
export default defineConfig({
|
||||
content: {
|
||||
filesystem: ['.vitepress/config.mts', '.vitepress/constants.ts']
|
||||
filesystem: ['.vitepress/config.mts', '.vitepress/constants.ts', '.vitepress/shared.ts']
|
||||
},
|
||||
theme: {
|
||||
colors: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue