mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-16 13:11:22 +08:00
1045 lines
30 KiB
Vue
1045 lines
30 KiB
Vue
<script setup lang="ts">
|
||
import type { CSSProperties } from 'vue'
|
||
import {
|
||
themeCustomizerPrimaryColors,
|
||
useThemeCustomizer,
|
||
type ThemeCustomizerLayout,
|
||
type ThemeCustomizerRadius,
|
||
type ThemeCustomizerShadow,
|
||
type ThemeCustomizerSkin,
|
||
type ThemeCustomizerTheme,
|
||
} from '@/composables/useThemeCustomizer'
|
||
import { usePWA } from '@/composables/usePWA'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { useTheme } from 'vuetify'
|
||
import { VDialog, VNavigationDrawer } from 'vuetify/components'
|
||
import { useDisplay } from 'vuetify'
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
modelValue?: boolean
|
||
}>(),
|
||
{
|
||
modelValue: false,
|
||
},
|
||
)
|
||
|
||
const emit = defineEmits<{
|
||
'update:modelValue': [value: boolean]
|
||
}>()
|
||
|
||
const customColorInput = ref<HTMLInputElement | null>(null)
|
||
|
||
const {
|
||
isCustomized,
|
||
resetSettings,
|
||
setLayout,
|
||
setPrimaryColor,
|
||
setRadius,
|
||
setSemiDarkMenu,
|
||
setShadow,
|
||
setSkin,
|
||
setTheme,
|
||
settings,
|
||
} = useThemeCustomizer()
|
||
const { appMode } = usePWA()
|
||
const { t } = useI18n()
|
||
const { global: globalTheme } = useTheme()
|
||
const display = useDisplay()
|
||
const defaultPrimaryColor = themeCustomizerPrimaryColors[0].value
|
||
const customizerViewportHeight = ref('100dvh')
|
||
|
||
const drawer = computed({
|
||
get: () => props.modelValue,
|
||
set: value => emit('update:modelValue', value),
|
||
})
|
||
|
||
function getVisibleViewportHeight() {
|
||
if (typeof window === 'undefined') return '100dvh'
|
||
|
||
const height = window.visualViewport?.height || window.innerHeight || document.documentElement.clientHeight
|
||
|
||
return height > 0 ? `${Math.round(height)}px` : '100dvh'
|
||
}
|
||
|
||
// iOS 小屏的可见视口会随地址栏和独立模式 safe area 变化,面板高度需要跟随真实可见高度。
|
||
function syncCustomizerViewportHeight() {
|
||
customizerViewportHeight.value = getVisibleViewportHeight()
|
||
}
|
||
|
||
// 将主题定制器打开状态同步到根节点,供全局悬浮按钮避让右侧面板。
|
||
function syncThemeCustomizerOpenState(isOpen: boolean) {
|
||
if (typeof document === 'undefined') return
|
||
|
||
if (isOpen) {
|
||
document.documentElement.setAttribute('data-theme-customizer-open', 'true')
|
||
document.body.setAttribute('data-theme-customizer-open', 'true')
|
||
|
||
return
|
||
}
|
||
|
||
document.documentElement.removeAttribute('data-theme-customizer-open')
|
||
document.body.removeAttribute('data-theme-customizer-open')
|
||
}
|
||
|
||
// 组件卸载时清理根节点状态,避免路由切换后悬浮按钮继续保持让位。
|
||
function clearThemeCustomizerOpenState() {
|
||
syncThemeCustomizerOpenState(false)
|
||
}
|
||
|
||
watch(drawer, syncThemeCustomizerOpenState, { immediate: true })
|
||
watch(drawer, isOpen => {
|
||
if (isOpen) nextTick(syncCustomizerViewportHeight)
|
||
})
|
||
|
||
onMounted(() => {
|
||
syncCustomizerViewportHeight()
|
||
window.addEventListener('resize', syncCustomizerViewportHeight)
|
||
window.addEventListener('orientationchange', syncCustomizerViewportHeight)
|
||
window.visualViewport?.addEventListener('resize', syncCustomizerViewportHeight)
|
||
window.visualViewport?.addEventListener('scroll', syncCustomizerViewportHeight)
|
||
})
|
||
|
||
onScopeDispose(clearThemeCustomizerOpenState)
|
||
onScopeDispose(() => {
|
||
if (typeof window === 'undefined') return
|
||
|
||
window.removeEventListener('resize', syncCustomizerViewportHeight)
|
||
window.removeEventListener('orientationchange', syncCustomizerViewportHeight)
|
||
window.visualViewport?.removeEventListener('resize', syncCustomizerViewportHeight)
|
||
window.visualViewport?.removeEventListener('scroll', syncCustomizerViewportHeight)
|
||
})
|
||
|
||
const customizerContainer = computed(() => (appMode.value ? VDialog : VNavigationDrawer))
|
||
|
||
const customizerContainerStyle = computed<CSSProperties>(() => {
|
||
if (!appMode.value) return {}
|
||
|
||
return {
|
||
'--theme-customizer-viewport-height': customizerViewportHeight.value,
|
||
}
|
||
})
|
||
|
||
const customizerContainerProps = computed(() => {
|
||
if (appMode.value && display.mdAndDown.value) {
|
||
return {
|
||
class: 'theme-customizer-dialog-overlay',
|
||
scrim: true,
|
||
fullscreen: !display.mdAndUp.value,
|
||
scrollable: true,
|
||
}
|
||
}
|
||
|
||
return {
|
||
class: 'theme-customizer-drawer',
|
||
location: 'right' as const,
|
||
scrim: false,
|
||
temporary: true,
|
||
width: 420,
|
||
persistent: false,
|
||
}
|
||
})
|
||
|
||
const themeOptions = computed<Array<{ icon: string; title: string; value: ThemeCustomizerTheme }>>(() => [
|
||
{ title: t('theme.light'), value: 'light', icon: 'mdi-white-balance-sunny' },
|
||
{ title: t('theme.dark'), value: 'dark', icon: 'mdi-weather-night' },
|
||
{ title: t('theme.auto'), value: 'auto', icon: 'mdi-monitor' },
|
||
{ title: t('theme.purple'), value: 'purple', icon: 'mdi-theme-light-dark' },
|
||
{ title: t('theme.transparent'), value: 'transparent', icon: 'mdi-blur' },
|
||
])
|
||
|
||
const skinOptions = computed<Array<{ title: string; value: ThemeCustomizerSkin }>>(() => [
|
||
{ title: t('theme.customizer.skinDefault'), value: 'default' },
|
||
{ title: t('theme.customizer.skinBordered'), value: 'bordered' },
|
||
])
|
||
|
||
const shadowOptions = computed<
|
||
Array<{
|
||
title: string
|
||
value: ThemeCustomizerShadow
|
||
}>
|
||
>(() => [
|
||
{
|
||
title: t('theme.customizer.shadowNone'),
|
||
value: 'none',
|
||
},
|
||
{
|
||
title: t('theme.customizer.shadowLow'),
|
||
value: 'low',
|
||
},
|
||
{
|
||
title: t('theme.customizer.shadowMedium'),
|
||
value: 'medium',
|
||
},
|
||
{
|
||
title: t('theme.customizer.shadowHigh'),
|
||
value: 'high',
|
||
},
|
||
])
|
||
|
||
const radiusOptions = computed<
|
||
Array<{
|
||
previewRadius: string
|
||
title: string
|
||
value: ThemeCustomizerRadius
|
||
}>
|
||
>(() => [
|
||
{
|
||
previewRadius: '4px',
|
||
title: t('theme.customizer.radiusSmall'),
|
||
value: 'small',
|
||
},
|
||
{
|
||
previewRadius: '8px',
|
||
title: t('theme.customizer.radiusDefault'),
|
||
value: 'default',
|
||
},
|
||
{
|
||
previewRadius: '12px',
|
||
title: t('theme.customizer.radiusLarge'),
|
||
value: 'large',
|
||
},
|
||
{
|
||
previewRadius: '16px',
|
||
title: t('theme.customizer.radiusExtra'),
|
||
value: 'extra',
|
||
},
|
||
{
|
||
previewRadius: '24px',
|
||
title: t('theme.customizer.radiusHuge'),
|
||
value: 'huge',
|
||
},
|
||
])
|
||
|
||
const layoutOptions = computed<Array<{ icon: string; title: string; value: ThemeCustomizerLayout }>>(() => [
|
||
{ title: t('theme.customizer.layoutVertical'), value: 'vertical', icon: 'mdi-dock-left' },
|
||
{ title: t('theme.customizer.layoutCollapsed'), value: 'collapsed', icon: 'mdi-dock-window' },
|
||
{ title: t('theme.customizer.layoutHorizontal'), value: 'horizontal', icon: 'mdi-dock-top' },
|
||
])
|
||
|
||
const showLayoutSection = computed(() => !appMode.value)
|
||
|
||
const hasAppModeCustomization = computed(() => {
|
||
return (
|
||
settings.value.primaryColor !== defaultPrimaryColor ||
|
||
settings.value.radius !== 'default' ||
|
||
settings.value.shadow !== 'none' ||
|
||
settings.value.skin !== 'default' ||
|
||
settings.value.theme !== 'auto'
|
||
)
|
||
})
|
||
|
||
const showResetBadge = computed(() => (appMode.value ? hasAppModeCustomization.value : isCustomized.value))
|
||
|
||
const showSemiDarkMenuOption = computed(() => {
|
||
return (
|
||
!appMode.value &&
|
||
!globalTheme.current.value.dark &&
|
||
(settings.value.layout === 'vertical' || settings.value.layout === 'collapsed')
|
||
)
|
||
})
|
||
|
||
function openColorPicker() {
|
||
customColorInput.value?.click()
|
||
}
|
||
|
||
function handleCustomColorInput(event: Event) {
|
||
const color = (event.target as HTMLInputElement).value
|
||
|
||
setPrimaryColor(color)
|
||
}
|
||
|
||
function handleLayoutChange(layout: ThemeCustomizerLayout) {
|
||
// App 模式固定使用移动端导航,避免切换桌面布局后破坏底部导航体验。
|
||
if (appMode.value) return
|
||
|
||
setLayout(layout)
|
||
}
|
||
|
||
async function handleResetSettings() {
|
||
if (!appMode.value) {
|
||
await resetSettings()
|
||
|
||
return
|
||
}
|
||
|
||
// App 模式共享定制器,但保留桌面导航相关偏好,只重置 App 侧可调整的外观设置。
|
||
await setPrimaryColor(defaultPrimaryColor)
|
||
await setRadius('default')
|
||
await setShadow('none')
|
||
await setSkin('default')
|
||
await setTheme('auto')
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<Teleport to="body">
|
||
<Transition name="theme-customizer-glass">
|
||
<div
|
||
v-if="drawer"
|
||
class="theme-customizer-glass-backdrop"
|
||
:class="{ 'theme-customizer-glass-backdrop--dialog': appMode }"
|
||
/>
|
||
</Transition>
|
||
|
||
<component
|
||
:is="customizerContainer"
|
||
v-model="drawer"
|
||
v-bind="customizerContainerProps"
|
||
:style="customizerContainerStyle"
|
||
>
|
||
<div class="theme-customizer-panel" :class="{ 'theme-customizer-panel--dialog': appMode }">
|
||
<div class="theme-customizer-header py-5 px-4">
|
||
<div>
|
||
<h2 class="theme-customizer-title">{{ t('theme.customizer.title') }}</h2>
|
||
</div>
|
||
<div class="theme-customizer-header-actions">
|
||
<VBadge color="error" dot :model-value="showResetBadge" location="top end" offset-x="2" offset-y="2">
|
||
<IconBtn :aria-label="t('theme.customizer.reset')" @click="handleResetSettings">
|
||
<VIcon class="text-high-emphasis" icon="mdi-refresh" />
|
||
</IconBtn>
|
||
</VBadge>
|
||
<IconBtn :aria-label="t('common.close')" @click="drawer = false">
|
||
<VIcon class="text-high-emphasis" icon="mdi-close" />
|
||
</IconBtn>
|
||
</div>
|
||
</div>
|
||
|
||
<VDivider />
|
||
|
||
<PerfectScrollbar class="theme-customizer-body" :options="{ wheelPropagation: false }">
|
||
<section class="theme-customizer-section">
|
||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.primaryColor') }}</h3>
|
||
<div class="theme-customizer-color-grid">
|
||
<div
|
||
v-for="color in themeCustomizerPrimaryColors"
|
||
:key="color.value"
|
||
class="theme-customizer-color-option"
|
||
:class="{ 'is-active': settings.primaryColor === color.value }"
|
||
:aria-label="t('theme.customizer.usePrimaryColor', { color: color.name })"
|
||
@click="setPrimaryColor(color.value)"
|
||
>
|
||
<span class="theme-customizer-color-swatch" :style="{ backgroundColor: color.value }" />
|
||
</div>
|
||
|
||
<div
|
||
v-if="!appMode"
|
||
class="theme-customizer-color-option theme-customizer-color-option--picker"
|
||
:class="{
|
||
'is-active': !themeCustomizerPrimaryColors.some(color => color.value === settings.primaryColor),
|
||
}"
|
||
:aria-label="t('theme.customizer.chooseCustomColor')"
|
||
@click="openColorPicker"
|
||
>
|
||
<VIcon class="theme-customizer-native-icon" icon="mdi-palette-outline" size="30" />
|
||
<input
|
||
ref="customColorInput"
|
||
class="theme-customizer-native-color"
|
||
type="color"
|
||
:value="settings.primaryColor"
|
||
@input="handleCustomColorInput"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 class="theme-customizer-section-title">{{ t('common.theme') }}</h3>
|
||
<div class="theme-customizer-option-grid theme-customizer-option-grid--theme">
|
||
<div
|
||
v-for="theme in themeOptions"
|
||
:key="theme.value"
|
||
class="theme-customizer-card-option"
|
||
:class="{ 'is-active': settings.theme === theme.value }"
|
||
@click="setTheme(theme.value)"
|
||
>
|
||
<VIcon class="theme-customizer-theme-icon" :icon="theme.icon" size="36" />
|
||
<span>{{ theme.title }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<VDivider class="mt-7" />
|
||
|
||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.skins') }}</h3>
|
||
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--skins">
|
||
<div
|
||
v-for="skin in skinOptions"
|
||
:key="skin.value"
|
||
class="theme-customizer-preview-option"
|
||
:class="{ 'is-active': settings.skin === skin.value }"
|
||
@click="setSkin(skin.value)"
|
||
>
|
||
<span class="theme-customizer-mini-layout" :class="`theme-customizer-mini-layout--${skin.value}`">
|
||
<span class="mini-sidebar">
|
||
<i />
|
||
<i />
|
||
<i />
|
||
<i />
|
||
</span>
|
||
<span class="mini-content">
|
||
<i />
|
||
<i />
|
||
<i />
|
||
</span>
|
||
</span>
|
||
<span>{{ skin.title }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<VDivider class="mt-7" />
|
||
|
||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.radius') }}</h3>
|
||
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--radius">
|
||
<div
|
||
v-for="radius in radiusOptions"
|
||
:key="radius.value"
|
||
class="theme-customizer-preview-option"
|
||
:class="{ 'is-active': settings.radius === radius.value }"
|
||
@click="setRadius(radius.value)"
|
||
>
|
||
<span
|
||
class="theme-customizer-radius-scene"
|
||
:style="{ '--theme-customizer-radius-preview': radius.previewRadius }"
|
||
>
|
||
<span class="theme-customizer-radius-scene__card">
|
||
<span class="theme-customizer-radius-scene__badge" />
|
||
<span class="theme-customizer-radius-scene__line" />
|
||
<span class="theme-customizer-radius-scene__line theme-customizer-radius-scene__line--short" />
|
||
</span>
|
||
</span>
|
||
<span>{{ radius.title }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<VDivider class="mt-7" />
|
||
|
||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.shadow') }}</h3>
|
||
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--shadow">
|
||
<div
|
||
v-for="shadow in shadowOptions"
|
||
:key="shadow.value"
|
||
class="theme-customizer-preview-option"
|
||
:class="{ 'is-active': settings.shadow === shadow.value }"
|
||
@click="setShadow(shadow.value)"
|
||
>
|
||
<span class="theme-customizer-shadow-scene" :class="`theme-customizer-shadow-scene--${shadow.value}`">
|
||
<span class="theme-customizer-shadow-scene__panel">
|
||
<span class="theme-customizer-shadow-scene__panel-line" />
|
||
<span
|
||
class="theme-customizer-shadow-scene__panel-line theme-customizer-shadow-scene__panel-line--short"
|
||
/>
|
||
</span>
|
||
|
||
<span class="theme-customizer-shadow-scene__card">
|
||
<span class="theme-customizer-shadow-scene__badge" />
|
||
<span class="theme-customizer-shadow-scene__line theme-customizer-shadow-scene__line--short" />
|
||
<span class="theme-customizer-shadow-scene__line" />
|
||
</span>
|
||
</span>
|
||
<span>{{ shadow.title }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="showSemiDarkMenuOption" class="theme-customizer-semi-dark">
|
||
<span>{{ t('theme.customizer.semiDarkMenu') }}</span>
|
||
<VSwitch
|
||
:model-value="settings.semiDarkMenu"
|
||
color="primary"
|
||
inset
|
||
hide-details
|
||
@update:model-value="setSemiDarkMenu(Boolean($event))"
|
||
/>
|
||
</div>
|
||
</section>
|
||
|
||
<VDivider v-if="showLayoutSection" />
|
||
|
||
<section v-if="showLayoutSection" class="theme-customizer-section">
|
||
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.layout') }}</h3>
|
||
<div class="theme-customizer-preview-grid">
|
||
<div
|
||
v-for="layout in layoutOptions"
|
||
:key="layout.value"
|
||
class="theme-customizer-preview-option"
|
||
:class="{ 'is-active': settings.layout === layout.value, 'is-disabled': appMode }"
|
||
@click="handleLayoutChange(layout.value)"
|
||
>
|
||
<span class="theme-customizer-mini-layout" :class="`theme-customizer-mini-layout--${layout.value}`">
|
||
<span class="mini-sidebar">
|
||
<i />
|
||
<i />
|
||
<i />
|
||
</span>
|
||
<span class="mini-content">
|
||
<i />
|
||
<i />
|
||
<i />
|
||
</span>
|
||
</span>
|
||
<span>{{ layout.title }}</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</PerfectScrollbar>
|
||
</div>
|
||
</component>
|
||
</Teleport>
|
||
</template>
|
||
|
||
<style lang="scss">
|
||
/* stylelint-disable no-descending-specificity */
|
||
|
||
.theme-customizer-drawer {
|
||
position: fixed !important;
|
||
z-index: 12000 !important;
|
||
overflow: hidden;
|
||
block-size: 100dvh !important;
|
||
border-inline-start: 1px solid rgba(var(--v-theme-on-surface), 0.08) !important;
|
||
box-shadow: -2px 0 6px rgba(0, 0, 0, 10%) !important;
|
||
inset-block: 0 !important;
|
||
inset-inline-end: 0 !important;
|
||
max-block-size: 100dvh !important;
|
||
|
||
.v-navigation-drawer__content {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
overflow: hidden;
|
||
flex-direction: column;
|
||
block-size: 100%;
|
||
}
|
||
}
|
||
|
||
.theme-customizer-dialog-overlay {
|
||
--theme-customizer-viewport-height: 100dvh;
|
||
|
||
z-index: 12000 !important;
|
||
}
|
||
|
||
.theme-customizer-dialog-overlay > .v-overlay__content {
|
||
overflow: hidden;
|
||
block-size: var(--theme-customizer-viewport-height);
|
||
margin-block: 0 !important;
|
||
max-block-size: var(--theme-customizer-viewport-height);
|
||
}
|
||
|
||
.theme-customizer-panel {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
block-size: 100%;
|
||
min-block-size: 0;
|
||
}
|
||
|
||
.theme-customizer-panel--dialog {
|
||
overflow: hidden;
|
||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||
border-radius: var(--app-surface-radius);
|
||
background: rgb(var(--v-theme-surface));
|
||
block-size: var(--theme-customizer-viewport-height, 100dvh);
|
||
max-block-size: var(--theme-customizer-viewport-height, 100dvh);
|
||
/* fullscreen dialog 会贴到 viewport-fit=cover 顶部,iOS 需要在面板内部避开系统状态栏。 */
|
||
padding-block-start: env(safe-area-inset-top);
|
||
}
|
||
|
||
.theme-customizer-panel--dialog .theme-customizer-body {
|
||
block-size: auto;
|
||
padding-block-end: env(safe-area-inset-bottom);
|
||
}
|
||
|
||
.theme-customizer-drawer.v-theme--transparent,
|
||
.v-theme--transparent .theme-customizer-drawer,
|
||
html[data-theme='transparent'] .theme-customizer-drawer,
|
||
.v-theme--transparent .theme-customizer-panel--dialog,
|
||
html[data-theme='transparent'] .theme-customizer-panel--dialog {
|
||
background: transparent !important;
|
||
}
|
||
|
||
.theme-customizer-glass-backdrop {
|
||
position: fixed;
|
||
z-index: 11999;
|
||
block-size: 100dvh;
|
||
inline-size: 420px;
|
||
inset-block: 0;
|
||
inset-inline-end: 0;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transform: translateX(0);
|
||
}
|
||
|
||
:is(html[data-theme='transparent'], .v-theme--transparent) .theme-customizer-glass-backdrop {
|
||
backdrop-filter: blur(var(--transparent-blur-heavy, 16px));
|
||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy, 0.5));
|
||
opacity: 1;
|
||
}
|
||
|
||
.theme-customizer-glass-enter-active,
|
||
.theme-customizer-glass-leave-active {
|
||
transition:
|
||
opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.theme-customizer-glass-enter-from,
|
||
.theme-customizer-glass-leave-to {
|
||
opacity: 0 !important;
|
||
transform: translateX(100%);
|
||
}
|
||
|
||
:is(html[data-theme='transparent'], .v-theme--transparent) .theme-customizer-drawer .v-navigation-drawer__content {
|
||
background: transparent !important;
|
||
}
|
||
|
||
@media (width <= 600px) {
|
||
.theme-customizer-glass-backdrop {
|
||
inline-size: 100vw;
|
||
}
|
||
}
|
||
|
||
// 透明主题的全局 overlay 毛玻璃会影响临时抽屉绘制,主题定制器改由 drawer 自身承担背景。
|
||
html[data-theme='transparent'] .v-overlay__content:has(.theme-customizer-drawer),
|
||
.v-theme--transparent .v-overlay__content:has(.theme-customizer-drawer) {
|
||
border-radius: 0 !important;
|
||
backdrop-filter: none !important;
|
||
background: transparent !important;
|
||
box-shadow: none !important;
|
||
}
|
||
|
||
:is(html[data-theme='transparent'], .v-theme--transparent) .theme-customizer-card-option .theme-customizer-theme-icon,
|
||
:is(html[data-theme='transparent'], .v-theme--transparent)
|
||
.theme-customizer-color-option
|
||
.theme-customizer-native-icon {
|
||
backdrop-filter: none !important;
|
||
background: transparent !important;
|
||
box-shadow: none !important;
|
||
}
|
||
|
||
.theme-customizer-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
}
|
||
|
||
.theme-customizer-title {
|
||
margin: 0;
|
||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||
font-size: 1.45rem;
|
||
font-weight: 600;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.theme-customizer-header-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.theme-customizer-body {
|
||
flex: 1 1 auto;
|
||
min-block-size: 0;
|
||
}
|
||
|
||
.theme-customizer-section {
|
||
padding-block-end: 28px;
|
||
padding-inline: 32px;
|
||
}
|
||
|
||
.theme-customizer-section-title {
|
||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
line-height: 1.25;
|
||
margin-block: 28px 16px;
|
||
}
|
||
|
||
.theme-customizer-section-note {
|
||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||
font-size: 0.875rem;
|
||
line-height: 1.45;
|
||
margin-block: -6px 16px;
|
||
}
|
||
|
||
.theme-customizer-color-grid {
|
||
display: grid;
|
||
gap: 12px;
|
||
grid-template-columns: repeat(auto-fill, 48px);
|
||
}
|
||
|
||
.theme-customizer-color-option {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
||
border-radius: 10px;
|
||
appearance: none;
|
||
block-size: 48px;
|
||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||
cursor: pointer;
|
||
inline-size: 48px;
|
||
transition:
|
||
border-color 0.18s ease,
|
||
background-color 0.18s ease,
|
||
box-shadow 0.18s ease;
|
||
|
||
&.is-active {
|
||
border-width: 2px;
|
||
border-color: rgb(var(--v-theme-primary));
|
||
box-shadow: 0 0 0 3px rgba(var(--v-theme-primary), 0.12);
|
||
}
|
||
}
|
||
|
||
.theme-customizer-color-swatch {
|
||
display: block;
|
||
border-radius: 8px;
|
||
block-size: 30px;
|
||
inline-size: 30px;
|
||
}
|
||
|
||
.theme-customizer-color-option--picker {
|
||
background: rgba(var(--v-theme-on-surface), 0.04);
|
||
}
|
||
|
||
.theme-customizer-native-color {
|
||
position: absolute;
|
||
block-size: 1px;
|
||
inline-size: 1px;
|
||
inset-block: 50% auto;
|
||
inset-inline: 50% auto;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.theme-customizer-option-grid {
|
||
display: grid;
|
||
gap: 16px;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
|
||
.theme-customizer-option-grid--theme {
|
||
grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
|
||
}
|
||
|
||
.theme-customizer-card-option,
|
||
.theme-customizer-preview-option {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
||
border-radius: 10px;
|
||
appearance: none;
|
||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||
cursor: pointer;
|
||
font-size: 1rem;
|
||
gap: 10px;
|
||
transition:
|
||
border-color 0.18s ease,
|
||
background-color 0.18s ease,
|
||
color 0.18s ease,
|
||
box-shadow 0.18s ease;
|
||
|
||
&.is-active {
|
||
border-width: 2px;
|
||
border-color: rgb(var(--v-theme-primary));
|
||
background: rgba(var(--v-theme-primary), 0.08);
|
||
box-shadow: 0 0 0 3px rgba(var(--v-theme-primary), 0.12);
|
||
color: rgb(var(--v-theme-primary));
|
||
}
|
||
}
|
||
|
||
.theme-customizer-card-option {
|
||
justify-content: center;
|
||
padding: 16px;
|
||
min-block-size: 112px;
|
||
}
|
||
|
||
.theme-customizer-preview-grid {
|
||
display: grid;
|
||
gap: 16px;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
|
||
.theme-customizer-preview-grid--skins {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.theme-customizer-preview-grid--shadow {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.theme-customizer-preview-grid--radius {
|
||
grid-template-columns: repeat(auto-fit, minmax(92px, 1fr));
|
||
}
|
||
|
||
.theme-customizer-preview-option {
|
||
align-items: flex-start;
|
||
padding: 0;
|
||
border: 0;
|
||
background: transparent;
|
||
box-shadow: none !important;
|
||
|
||
&.is-active {
|
||
background: transparent;
|
||
box-shadow: none !important;
|
||
|
||
.theme-customizer-mini-layout,
|
||
.theme-customizer-radius-scene,
|
||
.theme-customizer-shadow-scene {
|
||
border-width: 2px;
|
||
border-color: rgb(var(--v-theme-primary));
|
||
background: rgba(var(--v-theme-primary), 0.04);
|
||
}
|
||
}
|
||
|
||
> span:last-child {
|
||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||
padding-inline-start: 2px;
|
||
}
|
||
|
||
&.is-disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.52;
|
||
}
|
||
}
|
||
|
||
.theme-customizer-semi-dark {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
margin-block-start: 28px;
|
||
margin-inline: -32px;
|
||
padding-inline: 32px;
|
||
}
|
||
|
||
.theme-customizer-mini-layout {
|
||
display: grid;
|
||
overflow: hidden;
|
||
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
||
border-radius: 10px;
|
||
block-size: 74px;
|
||
grid-template-columns: 34% 1fr;
|
||
inline-size: 100%;
|
||
min-inline-size: 92px;
|
||
}
|
||
|
||
.theme-customizer-mini-layout--collapsed {
|
||
grid-template-columns: 18% 1fr;
|
||
}
|
||
|
||
.theme-customizer-mini-layout--horizontal {
|
||
grid-template-columns: 1fr;
|
||
grid-template-rows: 24% 1fr;
|
||
|
||
.mini-sidebar {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
}
|
||
|
||
.mini-sidebar,
|
||
.mini-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 10px;
|
||
gap: 8px;
|
||
}
|
||
|
||
.mini-sidebar {
|
||
background: rgba(var(--v-theme-on-surface), 0.04);
|
||
}
|
||
|
||
.mini-sidebar i,
|
||
.mini-content i {
|
||
display: block;
|
||
border-radius: 4px;
|
||
background: rgba(var(--v-theme-on-surface), 0.12);
|
||
block-size: 6px;
|
||
}
|
||
|
||
.mini-content i {
|
||
background: rgba(var(--v-theme-on-surface), 0.06);
|
||
block-size: 18px;
|
||
}
|
||
|
||
.theme-customizer-mini-layout--bordered {
|
||
.mini-content i,
|
||
.mini-sidebar i {
|
||
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
||
background: transparent;
|
||
}
|
||
}
|
||
|
||
.theme-customizer-radius-scene {
|
||
position: relative;
|
||
display: block;
|
||
overflow: hidden;
|
||
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
||
border-radius: 10px;
|
||
background:
|
||
linear-gradient(180deg, rgba(var(--v-theme-on-surface), 0.02), rgba(var(--v-theme-on-surface), 0.05)),
|
||
rgb(var(--v-theme-surface));
|
||
block-size: 90px;
|
||
inline-size: 100%;
|
||
min-inline-size: 0;
|
||
}
|
||
|
||
.theme-customizer-radius-scene__card {
|
||
position: absolute;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||
border-radius: var(--theme-customizer-radius-preview);
|
||
background: rgb(var(--v-theme-surface));
|
||
gap: 8px;
|
||
inset: 16px;
|
||
padding-block: 12px;
|
||
padding-inline: 14px;
|
||
}
|
||
|
||
.theme-customizer-radius-scene__badge,
|
||
.theme-customizer-radius-scene__line {
|
||
display: block;
|
||
border-radius: 999px;
|
||
background: rgba(var(--v-theme-on-surface), 0.1);
|
||
}
|
||
|
||
.theme-customizer-radius-scene__badge {
|
||
block-size: 8px;
|
||
inline-size: 42%;
|
||
min-inline-size: 28px;
|
||
}
|
||
|
||
.theme-customizer-radius-scene__line {
|
||
block-size: 7px;
|
||
}
|
||
|
||
.theme-customizer-radius-scene__line--short {
|
||
inline-size: 66%;
|
||
}
|
||
|
||
.theme-customizer-shadow-scene {
|
||
position: relative;
|
||
display: block;
|
||
overflow: hidden;
|
||
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
||
border-radius: 10px;
|
||
background:
|
||
linear-gradient(180deg, rgba(var(--v-theme-on-surface), 0.02), rgba(var(--v-theme-on-surface), 0.06)),
|
||
rgb(var(--v-theme-surface));
|
||
block-size: 110px;
|
||
inline-size: 100%;
|
||
min-inline-size: 0;
|
||
}
|
||
|
||
.theme-customizer-shadow-scene__panel,
|
||
.theme-customizer-shadow-scene__card {
|
||
position: absolute;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||
background: rgb(var(--v-theme-surface));
|
||
box-shadow: none;
|
||
transition: box-shadow 0.18s ease;
|
||
}
|
||
|
||
.theme-customizer-shadow-scene__panel {
|
||
padding: 12px;
|
||
gap: 8px;
|
||
inset-block-start: 16px;
|
||
inset-inline: 14px;
|
||
min-block-size: 54px;
|
||
}
|
||
|
||
.theme-customizer-shadow-scene__card {
|
||
gap: 8px;
|
||
inset-block-end: 12px;
|
||
inset-inline: 20px 16px;
|
||
min-block-size: 46px;
|
||
padding-block: 10px;
|
||
padding-inline: 12px;
|
||
}
|
||
|
||
.theme-customizer-shadow-scene__panel-line,
|
||
.theme-customizer-shadow-scene__line,
|
||
.theme-customizer-shadow-scene__badge {
|
||
display: block;
|
||
border-radius: 999px;
|
||
background: rgba(var(--v-theme-on-surface), 0.1);
|
||
}
|
||
|
||
.theme-customizer-shadow-scene__badge {
|
||
block-size: 6px;
|
||
inline-size: 34%;
|
||
min-inline-size: 28px;
|
||
}
|
||
|
||
.theme-customizer-shadow-scene__panel-line,
|
||
.theme-customizer-shadow-scene__line {
|
||
block-size: 7px;
|
||
}
|
||
|
||
.theme-customizer-shadow-scene__panel-line--short,
|
||
.theme-customizer-shadow-scene__line--short {
|
||
inline-size: 62%;
|
||
}
|
||
|
||
.theme-customizer-shadow-scene--low {
|
||
.theme-customizer-shadow-scene__panel {
|
||
box-shadow:
|
||
0 8px 18px rgba(var(--v-theme-on-surface), 0.08),
|
||
0 2px 6px rgba(var(--v-theme-on-surface), 0.05);
|
||
}
|
||
|
||
.theme-customizer-shadow-scene__card {
|
||
box-shadow:
|
||
0 10px 22px rgba(var(--v-theme-on-surface), 0.1),
|
||
0 4px 10px rgba(var(--v-theme-on-surface), 0.06);
|
||
}
|
||
}
|
||
|
||
.theme-customizer-shadow-scene--medium {
|
||
.theme-customizer-shadow-scene__panel {
|
||
box-shadow:
|
||
0 12px 28px rgba(var(--v-theme-on-surface), 0.12),
|
||
0 4px 12px rgba(var(--v-theme-on-surface), 0.08);
|
||
}
|
||
|
||
.theme-customizer-shadow-scene__card {
|
||
box-shadow:
|
||
0 16px 34px rgba(var(--v-theme-on-surface), 0.14),
|
||
0 6px 16px rgba(var(--v-theme-on-surface), 0.09);
|
||
}
|
||
}
|
||
|
||
.theme-customizer-shadow-scene--high {
|
||
.theme-customizer-shadow-scene__panel {
|
||
box-shadow:
|
||
0 16px 38px rgba(var(--v-theme-on-surface), 0.16),
|
||
0 6px 18px rgba(var(--v-theme-on-surface), 0.1);
|
||
}
|
||
|
||
.theme-customizer-shadow-scene__card {
|
||
box-shadow:
|
||
0 22px 48px rgba(var(--v-theme-on-surface), 0.18),
|
||
0 8px 22px rgba(var(--v-theme-on-surface), 0.12);
|
||
}
|
||
}
|
||
|
||
@media (width <= 600px) {
|
||
.theme-customizer-drawer {
|
||
inline-size: min(100vw, 420px) !important;
|
||
}
|
||
|
||
.theme-customizer-header,
|
||
.theme-customizer-section {
|
||
padding-inline: 22px;
|
||
}
|
||
|
||
.theme-customizer-preview-grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
</style>
|