feat(frontend): 搭建桌面端基础界面与 Workspace
- App Shell 壳层:主导航、次导航、Title Bar、状态栏 - Vault 入口页:最近 Vault、打开/创建 Vault、AI Core 状态 - Workspace 与文件树:浏览、打开、创建笔记/文件夹 - Design Token:浅色/深色主题 CSS Variables - Contracts / Service / Store / Router 基础架构 - 统一 ApiClient 与 SseClient,对接后端 API 契约
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useWorkspaceStore } from '@/stores/workspace'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import PrimarySidebar from './PrimarySidebar.vue'
|
||||
import SecondarySidebar from './SecondarySidebar.vue'
|
||||
import StatusBar from './StatusBar.vue'
|
||||
import TitleBar from './TitleBar.vue'
|
||||
|
||||
defineProps<{
|
||||
showSecondarySidebar?: boolean
|
||||
}>()
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const themeStore = useThemeStore()
|
||||
const editorStore = useEditorStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const agentStore = useAgentStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const routeName = computed(() => route.name as string)
|
||||
|
||||
const secondaryComponent = computed(() => {
|
||||
switch (routeName) {
|
||||
case 'workspace': return 'file-tree'
|
||||
case 'search': return 'search-filters'
|
||||
case 'chat': return 'conversation-list'
|
||||
case 'agent': return 'run-list'
|
||||
case 'tasks': return 'task-filters'
|
||||
case 'skills':
|
||||
case 'plugins': return 'extension-list'
|
||||
default: return null
|
||||
}
|
||||
})
|
||||
|
||||
function openCitation(noteId: string, blockId: string, filePath: string) {
|
||||
workspaceStore.openFile(filePath)
|
||||
editorStore.highlightBlock(blockId)
|
||||
router.push('/workspace')
|
||||
}
|
||||
|
||||
defineExpose({ openCitation })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-shell" :class="{ 'theme-dark': themeStore.isDark }">
|
||||
<TitleBar />
|
||||
<div class="app-body">
|
||||
<PrimarySidebar />
|
||||
<SecondarySidebar v-if="secondaryComponent" :component="secondaryComponent" />
|
||||
<main class="main-content">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
<StatusBar />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background: var(--color-background-primary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.app-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background: var(--color-background-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,124 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const navItems = [
|
||||
{ name: 'workspace', icon: '📁', label: '工作区' },
|
||||
{ name: 'search', icon: '🔍', label: '搜索' },
|
||||
{ name: 'chat', icon: '💬', label: 'AI 对话' },
|
||||
{ name: 'agent', icon: '🤖', label: 'Agent' },
|
||||
{ name: 'tasks', icon: '✅', label: '任务' },
|
||||
{ name: 'skills', icon: '⚡', label: 'Skill' },
|
||||
{ name: 'plugins', icon: '🧩', label: 'Plugin' },
|
||||
{ name: 'themes', icon: '🎨', label: '主题' },
|
||||
{ name: 'settings', icon: '⚙️', label: '设置' },
|
||||
]
|
||||
|
||||
const currentName = computed(() => {
|
||||
const name = route.name as string
|
||||
if (name === 'skills' || name === 'plugins') return 'skills'
|
||||
return name
|
||||
})
|
||||
|
||||
function navigate(name: string) {
|
||||
router.push({ name })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="primary-sidebar">
|
||||
<nav class="nav-list">
|
||||
<div
|
||||
v-for="item in navItems"
|
||||
:key="item.name"
|
||||
class="nav-item"
|
||||
:class="{ active: currentName === item.name }"
|
||||
@click="navigate(item.name)"
|
||||
:title="item.label"
|
||||
>
|
||||
<span class="nav-icon">{{ item.icon }}</span>
|
||||
<span class="nav-label">{{ item.label }}</span>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="nav-item" @click="navigate('settings')" title="设置">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.primary-sidebar {
|
||||
width: 56px;
|
||||
background: var(--color-background-secondary);
|
||||
border-right: 1px solid var(--color-border-subtle);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
z-index: var(--z-sidebar);
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
flex: 1;
|
||||
padding: var(--space-sm) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 50px;
|
||||
margin: 0 4px;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
color: var(--color-text-secondary);
|
||||
transition: all var(--motion-fast);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-background-hover);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent-primary);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 24px;
|
||||
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||||
background: var(--color-accent-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: var(--space-sm) 0;
|
||||
border-top: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import FileTreePanel from '@/features/workspace/FileTreePanel.vue'
|
||||
import ConversationListPanel from '@/features/chat/ConversationListPanel.vue'
|
||||
import RunListPanel from '@/features/agent/RunListPanel.vue'
|
||||
import SearchFiltersPanel from '@/features/search/SearchFiltersPanel.vue'
|
||||
import TaskFiltersPanel from '@/features/tasks/TaskFiltersPanel.vue'
|
||||
import ExtensionListPanel from '@/components/common/ExtensionListPanel.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const props = defineProps<{
|
||||
component: string | null
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const routeName = computed(() => route.name as string)
|
||||
|
||||
const sidebarTitle = computed(() => {
|
||||
const titles: Record<string, string> = {
|
||||
'file-tree': '文件',
|
||||
'conversation-list': '对话',
|
||||
'run-list': 'Agent Run',
|
||||
'search-filters': '搜索筛选',
|
||||
'task-filters': '任务筛选',
|
||||
'extension-list': '扩展',
|
||||
}
|
||||
return titles[props.component || ''] || ''
|
||||
})
|
||||
|
||||
const showSkillToggle = computed(() => routeName === 'skills' || routeName === 'plugins')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="secondary-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h3 class="sidebar-title">{{ sidebarTitle }}</h3>
|
||||
<div v-if="showSkillToggle" class="sidebar-tabs">
|
||||
<router-link to="/extensions/skills" class="tab" :class="{ active: routeName === 'skills' }">Skill</router-link>
|
||||
<router-link to="/extensions/plugins" class="tab" :class="{ active: routeName === 'plugins' }">Plugin</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
<FileTreePanel v-if="component === 'file-tree'" />
|
||||
<ConversationListPanel v-else-if="component === 'conversation-list'" />
|
||||
<RunListPanel v-else-if="component === 'run-list'" />
|
||||
<SearchFiltersPanel v-else-if="component === 'search-filters'" />
|
||||
<TaskFiltersPanel v-else-if="component === 'task-filters'" />
|
||||
<ExtensionListPanel v-else-if="component === 'extension-list'" />
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.secondary-sidebar {
|
||||
width: var(--sidebar-secondary-width);
|
||||
background: var(--color-background-primary);
|
||||
border-right: 1px solid var(--color-border-default);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 var(--space-sm) 0;
|
||||
}
|
||||
|
||||
.sidebar-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
background: var(--color-background-secondary);
|
||||
padding: 2px;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 4px 8px;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all var(--motion-fast);
|
||||
|
||||
&.active {
|
||||
background: var(--color-surface-primary);
|
||||
color: var(--color-text-primary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useProviderStore } from '@/stores/provider'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const editorStore = useEditorStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const providerStore = useProviderStore()
|
||||
const agentStore = useAgentStore()
|
||||
const route = useRoute()
|
||||
|
||||
const saveStatusText = computed(() => {
|
||||
const map: Record<string, string> = {
|
||||
idle: '',
|
||||
dirty: '未保存',
|
||||
saving: '保存中...',
|
||||
saved: '已保存',
|
||||
save_failed: '保存失败',
|
||||
external_changed: '外部已更新',
|
||||
conflict: '存在冲突',
|
||||
}
|
||||
return map[editorStore.saveStatus] || ''
|
||||
})
|
||||
|
||||
const saveStatusColor = computed(() => {
|
||||
const map: Record<string, string> = {
|
||||
dirty: 'var(--color-accent-primary)',
|
||||
saving: 'var(--color-info)',
|
||||
saved: 'var(--color-success)',
|
||||
save_failed: 'var(--color-error)',
|
||||
external_changed: 'var(--color-warning)',
|
||||
conflict: 'var(--color-error)',
|
||||
}
|
||||
return map[editorStore.saveStatus] || 'var(--color-text-tertiary)'
|
||||
})
|
||||
|
||||
const indexStatusText = computed(() => {
|
||||
const s = settingsStore.indexStatus.status
|
||||
return s === 'idle' ? '索引就绪' : s === 'indexing' ? `索引中 (${settingsStore.indexStatus.pending_jobs})` : '索引错误'
|
||||
})
|
||||
|
||||
const aiCoreStatusText = computed(() => {
|
||||
const map: Record<string, string> = {
|
||||
starting: 'AI Core 启动中',
|
||||
running: 'AI Core 运行中',
|
||||
stopped: 'AI Core 已停止',
|
||||
error: 'AI Core 错误',
|
||||
}
|
||||
return map[settingsStore.aiCoreStatus] || ''
|
||||
})
|
||||
|
||||
const aiCoreColor = computed(() => {
|
||||
const map: Record<string, string> = {
|
||||
starting: 'var(--color-warning)',
|
||||
running: 'var(--color-success)',
|
||||
stopped: 'var(--color-text-tertiary)',
|
||||
error: 'var(--color-error)',
|
||||
}
|
||||
return map[settingsStore.aiCoreStatus] || ''
|
||||
})
|
||||
|
||||
const defaultProvider = computed(() => providerStore.defaultProvider)
|
||||
|
||||
const showEditorInfo = computed(() => route.name === 'workspace')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="statusbar">
|
||||
<div class="statusbar-left">
|
||||
<span v-if="showEditorInfo && saveStatusText" class="status-item" :style="{ color: saveStatusColor }">
|
||||
<span class="status-dot" :style="{ background: saveStatusColor }" />
|
||||
{{ saveStatusText }}
|
||||
</span>
|
||||
<span class="status-item" :title="indexStatusText">
|
||||
<span class="status-dot" style="background: var(--color-success)" />
|
||||
索引就绪
|
||||
</span>
|
||||
<span class="status-item" :style="{ color: aiCoreColor }" @click>
|
||||
<span class="status-dot" :style="{ background: aiCoreColor }" />
|
||||
{{ aiCoreStatusText }}
|
||||
</span>
|
||||
<span v-if="agentStore.isRunning" class="status-item agent-status">
|
||||
<span class="spinner" />
|
||||
Agent 运行中
|
||||
</span>
|
||||
</div>
|
||||
<div class="statusbar-right">
|
||||
<span v-if="defaultProvider" class="status-item provider-info">
|
||||
{{ defaultProvider.name }} · {{ defaultProvider.default_model }}
|
||||
</span>
|
||||
<span v-if="showEditorInfo" class="status-item">
|
||||
{{ editorStore.lineCount }} 行
|
||||
</span>
|
||||
<span v-if="showEditorInfo" class="status-item">
|
||||
{{ editorStore.wordCount }} 字
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.statusbar {
|
||||
height: var(--statusbar-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 var(--space-md);
|
||||
background: var(--color-background-secondary);
|
||||
border-top: 1px solid var(--color-border-subtle);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-secondary);
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.statusbar-left,
|
||||
.statusbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agent-status {
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 2px solid var(--color-accent-soft);
|
||||
border-top-color: var(--color-accent-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.provider-info {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useWorkspaceStore } from '@/stores/workspace'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
|
||||
const route = useRoute()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const editorStore = useEditorStore()
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
const name = route.name as string
|
||||
const titles: Record<string, string> = {
|
||||
workspace: '工作区',
|
||||
search: '搜索',
|
||||
chat: 'AI 对话',
|
||||
agent: 'Agent Trace',
|
||||
tasks: '任务',
|
||||
skills: 'Skill 管理',
|
||||
plugins: 'Plugin 管理',
|
||||
themes: '主题管理',
|
||||
settings: '设置',
|
||||
}
|
||||
return titles[name] || '知笔知己'
|
||||
})
|
||||
|
||||
const currentFileName = computed(() => {
|
||||
if (route.name !== 'workspace') return pageTitle.value
|
||||
if (workspaceStore.activeFile) {
|
||||
return workspaceStore.activeFile.name
|
||||
}
|
||||
return pageTitle.value
|
||||
})
|
||||
|
||||
const isDirty = computed(() => editorStore.saveStatus === 'dirty' || editorStore.saveStatus === 'conflict')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="titlebar">
|
||||
<div class="titlebar-left">
|
||||
<span class="vault-name">{{ workspaceStore.vaultName }}</span>
|
||||
<span class="title-separator">/</span>
|
||||
<span class="file-name" :class="{ dirty: isDirty }">
|
||||
{{ currentFileName }}
|
||||
<span v-if="isDirty" class="dirty-dot" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="titlebar-center">
|
||||
<span class="app-name">知笔知己</span>
|
||||
</div>
|
||||
<div class="titlebar-right">
|
||||
<button class="icon-btn" @click="themeStore.toggleTheme()" :title="themeStore.isDark ? '切换浅色主题' : '切换深色主题'">
|
||||
<span class="icon">{{ themeStore.isDark ? '☀️' : '🌙' }}</span>
|
||||
</button>
|
||||
<div class="window-controls">
|
||||
<span class="win-btn minimize">—</span>
|
||||
<span class="win-btn maximize">▢</span>
|
||||
<span class="win-btn close">✕</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.titlebar {
|
||||
height: var(--titlebar-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 var(--space-md);
|
||||
background: var(--color-background-secondary);
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
font-size: var(--font-size-sm);
|
||||
flex-shrink: 0;
|
||||
z-index: var(--z-titlebar);
|
||||
user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.titlebar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
min-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vault-name {
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.title-separator {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.file-name {
|
||||
color: var(--color-text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&.dirty {
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.dirty-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.titlebar-center {
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.titlebar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
min-width: 200px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
-webkit-app-region: no-drag;
|
||||
transition: background var(--motion-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-background-hover);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.window-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.win-btn {
|
||||
width: 34px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: background var(--motion-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-background-hover);
|
||||
}
|
||||
|
||||
&.close:hover {
|
||||
background: var(--color-error);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user