mirror of
https://github.com/fmhy/edit.git
synced 2026-03-11 08:55:38 +00:00
This reverts commit a71bb4c0d0.
This commit is contained in:
parent
a71bb4c0d0
commit
daf15a1a8d
11 changed files with 29 additions and 1013 deletions
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
BIN
package-lock.json
generated
Binary file not shown.
|
|
@ -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": {
|
||||||
|
|
|
||||||
BIN
pnpm-lock.yaml
BIN
pnpm-lock.yaml
Binary file not shown.
|
|
@ -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'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue