mirror of
https://github.com/fmhy/edit.git
synced 2026-03-11 08:55:38 +00:00
Custom Color Theme Selector (#4641)
* Add custom theme color selector to ColorPicker Introduces a CustomColorSelector component and integrates it into the ColorPicker, allowing users to define and persist custom link, text, and background colors for the site theme. Updates dependencies to include tinycolor2 and its types for color manipulation. * Add custom theme mode with improved color handling Introduces a 'custom' display mode for themes, allowing users to define and persist their own color schemes. Updates ColorPicker and ThemeDropdown to support the custom mode, including UI logic to prevent switching from custom to default modes without explicit action. Enhances themeHandler to register and apply custom themes from localStorage, manage previous mode restoration, and apply additional CSS variables for custom backgrounds. * Remove unused variables from theme components Cleaned up ColorPicker.vue and ThemeDropdown.vue by removing unused variables and functions related to theme state. This improves code clarity and maintainability. * Remove close on overlay click in color selector modal The @click.self handler was removed from the modal overlay, so clicking the overlay no longer closes the CustomColorSelector modal. * Increase card background lightening for custom themes Adjusted the lightening values for card backgrounds in custom themes from 5/8 to 10/15 to improve visual distinction between cards and the main background. * Improve custom color theme handling in color picker Exclude the 'custom' theme from preset theme options in ColorPicker.vue and correct button text color assignments for custom themes. In CustomColorSelector.vue, update button styles to reflect selected custom colors dynamically, enhancing the user experience when previewing and applying custom color selections. * Update Vue version and config improvements Bump Vue dependency to 3.5.0 in package.json. Add SCSS preprocessor option to VitePress config for modern compiler API. Refactor UnoCSS config to use kebab-case CSS property names for consistency. * Update pnpm lockfile Regenerated pnpm-lock.yaml to reflect updated dependencies. * Update header description text color style Replaces the description paragraph's class-based text color with an inline style using the CSS variable '--vp-c-text-1' for improved consistency with theming. * Add contrast warnings to color selector Introduces computed warnings for low contrast between link/text and background colors based on WCAG AA standards. Displays warning messages in the UI when contrast ratios fall below 4.5:1 to improve accessibility awareness.
This commit is contained in:
parent
f19d5043d0
commit
a71bb4c0d0
11 changed files with 1014 additions and 30 deletions
|
|
@ -207,6 +207,13 @@ export default defineConfig({
|
|||
build: {
|
||||
// Shut the fuck up
|
||||
chunkSizeWarningLimit: Number.POSITIVE_INFINITY
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: 'modern-compiler'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
markdown: {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,37 @@
|
|||
<script setup lang="ts">
|
||||
import { colors } from '@fmhy/colors'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { watch, onMounted, nextTick } from 'vue'
|
||||
import { watch, onMounted, nextTick, ref, computed } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import { useTheme } from '../themes/themeHandler'
|
||||
import { themeRegistry } from '../themes/configs'
|
||||
import type { Theme } from '../themes/types'
|
||||
import Switch from './Switch.vue'
|
||||
import CustomColorSelector from './CustomColorSelector.vue'
|
||||
import tinycolor from 'tinycolor2'
|
||||
|
||||
type ColorNames = keyof typeof colors
|
||||
const selectedColor = useStorage<ColorNames>('preferred-color', 'swarm')
|
||||
|
||||
const { frontmatter, page } = useData()
|
||||
|
||||
const showPalette = computed(() => {
|
||||
// console.log('Current layout:', frontmatter.value.layout)
|
||||
// console.log('Page relative path:', page.value.relativePath)
|
||||
return frontmatter.value.layout !== 'home' && page.value.relativePath !== 'index.md'
|
||||
})
|
||||
|
||||
// Use the theme system
|
||||
const { amoledEnabled, setAmoledEnabled, setTheme, state, mode, themeName } = useTheme()
|
||||
const { amoledEnabled, setAmoledEnabled, setTheme, setMode, mode, themeName, restorePreviousMode } = useTheme()
|
||||
|
||||
// Custom color selector state
|
||||
const showCustomColorSelector = ref(false)
|
||||
|
||||
const colorOptions = Object.keys(colors).filter(
|
||||
(key) => typeof colors[key as keyof typeof colors] === 'object'
|
||||
) as Array<ColorNames>
|
||||
|
||||
// Preset themes (exclude dynamically generated color- themes)
|
||||
const presetThemeNames = Object.keys(themeRegistry).filter((k) => !k.startsWith('color-'))
|
||||
// Preset themes (exclude dynamically generated color- themes and custom theme)
|
||||
const presetThemeNames = Object.keys(themeRegistry).filter((k) => !k.startsWith('color-') && k !== 'custom')
|
||||
|
||||
const getThemePreviewStyle = (name: string) => {
|
||||
const theme = themeRegistry[name]
|
||||
|
|
@ -196,28 +209,217 @@ const normalizeColorName = (colorName: string) =>
|
|||
colorName.slice(1).replaceAll(/-/g, ' ')
|
||||
|
||||
onMounted(async () => {
|
||||
// apply saved theme on load
|
||||
if (selectedColor.value) {
|
||||
// Check if custom theme was last selected
|
||||
const savedTheme = localStorage.getItem('vitepress-theme-name')
|
||||
|
||||
if (savedTheme === 'custom') {
|
||||
// Load saved custom colors
|
||||
const savedLinkColor = localStorage.getItem('custom-theme-link-color') || '#ffffff'
|
||||
const savedTextColor = localStorage.getItem('custom-theme-text-color') || '#cccccc'
|
||||
const savedBgColor = localStorage.getItem('custom-theme-bg-color') || '#000000'
|
||||
|
||||
// Apply the custom theme
|
||||
applyCustomColors({ link: savedLinkColor, text: savedTextColor, background: savedBgColor })
|
||||
} else if (selectedColor.value) {
|
||||
// apply saved color theme on load
|
||||
const theme = generateThemeFromColor(selectedColor.value)
|
||||
themeRegistry[`color-${selectedColor.value}`] = theme
|
||||
await nextTick()
|
||||
setTheme(`color-${selectedColor.value}`)
|
||||
}
|
||||
|
||||
// Wait for next tick to ensure theme handler is fully initialized
|
||||
await nextTick()
|
||||
})
|
||||
|
||||
watch(selectedColor, async (color) => {
|
||||
if (!color) return;
|
||||
// Restore previous mode when switching away from custom
|
||||
restorePreviousMode()
|
||||
const theme = generateThemeFromColor(color)
|
||||
themeRegistry[`color-${color}`] = theme
|
||||
await nextTick()
|
||||
setTheme(`color-${color}`)
|
||||
})
|
||||
|
||||
const toggleAmoled = () => {
|
||||
setAmoledEnabled(!amoledEnabled.value)
|
||||
|
||||
|
||||
const openCustomColorSelector = () => {
|
||||
showCustomColorSelector.value = true
|
||||
}
|
||||
|
||||
const applyCustomColors = (colors: { link: string; text: string; background: string }) => {
|
||||
// Store custom colors
|
||||
const customLinkColor = useStorage('custom-theme-link-color', colors.link)
|
||||
const customTextColor = useStorage('custom-theme-text-color', colors.text)
|
||||
const customBgColor = useStorage('custom-theme-bg-color', colors.background)
|
||||
|
||||
customLinkColor.value = colors.link
|
||||
customTextColor.value = colors.text
|
||||
customBgColor.value = colors.background
|
||||
|
||||
// Create lighter versions of background for cards
|
||||
// Increase lightening to make cards more distinct
|
||||
const lightenedBg = tinycolor(colors.background).lighten(10).toString()
|
||||
const lightenedBgAlt = tinycolor(colors.background).lighten(15).toString()
|
||||
|
||||
// Generate a custom theme - link color for links, text color for body text
|
||||
const customTheme: Theme = {
|
||||
name: 'custom',
|
||||
displayName: 'Custom',
|
||||
preview: colors.background,
|
||||
modes: {
|
||||
light: {
|
||||
brand: {
|
||||
1: colors.link, // Links will use this color
|
||||
2: colors.link,
|
||||
3: colors.link,
|
||||
soft: colors.link
|
||||
},
|
||||
bg: colors.background,
|
||||
bgAlt: lightenedBg,
|
||||
bgElv: lightenedBgAlt,
|
||||
text: {
|
||||
1: colors.text, // Body text uses this color
|
||||
2: colors.text,
|
||||
3: colors.text
|
||||
},
|
||||
button: {
|
||||
brand: {
|
||||
bg: colors.link,
|
||||
border: colors.link,
|
||||
text: colors.text,
|
||||
hoverBorder: colors.link,
|
||||
hoverText: colors.text,
|
||||
hoverBg: colors.link,
|
||||
activeBorder: colors.link,
|
||||
activeText: colors.text,
|
||||
activeBg: colors.link
|
||||
},
|
||||
alt: {
|
||||
bg: '#484848',
|
||||
text: '#f0eeee',
|
||||
hoverBg: '#484848',
|
||||
hoverText: '#f0eeee'
|
||||
}
|
||||
},
|
||||
customBlock: {
|
||||
info: {
|
||||
bg: colors.background,
|
||||
border: colors.link,
|
||||
text: colors.text,
|
||||
textDeep: colors.text
|
||||
},
|
||||
tip: {
|
||||
bg: '#D8F8E4',
|
||||
border: '#447A61',
|
||||
text: '#2D6A58',
|
||||
textDeep: '#166534'
|
||||
},
|
||||
warning: {
|
||||
bg: '#FCEFC3',
|
||||
border: '#9A8034',
|
||||
text: '#9C701B',
|
||||
textDeep: '#92400e'
|
||||
},
|
||||
danger: {
|
||||
bg: '#FBE1E2',
|
||||
border: '#B3565E',
|
||||
text: '#912239',
|
||||
textDeep: '#991b1b'
|
||||
}
|
||||
},
|
||||
selection: {
|
||||
bg: colors.link
|
||||
},
|
||||
home: {
|
||||
heroNameColor: colors.link,
|
||||
heroNameBackground: colors.background,
|
||||
heroImageBackground: `linear-gradient(135deg, ${colors.background} 0%, ${colors.link} 100%)`,
|
||||
heroImageFilter: 'blur(44px)'
|
||||
}
|
||||
},
|
||||
dark: {
|
||||
brand: {
|
||||
1: colors.link, // Links will use this color
|
||||
2: colors.link,
|
||||
3: colors.link,
|
||||
soft: colors.link
|
||||
},
|
||||
bg: colors.background,
|
||||
bgAlt: lightenedBg,
|
||||
bgElv: lightenedBgAlt,
|
||||
text: {
|
||||
1: colors.text, // Body text uses this color
|
||||
2: colors.text,
|
||||
3: colors.text
|
||||
},
|
||||
button: {
|
||||
brand: {
|
||||
bg: colors.link,
|
||||
border: colors.link,
|
||||
text: colors.text,
|
||||
hoverBorder: colors.link,
|
||||
hoverText: colors.text,
|
||||
hoverBg: colors.link,
|
||||
activeBorder: colors.link,
|
||||
activeText: colors.text,
|
||||
activeBg: colors.link
|
||||
},
|
||||
alt: {
|
||||
bg: '#484848',
|
||||
text: '#f0eeee',
|
||||
hoverBg: '#484848',
|
||||
hoverText: '#f0eeee'
|
||||
}
|
||||
},
|
||||
customBlock: {
|
||||
info: {
|
||||
bg: colors.background,
|
||||
border: colors.link,
|
||||
text: colors.text,
|
||||
textDeep: colors.text
|
||||
},
|
||||
tip: {
|
||||
bg: '#0C2A20',
|
||||
border: '#184633',
|
||||
text: '#B0EBC9',
|
||||
textDeep: '#166534'
|
||||
},
|
||||
warning: {
|
||||
bg: '#403207',
|
||||
border: '#7E6211',
|
||||
text: '#F9DE88',
|
||||
textDeep: '#92400e'
|
||||
},
|
||||
danger: {
|
||||
bg: '#3F060A',
|
||||
border: '#7C0F18',
|
||||
text: '#F7C1BC',
|
||||
textDeep: '#991b1b'
|
||||
}
|
||||
},
|
||||
selection: {
|
||||
bg: colors.link
|
||||
},
|
||||
home: {
|
||||
heroNameColor: colors.link,
|
||||
heroNameBackground: colors.background,
|
||||
heroImageBackground: `linear-gradient(135deg, ${colors.background} 0%, ${colors.link} 100%)`,
|
||||
heroImageFilter: 'blur(44px)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register and apply the custom theme
|
||||
themeRegistry['custom'] = customTheme
|
||||
selectedColor.value = '' as ColorNames
|
||||
setTheme('custom')
|
||||
// Auto-set custom mode
|
||||
setMode('custom')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -228,7 +430,7 @@ const toggleAmoled = () => {
|
|||
<button
|
||||
:class="[
|
||||
'inline-block w-6 h-6 rounded-full transition-all duration-200 border-2',
|
||||
(themeName && themeName.value === `color-${color}`)
|
||||
(themeName === `color-${color}`)
|
||||
? 'border-slate-200 dark:border-slate-400 shadow-lg'
|
||||
: 'border-transparent'
|
||||
]"
|
||||
|
|
@ -247,11 +449,11 @@ const toggleAmoled = () => {
|
|||
<button
|
||||
:class="[
|
||||
'inline-block w-6 h-6 rounded-full transition-all duration-200 border-2',
|
||||
(themeName && themeName.value === t)
|
||||
(themeName === t)
|
||||
? 'border-slate-200 dark:border-slate-400 shadow-lg'
|
||||
: 'border-transparent'
|
||||
]"
|
||||
@click="selectedColor = '' as ColorNames; setTheme(t)"
|
||||
@click="selectedColor = '' as ColorNames; restorePreviousMode(); setTheme(t)"
|
||||
:title="themeRegistry[t].displayName"
|
||||
>
|
||||
<span
|
||||
|
|
@ -260,6 +462,32 @@ const toggleAmoled = () => {
|
|||
></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Custom theme button (after preset themes) -->
|
||||
<div v-if="showPalette">
|
||||
<button
|
||||
:class="[
|
||||
'inline-block w-6 h-6 rounded-full transition-all duration-200 border-2 relative overflow-hidden',
|
||||
(themeName === 'custom')
|
||||
? 'border-slate-200 dark:border-slate-400 shadow-lg'
|
||||
: 'border-transparent'
|
||||
]"
|
||||
@click="openCustomColorSelector"
|
||||
title="Custom Theme"
|
||||
>
|
||||
<span
|
||||
class="inline-block w-full h-full rounded-full bg-gradient-to-br from-purple-500 via-pink-500 to-red-500 flex items-center justify-center"
|
||||
>
|
||||
<div class="i-lucide-palette text-white text-xs" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Color Selector Modal -->
|
||||
<CustomColorSelector
|
||||
v-model="showCustomColorSelector"
|
||||
@apply="applyCustomColors"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
486
docs/.vitepress/theme/components/CustomColorSelector.vue
Normal file
486
docs/.vitepress/theme/components/CustomColorSelector.vue
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import tinycolor from 'tinycolor2'
|
||||
|
||||
/* ================= PROPS / EMITS ================= */
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'apply', colors: { link: string; text: string; background: string }): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
|
||||
|
||||
/* ================= RGB STATE ================= */
|
||||
const linkR = ref(255)
|
||||
const linkG = ref(255)
|
||||
const linkB = ref(255)
|
||||
|
||||
const textR = ref(255)
|
||||
const textG = ref(255)
|
||||
const textB = ref(255)
|
||||
|
||||
const bgR = ref(0)
|
||||
const bgG = ref(0)
|
||||
const bgB = ref(0)
|
||||
|
||||
// Active tab for compact view
|
||||
const activeTab = ref<'link' | 'text' | 'bg'>('link')
|
||||
|
||||
/* ================= COLOR HELPERS ================= */
|
||||
const rgbToHex = (r:number,g:number,b:number) =>
|
||||
tinycolor({ r, g, b }).toHexString()
|
||||
|
||||
const hexToRgb = (hex:string) => {
|
||||
const c = tinycolor(hex)
|
||||
if (!c.isValid()) return null
|
||||
const { r, g, b } = c.toRgb()
|
||||
return { r, g, b }
|
||||
}
|
||||
|
||||
/* ================= PICKER HANDLERS ================= */
|
||||
// (Removed complex 2D picker handlers)
|
||||
|
||||
/* ================= HEX COMPUTED ================= */
|
||||
const makeHex = (r:any,g:any,b:any) => computed({
|
||||
get: () => rgbToHex(r.value, g.value, b.value),
|
||||
set: (val:string) => {
|
||||
const rgb = hexToRgb(val)
|
||||
if (!rgb) return
|
||||
r.value = rgb.r
|
||||
g.value = rgb.g
|
||||
b.value = rgb.b
|
||||
}
|
||||
})
|
||||
|
||||
const linkHex = makeHex(linkR, linkG, linkB)
|
||||
const textHex = makeHex(textR, textG, textB)
|
||||
const bgHex = makeHex(bgR, bgG, bgB)
|
||||
|
||||
/* ================= PREVIEW ================= */
|
||||
const linkColor = computed(() => rgbToHex(linkR.value, linkG.value, linkB.value))
|
||||
const textColor = computed(() => rgbToHex(textR.value, textG.value, textB.value))
|
||||
const bgColor = computed(() => rgbToHex(bgR.value, bgG.value, bgB.value))
|
||||
|
||||
/* ================= WARNINGS ================= */
|
||||
const linkBgContrast = computed(() => tinycolor.readability(linkColor.value, bgColor.value))
|
||||
const textBgContrast = computed(() => tinycolor.readability(textColor.value, bgColor.value))
|
||||
|
||||
const warnings = computed(() => {
|
||||
const list: string[] = []
|
||||
// WCAG AA for normal text is 4.5:1
|
||||
if (linkBgContrast.value < 4.5) {
|
||||
list.push('Warning: Low contrast between Link and Background')
|
||||
}
|
||||
if (textBgContrast.value < 4.5) {
|
||||
list.push('Warning: Low contrast between Text and Background')
|
||||
}
|
||||
return list
|
||||
})
|
||||
|
||||
/* ================= STORAGE ================= */
|
||||
const savedLink = useStorage('custom-theme-link-color', '#ffffff')
|
||||
const savedText = useStorage('custom-theme-text-color', '#cccccc')
|
||||
const savedBg = useStorage('custom-theme-bg-color', '#000000')
|
||||
|
||||
const initColors = () => {
|
||||
const a = hexToRgb(savedLink.value)
|
||||
const b = hexToRgb(savedText.value)
|
||||
const c = hexToRgb(savedBg.value)
|
||||
if (a) { linkR.value = a.r; linkG.value = a.g; linkB.value = a.b }
|
||||
if (b) { textR.value = b.r; textG.value = b.g; textB.value = b.b }
|
||||
if (c) { bgR.value = c.r; bgG.value = c.g; bgB.value = c.b }
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, v => v && initColors())
|
||||
|
||||
/* ================= ACTIONS ================= */
|
||||
const close = () => emit('update:modelValue', false)
|
||||
|
||||
const apply = () => {
|
||||
// Save to persistence
|
||||
savedLink.value = linkColor.value
|
||||
savedText.value = textColor.value
|
||||
savedBg.value = bgColor.value
|
||||
|
||||
emit('apply', {
|
||||
link: linkColor.value,
|
||||
text: textColor.value,
|
||||
background: bgColor.value
|
||||
})
|
||||
close()
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="modal">
|
||||
<div
|
||||
v-if="modelValue"
|
||||
class="fixed inset-0 z-999 flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
||||
|
||||
>
|
||||
<div
|
||||
class="bg-$vp-c-bg border-$vp-c-default-soft relative w-full max-w-xl rounded-lg border-2 p-6 shadow-xl select-none"
|
||||
@click.stop
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="text-$vp-c-text-1 text-lg font-bold">Custom Theme</h3>
|
||||
<button
|
||||
class="text-$vp-c-text-2 hover:text-$vp-c-text-1 transition-colors"
|
||||
@click="close"
|
||||
aria-label="Close"
|
||||
>
|
||||
<div class="i-carbon-close h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Preview Section (Compact) -->
|
||||
<div class="mb-4 select-none">
|
||||
<div
|
||||
class="rounded-lg border-2 border-$vp-c-divider p-3 text-sm transition-all duration-300 select-none"
|
||||
:style="{ backgroundColor: bgColor, color: textColor }"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<div>
|
||||
<strong :style="{ color: linkColor }">⭐ AnimeKai</strong>
|
||||
<span :style="{ color: textColor }">, </span>
|
||||
<a href="#" :style="{ color: linkColor }" class="hover:underline">2</a>
|
||||
<span :style="{ color: textColor }">, </span>
|
||||
<a href="#" :style="{ color: linkColor }" class="hover:underline">3</a>
|
||||
<span :style="{ color: textColor }">, </span>
|
||||
<a href="#" :style="{ color: linkColor }" class="hover:underline">4</a>
|
||||
<span :style="{ color: textColor }">, </span>
|
||||
<a href="#" :style="{ color: linkColor }" class="hover:underline">5</a>
|
||||
<span :style="{ color: textColor }">, </span>
|
||||
<a href="#" :style="{ color: linkColor }" class="hover:underline">6</a>
|
||||
<span :style="{ color: textColor }">, </span>
|
||||
<a href="#" :style="{ color: linkColor }" class="hover:underline">7</a>
|
||||
<span :style="{ color: textColor }"> or </span>
|
||||
<strong :style="{ color: linkColor }">AniGo</strong>
|
||||
<span :style="{ color: textColor }"> - Hard Subs / Dub / Auto-Next</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong :style="{ color: linkColor }">⭐ Miruro</strong>
|
||||
<span :style="{ color: textColor }"> - Hard Subs / Dub / Auto-Next</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong :style="{ color: linkColor }">⭐ HiAnime</strong>
|
||||
<span :style="{ color: textColor }">, </span>
|
||||
<a href="#" :style="{ color: linkColor }" class="hover:underline">2</a>
|
||||
<span :style="{ color: textColor }">, </span>
|
||||
<a href="#" :style="{ color: linkColor }" class="hover:underline">3</a>
|
||||
<span :style="{ color: textColor }">, </span>
|
||||
<a href="#" :style="{ color: linkColor }" class="hover:underline">4</a>
|
||||
<span :style="{ color: textColor }">, </span>
|
||||
<a href="#" :style="{ color: linkColor }" class="hover:underline">5</a>
|
||||
<span :style="{ color: textColor }"> - Sub / Dub / Auto-Next</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="mb-4 flex gap-2">
|
||||
<button
|
||||
@click="activeTab = 'link'"
|
||||
:class="[
|
||||
'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-colors',
|
||||
activeTab === 'link'
|
||||
? ''
|
||||
: 'bg-$vp-c-bg-alt text-$vp-c-text-2 hover:text-$vp-c-text-1'
|
||||
]"
|
||||
:style="activeTab === 'link' ? { backgroundColor: linkColor, color: textColor } : {}"
|
||||
>
|
||||
Link
|
||||
</button>
|
||||
<button
|
||||
@click="activeTab = 'bg'"
|
||||
:class="[
|
||||
'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-colors',
|
||||
activeTab === 'bg'
|
||||
? ''
|
||||
: 'bg-$vp-c-bg-alt text-$vp-c-text-2 hover:text-$vp-c-text-1'
|
||||
]"
|
||||
:style="activeTab === 'bg' ? { backgroundColor: linkColor, color: textColor } : {}"
|
||||
>
|
||||
Background
|
||||
</button>
|
||||
<button
|
||||
@click="activeTab = 'text'"
|
||||
:class="[
|
||||
'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-colors',
|
||||
activeTab === 'text'
|
||||
? ''
|
||||
: 'bg-$vp-c-bg-alt text-$vp-c-text-2 hover:text-$vp-c-text-1'
|
||||
]"
|
||||
:style="activeTab === 'text' ? { backgroundColor: linkColor, color: textColor } : {}"
|
||||
>
|
||||
Text
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- RGB Sliders Section -->
|
||||
<div class="mb-4">
|
||||
<!-- Link sliders -->
|
||||
<div v-show="activeTab === 'link'" class="space-y-4">
|
||||
<div class="space-y-1">
|
||||
<div class="flex justify-between text-xs text-$vp-c-text-2">
|
||||
<span>Red</span>
|
||||
<span>{{ linkR }}</span>
|
||||
</div>
|
||||
<input type="range" min="0" max="255" v-model.number="linkR" class="custom-slider w-full" style="--slider-color: #ff4d4d" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex justify-between text-xs text-$vp-c-text-2">
|
||||
<span>Green</span>
|
||||
<span>{{ linkG }}</span>
|
||||
</div>
|
||||
<input type="range" min="0" max="255" v-model.number="linkG" class="custom-slider w-full" style="--slider-color: #4dff4d" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex justify-between text-xs text-$vp-c-text-2">
|
||||
<span>Blue</span>
|
||||
<span>{{ linkB }}</span>
|
||||
</div>
|
||||
<input type="range" min="0" max="255" v-model.number="linkB" class="custom-slider w-full" style="--slider-color: #4d4dff" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Background sliders -->
|
||||
<div v-show="activeTab === 'bg'" class="space-y-4">
|
||||
<div class="space-y-1">
|
||||
<div class="flex justify-between text-xs text-$vp-c-text-2">
|
||||
<span>Red</span>
|
||||
<span>{{ bgR }}</span>
|
||||
</div>
|
||||
<input type="range" min="0" max="255" v-model.number="bgR" class="custom-slider w-full" style="--slider-color: #ff4d4d" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex justify-between text-xs text-$vp-c-text-2">
|
||||
<span>Green</span>
|
||||
<span>{{ bgG }}</span>
|
||||
</div>
|
||||
<input type="range" min="0" max="255" v-model.number="bgG" class="custom-slider w-full" style="--slider-color: #4dff4d" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex justify-between text-xs text-$vp-c-text-2">
|
||||
<span>Blue</span>
|
||||
<span>{{ bgB }}</span>
|
||||
</div>
|
||||
<input type="range" min="0" max="255" v-model.number="bgB" class="custom-slider w-full" style="--slider-color: #4d4dff" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text sliders -->
|
||||
<div v-show="activeTab === 'text'" class="space-y-4">
|
||||
<div class="space-y-1">
|
||||
<div class="flex justify-between text-xs text-$vp-c-text-2">
|
||||
<span>Red</span>
|
||||
<span>{{ textR }}</span>
|
||||
</div>
|
||||
<input type="range" min="0" max="255" v-model.number="textR" class="custom-slider w-full" style="--slider-color: #ff4d4d" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex justify-between text-xs text-$vp-c-text-2">
|
||||
<span>Green</span>
|
||||
<span>{{ textG }}</span>
|
||||
</div>
|
||||
<input type="range" min="0" max="255" v-model.number="textG" class="custom-slider w-full" style="--slider-color: #4dff4d" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex justify-between text-xs text-$vp-c-text-2">
|
||||
<span>Blue</span>
|
||||
<span>{{ textB }}</span>
|
||||
</div>
|
||||
<input type="range" min="0" max="255" v-model.number="textB" class="custom-slider w-full" style="--slider-color: #4d4dff" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Fields (shown for active tab) -->
|
||||
<div class="mb-6">
|
||||
<!-- Link Inputs -->
|
||||
<div v-show="activeTab === 'link'" class="grid grid-cols-5 gap-2">
|
||||
<div class="col-span-2">
|
||||
<label class="text-$vp-c-text-2 text-xs block mb-1">HEX</label>
|
||||
<input
|
||||
v-model="linkHex"
|
||||
type="text"
|
||||
class="bg-$vp-c-bg-alt border-$vp-c-divider text-$vp-c-text-1 w-full rounded border px-2 py-1.5 text-xs font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-$vp-c-text-2 text-xs block mb-1">R</label>
|
||||
<input v-model.number="linkR" type="number" min="0" max="255"
|
||||
class="bg-$vp-c-bg-alt border-$vp-c-divider text-$vp-c-text-1 w-full rounded border px-2 py-1.5 text-xs" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-$vp-c-text-2 text-xs block mb-1">G</label>
|
||||
<input v-model.number="linkG" type="number" min="0" max="255"
|
||||
class="bg-$vp-c-bg-alt border-$vp-c-divider text-$vp-c-text-1 w-full rounded border px-2 py-1.5 text-xs" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-$vp-c-text-2 text-xs block mb-1">B</label>
|
||||
<input v-model.number="linkB" type="number" min="0" max="255"
|
||||
class="bg-$vp-c-bg-alt border-$vp-c-divider text-$vp-c-text-1 w-full rounded border px-2 py-1.5 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Background Inputs -->
|
||||
<div v-show="activeTab === 'bg'" class="grid grid-cols-5 gap-2">
|
||||
<div class="col-span-2">
|
||||
<label class="text-$vp-c-text-2 text-xs block mb-1">HEX</label>
|
||||
<input
|
||||
v-model="bgHex"
|
||||
type="text"
|
||||
class="bg-$vp-c-bg-alt border-$vp-c-divider text-$vp-c-text-1 w-full rounded border px-2 py-1.5 text-xs font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-$vp-c-text-2 text-xs block mb-1">R</label>
|
||||
<input v-model.number="bgR" type="number" min="0" max="255"
|
||||
class="bg-$vp-c-bg-alt border-$vp-c-divider text-$vp-c-text-1 w-full rounded border px-2 py-1.5 text-xs" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-$vp-c-text-2 text-xs block mb-1">G</label>
|
||||
<input v-model.number="bgG" type="number" min="0" max="255"
|
||||
class="bg-$vp-c-bg-alt border-$vp-c-divider text-$vp-c-text-1 w-full rounded border px-2 py-1.5 text-xs" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-$vp-c-text-2 text-xs block mb-1">B</label>
|
||||
<input v-model.number="bgB" type="number" min="0" max="255"
|
||||
class="bg-$vp-c-bg-alt border-$vp-c-divider text-$vp-c-text-1 w-full rounded border px-2 py-1.5 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text Inputs -->
|
||||
<div v-show="activeTab === 'text'" class="grid grid-cols-5 gap-2">
|
||||
<div class="col-span-2">
|
||||
<label class="text-$vp-c-text-2 text-xs block mb-1">HEX</label>
|
||||
<input
|
||||
v-model="textHex"
|
||||
type="text"
|
||||
class="bg-$vp-c-bg-alt border-$vp-c-divider text-$vp-c-text-1 w-full rounded border px-2 py-1.5 text-xs font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-$vp-c-text-2 text-xs block mb-1">R</label>
|
||||
<input v-model.number="textR" type="number" min="0" max="255"
|
||||
class="bg-$vp-c-bg-alt border-$vp-c-divider text-$vp-c-text-1 w-full rounded border px-2 py-1.5 text-xs" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-$vp-c-text-2 text-xs block mb-1">G</label>
|
||||
<input v-model.number="textG" type="number" min="0" max="255"
|
||||
class="bg-$vp-c-bg-alt border-$vp-c-divider text-$vp-c-text-1 w-full rounded border px-2 py-1.5 text-xs" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-$vp-c-text-2 text-xs block mb-1">B</label>
|
||||
<input v-model.number="textB" type="number" min="0" max="255"
|
||||
class="bg-$vp-c-bg-alt border-$vp-c-divider text-$vp-c-text-1 w-full rounded border px-2 py-1.5 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warnings -->
|
||||
<div v-if="warnings.length" class="mb-4 rounded-lg bg-yellow-500/10 border border-yellow-500/20 p-3">
|
||||
<div v-for="(warn, i) in warnings" :key="i" class="flex items-center gap-2 text-yellow-500 text-xs font-medium">
|
||||
<div class="i-carbon-warning h-4 w-4 shrink-0" />
|
||||
<span>{{ warn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
class="hover:bg-$vp-c-bg-alt border-$vp-c-divider text-$vp-c-text-1 flex-1 rounded-lg border px-4 py-2 font-medium transition-colors"
|
||||
@click="close"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 rounded-lg px-4 py-2 font-medium transition-colors"
|
||||
:style="{ backgroundColor: linkColor, color: textColor }"
|
||||
@click="apply"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-enter-active,
|
||||
.modal-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from,
|
||||
.modal-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter-active .bg-\$vp-c-bg,
|
||||
.modal-leave-active .bg-\$vp-c-bg {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from .bg-\$vp-c-bg,
|
||||
.modal-leave-to .bg-\$vp-c-bg {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
/* Custom slider styling */
|
||||
.custom-slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent 0%,
|
||||
var(--slider-color, var(--vp-c-brand-1)) 100%
|
||||
);
|
||||
outline: none;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.custom-slider:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.custom-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-brand-1);
|
||||
cursor: pointer;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.custom-slider::-moz-range-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-brand-1);
|
||||
cursor: pointer;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,7 +3,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|||
import { useTheme } from '../themes/themeHandler'
|
||||
import type { DisplayMode } from '../themes/types'
|
||||
|
||||
const { mode, setMode, state, amoledEnabled, setAmoledEnabled } = useTheme()
|
||||
const { mode, setMode, amoledEnabled, setAmoledEnabled } = useTheme()
|
||||
|
||||
const isOpen = ref(false)
|
||||
const dropdownRef = ref<HTMLElement | null>(null)
|
||||
|
|
@ -15,18 +15,33 @@ interface ModeChoice {
|
|||
isAmoled?: boolean
|
||||
}
|
||||
|
||||
const modeChoices: ModeChoice[] = [
|
||||
const baseModeChoices: 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: 'custom', label: 'Custom', icon: 'i-lucide-palette' }
|
||||
]
|
||||
|
||||
// Only show custom mode when already in custom mode
|
||||
const modeChoices = computed(() => {
|
||||
const current = (mode && (mode as any).value) ? (mode as any).value : 'light'
|
||||
if (current === 'custom') {
|
||||
return baseModeChoices
|
||||
}
|
||||
// Filter out custom mode when not in custom
|
||||
return baseModeChoices.filter(choice => choice.mode !== 'custom')
|
||||
})
|
||||
|
||||
const currentChoice = computed(() => {
|
||||
const current = (mode && (mode as any).value) ? (mode as any).value : 'light'
|
||||
if (current === 'dark' && amoledEnabled.value) {
|
||||
return modeChoices[2] // AMOLED option
|
||||
// Handle custom mode
|
||||
if (current === 'custom') {
|
||||
return baseModeChoices[3] // Custom option
|
||||
}
|
||||
return modeChoices.find(choice => choice.mode === current && !choice.isAmoled) || modeChoices[0]
|
||||
if (current === 'dark' && amoledEnabled.value) {
|
||||
return baseModeChoices[2] // AMOLED option
|
||||
}
|
||||
return baseModeChoices.find(choice => choice.mode === current && !choice.isAmoled) || baseModeChoices[0]
|
||||
})
|
||||
|
||||
const toggleDropdown = () => {
|
||||
|
|
@ -34,6 +49,20 @@ const toggleDropdown = () => {
|
|||
}
|
||||
|
||||
const selectMode = (choice: ModeChoice) => {
|
||||
const current = (mode && (mode as any).value) ? (mode as any).value : 'light'
|
||||
|
||||
// Prevent switching to Light/Dark/AMOLED when in custom mode
|
||||
if (current === 'custom' && choice.mode !== 'custom') {
|
||||
isOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent switching when clicking custom (clicking custom does nothing)
|
||||
if (choice.mode === 'custom') {
|
||||
isOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (choice.isAmoled) {
|
||||
setMode('dark')
|
||||
setAmoledEnabled(true)
|
||||
|
|
@ -46,12 +75,31 @@ const selectMode = (choice: ModeChoice) => {
|
|||
|
||||
const isActiveChoice = (choice: ModeChoice) => {
|
||||
const current = (mode && (mode as any).value) ? (mode as any).value : 'light'
|
||||
// Handle custom mode
|
||||
if (choice.mode === 'custom') {
|
||||
return current === 'custom'
|
||||
}
|
||||
if (choice.isAmoled) {
|
||||
return current === 'dark' && amoledEnabled.value
|
||||
}
|
||||
return choice.mode === current && !choice.isAmoled && !amoledEnabled.value
|
||||
}
|
||||
|
||||
// Check if a choice should be disabled
|
||||
const isDisabled = (choice: ModeChoice) => {
|
||||
const current = (mode && (mode as any).value) ? (mode as any).value : 'light'
|
||||
// Disable Light/Dark/AMOLED when in custom mode
|
||||
return current === 'custom' && choice.mode !== 'custom'
|
||||
}
|
||||
|
||||
// Get tooltip for disabled items
|
||||
const getTooltip = (choice: ModeChoice) => {
|
||||
if (isDisabled(choice)) {
|
||||
return 'Use default themes to select this'
|
||||
}
|
||||
return choice.label
|
||||
}
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
|
||||
isOpen.value = false
|
||||
|
|
@ -86,7 +134,8 @@ onUnmounted(() => {
|
|||
v-for="(choice, index) in modeChoices"
|
||||
:key="index"
|
||||
class="theme-dropdown-item"
|
||||
:class="{ active: isActiveChoice(choice) }"
|
||||
:class="{ active: isActiveChoice(choice), disabled: isDisabled(choice) }"
|
||||
:title="getTooltip(choice)"
|
||||
@click="selectMode(choice)"
|
||||
>
|
||||
<div :class="[choice.icon, 'text-lg']" />
|
||||
|
|
@ -166,6 +215,16 @@ onUnmounted(() => {
|
|||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: var(--vp-c-text-3);
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@
|
|||
import { ref, onMounted, computed } from 'vue'
|
||||
import type { DisplayMode, ThemeState, Theme, ModeColors } from './types'
|
||||
import { themeRegistry } from './configs'
|
||||
import tinycolor from 'tinycolor2'
|
||||
|
||||
const STORAGE_KEY_THEME = 'vitepress-theme-name'
|
||||
const STORAGE_KEY_MODE = 'vitepress-display-mode'
|
||||
const STORAGE_KEY_AMOLED = 'vitepress-amoled-enabled'
|
||||
const STORAGE_KEY_PREVIOUS_MODE = 'vitepress-previous-mode'
|
||||
|
||||
export class ThemeHandler {
|
||||
private state = ref<ThemeState>({
|
||||
|
|
@ -29,11 +31,178 @@ export class ThemeHandler {
|
|||
theme: null
|
||||
})
|
||||
private amoledEnabled = ref(false)
|
||||
private previousMode = ref<DisplayMode>('light')
|
||||
|
||||
constructor() {
|
||||
this.initializeTheme()
|
||||
}
|
||||
|
||||
private registerCustomThemeFromStorage() {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
// Load saved custom colors from localStorage
|
||||
const savedLinkColor = localStorage.getItem('custom-theme-link-color') || '#ffffff'
|
||||
const savedTextColor = localStorage.getItem('custom-theme-text-color') || '#cccccc'
|
||||
const savedBgColor = localStorage.getItem('custom-theme-bg-color') || '#000000'
|
||||
|
||||
// Create lighter versions of background for cards
|
||||
// Increase lightening to make cards more distinct
|
||||
const lightenedBg = tinycolor(savedBgColor).lighten(10).toString()
|
||||
const lightenedBgAlt = tinycolor(savedBgColor).lighten(15).toString()
|
||||
|
||||
// Create custom theme with saved colors
|
||||
const customTheme = {
|
||||
name: 'custom',
|
||||
displayName: 'Custom',
|
||||
preview: savedBgColor,
|
||||
modes: {
|
||||
light: {
|
||||
brand: {
|
||||
1: savedLinkColor,
|
||||
2: savedLinkColor,
|
||||
3: savedLinkColor,
|
||||
soft: savedLinkColor
|
||||
},
|
||||
bg: savedBgColor,
|
||||
bgAlt: lightenedBg,
|
||||
bgElv: lightenedBgAlt,
|
||||
text: {
|
||||
1: savedTextColor,
|
||||
2: savedTextColor,
|
||||
3: savedTextColor
|
||||
},
|
||||
button: {
|
||||
brand: {
|
||||
bg: savedLinkColor,
|
||||
border: savedLinkColor,
|
||||
text: savedBgColor,
|
||||
hoverBorder: savedLinkColor,
|
||||
hoverText: savedBgColor,
|
||||
hoverBg: savedLinkColor,
|
||||
activeBorder: savedLinkColor,
|
||||
activeText: savedBgColor,
|
||||
activeBg: savedLinkColor
|
||||
},
|
||||
alt: {
|
||||
bg: '#484848',
|
||||
text: '#f0eeee',
|
||||
hoverBg: '#484848',
|
||||
hoverText: '#f0eeee'
|
||||
}
|
||||
},
|
||||
customBlock: {
|
||||
info: {
|
||||
bg: savedBgColor,
|
||||
border: savedLinkColor,
|
||||
text: savedTextColor,
|
||||
textDeep: savedTextColor
|
||||
},
|
||||
tip: {
|
||||
bg: '#D8F8E4',
|
||||
border: '#447A61',
|
||||
text: '#2D6A58',
|
||||
textDeep: '#166534'
|
||||
},
|
||||
warning: {
|
||||
bg: '#FCEFC3',
|
||||
border: '#9A8034',
|
||||
text: '#9C701B',
|
||||
textDeep: '#92400e'
|
||||
},
|
||||
danger: {
|
||||
bg: '#FBE1E2',
|
||||
border: '#B3565E',
|
||||
text: '#912239',
|
||||
textDeep: '#991b1b'
|
||||
}
|
||||
},
|
||||
selection: {
|
||||
bg: savedLinkColor
|
||||
},
|
||||
home: {
|
||||
heroNameColor: savedLinkColor,
|
||||
heroNameBackground: savedBgColor,
|
||||
heroImageBackground: `linear-gradient(135deg, ${savedBgColor} 0%, ${savedLinkColor} 100%)`,
|
||||
heroImageFilter: 'blur(44px)'
|
||||
}
|
||||
},
|
||||
dark: {
|
||||
brand: {
|
||||
1: savedLinkColor,
|
||||
2: savedLinkColor,
|
||||
3: savedLinkColor,
|
||||
soft: savedLinkColor
|
||||
},
|
||||
bg: savedBgColor,
|
||||
bgAlt: lightenedBg,
|
||||
bgElv: lightenedBgAlt,
|
||||
text: {
|
||||
1: savedTextColor,
|
||||
2: savedTextColor,
|
||||
3: savedTextColor
|
||||
},
|
||||
button: {
|
||||
brand: {
|
||||
bg: savedLinkColor,
|
||||
border: savedLinkColor,
|
||||
text: savedBgColor,
|
||||
hoverBorder: savedLinkColor,
|
||||
hoverText: savedBgColor,
|
||||
hoverBg: savedLinkColor,
|
||||
activeBorder: savedLinkColor,
|
||||
activeText: savedBgColor,
|
||||
activeBg: savedLinkColor
|
||||
},
|
||||
alt: {
|
||||
bg: '#484848',
|
||||
text: '#f0eeee',
|
||||
hoverBg: '#484848',
|
||||
hoverText: '#f0eeee'
|
||||
}
|
||||
},
|
||||
customBlock: {
|
||||
info: {
|
||||
bg: savedBgColor,
|
||||
border: savedLinkColor,
|
||||
text: savedTextColor,
|
||||
textDeep: savedTextColor
|
||||
},
|
||||
tip: {
|
||||
bg: '#0C2A20',
|
||||
border: '#184633',
|
||||
text: '#B0EBC9',
|
||||
textDeep: '#166534'
|
||||
},
|
||||
warning: {
|
||||
bg: '#403207',
|
||||
border: '#7E6211',
|
||||
text: '#F9DE88',
|
||||
textDeep: '#92400e'
|
||||
},
|
||||
danger: {
|
||||
bg: '#3F060A',
|
||||
border: '#7C0F18',
|
||||
text: '#F7C1BC',
|
||||
textDeep: '#991b1b'
|
||||
}
|
||||
},
|
||||
selection: {
|
||||
bg: savedLinkColor
|
||||
},
|
||||
home: {
|
||||
heroNameColor: savedLinkColor,
|
||||
heroNameBackground: savedBgColor,
|
||||
heroImageBackground: `linear-gradient(135deg, ${savedBgColor} 0%, ${savedLinkColor} 100%)`,
|
||||
heroImageFilter: 'blur(44px)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register custom theme
|
||||
themeRegistry['custom'] = customTheme
|
||||
}
|
||||
|
||||
private initializeTheme() {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
|
|
@ -42,6 +211,11 @@ export class ThemeHandler {
|
|||
const savedMode = localStorage.getItem(STORAGE_KEY_MODE) as DisplayMode | null
|
||||
const savedAmoled = localStorage.getItem(STORAGE_KEY_AMOLED) === 'true'
|
||||
|
||||
// If custom theme was saved, register it early from localStorage
|
||||
if (savedTheme === 'custom') {
|
||||
this.registerCustomThemeFromStorage()
|
||||
}
|
||||
|
||||
if (themeRegistry[savedTheme]) {
|
||||
this.state.value.currentTheme = savedTheme
|
||||
this.state.value.theme = themeRegistry[savedTheme]
|
||||
|
|
@ -91,7 +265,9 @@ export class ThemeHandler {
|
|||
|
||||
if (!theme) return
|
||||
|
||||
const modeColors = theme.modes[currentMode]
|
||||
// Custom mode uses dark mode colors from the theme
|
||||
const effectiveMode = currentMode === 'custom' ? 'dark' : currentMode
|
||||
const modeColors = theme.modes[effectiveMode]
|
||||
|
||||
this.applyDOMClasses(currentMode)
|
||||
this.applyCSSVariables(modeColors, theme)
|
||||
|
|
@ -107,7 +283,7 @@ export class ThemeHandler {
|
|||
const root = document.documentElement
|
||||
|
||||
// Remove all mode classes
|
||||
root.classList.remove('dark', 'light', 'amoled')
|
||||
root.classList.remove('dark', 'light', 'amoled', 'custom')
|
||||
|
||||
// Add current mode class
|
||||
root.classList.add(mode)
|
||||
|
|
@ -159,6 +335,14 @@ export class ThemeHandler {
|
|||
root.style.setProperty('--vp-c-bg', bgColor)
|
||||
root.style.setProperty('--vp-c-bg-alt', bgAltColor)
|
||||
root.style.setProperty('--vp-c-bg-elv', bgElvColor)
|
||||
|
||||
// Apply additional background variables for cards and other elements
|
||||
root.style.setProperty('--vp-c-bg-soft', bgAltColor)
|
||||
root.style.setProperty('--vp-c-default-soft', bgElvColor)
|
||||
root.style.setProperty('--vp-c-default-1', bgAltColor)
|
||||
root.style.setProperty('--vp-c-default-2', bgElvColor)
|
||||
root.style.setProperty('--vp-c-default-3', bgColor)
|
||||
|
||||
if (colors.bgMark) {
|
||||
root.style.setProperty('--vp-c-bg-mark', colors.bgMark)
|
||||
}
|
||||
|
|
@ -282,6 +466,12 @@ export class ThemeHandler {
|
|||
}
|
||||
|
||||
public setMode(mode: DisplayMode) {
|
||||
// Save current mode as previous mode before switching to custom
|
||||
if (mode === 'custom' && this.state.value.currentMode !== 'custom') {
|
||||
this.previousMode.value = this.state.value.currentMode
|
||||
localStorage.setItem(STORAGE_KEY_PREVIOUS_MODE, this.previousMode.value)
|
||||
}
|
||||
|
||||
this.state.value.currentMode = mode
|
||||
localStorage.setItem(STORAGE_KEY_MODE, mode)
|
||||
this.applyTheme()
|
||||
|
|
@ -319,7 +509,9 @@ export class ThemeHandler {
|
|||
if (!theme) return
|
||||
// If theme doesn't specify brand colors, force ColorPicker to reapply its selection
|
||||
const currentMode = this.state.value.currentMode
|
||||
const modeColors = theme.modes[currentMode]
|
||||
// Custom mode uses dark mode colors
|
||||
const effectiveMode = currentMode === 'custom' ? 'dark' : currentMode
|
||||
const modeColors = theme.modes[effectiveMode]
|
||||
|
||||
if (!modeColors.brand || !modeColors.brand[1]) {
|
||||
// Trigger a custom event that ColorPicker can listen to
|
||||
|
|
@ -360,6 +552,15 @@ export class ThemeHandler {
|
|||
public isAmoledMode() {
|
||||
return this.state.value.currentMode === 'dark' && this.amoledEnabled.value
|
||||
}
|
||||
|
||||
public restorePreviousMode() {
|
||||
// Only restore if currently in custom mode
|
||||
if (this.state.value.currentMode === 'custom') {
|
||||
const savedPreviousMode = localStorage.getItem(STORAGE_KEY_PREVIOUS_MODE) as DisplayMode | null
|
||||
const modeToRestore = savedPreviousMode || this.previousMode.value
|
||||
this.setMode(modeToRestore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global theme handler instance
|
||||
|
|
@ -395,6 +596,7 @@ export function useTheme() {
|
|||
amoledEnabled: handler.getAmoledEnabledRef(),
|
||||
setAmoledEnabled: (enabled: boolean) => handler.setAmoledEnabled(enabled),
|
||||
toggleAmoled: () => handler.toggleAmoled(),
|
||||
restorePreviousMode: () => handler.restorePreviousMode(),
|
||||
state
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type DisplayMode = 'light' | 'dark'
|
||||
export type DisplayMode = 'light' | 'dark' | 'custom'
|
||||
|
||||
export interface ModeColors {
|
||||
// Brand colors (optional - if not specified, ColorPicker values are used)
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ export function getHeader(id: string) {
|
|||
const title =
|
||||
'<div class="space-y-2 not-prose"><h1 class="text-4xl font-extrabold tracking-tight text-primary underline lg:text-5xl lg:leading-[3.5rem]">'
|
||||
|
||||
const description = '<p class="text-black dark:text-text-2">'
|
||||
const description = '<p style="color: var(--vp-c-text-1)">'
|
||||
|
||||
const feedback = meta.build.api ? '<Feedback />' : ''
|
||||
|
||||
|
|
|
|||
BIN
package-lock.json
generated
BIN
package-lock.json
generated
Binary file not shown.
|
|
@ -36,9 +36,9 @@
|
|||
"nprogress": "^0.2.0",
|
||||
"pathe": "^2.0.3",
|
||||
"reka-ui": "^2.6.1",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"unocss": "66.5.10",
|
||||
"vitepress": "^1.6.4",
|
||||
"vue": "^3.5.25",
|
||||
"x-satori": "^0.4.0",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
|
|
@ -61,6 +61,7 @@
|
|||
"@iconify/utils": "^3.1.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/tinycolor2": "^1.4.6",
|
||||
"@vue/compiler-sfc": "^3.5.27",
|
||||
"floating-vue": "^5.2.2",
|
||||
"nitro-cloudflare-dev": "^0.2.2",
|
||||
|
|
@ -73,6 +74,7 @@
|
|||
"vite-plugin-optimize-exclude": "^0.0.1",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vite-plugin-terminal": "^1.3.0",
|
||||
"vue": "^3.5.0",
|
||||
"wrangler": "^4.52.1"
|
||||
},
|
||||
"pnpm": {
|
||||
|
|
|
|||
BIN
pnpm-lock.yaml
BIN
pnpm-lock.yaml
Binary file not shown.
|
|
@ -102,12 +102,12 @@ export default defineConfig({
|
|||
{
|
||||
display: 'inline-block',
|
||||
padding: '0.2em 0.4em',
|
||||
fontSize: '0.75em',
|
||||
fontWeight: '500',
|
||||
lineHeight: '1',
|
||||
'font-size': '0.75em',
|
||||
'font-weight': '500',
|
||||
'line-height': '1',
|
||||
color: 'var(--vp-c-text-1)',
|
||||
backgroundColor: 'rgb(var(--vp-c-bg-alt))',
|
||||
borderRadius: '4px'
|
||||
'background-color': 'rgb(var(--vp-c-bg-alt))',
|
||||
'border-radius': '4px'
|
||||
}
|
||||
]
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in a new issue