Revert "Custom Color Theme Selector (#4641)" (#4661)

This reverts commit a71bb4c0d0.
This commit is contained in:
nbats 2026-01-29 18:37:56 -08:00 committed by GitHub
parent a71bb4c0d0
commit daf15a1a8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 29 additions and 1013 deletions

View file

@ -207,13 +207,6 @@ export default defineConfig({
build: { build: {
// Shut the fuck up // Shut the fuck up
chunkSizeWarningLimit: Number.POSITIVE_INFINITY chunkSizeWarningLimit: Number.POSITIVE_INFINITY
},
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler'
}
}
} }
}, },
markdown: { markdown: {

View file

@ -1,37 +1,24 @@
<script setup lang="ts"> <script setup lang="ts">
import { colors } from '@fmhy/colors' import { colors } from '@fmhy/colors'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { watch, onMounted, nextTick, ref, computed } from 'vue' import { watch, onMounted, nextTick } from 'vue'
import { useData } from 'vitepress'
import { useTheme } from '../themes/themeHandler' import { useTheme } from '../themes/themeHandler'
import { themeRegistry } from '../themes/configs' import { themeRegistry } from '../themes/configs'
import type { Theme } from '../themes/types' import type { Theme } from '../themes/types'
import CustomColorSelector from './CustomColorSelector.vue' import Switch from './Switch.vue'
import tinycolor from 'tinycolor2'
type ColorNames = keyof typeof colors type ColorNames = keyof typeof colors
const selectedColor = useStorage<ColorNames>('preferred-color', 'swarm') 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 // Use the theme system
const { amoledEnabled, setAmoledEnabled, setTheme, setMode, mode, themeName, restorePreviousMode } = useTheme() const { amoledEnabled, setAmoledEnabled, setTheme, state, mode, themeName } = useTheme()
// Custom color selector state
const showCustomColorSelector = ref(false)
const colorOptions = Object.keys(colors).filter( const colorOptions = Object.keys(colors).filter(
(key) => typeof colors[key as keyof typeof colors] === 'object' (key) => typeof colors[key as keyof typeof colors] === 'object'
) as Array<ColorNames> ) as Array<ColorNames>
// Preset themes (exclude dynamically generated color- themes and custom theme) // Preset themes (exclude dynamically generated color- themes)
const presetThemeNames = Object.keys(themeRegistry).filter((k) => !k.startsWith('color-') && k !== 'custom') const presetThemeNames = Object.keys(themeRegistry).filter((k) => !k.startsWith('color-'))
const getThemePreviewStyle = (name: string) => { const getThemePreviewStyle = (name: string) => {
const theme = themeRegistry[name] const theme = themeRegistry[name]
@ -209,217 +196,28 @@ const normalizeColorName = (colorName: string) =>
colorName.slice(1).replaceAll(/-/g, ' ') colorName.slice(1).replaceAll(/-/g, ' ')
onMounted(async () => { onMounted(async () => {
// Check if custom theme was last selected // apply saved theme on load
const savedTheme = localStorage.getItem('vitepress-theme-name') if (selectedColor.value) {
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) const theme = generateThemeFromColor(selectedColor.value)
themeRegistry[`color-${selectedColor.value}`] = theme themeRegistry[`color-${selectedColor.value}`] = theme
await nextTick() await nextTick()
setTheme(`color-${selectedColor.value}`) setTheme(`color-${selectedColor.value}`)
} }
// Wait for next tick to ensure theme handler is fully initialized // Wait for next tick to ensure theme handler is fully initialized
await nextTick() await nextTick()
}) })
watch(selectedColor, async (color) => { watch(selectedColor, async (color) => {
if (!color) return; if (!color) return;
// Restore previous mode when switching away from custom
restorePreviousMode()
const theme = generateThemeFromColor(color) const theme = generateThemeFromColor(color)
themeRegistry[`color-${color}`] = theme themeRegistry[`color-${color}`] = theme
await nextTick() await nextTick()
setTheme(`color-${color}`) 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> </script>
<template> <template>
@ -430,7 +228,7 @@ const applyCustomColors = (colors: { link: string; text: string; background: str
<button <button
:class="[ :class="[
'inline-block w-6 h-6 rounded-full transition-all duration-200 border-2', 'inline-block w-6 h-6 rounded-full transition-all duration-200 border-2',
(themeName === `color-${color}`) (themeName && themeName.value === `color-${color}`)
? 'border-slate-200 dark:border-slate-400 shadow-lg' ? 'border-slate-200 dark:border-slate-400 shadow-lg'
: 'border-transparent' : 'border-transparent'
]" ]"
@ -449,11 +247,11 @@ const applyCustomColors = (colors: { link: string; text: string; background: str
<button <button
:class="[ :class="[
'inline-block w-6 h-6 rounded-full transition-all duration-200 border-2', 'inline-block w-6 h-6 rounded-full transition-all duration-200 border-2',
(themeName === t) (themeName && themeName.value === t)
? 'border-slate-200 dark:border-slate-400 shadow-lg' ? 'border-slate-200 dark:border-slate-400 shadow-lg'
: 'border-transparent' : 'border-transparent'
]" ]"
@click="selectedColor = '' as ColorNames; restorePreviousMode(); setTheme(t)" @click="selectedColor = '' as ColorNames; setTheme(t)"
:title="themeRegistry[t].displayName" :title="themeRegistry[t].displayName"
> >
<span <span
@ -462,32 +260,6 @@ const applyCustomColors = (colors: { link: string; text: string; background: str
></span> ></span>
</button> </button>
</div> </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> </div>
<!-- Custom Color Selector Modal -->
<CustomColorSelector
v-model="showCustomColorSelector"
@apply="applyCustomColors"
/>
</div> </div>
</template> </template>

View file

@ -1,486 +0,0 @@
<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>

View file

@ -3,7 +3,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useTheme } from '../themes/themeHandler' import { useTheme } from '../themes/themeHandler'
import type { DisplayMode } from '../themes/types' import type { DisplayMode } from '../themes/types'
const { mode, setMode, amoledEnabled, setAmoledEnabled } = useTheme() const { mode, setMode, state, amoledEnabled, setAmoledEnabled } = useTheme()
const isOpen = ref(false) const isOpen = ref(false)
const dropdownRef = ref<HTMLElement | null>(null) const dropdownRef = ref<HTMLElement | null>(null)
@ -15,33 +15,18 @@ interface ModeChoice {
isAmoled?: boolean isAmoled?: boolean
} }
const baseModeChoices: ModeChoice[] = [ const modeChoices: ModeChoice[] = [
{ mode: 'light', label: 'Light', icon: 'i-ph-sun-duotone' }, { mode: 'light', label: 'Light', icon: 'i-ph-sun-duotone' },
{ mode: 'dark', label: 'Dark', icon: 'i-ph-moon-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 currentChoice = computed(() => {
const current = (mode && (mode as any).value) ? (mode as any).value : 'light' const current = (mode && (mode as any).value) ? (mode as any).value : 'light'
// Handle custom mode
if (current === 'custom') {
return baseModeChoices[3] // Custom option
}
if (current === 'dark' && amoledEnabled.value) { if (current === 'dark' && amoledEnabled.value) {
return baseModeChoices[2] // AMOLED option return modeChoices[2] // AMOLED option
} }
return baseModeChoices.find(choice => choice.mode === current && !choice.isAmoled) || baseModeChoices[0] return modeChoices.find(choice => choice.mode === current && !choice.isAmoled) || modeChoices[0]
}) })
const toggleDropdown = () => { const toggleDropdown = () => {
@ -49,20 +34,6 @@ const toggleDropdown = () => {
} }
const selectMode = (choice: ModeChoice) => { 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) { if (choice.isAmoled) {
setMode('dark') setMode('dark')
setAmoledEnabled(true) setAmoledEnabled(true)
@ -75,31 +46,12 @@ const selectMode = (choice: ModeChoice) => {
const isActiveChoice = (choice: ModeChoice) => { const isActiveChoice = (choice: ModeChoice) => {
const current = (mode && (mode as any).value) ? (mode as any).value : 'light' 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) { if (choice.isAmoled) {
return current === 'dark' && amoledEnabled.value return current === 'dark' && amoledEnabled.value
} }
return choice.mode === current && !choice.isAmoled && !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) => { const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) { if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
isOpen.value = false isOpen.value = false
@ -134,8 +86,7 @@ onUnmounted(() => {
v-for="(choice, index) in modeChoices" v-for="(choice, index) in modeChoices"
:key="index" :key="index"
class="theme-dropdown-item" class="theme-dropdown-item"
:class="{ active: isActiveChoice(choice), disabled: isDisabled(choice) }" :class="{ active: isActiveChoice(choice) }"
:title="getTooltip(choice)"
@click="selectMode(choice)" @click="selectMode(choice)"
> >
<div :class="[choice.icon, 'text-lg']" /> <div :class="[choice.icon, 'text-lg']" />
@ -215,16 +166,6 @@ onUnmounted(() => {
font-weight: 500; font-weight: 500;
} }
&.disabled {
color: var(--vp-c-text-3);
opacity: 0.5;
cursor: not-allowed;
&:hover {
background: transparent;
}
}
span { span {
flex: 1; flex: 1;
} }

View file

@ -17,12 +17,10 @@
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import type { DisplayMode, ThemeState, Theme, ModeColors } from './types' import type { DisplayMode, ThemeState, Theme, ModeColors } from './types'
import { themeRegistry } from './configs' import { themeRegistry } from './configs'
import tinycolor from 'tinycolor2'
const STORAGE_KEY_THEME = 'vitepress-theme-name' const STORAGE_KEY_THEME = 'vitepress-theme-name'
const STORAGE_KEY_MODE = 'vitepress-display-mode' const STORAGE_KEY_MODE = 'vitepress-display-mode'
const STORAGE_KEY_AMOLED = 'vitepress-amoled-enabled' const STORAGE_KEY_AMOLED = 'vitepress-amoled-enabled'
const STORAGE_KEY_PREVIOUS_MODE = 'vitepress-previous-mode'
export class ThemeHandler { export class ThemeHandler {
private state = ref<ThemeState>({ private state = ref<ThemeState>({
@ -31,178 +29,11 @@ export class ThemeHandler {
theme: null theme: null
}) })
private amoledEnabled = ref(false) private amoledEnabled = ref(false)
private previousMode = ref<DisplayMode>('light')
constructor() { constructor() {
this.initializeTheme() 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() { private initializeTheme() {
if (typeof window === 'undefined') return if (typeof window === 'undefined') return
@ -211,11 +42,6 @@ export class ThemeHandler {
const savedMode = localStorage.getItem(STORAGE_KEY_MODE) as DisplayMode | null const savedMode = localStorage.getItem(STORAGE_KEY_MODE) as DisplayMode | null
const savedAmoled = localStorage.getItem(STORAGE_KEY_AMOLED) === 'true' 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]) { if (themeRegistry[savedTheme]) {
this.state.value.currentTheme = savedTheme this.state.value.currentTheme = savedTheme
this.state.value.theme = themeRegistry[savedTheme] this.state.value.theme = themeRegistry[savedTheme]
@ -265,9 +91,7 @@ export class ThemeHandler {
if (!theme) return if (!theme) return
// Custom mode uses dark mode colors from the theme const modeColors = theme.modes[currentMode]
const effectiveMode = currentMode === 'custom' ? 'dark' : currentMode
const modeColors = theme.modes[effectiveMode]
this.applyDOMClasses(currentMode) this.applyDOMClasses(currentMode)
this.applyCSSVariables(modeColors, theme) this.applyCSSVariables(modeColors, theme)
@ -283,7 +107,7 @@ export class ThemeHandler {
const root = document.documentElement const root = document.documentElement
// Remove all mode classes // Remove all mode classes
root.classList.remove('dark', 'light', 'amoled', 'custom') root.classList.remove('dark', 'light', 'amoled')
// Add current mode class // Add current mode class
root.classList.add(mode) root.classList.add(mode)
@ -335,14 +159,6 @@ export class ThemeHandler {
root.style.setProperty('--vp-c-bg', bgColor) root.style.setProperty('--vp-c-bg', bgColor)
root.style.setProperty('--vp-c-bg-alt', bgAltColor) root.style.setProperty('--vp-c-bg-alt', bgAltColor)
root.style.setProperty('--vp-c-bg-elv', bgElvColor) 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) { if (colors.bgMark) {
root.style.setProperty('--vp-c-bg-mark', colors.bgMark) root.style.setProperty('--vp-c-bg-mark', colors.bgMark)
} }
@ -466,12 +282,6 @@ export class ThemeHandler {
} }
public setMode(mode: DisplayMode) { 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 this.state.value.currentMode = mode
localStorage.setItem(STORAGE_KEY_MODE, mode) localStorage.setItem(STORAGE_KEY_MODE, mode)
this.applyTheme() this.applyTheme()
@ -509,9 +319,7 @@ export class ThemeHandler {
if (!theme) return if (!theme) return
// If theme doesn't specify brand colors, force ColorPicker to reapply its selection // If theme doesn't specify brand colors, force ColorPicker to reapply its selection
const currentMode = this.state.value.currentMode const currentMode = this.state.value.currentMode
// Custom mode uses dark mode colors const modeColors = theme.modes[currentMode]
const effectiveMode = currentMode === 'custom' ? 'dark' : currentMode
const modeColors = theme.modes[effectiveMode]
if (!modeColors.brand || !modeColors.brand[1]) { if (!modeColors.brand || !modeColors.brand[1]) {
// Trigger a custom event that ColorPicker can listen to // Trigger a custom event that ColorPicker can listen to
@ -552,15 +360,6 @@ export class ThemeHandler {
public isAmoledMode() { public isAmoledMode() {
return this.state.value.currentMode === 'dark' && this.amoledEnabled.value 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 // Global theme handler instance
@ -596,7 +395,6 @@ export function useTheme() {
amoledEnabled: handler.getAmoledEnabledRef(), amoledEnabled: handler.getAmoledEnabledRef(),
setAmoledEnabled: (enabled: boolean) => handler.setAmoledEnabled(enabled), setAmoledEnabled: (enabled: boolean) => handler.setAmoledEnabled(enabled),
toggleAmoled: () => handler.toggleAmoled(), toggleAmoled: () => handler.toggleAmoled(),
restorePreviousMode: () => handler.restorePreviousMode(),
state state
} }
} }

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
export type DisplayMode = 'light' | 'dark' | 'custom' export type DisplayMode = 'light' | 'dark'
export interface ModeColors { export interface ModeColors {
// Brand colors (optional - if not specified, ColorPicker values are used) // Brand colors (optional - if not specified, ColorPicker values are used)

View file

@ -147,7 +147,7 @@ export function getHeader(id: string) {
const title = 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]">' '<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 style="color: var(--vp-c-text-1)">' const description = '<p class="text-black dark:text-text-2">'
const feedback = meta.build.api ? '<Feedback />' : '' const feedback = meta.build.api ? '<Feedback />' : ''

BIN
package-lock.json generated

Binary file not shown.

View file

@ -36,9 +36,9 @@
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pathe": "^2.0.3", "pathe": "^2.0.3",
"reka-ui": "^2.6.1", "reka-ui": "^2.6.1",
"tinycolor2": "^1.6.0",
"unocss": "66.5.10", "unocss": "66.5.10",
"vitepress": "^1.6.4", "vitepress": "^1.6.4",
"vue": "^3.5.25",
"x-satori": "^0.4.0", "x-satori": "^0.4.0",
"zod": "^4.1.13" "zod": "^4.1.13"
}, },
@ -61,7 +61,6 @@
"@iconify/utils": "^3.1.0", "@iconify/utils": "^3.1.0",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/tinycolor2": "^1.4.6",
"@vue/compiler-sfc": "^3.5.27", "@vue/compiler-sfc": "^3.5.27",
"floating-vue": "^5.2.2", "floating-vue": "^5.2.2",
"nitro-cloudflare-dev": "^0.2.2", "nitro-cloudflare-dev": "^0.2.2",
@ -74,7 +73,6 @@
"vite-plugin-optimize-exclude": "^0.0.1", "vite-plugin-optimize-exclude": "^0.0.1",
"vite-plugin-pwa": "^1.2.0", "vite-plugin-pwa": "^1.2.0",
"vite-plugin-terminal": "^1.3.0", "vite-plugin-terminal": "^1.3.0",
"vue": "^3.5.0",
"wrangler": "^4.52.1" "wrangler": "^4.52.1"
}, },
"pnpm": { "pnpm": {

Binary file not shown.

View file

@ -102,12 +102,12 @@ export default defineConfig({
{ {
display: 'inline-block', display: 'inline-block',
padding: '0.2em 0.4em', padding: '0.2em 0.4em',
'font-size': '0.75em', fontSize: '0.75em',
'font-weight': '500', fontWeight: '500',
'line-height': '1', lineHeight: '1',
color: 'var(--vp-c-text-1)', color: 'var(--vp-c-text-1)',
'background-color': 'rgb(var(--vp-c-bg-alt))', backgroundColor: 'rgb(var(--vp-c-bg-alt))',
'border-radius': '4px' borderRadius: '4px'
} }
] ]
], ],