Files
MoviePilot-Frontend/src/components/ThemeCustomizer.vue
2026-06-05 23:29:11 +08:00

1045 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>