mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-17 19:54:16 +08:00
Refine assistant layering and plugin source settings
This commit is contained in:
@@ -11,6 +11,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import AgentPetStage from './pet/AgentPetStage.vue'
|
||||
import type { AgentPetActionName, AgentPetIntent } from './pet/types'
|
||||
import { useAgentPetMachine } from './pet/useAgentPetMachine'
|
||||
import { AGENT_ASSISTANT_LAYER_Z_INDEX } from '@/constants/agentAssistant'
|
||||
|
||||
interface AgentAssistantEntryBubble {
|
||||
id: string
|
||||
@@ -163,6 +164,7 @@ const fabPositionStyle = computed(() => {
|
||||
...fabPointerStyle.value,
|
||||
'--agent-assistant-fab-x': `${position.x}px`,
|
||||
'--agent-assistant-fab-y': `${position.y}px`,
|
||||
zIndex: AGENT_ASSISTANT_LAYER_Z_INDEX.entry,
|
||||
}
|
||||
})
|
||||
const fabBubblePlacement = ref<FabBubblePlacement>('top')
|
||||
@@ -1570,9 +1572,6 @@ defineExpose({
|
||||
.agent-assistant-fab {
|
||||
position: fixed;
|
||||
|
||||
/* 保持机器人和提示气泡高于 Vuetify 弹窗(2400)及全局 Toast(2500)。 */
|
||||
z-index: 2600;
|
||||
|
||||
--agent-assistant-robot-outline: color-mix(in srgb, rgb(var(--v-theme-primary)) 72%, #090510 28%);
|
||||
--agent-assistant-robot-outline-soft: color-mix(in srgb, rgb(var(--v-theme-primary)) 84%, #090510 16%);
|
||||
--agent-assistant-robot-shell-start: color-mix(in srgb, rgb(var(--v-theme-primary)) 38%, white 62%);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore, useUserStore } from '@/stores'
|
||||
import { getCurrentLocale } from '@/plugins/i18n'
|
||||
import { AGENT_ASSISTANT_LAYER_Z_INDEX } from '@/constants/agentAssistant'
|
||||
|
||||
type AgentMessageRole = 'user' | 'assistant'
|
||||
type AgentMessageStatus = 'idle' | 'streaming' | 'done' | 'error'
|
||||
@@ -296,6 +297,7 @@ const isOpen = computed({
|
||||
})
|
||||
const drawerStyle = computed(() => ({
|
||||
'--agent-assistant-panel-width': drawerWidth.value,
|
||||
zIndex: AGENT_ASSISTANT_LAYER_Z_INDEX.panel,
|
||||
}))
|
||||
|
||||
// 创建前端展示用的临时 ID。
|
||||
@@ -2130,11 +2132,11 @@ onScopeDispose(() => {
|
||||
<VMenu
|
||||
v-model="historyMenuOpen"
|
||||
:close-on-content-click="false"
|
||||
content-class="agent-assistant-history-overlay"
|
||||
location="bottom end"
|
||||
offset="8"
|
||||
max-width="360"
|
||||
:z-index="2603"
|
||||
:style="{ zIndex: AGENT_ASSISTANT_LAYER_Z_INDEX.overlay }"
|
||||
:z-index="AGENT_ASSISTANT_LAYER_Z_INDEX.overlay"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" :title="t('agentAssistant.history')" :aria-label="t('agentAssistant.history')">
|
||||
@@ -2488,21 +2490,12 @@ onScopeDispose(() => {
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.agent-assistant-history-overlay {
|
||||
z-index: 2603 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* stylelint-disable selector-pseudo-class-no-unknown */
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
|
||||
.agent-assistant-panel {
|
||||
position: fixed;
|
||||
|
||||
/* Agent 会话层保持高于入口(2600)和业务弹窗,同时低于自身弹出菜单。 */
|
||||
z-index: 2601;
|
||||
overflow: hidden;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import AgentAssistantEntry from './AgentAssistantEntry.vue'
|
||||
import AgentAssistantPanel from './AgentAssistantPanel.vue'
|
||||
import { useAppActivityLifecycle } from '@/composables/useAppActivityLifecycle'
|
||||
import { useTheme } from 'vuetify'
|
||||
|
||||
type AgentAssistantEntryRef = InstanceType<typeof AgentAssistantEntry>
|
||||
|
||||
@@ -9,6 +10,7 @@ const panelOpen = ref(false)
|
||||
const thinking = ref(false)
|
||||
const entryRef = ref<AgentAssistantEntryRef | null>(null)
|
||||
const { allowsDecorativeMotion } = useAppActivityLifecycle()
|
||||
const { themeClasses } = useTheme()
|
||||
|
||||
// 打开 Agent 面板并清空入口预览气泡。
|
||||
function openPanel() {
|
||||
@@ -25,17 +27,22 @@ function handleAssistantPreview(value: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AgentAssistantEntry
|
||||
ref="entryRef"
|
||||
:active="!panelOpen"
|
||||
:motion-active="allowsDecorativeMotion"
|
||||
:thinking="thinking"
|
||||
@open="openPanel"
|
||||
/>
|
||||
<AgentAssistantPanel
|
||||
v-model="panelOpen"
|
||||
:motion-active="allowsDecorativeMotion"
|
||||
@assistant-preview="handleAssistantPreview"
|
||||
@thinking-change="thinking = $event"
|
||||
/>
|
||||
<!-- 脱离 .v-application 的层叠上下文,确保弹窗打开时入口、消息气泡和面板仍在最上层。 -->
|
||||
<Teleport to="body">
|
||||
<div class="agent-assistant-layer" :class="themeClasses">
|
||||
<AgentAssistantEntry
|
||||
ref="entryRef"
|
||||
:active="!panelOpen"
|
||||
:motion-active="allowsDecorativeMotion"
|
||||
:thinking="thinking"
|
||||
@open="openPanel"
|
||||
/>
|
||||
<AgentAssistantPanel
|
||||
v-model="panelOpen"
|
||||
:motion-active="allowsDecorativeMotion"
|
||||
@assistant-preview="handleAssistantPreview"
|
||||
@thinking-change="thinking = $event"
|
||||
/>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
53
src/components/agent/__tests__/AgentAssistantWidget.spec.ts
Normal file
53
src/components/agent/__tests__/AgentAssistantWidget.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { ref } from 'vue'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import AgentAssistantWidget from '@/components/agent/AgentAssistantWidget.vue'
|
||||
import { AGENT_ASSISTANT_LAYER_Z_INDEX } from '@/constants/agentAssistant'
|
||||
|
||||
vi.mock('vuetify', () => ({
|
||||
useTheme: () => ({ themeClasses: ref('v-theme--test') }),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useAppActivityLifecycle', () => ({
|
||||
useAppActivityLifecycle: () => ({ allowsDecorativeMotion: ref(true) }),
|
||||
}))
|
||||
|
||||
describe('AgentAssistantWidget layering', () => {
|
||||
afterEach(() => {
|
||||
document.body
|
||||
.querySelectorAll('.agent-assistant-layer, .agent-assistant-test-host')
|
||||
.forEach(element => element.remove())
|
||||
})
|
||||
|
||||
it('teleports the assistant outside the application stacking context while preserving the active theme', () => {
|
||||
const host = document.createElement('div')
|
||||
host.className = 'agent-assistant-test-host v-application'
|
||||
document.body.append(host)
|
||||
|
||||
const wrapper = mount(AgentAssistantWidget, {
|
||||
attachTo: host,
|
||||
global: {
|
||||
stubs: {
|
||||
AgentAssistantEntry: { template: '<div data-agent-assistant-entry />' },
|
||||
AgentAssistantPanel: { template: '<div data-agent-assistant-panel />' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const layer = document.body.querySelector(':scope > .agent-assistant-layer')
|
||||
|
||||
expect(layer).not.toBeNull()
|
||||
expect(host.contains(layer)).toBe(false)
|
||||
expect(layer).toHaveClass('v-theme--test')
|
||||
expect(layer?.querySelector('[data-agent-assistant-entry]')).not.toBeNull()
|
||||
expect(layer?.querySelector('[data-agent-assistant-panel]')).not.toBeNull()
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('reserves the highest CSS stacking levels in assistant display order', () => {
|
||||
expect(AGENT_ASSISTANT_LAYER_Z_INDEX.entry).toBe(2_147_483_645)
|
||||
expect(AGENT_ASSISTANT_LAYER_Z_INDEX.panel).toBe(2_147_483_646)
|
||||
expect(AGENT_ASSISTANT_LAYER_Z_INDEX.overlay).toBe(2_147_483_647)
|
||||
})
|
||||
})
|
||||
@@ -24,7 +24,7 @@ const repoText = ref('')
|
||||
const newRepoUrl = ref('')
|
||||
const editingIndex = ref<number | null>(null)
|
||||
const editingUrl = ref('')
|
||||
const syncingWiki = ref(false)
|
||||
const syncingSources = ref(false)
|
||||
|
||||
const emit = defineEmits(['save', 'close'])
|
||||
|
||||
@@ -139,10 +139,10 @@ async function saveHandle() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 Wiki 同步公开插件仓库清单并写入配置。 */
|
||||
async function syncWikiRepos() {
|
||||
/** 同步公开插件源清单并写入配置。 */
|
||||
async function syncPluginSources() {
|
||||
try {
|
||||
syncingWiki.value = true
|
||||
syncingSources.value = true
|
||||
const result: { [key: string]: any } = await api.post('system/setting/PLUGIN_MARKET/sync-wiki', {})
|
||||
|
||||
if (result.success) {
|
||||
@@ -164,7 +164,7 @@ async function syncWikiRepos() {
|
||||
console.log(error)
|
||||
$toast.error(t('dialog.pluginMarketSetting.syncFailed', { message: error instanceof Error ? error.message : '' }))
|
||||
} finally {
|
||||
syncingWiki.value = false
|
||||
syncingSources.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,11 +483,11 @@ onMounted(() => {
|
||||
color="success"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-cloud-sync-outline"
|
||||
:loading="syncingWiki"
|
||||
:disabled="syncingWiki"
|
||||
@click="syncWikiRepos"
|
||||
:loading="syncingSources"
|
||||
:disabled="syncingSources"
|
||||
@click="syncPluginSources"
|
||||
>
|
||||
{{ t('dialog.pluginMarketSetting.syncWiki') }}
|
||||
{{ t('dialog.pluginMarketSetting.syncSources') }}
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
@@ -506,6 +506,8 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* stylelint-disable selector-pseudo-class-no-unknown */
|
||||
|
||||
.plugin-market-dialog-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -570,9 +572,12 @@ onMounted(() => {
|
||||
.plugin-market-mode-switch {
|
||||
display: inline-flex;
|
||||
padding: 0.125rem;
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
border-radius: 0.375rem;
|
||||
background: rgba(var(--v-theme-surface), 0.72);
|
||||
border: var(--app-grouped-list-border);
|
||||
border-radius: var(--app-control-radius);
|
||||
-webkit-backdrop-filter: var(--app-grouped-list-backdrop-filter);
|
||||
backdrop-filter: var(--app-grouped-list-backdrop-filter);
|
||||
background: var(--app-grouped-list-background);
|
||||
box-shadow: var(--app-surface-shadow);
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
@@ -582,7 +587,7 @@ onMounted(() => {
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0.375rem;
|
||||
border-radius: var(--app-control-radius);
|
||||
background: transparent;
|
||||
block-size: 2.25rem;
|
||||
color: rgba(var(--v-theme-on-surface), 0.68);
|
||||
@@ -594,7 +599,7 @@ onMounted(() => {
|
||||
color 0.16s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--v-theme-primary), 0.07);
|
||||
background: var(--app-grouped-list-hover-background);
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
}
|
||||
|
||||
@@ -604,7 +609,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: rgba(var(--v-theme-primary), 0.12);
|
||||
background: var(--app-grouped-list-active-background);
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
}
|
||||
@@ -624,13 +629,23 @@ onMounted(() => {
|
||||
|
||||
.plugin-market-list-wrap {
|
||||
flex: 1;
|
||||
background: rgba(var(--v-theme-surface), 0.72);
|
||||
border: var(--app-grouped-list-border);
|
||||
border-radius: var(--app-grouped-list-radius);
|
||||
-webkit-backdrop-filter: var(--app-grouped-list-backdrop-filter);
|
||||
backdrop-filter: var(--app-grouped-list-backdrop-filter);
|
||||
background: var(--app-grouped-list-background);
|
||||
box-shadow: var(--app-surface-shadow);
|
||||
min-block-size: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.plugin-market-repo-list {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: inherit !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
backdrop-filter: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.plugin-market-repo-item {
|
||||
@@ -681,7 +696,12 @@ onMounted(() => {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
background: rgba(var(--v-theme-surface), 0.72);
|
||||
border: var(--app-grouped-list-border);
|
||||
border-radius: var(--app-grouped-list-radius);
|
||||
-webkit-backdrop-filter: var(--app-grouped-list-backdrop-filter);
|
||||
backdrop-filter: var(--app-grouped-list-backdrop-filter);
|
||||
background: var(--app-grouped-list-background);
|
||||
box-shadow: var(--app-surface-shadow);
|
||||
min-block-size: 0;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { cwd } from 'node:process'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const dialogSource = readFileSync(resolve(cwd(), 'src/components/dialog/PluginMarketSettingDialog.vue'), 'utf8')
|
||||
|
||||
function getStyleRule(selector: string) {
|
||||
const ruleStart = dialogSource.indexOf(`${selector} {`)
|
||||
const ruleEnd = dialogSource.indexOf('\n}', ruleStart)
|
||||
|
||||
expect(ruleStart).toBeGreaterThanOrEqual(0)
|
||||
expect(ruleEnd).toBeGreaterThan(ruleStart)
|
||||
|
||||
return dialogSource.slice(ruleStart, ruleEnd)
|
||||
}
|
||||
|
||||
describe('PluginMarketSettingDialog theme surfaces', () => {
|
||||
it('uses shared theme tokens for the view switch and editor containers', () => {
|
||||
const modeSwitchRule = getStyleRule('.plugin-market-mode-switch')
|
||||
const listWrapRule = getStyleRule('.plugin-market-list-wrap')
|
||||
const textareaRule = getStyleRule('.plugin-market-textarea-field')
|
||||
|
||||
expect(modeSwitchRule).toContain('border: var(--app-grouped-list-border)')
|
||||
expect(modeSwitchRule).toContain('backdrop-filter: var(--app-grouped-list-backdrop-filter)')
|
||||
expect(modeSwitchRule).toContain('background: var(--app-grouped-list-background)')
|
||||
expect(dialogSource).toContain('background: var(--app-grouped-list-hover-background)')
|
||||
expect(dialogSource).toContain('background: var(--app-grouped-list-active-background)')
|
||||
expect(listWrapRule).toContain('border-radius: var(--app-grouped-list-radius)')
|
||||
expect(listWrapRule).toContain('background: var(--app-grouped-list-background)')
|
||||
expect(textareaRule).toContain('background: var(--app-grouped-list-background)')
|
||||
})
|
||||
|
||||
it('uses plugin-source wording instead of exposing the Wiki implementation detail', () => {
|
||||
expect(dialogSource).toContain("t('dialog.pluginMarketSetting.syncSources')")
|
||||
expect(dialogSource).not.toContain("t('dialog.pluginMarketSetting.syncWiki')")
|
||||
})
|
||||
})
|
||||
8
src/constants/agentAssistant.ts
Normal file
8
src/constants/agentAssistant.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// Agent 助手需要覆盖应用内所有普通浮层,因此从 CSS 32 位层级上限向下预留内部顺序。
|
||||
const MAX_CSS_Z_INDEX = 2_147_483_647
|
||||
|
||||
export const AGENT_ASSISTANT_LAYER_Z_INDEX = {
|
||||
entry: MAX_CSS_Z_INDEX - 2,
|
||||
panel: MAX_CSS_Z_INDEX - 1,
|
||||
overlay: MAX_CSS_Z_INDEX,
|
||||
} as const
|
||||
@@ -4,7 +4,12 @@ import { useDisplay } from 'vuetify'
|
||||
import { NavMenu } from '@/@layouts/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { buildUserPermissionContext, filterItemsByPermission, filterMenusByPermission, hasItemPermission } from '@/utils/permission'
|
||||
import {
|
||||
buildUserPermissionContext,
|
||||
filterItemsByPermission,
|
||||
filterMenusByPermission,
|
||||
hasItemPermission,
|
||||
} from '@/utils/permission'
|
||||
import { useLaunchLoading } from '@/composables/useLaunchLoading'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import type { DynamicButtonMenuItem } from '@/composables/useDynamicButton'
|
||||
@@ -327,8 +332,7 @@ function handleDynamicMenuItemClick(item: DynamicButtonMenuItem) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// 底部导航挂载在 body 上,而主题定制器与 Agent 面板受 .v-application 的层叠上下文限制,
|
||||
// 无法仅靠 z-index 压过它。移动端两者都是全屏面板,打开时直接隐藏底部导航。
|
||||
// 移动端两个设置面板都是全屏展示,打开时隐藏底部导航,避免不可见控件继续参与焦点和合成。
|
||||
html[data-theme-customizer-open='true'],
|
||||
html[data-agent-assistant-open='true'] {
|
||||
.footer-nav-container {
|
||||
|
||||
@@ -3061,9 +3061,9 @@ export default {
|
||||
invalidText: 'There are {count} invalid URLs in the text. Fix them before saving.',
|
||||
invalidTextIgnored: '{count} invalid URLs ignored',
|
||||
duplicateTextIgnored: 'Duplicate URLs will be removed automatically when saving.',
|
||||
syncWiki: 'Sync Wiki',
|
||||
syncSuccess: 'Plugin repositories synced from Wiki. {added} added, {total} total.',
|
||||
syncFailed: 'Failed to sync Wiki: {message}!',
|
||||
syncSources: 'Sync Plugin Sources',
|
||||
syncSuccess: 'Plugin sources synced. {added} added, {total} total.',
|
||||
syncFailed: 'Failed to sync plugin sources: {message}!',
|
||||
close: 'Close',
|
||||
save: 'Save',
|
||||
saveSuccess: 'Plugin repository saved successfully',
|
||||
|
||||
@@ -3006,9 +3006,9 @@ export default {
|
||||
invalidText: '文本中有 {count} 个无效地址,请修正后保存。',
|
||||
invalidTextIgnored: '已忽略 {count} 个无效地址',
|
||||
duplicateTextIgnored: '重复地址会在保存时自动去重。',
|
||||
syncWiki: '同步 Wiki',
|
||||
syncSuccess: '已从 Wiki 同步插件仓库,新增 {added} 个,共 {total} 个',
|
||||
syncFailed: '同步 Wiki 失败:{message}!',
|
||||
syncSources: '同步插件源',
|
||||
syncSuccess: '已同步插件源,新增 {added} 个,共 {total} 个',
|
||||
syncFailed: '同步插件源失败:{message}!',
|
||||
close: '关闭',
|
||||
save: '保存',
|
||||
saveSuccess: '插件仓库保存成功',
|
||||
|
||||
@@ -3005,9 +3005,9 @@ export default {
|
||||
invalidText: '文字中有 {count} 個無效地址,請修正後儲存。',
|
||||
invalidTextIgnored: '已忽略 {count} 個無效地址',
|
||||
duplicateTextIgnored: '重複地址會在儲存時自動去重。',
|
||||
syncWiki: '同步 Wiki',
|
||||
syncSuccess: '已從 Wiki 同步插件倉庫,新增 {added} 個,共 {total} 個',
|
||||
syncFailed: '同步 Wiki 失敗:{message}!',
|
||||
syncSources: '同步插件源',
|
||||
syncSuccess: '已同步插件源,新增 {added} 個,共 {total} 個',
|
||||
syncFailed: '同步插件源失敗:{message}!',
|
||||
close: '關閉',
|
||||
save: '儲存',
|
||||
saveSuccess: '插件倉庫儲存成功',
|
||||
|
||||
@@ -139,6 +139,23 @@ describe('glass overlay material styles', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('reuses the popup menu material for compact FAB buttons', () => {
|
||||
const styles = readFileSync(resolve(cwd(), 'src/styles/themes/glass.scss'), 'utf8')
|
||||
const ruleStart = styles.indexOf('.compact-fab .v-btn {')
|
||||
const ruleEnd = styles.indexOf('\n }', ruleStart)
|
||||
const rule = styles.slice(ruleStart, ruleEnd)
|
||||
|
||||
expect(ruleStart).toBeGreaterThanOrEqual(0)
|
||||
expect(rule).toContain('border: 1px solid var(--glass-border-raised) !important')
|
||||
expect(rule).toContain('backdrop-filter: var(--glass-overlay-backdrop-filter) !important')
|
||||
expect(rule).toContain('background-color: var(--glass-overlay-surface) !important')
|
||||
expect(rule).toContain('background-image: var(--glass-sheen) !important')
|
||||
expect(rule).toContain('box-shadow: var(--glass-shadow-raised) !important')
|
||||
expect(styles).toMatch(
|
||||
/\.compact-fab \.v-btn:hover\s*\{\s*background-color:\s*var\(--glass-overlay-surface\)\s*!important;/,
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the shared hover-card contract instead of a Dashboard-specific shadow rule', () => {
|
||||
const styles = readFileSync(resolve(cwd(), 'src/styles/themes/glass.scss'), 'utf8')
|
||||
|
||||
|
||||
@@ -603,7 +603,6 @@ html[data-theme='glass'] {
|
||||
}
|
||||
|
||||
.nav-button,
|
||||
.compact-fab .v-btn,
|
||||
.global-action-buttons .v-btn {
|
||||
border: 1px solid var(--glass-border) !important;
|
||||
-webkit-backdrop-filter: var(--glass-raised-backdrop-filter);
|
||||
@@ -613,6 +612,16 @@ html[data-theme='glass'] {
|
||||
box-shadow: var(--glass-control-shadow) !important;
|
||||
}
|
||||
|
||||
// 聚合 FAB 与弹出菜单使用同一磨砂材质,展开后保持稳定底色和背景采样。
|
||||
.compact-fab .v-btn {
|
||||
border: 1px solid var(--glass-border-raised) !important;
|
||||
-webkit-backdrop-filter: var(--glass-overlay-backdrop-filter) !important;
|
||||
backdrop-filter: var(--glass-overlay-backdrop-filter) !important;
|
||||
background-color: var(--glass-overlay-surface) !important;
|
||||
background-image: var(--glass-sheen) !important;
|
||||
box-shadow: var(--glass-shadow-raised) !important;
|
||||
}
|
||||
|
||||
.v-overlay__scrim {
|
||||
-webkit-backdrop-filter: none;
|
||||
backdrop-filter: none;
|
||||
@@ -1320,6 +1329,11 @@ html[data-theme='glass'] {
|
||||
background-color: rgba(var(--v-theme-error), 42%) !important;
|
||||
}
|
||||
|
||||
// FAB 悬浮时保持弹出菜单材质,仅通过描边和阴影反馈交互,避免背景进一步透明。
|
||||
.compact-fab .v-btn:hover {
|
||||
background-color: var(--glass-overlay-surface) !important;
|
||||
}
|
||||
|
||||
.v-card--link:hover {
|
||||
border-color: var(--glass-border-hover) !important;
|
||||
box-shadow: var(--glass-shadow-hover) !important;
|
||||
|
||||
Reference in New Issue
Block a user