From bfc15e81415364bc49a702fe253a75b8f648a9a2 Mon Sep 17 00:00:00 2001 From: Zenith Rifle <84105075+eli32-vlc@users.noreply.github.com> Date: Sun, 4 Jan 2026 16:22:51 +0800 Subject: [PATCH] feat: add monochrome theme support (#4537) * feat: add monochrome theme support * refactor: implement dedicated monochrome mode --- .../theme/components/ThemeDropdown.vue | 22 ++++-- docs/.vitepress/theme/style.scss | 35 ++++++--- docs/.vitepress/theme/themes/themeHandler.ts | 71 ++++++++++++++----- docs/.vitepress/theme/themes/types.ts | 2 +- 4 files changed, 96 insertions(+), 34 deletions(-) diff --git a/docs/.vitepress/theme/components/ThemeDropdown.vue b/docs/.vitepress/theme/components/ThemeDropdown.vue index e7f2d6994..465946360 100644 --- a/docs/.vitepress/theme/components/ThemeDropdown.vue +++ b/docs/.vitepress/theme/components/ThemeDropdown.vue @@ -18,11 +18,15 @@ interface ModeChoice { const modeChoices: ModeChoice[] = [ { mode: 'light', label: 'Light', icon: 'i-ph-sun-duotone' }, { mode: 'dark', label: 'Dark', icon: 'i-ph-moon-duotone' }, - { mode: 'dark', label: 'AMOLED', icon: 'i-ph-moon-stars-duotone', isAmoled: true } + { mode: 'dark', label: 'AMOLED', icon: 'i-ph-moon-stars-duotone', isAmoled: true }, + { mode: 'monochrome', label: 'Monochrome', icon: 'i-ph-circle-half-tilt-duotone' } ] const currentChoice = computed(() => { const current = (mode && (mode as any).value) ? (mode as any).value : 'light' + if (current === 'monochrome') { + return modeChoices[3] // Monochrome option + } if (current === 'dark' && amoledEnabled.value) { return modeChoices[2] // AMOLED option } @@ -34,18 +38,28 @@ const toggleDropdown = () => { } const selectMode = (choice: ModeChoice) => { + setMode(choice.mode) + if (choice.isAmoled) { - setMode('dark') setAmoledEnabled(true) } else { - setMode(choice.mode) - setAmoledEnabled(false) + // Only disable AMOLED if we are explicitly switching away from it + // But wait, if we switch to 'monochrome', 'amoled' flag might still be true? + // It doesn't matter because amoled is only checked if mode is 'dark'. + // However, if we switch back to 'dark', should it be amoled or not? + // Standard behavior: clicking 'Dark' (non-amoled) disables amoled. + if (choice.mode === 'dark') { + setAmoledEnabled(false) + } } isOpen.value = false } const isActiveChoice = (choice: ModeChoice) => { const current = (mode && (mode as any).value) ? (mode as any).value : 'light' + if (choice.mode === 'monochrome') { + return current === 'monochrome' + } if (choice.isAmoled) { return current === 'dark' && amoledEnabled.value } diff --git a/docs/.vitepress/theme/style.scss b/docs/.vitepress/theme/style.scss index 6c1b54432..8ca7d8560 100644 --- a/docs/.vitepress/theme/style.scss +++ b/docs/.vitepress/theme/style.scss @@ -138,17 +138,13 @@ */ :root { --vp-home-hero-name-color: transparent; - --vp-home-hero-name-background: -webkit-linear-gradient( - 120deg, - #c4b5fd 30%, - #7bc5e4 - ); + --vp-home-hero-name-background: -webkit-linear-gradient(120deg, + #c4b5fd 30%, + #7bc5e4); - --vp-home-hero-image-background-image: linear-gradient( - -45deg, - #c4b5fd 50%, - #47caff 50% - ); + --vp-home-hero-image-background-image: linear-gradient(-45deg, + #c4b5fd 50%, + #47caff 50%); --vp-home-hero-image-filter: blur(44px); } @@ -223,6 +219,7 @@ animation: nprogress-spinner 400ms linear infinite; } } + .nprogress-custom-parent { overflow: hidden; position: relative; @@ -253,7 +250,7 @@ } } -#VPContent strong > a { +#VPContent strong>a { font-weight: bold; } @@ -358,4 +355,20 @@ mask-size: 100% 100%; background-color: currentColor; color: inherit; +} + +/* Monochrome Specifics */ +html.monochrome { + filter: grayscale(100%); + + img, + video, + iframe { + filter: grayscale(100%); + } + + ::selection { + background-color: #333; + color: #fff; + } } \ No newline at end of file diff --git a/docs/.vitepress/theme/themes/themeHandler.ts b/docs/.vitepress/theme/themes/themeHandler.ts index 9016cec48..487429693 100644 --- a/docs/.vitepress/theme/themes/themeHandler.ts +++ b/docs/.vitepress/theme/themes/themeHandler.ts @@ -66,8 +66,8 @@ export class ThemeHandler { if (!localStorage.getItem(STORAGE_KEY_MODE)) { this.state.value.currentMode = e.matches ? 'dark' : 'light' this.applyTheme() - } - else { + } + else { this.applyTheme() } }) @@ -80,11 +80,17 @@ export class ThemeHandler { // Is this the WORST fix of all time??? const root = document.documentElement - const bgColor = currentMode === 'dark' && this.amoledEnabled.value ? '#000000' : currentMode === 'dark' ? '#1A1A1A' : '#f8fafc' + const isMonochrome = currentMode === 'monochrome' + + // Monochrome overrides everything to pure black/white + // Standard Dark/Amoled logic applies otherwise + const bgColor = isMonochrome ? '#000000' : currentMode === 'dark' && this.amoledEnabled.value ? '#000000' : currentMode === 'dark' ? '#1A1A1A' : '#f8fafc' root.style.setProperty('--vp-c-bg', bgColor) - const bgAltColor = currentMode === 'dark' && this.amoledEnabled.value ? '#000000' : currentMode === 'dark' ? '#171717' : '#eef2f5' + + const bgAltColor = isMonochrome ? '#000000' : currentMode === 'dark' && this.amoledEnabled.value ? '#000000' : currentMode === 'dark' ? '#171717' : '#eef2f5' root.style.setProperty('--vp-c-bg-alt', bgAltColor) - const bgElvColor = currentMode === 'dark' && this.amoledEnabled.value ? 'rgba(0, 0, 0, 0.9)' : currentMode === 'dark' ? '#1a1a1acc' : 'rgba(255, 255, 255, 0.8)' + + const bgElvColor = isMonochrome ? 'rgba(0, 0, 0, 0.9)' : currentMode === 'dark' && this.amoledEnabled.value ? 'rgba(0, 0, 0, 0.9)' : currentMode === 'dark' ? '#1a1a1acc' : 'rgba(255, 255, 255, 0.8)' root.style.setProperty('--vp-c-bg-elv', bgElvColor) this.applyDOMClasses(currentMode) @@ -99,17 +105,24 @@ export class ThemeHandler { private applyDOMClasses(mode: DisplayMode) { const root = document.documentElement - + // Remove all mode classes - root.classList.remove('dark', 'light', 'amoled') - + root.classList.remove('dark', 'light', 'amoled', 'monochrome') + // Add current mode class root.classList.add(mode) - + // Add amoled class if enabled in dark mode if (mode === 'dark' && this.amoledEnabled.value) { root.classList.add('amoled') } + + // Add monochrome class if enabled + if (mode === 'monochrome') { + root.classList.add('monochrome') + // Also add dark class because monochrome is effectively a high contrast dark mode + root.classList.add('dark') + } } private applyCSSVariables(colors: ModeColors, theme: Theme) { @@ -127,16 +140,32 @@ export class ThemeHandler { let bgColor = colors.bg let bgAltColor = colors.bgAlt let bgElvColor = colors.bgElv - + + const isMonochrome = this.state.value.currentMode === 'monochrome' + if (this.state.value.currentMode === 'dark' && this.amoledEnabled.value) { bgColor = '#000000' bgAltColor = '#000000' bgElvColor = 'rgba(0, 0, 0, 0.9)' } - // Apply brand colors only if theme specifies them - // Otherwise, remove inline styles to let ColorPicker CSS take effect - if (colors.brand && (colors.brand[1] || colors.brand[2] || colors.brand[3] || colors.brand.soft)) { + if (isMonochrome) { + bgColor = '#000000' + bgAltColor = '#000000' + bgElvColor = 'rgba(0, 0, 0, 0.9)' + } + + // Apply brand colors only if theme specifies them OR if monochrome to override + if (isMonochrome) { + root.style.setProperty('--vp-c-brand-1', '#d4d4d4') + root.style.setProperty('--vp-c-brand-2', '#a3a3a3') + root.style.setProperty('--vp-c-brand-3', '#737373') + root.style.setProperty('--vp-c-brand-soft', '#525252') + + root.style.setProperty('--vp-c-text-1', '#ffffff') + root.style.setProperty('--vp-c-text-2', '#a3a3a3') + root.style.setProperty('--vp-c-text-3', '#737373') + } else if (colors.brand && (colors.brand[1] || colors.brand[2] || colors.brand[3] || colors.brand.soft)) { if (colors.brand[1]) root.style.setProperty('--vp-c-brand-1', colors.brand[1]) if (colors.brand[2]) root.style.setProperty('--vp-c-brand-2', colors.brand[2]) if (colors.brand[3]) root.style.setProperty('--vp-c-brand-3', colors.brand[3]) @@ -158,11 +187,12 @@ export class ThemeHandler { } // Apply text colors - always set them to ensure proper theme switching - if (colors.text) { + // Except whenever Monochrome is active, we handled text colors above + if (!isMonochrome && colors.text) { if (colors.text[1]) root.style.setProperty('--vp-c-text-1', colors.text[1]) if (colors.text[2]) root.style.setProperty('--vp-c-text-2', colors.text[2]) if (colors.text[3]) root.style.setProperty('--vp-c-text-3', colors.text[3]) - } else { + } else if (!isMonochrome) { // Remove inline styles if theme doesn't specify text colors // This allows CSS variables from style.scss to take effect root.style.removeProperty('--vp-c-text-1') @@ -284,7 +314,7 @@ export class ThemeHandler { this.state.value.theme = themeRegistry[themeName] localStorage.setItem(STORAGE_KEY_THEME, themeName) this.applyTheme() - + // Force re-apply ColorPicker colors if theme doesn't specify brand colors this.ensureColorPickerColors() } @@ -297,10 +327,10 @@ export class ThemeHandler { public toggleMode() { const currentMode = this.state.value.currentMode - + // Toggle between light and dark const newMode: DisplayMode = currentMode === 'light' ? 'dark' : 'light' - + this.setMode(newMode) } @@ -368,6 +398,10 @@ export class ThemeHandler { public isAmoledMode() { return this.state.value.currentMode === 'dark' && this.amoledEnabled.value } + + public isMonochromeMode() { + return this.state.value.currentMode === 'monochrome' + } } // Global theme handler instance @@ -403,6 +437,7 @@ export function useTheme() { amoledEnabled: handler.getAmoledEnabledRef(), setAmoledEnabled: (enabled: boolean) => handler.setAmoledEnabled(enabled), toggleAmoled: () => handler.toggleAmoled(), + isMonochromeMode: () => handler.isMonochromeMode(), state } } \ No newline at end of file diff --git a/docs/.vitepress/theme/themes/types.ts b/docs/.vitepress/theme/themes/types.ts index de54fa80a..9709c020d 100644 --- a/docs/.vitepress/theme/themes/types.ts +++ b/docs/.vitepress/theme/themes/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export type DisplayMode = 'light' | 'dark' +export type DisplayMode = 'light' | 'dark' | 'monochrome' export interface ModeColors { // Brand colors (optional - if not specified, ColorPicker values are used)