fix(desktop): 扩充应用菜单与元数据快捷键
This commit is contained in:
@@ -3,19 +3,26 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { useWorkspaceStore } from '@/stores/workspace'
|
||||
import TitleBarMenu from './TitleBarMenu.vue'
|
||||
|
||||
const execute = vi.hoisted(() => vi.fn())
|
||||
const capabilities = vi.hoisted(() => vi.fn())
|
||||
const routerPush = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/services/editorCommandService', () => ({
|
||||
executeEditorCommand: execute,
|
||||
getEditorCommandCapabilities: capabilities,
|
||||
subscribeEditorCommandCapabilities: () => () => undefined,
|
||||
}))
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({ push: routerPush }),
|
||||
useRoute: () => ({ name: 'workspace' }),
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
execute.mockReset().mockResolvedValue({ ok: true })
|
||||
routerPush.mockReset()
|
||||
capabilities.mockReturnValue([
|
||||
{ id: 'editor.paragraph', supported: true, enabled: true },
|
||||
{ id: 'editor.heading', supported: true, enabled: true },
|
||||
@@ -25,6 +32,33 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
describe('桌面顶部段落菜单', () => {
|
||||
it('文件菜单承载实际工作区命令和笔记导出', async () => {
|
||||
const editor = useEditorStore()
|
||||
editor.currentFilePath = '/示例.md'
|
||||
editor.content = '# 示例'
|
||||
editor.saveStatus = 'saved'
|
||||
const wrapper = mount(TitleBarMenu, { global: { stubs: { ExportDialog: { template: '<div data-testid="export-dialog" />' } } } })
|
||||
await wrapper.get('[data-menu="file"] .menu-trigger').trigger('click')
|
||||
expect(wrapper.text()).toContain('新建笔记…')
|
||||
expect(wrapper.text()).toContain('打开其他知识库…')
|
||||
expect(wrapper.text()).toContain('刷新文件树')
|
||||
expect(wrapper.text()).toContain('下载 Markdown 副本')
|
||||
await wrapper.get('.export-command').trigger('click')
|
||||
expect(wrapper.find('[data-testid="export-dialog"]').exists()).toBe(true)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('视图和帮助菜单只连接项目已有页面', async () => {
|
||||
useWorkspaceStore().hasVault = true
|
||||
const wrapper = mount(TitleBarMenu)
|
||||
await wrapper.get('[data-menu="view"] .menu-trigger').trigger('click')
|
||||
expect(wrapper.text()).toContain('工作区搜索AI 对话智能体任务音视频')
|
||||
expect(wrapper.text()).toContain('Skill 管理Plugin 管理MCP 服务器')
|
||||
await wrapper.get('[data-menu="help"] .menu-trigger').trigger('click')
|
||||
expect(wrapper.text()).toContain('运行日志Benchmark 评测社区目录设置与诊断…')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('源码笔记通过统一编辑命令执行属性导入', async () => {
|
||||
const editor = useEditorStore()
|
||||
editor.mode = 'source'
|
||||
|
||||
@@ -1,23 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { useWorkspaceStore } from '@/stores/workspace'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { executeEditorCommand, getEditorCommandCapabilities, subscribeEditorCommandCapabilities, type EditorCommandId } from '@/services/editorCommandService'
|
||||
import { calloutMenuCommands, formatMenuSections, paragraphMenuSections, type EditorMenuCommand } from '@/services/editorMenu'
|
||||
import * as workspaceService from '@/services/workspaceService'
|
||||
import ExportDialog from '@/features/editor/ExportDialog.vue'
|
||||
import ActionDialog from './ActionDialog.vue'
|
||||
import { useActionDialog } from '@/composables/useActionDialog'
|
||||
import { t } from '@/i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
type MenuName = 'file' | 'edit' | 'paragraph' | 'format' | 'view' | 'theme' | 'help'
|
||||
const editor = useEditorStore()
|
||||
const workspace = useWorkspaceStore()
|
||||
const theme = useThemeStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { actionDialog, resolveAction, askPrompt } = useActionDialog()
|
||||
const open = ref<MenuName | null>(null)
|
||||
const nestedOpen = ref(false)
|
||||
const exportOpen = ref(false)
|
||||
const fileBusy = ref(false)
|
||||
const bar = ref<HTMLElement | null>(null)
|
||||
const error = ref('')
|
||||
const capabilities = ref(new Map<EditorCommandId, boolean>())
|
||||
const shortcut = computed(() => /Mac|iPhone|iPad/.test(navigator.platform) ? '⌘⌥P' : 'Ctrl+Alt+P')
|
||||
const menuOrder: readonly MenuName[] = ['file', 'edit', 'paragraph', 'format', 'view', 'theme', 'help']
|
||||
const editorBlocked = computed(() => !editor.currentFilePath || ['saving', 'conflict', 'external_changed'].includes(editor.saveStatus))
|
||||
const viewDestinations = computed(() => [
|
||||
{ name: 'workspace', label: t('工作区', 'Workspace') },
|
||||
{ name: 'search', label: t('搜索', 'Search') },
|
||||
{ name: 'chat', label: t('AI 对话', 'AI Chat') },
|
||||
{ name: 'agent', label: t('智能体', 'Agent') },
|
||||
{ name: 'tasks', label: t('任务', 'Tasks') },
|
||||
{ name: 'media', label: t('音视频', 'Media') },
|
||||
])
|
||||
const extensionDestinations = computed(() => [
|
||||
{ name: 'skills', label: t('Skill 管理', 'Skill management') },
|
||||
{ name: 'plugins', label: t('Plugin 管理', 'Plugin management') },
|
||||
{ name: 'mcp-servers', label: t('MCP 服务器', 'MCP servers') },
|
||||
])
|
||||
|
||||
function enabled(id: EditorCommandId) {
|
||||
return capabilities.value.get(id) ?? false
|
||||
@@ -53,11 +77,92 @@ async function metadataCommand() {
|
||||
if (imported.ok) { close(); return }
|
||||
await command('editor.metadata.edit')
|
||||
}
|
||||
async function save() { await editor.save(); close() }
|
||||
async function save() {
|
||||
await editor.save()
|
||||
if (['saved', 'idle'].includes(editor.saveStatus)) close()
|
||||
else error.value = t('当前笔记尚未安全保存。', 'The current note has not been saved safely.')
|
||||
}
|
||||
function setMode(mode: 'source' | 'wysiwyg') { editor.setMode(mode); close() }
|
||||
function toggleTheme() { theme.toggleTheme(); close() }
|
||||
function applyTheme(id: string) { theme.applyTheme(id); close() }
|
||||
function navigate(path: string) { void router.push(path); close() }
|
||||
function navigateNamed(name: string) { void router.push({ name }); close() }
|
||||
|
||||
function message(reason: unknown) {
|
||||
return reason instanceof Error ? reason.message : String(reason)
|
||||
}
|
||||
async function ensureCurrentNoteSaved() {
|
||||
if (!editor.currentFilePath) return true
|
||||
if (['dirty', 'saving', 'save_failed'].includes(editor.saveStatus)) await editor.save()
|
||||
if (['saved', 'idle'].includes(editor.saveStatus)) return true
|
||||
error.value = t('请先处理当前笔记的保存或外部修改冲突。', 'Resolve the current note save or external-change conflict first.')
|
||||
return false
|
||||
}
|
||||
async function runFileAction(action: () => Promise<void>) {
|
||||
if (fileBusy.value) return
|
||||
close()
|
||||
error.value = ''
|
||||
fileBusy.value = true
|
||||
try { await action() }
|
||||
catch (reason) { error.value = message(reason) }
|
||||
finally { fileBusy.value = false }
|
||||
}
|
||||
async function createWorkspaceItem(type: 'file' | 'folder') {
|
||||
close()
|
||||
const rawName = (await askPrompt(type === 'file' ? t('笔记名称', 'Note name') : t('文件夹名称', 'Folder name')))?.trim()
|
||||
if (!rawName) return
|
||||
if (/[\\/]/.test(rawName) || ['.', '..'].includes(rawName)) {
|
||||
error.value = t('名称不能包含路径分隔符。', 'Names cannot contain path separators.')
|
||||
return
|
||||
}
|
||||
await runFileAction(async () => {
|
||||
if (!(await ensureCurrentNoteSaved())) return
|
||||
if (type === 'folder') {
|
||||
const folder = await workspaceService.createFolder('/', rawName)
|
||||
workspace.addFileToTree('/', folder)
|
||||
return
|
||||
}
|
||||
const title = rawName.replace(/\.md$/i, '')
|
||||
const file = await workspaceService.createFile('/', rawName, `# ${title}\n\n`)
|
||||
workspace.addFileToTree('/', file)
|
||||
await editor.loadFile(file.path)
|
||||
workspace.openFile(file.path)
|
||||
await router.push('/workspace')
|
||||
})
|
||||
}
|
||||
function openExport() { close(); exportOpen.value = true }
|
||||
function downloadMarkdown() {
|
||||
if (!editor.currentFilePath) return
|
||||
const url = URL.createObjectURL(new Blob([editor.content], { type: 'text/markdown;charset=utf-8' }))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = editor.currentFilePath.split('/').at(-1) ?? 'note.md'
|
||||
document.body.append(link); link.click(); link.remove()
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||||
close()
|
||||
}
|
||||
async function chooseVault() {
|
||||
await runFileAction(async () => {
|
||||
if (!(await ensureCurrentNoteSaved())) return
|
||||
await workspace.openVault('')
|
||||
editor.closeFile()
|
||||
await router.push('/workspace')
|
||||
})
|
||||
}
|
||||
async function refreshWorkspace() {
|
||||
await runFileAction(async () => { await workspace.refreshFileTree() })
|
||||
}
|
||||
async function closeCurrentNote() {
|
||||
await runFileAction(async () => {
|
||||
if (!(await ensureCurrentNoteSaved())) return
|
||||
const path = editor.currentFilePath
|
||||
if (!path) return
|
||||
workspace.closeFile(path)
|
||||
const next = workspace.activeFilePath
|
||||
editor.closeFile()
|
||||
if (next) await editor.loadFile(next)
|
||||
})
|
||||
}
|
||||
|
||||
async function switchMenu(offset: number, expand = open.value !== null) {
|
||||
const current = open.value ? menuOrder.indexOf(open.value) : 0
|
||||
@@ -128,25 +233,39 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ExportDialog v-if="exportOpen" @close="exportOpen = false" />
|
||||
<ActionDialog v-if="actionDialog" v-bind="actionDialog" @resolve="resolveAction" />
|
||||
<nav ref="bar" class="desktop-menu-bar" role="menubar" :aria-label="t('应用菜单', 'Application menu')" @keydown="handleKeys">
|
||||
<div class="menu-group" data-menu="file">
|
||||
<div class="menu-group" role="none" data-menu="file">
|
||||
<button class="menu-trigger" role="menuitem" :aria-expanded="open === 'file'" aria-haspopup="menu" @click="toggle('file')" @pointerenter="open && focusFirst('file')">{{ t('文件', 'File') }}</button>
|
||||
<div v-if="open === 'file'" class="menu-popover" role="menu" :aria-label="t('文件', 'File')">
|
||||
<button data-menu-item role="menuitem" @click="navigate('/')"><span>{{ t('选择知识库…', 'Choose knowledge base…') }}</span></button>
|
||||
<div v-if="open === 'file'" class="menu-popover file-menu" role="menu" :aria-label="t('文件', 'File')">
|
||||
<button data-menu-item role="menuitem" :disabled="!workspace.hasVault || fileBusy" @click="createWorkspaceItem('file')"><span>{{ t('新建笔记…', 'New note…') }}</span></button>
|
||||
<button data-menu-item role="menuitem" :disabled="!workspace.hasVault || fileBusy" @click="createWorkspaceItem('folder')"><span>{{ t('新建文件夹…', 'New folder…') }}</span></button>
|
||||
<span class="menu-separator" role="separator" />
|
||||
<button data-menu-item role="menuitem" :disabled="!editor.currentFilePath || ['saving','conflict','external_changed'].includes(editor.saveStatus)" @click="save">
|
||||
<button data-menu-item role="menuitem" :disabled="fileBusy" @click="chooseVault"><span>{{ t('打开其他知识库…', 'Open another knowledge base…') }}</span></button>
|
||||
<button data-menu-item role="menuitem" :disabled="!workspace.hasVault || fileBusy" @click="navigate('/workspace')"><span>{{ t('返回工作区', 'Return to workspace') }}</span></button>
|
||||
<button data-menu-item role="menuitem" :disabled="!workspace.hasVault || fileBusy" @click="refreshWorkspace"><span>{{ t('刷新文件树', 'Refresh file tree') }}</span></button>
|
||||
<small v-if="workspace.hasVault" class="menu-context">{{ workspace.vaultName }} · {{ workspace.vaultPath }}</small>
|
||||
<span class="menu-separator" role="separator" />
|
||||
<button data-menu-item role="menuitem" :disabled="editorBlocked || fileBusy" @click="save">
|
||||
<span>{{ t('保存', 'Save') }}</span><kbd>Ctrl+S</kbd>
|
||||
</button>
|
||||
<button class="export-command" data-menu-item role="menuitem" :disabled="!editor.currentFilePath || !editor.content.trim() || fileBusy" @click="openExport"><span>{{ t('导出…', 'Export…') }}</span></button>
|
||||
<button data-menu-item role="menuitem" :disabled="!editor.currentFilePath || fileBusy" @click="downloadMarkdown"><span>{{ t('下载 Markdown 副本', 'Download Markdown copy') }}</span></button>
|
||||
<span class="menu-separator" role="separator" />
|
||||
<button data-menu-item role="menuitem" :disabled="!editor.currentFilePath || fileBusy" @click="closeCurrentNote"><span>{{ t('关闭当前笔记', 'Close current note') }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-group" data-menu="edit">
|
||||
<div class="menu-group" role="none" data-menu="edit">
|
||||
<button class="menu-trigger" role="menuitem" :aria-expanded="open === 'edit'" aria-haspopup="menu" @click="toggle('edit')" @pointerenter="open && focusFirst('edit')">{{ t('编辑', 'Edit') }}</button>
|
||||
<div v-if="open === 'edit'" class="menu-popover" role="menu" :aria-label="t('编辑', 'Edit')">
|
||||
<button data-menu-item role="menuitem" :disabled="!enabled('editor.undo')" @click="command('editor.undo')"><span>{{ t('撤销', 'Undo') }}</span><kbd>Ctrl+Z</kbd></button>
|
||||
<button data-menu-item role="menuitem" :disabled="!enabled('editor.redo')" @click="command('editor.redo')"><span>{{ t('重做', 'Redo') }}</span><kbd>Ctrl+Shift+Z</kbd></button>
|
||||
<span class="menu-separator" role="separator" />
|
||||
<button data-menu-item role="menuitem" :disabled="!workspace.hasVault" @click="navigate('/search')"><span>{{ t('在知识库中搜索…', 'Search knowledge base…') }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-group" data-menu="paragraph">
|
||||
<div class="menu-group" role="none" data-menu="paragraph">
|
||||
<button class="menu-trigger" role="menuitem" :aria-expanded="open === 'paragraph'" aria-haspopup="menu" @click="toggle('paragraph')" @pointerenter="open && focusFirst('paragraph')">{{ t('段落', 'Paragraph') }}</button>
|
||||
<div v-if="open === 'paragraph'" class="menu-popover paragraph-menu" role="menu" :aria-label="t('段落', 'Paragraph')">
|
||||
<template v-for="(section, sectionIndex) in paragraphMenuSections" :key="sectionIndex">
|
||||
@@ -160,7 +279,7 @@ onBeforeUnmount(() => {
|
||||
<small v-if="!enabled('editor.import-note-properties')">{{ t('请在无冲突的 Markdown 源码笔记中使用', 'Available in a conflict-free Markdown source note') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-group" data-menu="format">
|
||||
<div class="menu-group" role="none" data-menu="format">
|
||||
<button class="menu-trigger" role="menuitem" :aria-expanded="open === 'format'" aria-haspopup="menu" @click="toggle('format')" @pointerenter="open && focusFirst('format')">{{ t('格式', 'Format') }}</button>
|
||||
<div v-if="open === 'format'" class="menu-popover format-menu" role="menu" :aria-label="t('格式', 'Format')">
|
||||
<template v-for="item in formatMenuSections[0]" :key="item.id">
|
||||
@@ -184,17 +303,21 @@ onBeforeUnmount(() => {
|
||||
<button class="metadata-command" data-menu-item role="menuitem" :disabled="!enabled('editor.import-note-properties') && !enabled('editor.metadata.edit')" @click="metadataCommand"><span>{{ t('元数据 / YAML Front Matter…', 'Metadata / YAML Front Matter…') }}</span><kbd>{{ shortcut }}</kbd></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-group" data-menu="view">
|
||||
<div class="menu-group" role="none" data-menu="view">
|
||||
<button class="menu-trigger" role="menuitem" :aria-expanded="open === 'view'" aria-haspopup="menu" @click="toggle('view')" @pointerenter="open && focusFirst('view')">{{ t('视图', 'View') }}</button>
|
||||
<div v-if="open === 'view'" class="menu-popover" role="menu" :aria-label="t('视图', 'View')">
|
||||
<button data-menu-item role="menuitemradio" :aria-checked="editor.mode === 'wysiwyg'" @click="setMode('wysiwyg')"><span>{{ editor.mode === 'wysiwyg' ? '✓ ' : '' }}{{ t('写作模式', 'Writing mode') }}</span></button>
|
||||
<button data-menu-item role="menuitemradio" :aria-checked="editor.mode === 'source'" @click="setMode('source')"><span>{{ editor.mode === 'source' ? '✓ ' : '' }}{{ t('源码模式', 'Source mode') }}</span></button>
|
||||
<div v-if="open === 'view'" class="menu-popover view-menu" role="menu" :aria-label="t('视图', 'View')">
|
||||
<button v-for="item in viewDestinations" :key="item.name" data-menu-item role="menuitemradio" :aria-checked="route.name === item.name" :disabled="!workspace.hasVault" @click="navigateNamed(item.name)"><span>{{ route.name === item.name ? '✓ ' : '' }}{{ item.label }}</span></button>
|
||||
<span class="menu-separator" role="separator" />
|
||||
<button v-for="item in extensionDestinations" :key="item.name" data-menu-item role="menuitemradio" :aria-checked="route.name === item.name" :disabled="!workspace.hasVault" @click="navigateNamed(item.name)"><span>{{ route.name === item.name ? '✓ ' : '' }}{{ item.label }}</span></button>
|
||||
<span class="menu-separator" role="separator" />
|
||||
<button data-menu-item role="menuitemradio" :aria-checked="editor.mode === 'wysiwyg'" :disabled="!editor.currentFilePath" @click="setMode('wysiwyg')"><span>{{ editor.mode === 'wysiwyg' && editor.currentFilePath ? '✓ ' : '' }}{{ t('写作模式', 'Writing mode') }}</span></button>
|
||||
<button data-menu-item role="menuitemradio" :aria-checked="editor.mode === 'source'" :disabled="!editor.currentFilePath" @click="setMode('source')"><span>{{ editor.mode === 'source' && editor.currentFilePath ? '✓ ' : '' }}{{ t('源码模式', 'Source mode') }}</span></button>
|
||||
<span class="menu-separator" role="separator" />
|
||||
<button data-menu-item role="menuitem" :disabled="!enabled('editor.heading.fold-all')" @click="command('editor.heading.fold-all')"><span>{{ t('全部折叠标题', 'Fold all headings') }}</span></button>
|
||||
<button data-menu-item role="menuitem" :disabled="!enabled('editor.heading.unfold-all')" @click="command('editor.heading.unfold-all')"><span>{{ t('全部展开标题', 'Unfold all headings') }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-group" data-menu="theme">
|
||||
<div class="menu-group" role="none" data-menu="theme">
|
||||
<button class="menu-trigger" role="menuitem" :aria-expanded="open === 'theme'" aria-haspopup="menu" @click="toggle('theme')" @pointerenter="open && focusFirst('theme')">{{ t('主题', 'Theme') }}</button>
|
||||
<div v-if="open === 'theme'" class="menu-popover" role="menu" :aria-label="t('主题', 'Theme')">
|
||||
<button v-for="item in theme.allThemes" :key="item.theme_id" data-menu-item role="menuitemradio" :aria-checked="theme.currentThemeId === item.theme_id" @click="applyTheme(item.theme_id)"><span>{{ theme.currentThemeId === item.theme_id ? '✓ ' : '' }}{{ item.name }}</span></button>
|
||||
@@ -203,11 +326,14 @@ onBeforeUnmount(() => {
|
||||
<button data-menu-item role="menuitem" @click="navigate('/themes')"><span>{{ t('管理主题…', 'Manage themes…') }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-group" data-menu="help">
|
||||
<div class="menu-group" role="none" data-menu="help">
|
||||
<button class="menu-trigger" role="menuitem" :aria-expanded="open === 'help'" aria-haspopup="menu" @click="toggle('help')" @pointerenter="open && focusFirst('help')">{{ t('帮助', 'Help') }}</button>
|
||||
<div v-if="open === 'help'" class="menu-popover" role="menu" :aria-label="t('帮助', 'Help')">
|
||||
<button data-menu-item role="menuitem" @click="navigate('/logs')"><span>{{ t('运行日志', 'Operation logs') }}</span></button>
|
||||
<button data-menu-item role="menuitem" @click="navigate('/settings')"><span>{{ t('设置与诊断', 'Settings and diagnostics') }}</span></button>
|
||||
<button data-menu-item role="menuitem" @click="navigate('/benchmarks')"><span>{{ t('Benchmark 评测', 'Benchmarks') }}</span></button>
|
||||
<button data-menu-item role="menuitem" @click="navigate('/community')"><span>{{ t('社区目录', 'Community catalog') }}</span></button>
|
||||
<span class="menu-separator" role="separator" />
|
||||
<button data-menu-item role="menuitem" :disabled="!workspace.hasVault" @click="navigate('/settings')"><span>{{ t('设置与诊断…', 'Settings and diagnostics…') }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="error" class="menu-error" role="alert">{{ error }}</span>
|
||||
@@ -220,8 +346,10 @@ onBeforeUnmount(() => {
|
||||
.menu-trigger { min-width: 52px; height: 28px; padding: 2px 12px; border-radius: var(--radius-sm); color: var(--color-text-secondary); }
|
||||
.menu-trigger:hover, .menu-trigger[aria-expanded='true'] { background: var(--color-background-hover); color: var(--color-text-primary); }
|
||||
.menu-popover { position: absolute; top: calc(100% + 2px); left: 0; z-index: calc(var(--z-titlebar) + 2); display: grid; min-width: 230px; padding: var(--space-xs); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-elevated); box-shadow: var(--shadow-lg); }
|
||||
.file-menu { min-width: 320px; }
|
||||
.paragraph-menu { min-width: 270px; }
|
||||
.format-menu { min-width: 330px; }
|
||||
.view-menu { min-width: 250px; }
|
||||
.menu-popover button { display: flex; align-items: center; justify-content: space-between; gap: var(--space-xl); width: 100%; padding: 7px var(--space-md); border-radius: var(--radius-sm); text-align: left; white-space: nowrap; }
|
||||
.menu-popover button:hover:not(:disabled), .menu-popover button:focus-visible { background: var(--color-accent-soft); color: var(--color-accent-primary); }
|
||||
.menu-popover button:disabled { opacity: .45; cursor: not-allowed; }
|
||||
@@ -236,5 +364,6 @@ onBeforeUnmount(() => {
|
||||
.menu-popover.format-menu { max-height: none; overflow: visible; }
|
||||
.menu-separator { height: 1px; margin: var(--space-xs); background: var(--color-border-subtle); }
|
||||
.menu-popover small { padding: var(--space-xs) var(--space-md); color: var(--color-text-tertiary); white-space: normal; }
|
||||
.menu-context { display: block; max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap !important; }
|
||||
.menu-error { margin-left: var(--space-md); color: var(--color-error); font-size: var(--font-size-xs); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user