Merge pull request 'feat(frontend): 统一页面视觉并完善 GitHub 代码主题' (#6) from feat/frontend-visual-polish into main

Reviewed-on: #6
This commit is contained in:
2026-08-30 22:39:15 +08:00
32 changed files with 501 additions and 142 deletions
+1
View File
@@ -136,6 +136,7 @@ pnpm test
| [前端页面需求](docs/前端页面需求说明-开发版.md) | 页面、交互、状态与验收基线 | | [前端页面需求](docs/前端页面需求说明-开发版.md) | 页面、交互、状态与验收基线 |
| [前端实现说明](docs/前端壳子与接口层开发说明.md) | 当前前端目录、Service、SSE 和运行边界 | | [前端实现说明](docs/前端壳子与接口层开发说明.md) | 当前前端目录、Service、SSE 和运行边界 |
| [前端写作体验](docs/前端写作体验优化开发说明.md) | Milkdown、CodeMirror、格式栏和 Shiki | | [前端写作体验](docs/前端写作体验优化开发说明.md) | Milkdown、CodeMirror、格式栏和 Shiki |
| [前端视觉与轻量动效](docs/前端视觉与轻量动效优化开发说明.md) | Design Token、页面美化、性能边界与主题注入约定 |
| [Git 使用细则](docs/Git使用细则-团队开发版.md) | 分支、提交、PR、Review 与合并流程 | | [Git 使用细则](docs/Git使用细则-团队开发版.md) | 分支、提交、PR、Review 与合并流程 |
| [后端审阅复盘](docs/后端全面审阅问题与修复复盘.md) | 后端问题原因、后果与修复方案 | | [后端审阅复盘](docs/后端全面审阅问题与修复复盘.md) | 后端问题原因、后果与修复方案 |
| [Knowledge/Retrieval 复盘](docs/Knowledge与Retrieval-Core问题与修复复盘.md) | 检索与事务问题复盘 | | [Knowledge/Retrieval 复盘](docs/Knowledge与Retrieval-Core问题与修复复盘.md) | 检索与事务问题复盘 |
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#171717" /> <meta name="theme-color" content="#171717" />
<title>Notes Agent</title> <title>NotesAgent</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+2 -1
View File
@@ -73,7 +73,7 @@ defineExpose({ openCitation })
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
background: var(--color-background-primary); background: var(--color-background-secondary);
color: var(--color-text-primary); color: var(--color-text-primary);
} }
@@ -89,5 +89,6 @@ defineExpose({ openCitation })
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
background: var(--color-background-primary); background: var(--color-background-primary);
isolation: isolate;
} }
</style> </style>
@@ -89,13 +89,17 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown))
</template> </template>
<style scoped> <style scoped>
.command-backdrop { position: fixed; inset: 0; z-index: var(--z-modal); display: flex; justify-content: center; align-items: flex-start; padding-top: 12vh; background: var(--color-background-overlay); } .command-backdrop { position: fixed; inset: 0; z-index: var(--z-modal); display: flex; justify-content: center; align-items: flex-start; padding-top: 12vh; background: var(--color-background-overlay); animation: command-backdrop-in var(--motion-fast) both; }
.command-palette { width: min(600px, calc(100vw - 32px)); overflow: hidden; border: 1px solid var(--color-border-default); border-radius: var(--radius-lg); background: var(--color-surface-elevated); box-shadow: var(--shadow-xl); } .command-palette { width: min(620px, calc(100vw - 32px)); overflow: hidden; border: 1px solid var(--color-border-default); border-radius: var(--radius-xl); background: var(--color-surface-elevated); box-shadow: var(--shadow-xl); animation: command-palette-in var(--motion-normal) both; }
.command-input { width: 100%; padding: var(--space-lg); border: 0; border-bottom: 1px solid var(--color-border-default); outline: 0; background: transparent; font-size: var(--font-size-xl); } .command-input { width: 100%; padding: var(--space-xl); border: 0; border-bottom: 1px solid var(--color-border-default); outline: 0; background: transparent; color: var(--color-text-primary); font-size: var(--font-size-xl); }
.command-list { max-height: 360px; overflow: auto; padding: var(--space-sm); } .command-list { max-height: 360px; overflow: auto; padding: var(--space-sm); }
.command-list button { display: flex; justify-content: space-between; width: 100%; padding: var(--space-md); border-radius: var(--radius-md); text-align: left; } .command-list button { display: flex; justify-content: space-between; width: 100%; padding: var(--space-md) var(--space-lg); border-radius: var(--radius-md); text-align: left; transition: color var(--motion-fast), background-color var(--motion-fast), transform var(--motion-fast); }
.command-list button:hover, .command-list button:focus { outline: 0; background: var(--color-accent-soft); color: var(--color-accent-primary); } .command-list button:hover, .command-list button:focus { outline: 0; background: var(--color-accent-soft); color: var(--color-accent-primary); }
.command-list button:hover { transform: translateX(2px); }
.command-list small, .command-list p, footer { color: var(--color-text-tertiary); } .command-list small, .command-list p, footer { color: var(--color-text-tertiary); }
.command-list p { padding: var(--space-xl); text-align: center; } .command-list p { padding: var(--space-xl); text-align: center; }
footer { display: flex; gap: var(--space-lg); padding: var(--space-sm) var(--space-lg); border-top: 1px solid var(--color-border-subtle); font-size: var(--font-size-xs); } footer { display: flex; gap: var(--space-lg); padding: var(--space-sm) var(--space-lg); border-top: 1px solid var(--color-border-subtle); font-size: var(--font-size-xs); }
@keyframes command-backdrop-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes command-palette-in { from { opacity: 0; transform: translateY(-8px) scale(.99); } to { opacity: 1; transform: translateY(0) scale(1); } }
</style> </style>
@@ -17,24 +17,33 @@ watch(() => props.source, async (source) => {
<div class="markdown-content" v-html="html" /> <div class="markdown-content" v-html="html" />
</template> </template>
<style scoped> <style>
.markdown-content { white-space: normal; user-select: text; } .markdown-content { white-space: normal; user-select: text; }
.markdown-content :deep(p), .markdown-content :deep(ul), .markdown-content :deep(ol), .markdown-content :deep(pre), .markdown-content :deep(blockquote) { margin: .65em 0; } .markdown-content p, .markdown-content ul, .markdown-content ol, .markdown-content pre, .markdown-content blockquote { margin: .65em 0; }
.markdown-content :deep(h1), .markdown-content :deep(h2), .markdown-content :deep(h3) { margin: 1em 0 .5em; line-height: var(--line-height-tight); } .markdown-content h1, .markdown-content h2, .markdown-content h3 { margin: 1em 0 .5em; line-height: var(--line-height-tight); }
.markdown-content :deep(ul) { padding-left: 1.5em; list-style: disc; } .markdown-content ul { padding-left: 1.5em; list-style: disc; }
.markdown-content :deep(ol) { padding-left: 1.5em; list-style: decimal; } .markdown-content ol { padding-left: 1.5em; list-style: decimal; }
.markdown-content :deep(.shiki) { overflow: auto; padding: var(--space-md); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); } .markdown-content li::marker { color: var(--color-markdown-marker); font-weight: 700; }
.markdown-content :deep(code) { padding: .1em .3em; border-radius: var(--radius-sm); background: var(--color-background-tertiary); font-family: var(--font-ui-mono); } .markdown-content .shiki { overflow: auto; margin: .85em 0; padding: 16px; border: 1px solid var(--color-code-border); border-radius: 6px; background: var(--color-code-background) !important; color: var(--color-code-text); font-family: var(--font-ui-mono); font-size: .875em; line-height: 1.45; tab-size: 4; }
.markdown-content :deep(pre code) { padding: 0; background: transparent; } .markdown-content code { padding: .1em .3em; border-radius: var(--radius-sm); background: var(--color-background-tertiary); font-family: var(--font-ui-mono); }
.markdown-content :deep(blockquote) { padding-left: 1em; border-left: 3px solid var(--color-accent-primary); color: var(--color-text-secondary); } .markdown-content .shiki code { display: block; min-width: max-content; padding: 0; background: transparent; font: inherit; }
.markdown-content :deep(table) { width: 100%; margin: .65em 0; border-collapse: collapse; } .markdown-content .shiki .line { display: block; min-height: 1.45em; }
.markdown-content :deep(th), .markdown-content :deep(td) { padding: .45em .65em; border: 1px solid var(--color-border-default); text-align: left; } .markdown-content blockquote { padding-left: 1em; border-left: 3px solid var(--color-accent-primary); color: var(--color-text-secondary); }
.markdown-content :deep(img) { max-width: 100%; } .markdown-content table { width: 100%; margin: .65em 0; border-collapse: collapse; }
.markdown-content :deep(hr) { margin: 1em 0; border: 0; border-top: 1px solid var(--color-border-default); } .markdown-content th, .markdown-content td { padding: .45em .65em; border: 1px solid var(--color-markdown-grid); text-align: left; }
:global([data-theme='dark']) .markdown-content :deep(.shiki), .markdown-content th { background: var(--color-markdown-table-header); font-weight: 700; }
:global([data-theme='dark']) .markdown-content :deep(.shiki span) { .markdown-content img { max-width: 100%; }
.markdown-content hr { margin: 1em 0; border: 0; border-top: 1px solid var(--color-border-default); }
[data-code-theme='github-light'] .markdown-content .shiki,
[data-code-theme='github-light'] .markdown-content .shiki span {
color: var(--shiki-light) !important;
font-style: var(--shiki-light-font-style) !important;
font-weight: var(--shiki-light-font-weight) !important;
text-decoration: var(--shiki-light-text-decoration) !important;
}
[data-code-theme='github-dark'] .markdown-content .shiki,
[data-code-theme='github-dark'] .markdown-content .shiki span {
color: var(--shiki-dark) !important; color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
font-style: var(--shiki-dark-font-style) !important; font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important; font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important; text-decoration: var(--shiki-dark-text-decoration) !important;
@@ -60,13 +60,14 @@ function toggleExpanded() {
<style scoped> <style scoped>
.primary-sidebar { .primary-sidebar {
width: 56px; width: var(--sidebar-primary-width);
background: var(--color-background-secondary); background: var(--color-background-secondary);
border-right: 1px solid var(--color-border-subtle); border-right: 1px solid var(--color-border-subtle);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-shrink: 0; flex-shrink: 0;
z-index: var(--z-sidebar); z-index: var(--z-sidebar);
transition: width var(--motion-normal), background-color var(--motion-normal);
} }
.primary-sidebar.expanded { width: var(--sidebar-primary-width-expanded); } .primary-sidebar.expanded { width: var(--sidebar-primary-width-expanded); }
@@ -76,7 +77,7 @@ function toggleExpanded() {
.nav-list { .nav-list {
flex: 1; flex: 1;
padding: var(--space-sm) 0; padding: var(--space-md) 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
@@ -87,31 +88,34 @@ function toggleExpanded() {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 50px; height: 48px;
margin: 0 4px; margin: 0 6px;
border-radius: var(--radius-md); border-radius: var(--radius-md);
cursor: pointer; cursor: pointer;
color: var(--color-text-secondary); color: var(--color-text-secondary);
transition: all var(--motion-fast); border: 1px solid transparent;
transition: color var(--motion-fast), background-color var(--motion-fast), border-color var(--motion-fast), transform var(--motion-fast);
position: relative; position: relative;
&:hover { &:hover {
background: var(--color-background-hover); background: var(--color-background-hover);
color: var(--color-text-primary); color: var(--color-text-primary);
transform: translateX(2px);
} }
&.active { &.active {
background: var(--color-accent-soft); background: var(--color-accent-soft);
color: var(--color-accent-primary); color: var(--color-accent-primary);
border-color: color-mix(in srgb, var(--color-accent-primary) 16%, transparent);
&::before { &::before {
content: ''; content: '';
position: absolute; position: absolute;
left: -4px; left: -7px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
width: 3px; width: 4px;
height: 24px; height: 22px;
border-radius: 0 var(--radius-sm) var(--radius-sm) 0; border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
background: var(--color-accent-primary); background: var(--color-accent-primary);
} }
@@ -127,6 +131,7 @@ function toggleExpanded() {
.nav-label { .nav-label {
font-size: 10px; font-size: 10px;
line-height: 1.2; line-height: 1.2;
font-weight: 550;
} }
.sidebar-footer { .sidebar-footer {
@@ -134,5 +139,5 @@ function toggleExpanded() {
border-top: 1px solid var(--color-border-subtle); border-top: 1px solid var(--color-border-subtle);
} }
.collapse-button { width: calc(100% - 8px); } .collapse-button { width: calc(100% - 12px); }
</style> </style>
@@ -53,7 +53,7 @@ const showSkillToggle = computed(() => routeName.value === 'skills' || routeName
<style scoped> <style scoped>
.secondary-sidebar { .secondary-sidebar {
width: var(--sidebar-secondary-width); width: var(--sidebar-secondary-width);
background: var(--color-background-primary); background: var(--color-surface-secondary);
border-right: 1px solid var(--color-border-default); border-right: 1px solid var(--color-border-default);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -62,36 +62,36 @@ const showSkillToggle = computed(() => routeName.value === 'skills' || routeName
} }
.sidebar-header { .sidebar-header {
padding: var(--space-md) var(--space-lg); padding: var(--space-lg);
border-bottom: 1px solid var(--color-border-subtle); border-bottom: 1px solid var(--color-border-subtle);
flex-shrink: 0; flex-shrink: 0;
} }
.sidebar-title { .sidebar-title {
font-size: var(--font-size-sm); font-size: var(--font-size-lg);
font-weight: 600; font-weight: 700;
color: var(--color-text-primary); color: var(--color-text-primary);
margin: 0 0 var(--space-sm) 0; margin: 0 0 var(--space-md) 0;
} }
.sidebar-tabs { .sidebar-tabs {
display: flex; display: flex;
gap: 2px; gap: 2px;
background: var(--color-background-secondary); background: var(--color-background-secondary);
padding: 2px; padding: 3px;
border-radius: var(--radius-md); border-radius: var(--radius-md);
} }
.tab { .tab {
flex: 1; flex: 1;
text-align: center; text-align: center;
padding: 4px 8px; padding: 6px 8px;
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--color-text-secondary); color: var(--color-text-secondary);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
transition: all var(--motion-fast); transition: color var(--motion-fast), background-color var(--motion-fast), box-shadow var(--motion-fast);
&.active { &.active {
background: var(--color-surface-primary); background: var(--color-surface-primary);
@@ -108,6 +108,7 @@ const showSkillToggle = computed(() => routeName.value === 'skills' || routeName
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
scrollbar-gutter: stable;
} }
</style> </style>
+9 -2
View File
@@ -107,8 +107,8 @@ const showEditorInfo = computed(() => route.name === 'workspace')
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 var(--space-md); padding: 0 var(--space-lg);
background: var(--color-background-secondary); background: var(--color-surface-secondary);
border-top: 1px solid var(--color-border-subtle); border-top: 1px solid var(--color-border-subtle);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--color-text-secondary); color: var(--color-text-secondary);
@@ -129,6 +129,7 @@ const showEditorInfo = computed(() => route.name === 'workspace')
gap: 6px; gap: 6px;
white-space: nowrap; white-space: nowrap;
cursor: default; cursor: default;
transition: color var(--motion-fast);
&:hover { &:hover {
color: var(--color-text-primary); color: var(--color-text-primary);
@@ -140,6 +141,7 @@ const showEditorInfo = computed(() => route.name === 'workspace')
height: 6px; height: 6px;
border-radius: 50%; border-radius: 50%;
flex-shrink: 0; flex-shrink: 0;
box-shadow: 0 0 0 2px var(--color-surface-secondary);
} }
.agent-status { .agent-status {
@@ -162,4 +164,9 @@ const showEditorInfo = computed(() => route.name === 'workspace')
.provider-info { .provider-info {
color: var(--color-text-tertiary); color: var(--color-text-tertiary);
} }
@media (max-width: 760px) {
.statusbar-left, .statusbar-right { gap: var(--space-sm); }
.provider-info { display: none; }
}
</style> </style>
+22 -9
View File
@@ -25,7 +25,7 @@ const pageTitle = computed(() => {
themes: '主题管理', themes: '主题管理',
settings: '设置', settings: '设置',
} }
return titles[name] || '知笔知己' return titles[name] || 'NotesAgent'
}) })
const currentFileName = computed(() => { const currentFileName = computed(() => {
@@ -50,7 +50,7 @@ const isDirty = computed(() => editorStore.saveStatus === 'dirty' || editorStore
</span> </span>
</div> </div>
<div class="titlebar-center"> <div class="titlebar-center">
<span class="app-name">知笔知己</span> <span class="app-name">NotesAgent</span>
</div> </div>
<div class="titlebar-right"> <div class="titlebar-right">
<button class="icon-btn" @click="themeStore.toggleTheme()" :title="themeStore.isDark ? '切换浅色主题' : '切换深色主题'"> <button class="icon-btn" @click="themeStore.toggleTheme()" :title="themeStore.isDark ? '切换浅色主题' : '切换深色主题'">
@@ -71,8 +71,8 @@ const isDirty = computed(() => editorStore.saveStatus === 'dirty' || editorStore
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 var(--space-md); padding: 0 var(--space-lg);
background: var(--color-background-secondary); background: var(--color-surface-secondary);
border-bottom: 1px solid var(--color-border-subtle); border-bottom: 1px solid var(--color-border-subtle);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
flex-shrink: 0; flex-shrink: 0;
@@ -129,7 +129,13 @@ const isDirty = computed(() => editorStore.saveStatus === 'dirty' || editorStore
} }
.app-name { .app-name {
font-weight: 500; padding: 3px 10px;
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-full);
background: var(--color-surface-primary);
color: var(--color-text-secondary);
font-weight: 650;
letter-spacing: .04em;
} }
.titlebar-right { .titlebar-right {
@@ -141,8 +147,8 @@ const isDirty = computed(() => editorStore.saveStatus === 'dirty' || editorStore
} }
.icon-btn { .icon-btn {
width: 28px; width: 30px;
height: 28px; height: 30px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -150,11 +156,12 @@ const isDirty = computed(() => editorStore.saveStatus === 'dirty' || editorStore
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-size: 14px; font-size: 14px;
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
transition: background var(--motion-fast); transition: background-color var(--motion-fast), color var(--motion-fast), transform var(--motion-fast);
&:hover { &:hover {
background: var(--color-background-hover); background: var(--color-background-hover);
color: var(--color-text-primary); color: var(--color-text-primary);
transform: rotate(8deg);
} }
} }
@@ -179,7 +186,7 @@ const isDirty = computed(() => editorStore.saveStatus === 'dirty' || editorStore
color: var(--color-text-secondary); color: var(--color-text-secondary);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
cursor: pointer; cursor: pointer;
transition: background var(--motion-fast); transition: background-color var(--motion-fast), color var(--motion-fast);
&:hover { &:hover {
background: var(--color-background-hover); background: var(--color-background-hover);
@@ -190,4 +197,10 @@ const isDirty = computed(() => editorStore.saveStatus === 'dirty' || editorStore
color: white; color: white;
} }
} }
@media (max-width: 760px) {
.titlebar-left, .titlebar-right { min-width: 0; }
.titlebar-center, .window-controls { display: none; }
.file-name { max-width: 42vw; }
}
</style> </style>
+1
View File
@@ -329,6 +329,7 @@ export interface ThemeConfig {
is_dark: boolean is_dark: boolean
author?: string author?: string
builtin: boolean builtin: boolean
code_theme?: 'github-light' | 'github-dark'
} }
// ============ Index ============ // ============ Index ============
+9 -3
View File
@@ -108,16 +108,22 @@ function eventText(event: AgentEvent) {
</template> </template>
<style scoped> <style scoped>
.run-form { display: grid; gap: var(--space-xl); max-width: 980px; } .agent-page > * { width: min(100%, 1080px); margin-inline: auto; }
.run-form { display: grid; gap: var(--space-xl); }
.tool-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: var(--space-sm); } .tool-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: var(--space-sm); }
.tool-option { display: flex; gap: var(--space-sm); padding: var(--space-sm); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); } .tool-option { display: flex; gap: var(--space-sm); padding: var(--space-md); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-primary); cursor: pointer; transition: border-color var(--motion-fast), background-color var(--motion-fast), transform var(--motion-fast), box-shadow var(--motion-fast); }
.tool-option:hover { border-color: var(--color-accent-secondary); transform: translateY(-1px); box-shadow: var(--shadow-sm); }
.tool-option:has(input:checked) { border-color: var(--color-accent-primary); background: var(--color-accent-soft); box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-accent-primary) 10%, transparent); }
.tool-option small { display: block; color: var(--color-text-secondary); } .tool-option small { display: block; color: var(--color-text-secondary); }
.tool-option code { display: block; margin: 2px 0; color: var(--color-text-tertiary); font-size: var(--font-size-xs); } .tool-option code { display: block; margin: 2px 0; color: var(--color-text-tertiary); font-size: var(--font-size-xs); }
.network { display: flex; gap: var(--space-sm); } .network { display: flex; gap: var(--space-sm); }
.trace-layout { display: grid; gap: var(--space-lg); } .trace-layout { display: grid; gap: var(--space-lg); }
.run-summary, .event-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); } .run-summary, .event-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); }
.run-summary h2 { margin-top: var(--space-sm); font-family: var(--font-ui-mono); font-size: var(--font-size-lg); } .run-summary h2 { margin-top: var(--space-sm); font-family: var(--font-ui-mono); font-size: var(--font-size-lg); }
.timeline { display: grid; gap: var(--space-md); } .timeline { position: relative; display: grid; gap: var(--space-md); padding-left: var(--space-md); }
.timeline::before { content: ''; position: absolute; top: 10px; bottom: 10px; left: 1px; width: 2px; border-radius: var(--radius-full); background: var(--color-border-default); }
.event-card { position: relative; }
.event-card::before { content: ''; position: absolute; top: 20px; left: calc(-1 * var(--space-md) - 5px); width: 8px; height: 8px; border: 2px solid var(--color-surface-primary); border-radius: var(--radius-full); background: var(--color-accent-primary); box-shadow: 0 0 0 1px var(--color-accent-secondary); }
.event-head { color: var(--color-text-tertiary); font-size: var(--font-size-xs); } .event-head { color: var(--color-text-tertiary); font-size: var(--font-size-xs); }
.event-text { margin-top: var(--space-md); white-space: pre-wrap; line-height: var(--line-height-relaxed); } .event-text { margin-top: var(--space-md); white-space: pre-wrap; line-height: var(--line-height-relaxed); }
pre { margin-top: var(--space-md); max-height: 260px; overflow: auto; padding: var(--space-md); border-radius: var(--radius-md); background: var(--color-background-secondary); font-family: var(--font-ui-mono); font-size: var(--font-size-xs); white-space: pre-wrap; user-select: text; } pre { margin-top: var(--space-md); max-height: 260px; overflow: auto; padding: var(--space-md); border-radius: var(--radius-md); background: var(--color-background-secondary); font-family: var(--font-ui-mono); font-size: var(--font-size-xs); white-space: pre-wrap; user-select: text; }
+11 -6
View File
@@ -94,24 +94,29 @@ async function openCitation(citation: Citation) {
</template> </template>
<style scoped> <style scoped>
.chat-page { display: grid; grid-template-rows: auto auto 1fr auto; height: 100%; min-height: 0; background: var(--color-background-primary); } .chat-page { display: grid; grid-template-rows: auto auto 1fr auto; height: 100%; min-height: 0; background: radial-gradient(circle at 85% -10%, var(--color-accent-soft), transparent 30%), var(--color-background-primary); }
.chat-toolbar { display: flex; align-items: end; flex-wrap: wrap; gap: var(--space-md); padding: var(--space-md) var(--space-xl); border-bottom: 1px solid var(--color-border-default); } .chat-toolbar { display: flex; align-items: end; flex-wrap: wrap; gap: var(--space-md); padding: var(--space-md) var(--space-xl); border-bottom: 1px solid var(--color-border-default); background: var(--color-surface-secondary); box-shadow: var(--shadow-sm); z-index: 1; }
.compact { min-width: 160px; } .compact { min-width: 160px; }
.rag-toggle { display: flex; align-items: center; gap: var(--space-xs); min-height: 36px; color: var(--color-text-secondary); } .rag-toggle { display: flex; align-items: center; gap: var(--space-xs); min-height: 36px; color: var(--color-text-secondary); }
.chat-error { margin: var(--space-md) var(--space-xl) 0; } .chat-error { margin: var(--space-md) var(--space-xl) 0; }
.message-timeline { min-height: 0; overflow: auto; padding: var(--space-xl) max(var(--space-xl), calc((100% - 820px) / 2)); user-select: text; } .message-timeline { min-height: 0; overflow: auto; padding: var(--space-xl) max(var(--space-xl), calc((100% - 820px) / 2)); user-select: text; }
.message { display: grid; grid-template-columns: 36px 1fr; gap: var(--space-md); margin-bottom: var(--space-xl); } .message { display: grid; grid-template-columns: 36px 1fr; gap: var(--space-md); margin-bottom: var(--space-xl); animation: message-in var(--motion-normal) both; }
.avatar { display: grid; place-items: center; width: 34px; height: 34px; border-radius: var(--radius-full); background: var(--color-background-tertiary); font-weight: 700; } .avatar { display: grid; place-items: center; width: 34px; height: 34px; border: 1px solid var(--color-border-default); border-radius: var(--radius-full); background: var(--color-background-tertiary); box-shadow: var(--shadow-sm); font-weight: 700; }
.assistant .avatar { background: var(--color-accent-soft); color: var(--color-accent-primary); } .assistant .avatar { background: var(--color-accent-soft); color: var(--color-accent-primary); }
.message-body { min-width: 0; padding: var(--space-md) var(--space-lg); border: 1px solid var(--color-border-subtle); border-radius: 4px var(--radius-lg) var(--radius-lg) var(--radius-lg); background: color-mix(in srgb, var(--color-surface-primary) 88%, transparent); box-shadow: var(--shadow-sm); }
.user .message-body { background: var(--color-accent-soft); border-color: color-mix(in srgb, var(--color-accent-primary) 14%, transparent); }
.message-content { white-space: pre-wrap; line-height: var(--line-height-relaxed); } .message-content { white-space: pre-wrap; line-height: var(--line-height-relaxed); }
.thinking { margin-bottom: var(--space-sm); color: var(--color-text-secondary); }.thinking p { margin-top: var(--space-sm); white-space: pre-wrap; } .thinking { margin-bottom: var(--space-sm); color: var(--color-text-secondary); }.thinking p { margin-top: var(--space-sm); white-space: pre-wrap; }
.tool-calls { display: grid; gap: var(--space-sm); margin-top: var(--space-md); }.tool-calls .item-card { display: grid; gap: var(--space-xs); }.tool-calls pre { overflow: auto; font-size: var(--font-size-xs); } .tool-calls { display: grid; gap: var(--space-sm); margin-top: var(--space-md); }.tool-calls .item-card { display: grid; gap: var(--space-xs); }.tool-calls pre { overflow: auto; font-size: var(--font-size-xs); }
.usage { display: block; margin-top: var(--space-xs); color: var(--color-text-tertiary); } .usage { display: block; margin-top: var(--space-xs); color: var(--color-text-tertiary); }
.message time { display: block; margin-top: var(--space-sm); color: var(--color-text-tertiary); font-size: var(--font-size-xs); } .message time { display: block; margin-top: var(--space-sm); color: var(--color-text-tertiary); font-size: var(--font-size-xs); }
.citations { display: grid; gap: var(--space-sm); margin-top: var(--space-md); } .citations { display: grid; gap: var(--space-sm); margin-top: var(--space-md); }
.citation-card { display: flex; align-items: flex-start; gap: var(--space-sm); padding: var(--space-sm); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); text-align: left; } .citation-card { display: flex; align-items: flex-start; gap: var(--space-sm); padding: var(--space-md); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-primary); text-align: left; transition: border-color var(--motion-fast), transform var(--motion-fast), box-shadow var(--motion-fast); }
.citation-card:hover { border-color: var(--color-accent-secondary); transform: translateY(-1px); box-shadow: var(--shadow-sm); }
.citation-card small { display: block; margin-top: 2px; color: var(--color-text-secondary); } .citation-card small { display: block; margin-top: 2px; color: var(--color-text-secondary); }
.composer { padding: var(--space-md) max(var(--space-xl), calc((100% - 820px) / 2)); border-top: 1px solid var(--color-border-default); background: var(--color-surface-primary); } .composer { padding: var(--space-md) max(var(--space-xl), calc((100% - 820px) / 2)); border-top: 1px solid var(--color-border-default); background: var(--color-surface-secondary); box-shadow: 0 -8px 24px color-mix(in srgb, var(--color-text-primary) 5%, transparent); }
.composer .textarea { min-height: 72px; } .composer .textarea { min-height: 72px; }
.composer-actions { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); margin-top: var(--space-sm); } .composer-actions { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); margin-top: var(--space-sm); }
@keyframes message-in { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
</style> </style>
@@ -30,9 +30,9 @@ afterEach(() => {
describe('EditorPane file switching', () => { describe('EditorPane file switching', () => {
it('recreates the visual editor with the newly loaded file content', async () => { it('recreates the visual editor with the newly loaded file content', async () => {
const store = useEditorStore() const store = useEditorStore()
await store.loadFile('/欢迎使用知笔知己.md') await store.loadFile('/欢迎使用 NotesAgent.md')
wrapper = mount(EditorPane, { attachTo: document.body }) wrapper = mount(EditorPane, { attachTo: document.body })
await waitForText('欢迎使用知笔知己') await waitForText('欢迎使用 NotesAgent')
await store.loadFile('/数据结构/红黑树.md') await store.loadFile('/数据结构/红黑树.md')
await nextTick() await nextTick()
+3 -1
View File
@@ -1,10 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { useEditorStore } from '@/stores/editor' import { useEditorStore } from '@/stores/editor'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { useThemeStore } from '@/stores/theme'
import VisualMarkdownEditor from './VisualMarkdownEditor.vue' import VisualMarkdownEditor from './VisualMarkdownEditor.vue'
const editorStore = useEditorStore() const editorStore = useEditorStore()
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const themeStore = useThemeStore()
function updateContent(event: Event) { function updateContent(event: Event) {
editorStore.updateContent((event.target as HTMLTextAreaElement).value) editorStore.updateContent((event.target as HTMLTextAreaElement).value)
editorStore.scheduleAutoSave(settingsStore.autoSaveInterval) editorStore.scheduleAutoSave(settingsStore.autoSaveInterval)
@@ -12,7 +14,7 @@ function updateContent(event: Event) {
</script> </script>
<template> <template>
<VisualMarkdownEditor v-if="editorStore.mode === 'wysiwyg'" :key="editorStore.currentFilePath ?? 'empty'" <VisualMarkdownEditor v-if="editorStore.mode === 'wysiwyg'" :key="`${editorStore.currentFilePath ?? 'empty'}:${themeStore.resolvedCodeBlockTheme}`"
:initial-content="editorStore.content" /> :initial-content="editorStore.content" />
<textarea v-else class="editor-pane source" :value="editorStore.content" :spellcheck="false" <textarea v-else class="editor-pane source" :value="editorStore.content" :spellcheck="false"
aria-label="Markdown 源码编辑器" @input="updateContent" /> aria-label="Markdown 源码编辑器" @input="updateContent" />
@@ -2,6 +2,7 @@
import { onBeforeUnmount, onMounted, ref } from 'vue' import { onBeforeUnmount, onMounted, ref } from 'vue'
import { Link } from '@element-plus/icons-vue' import { Link } from '@element-plus/icons-vue'
import { Crepe } from '@milkdown/crepe' import { Crepe } from '@milkdown/crepe'
import { oneDark } from '@codemirror/theme-one-dark'
import { import {
createCodeBlockCommand, createCodeBlockCommand,
toggleEmphasisCommand, toggleEmphasisCommand,
@@ -19,6 +20,7 @@ import { callCommand } from '@milkdown/kit/utils'
import AppIcon from '@/components/common/AppIcon.vue' import AppIcon from '@/components/common/AppIcon.vue'
import { useEditorStore } from '@/stores/editor' import { useEditorStore } from '@/stores/editor'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { useThemeStore } from '@/stores/theme'
import { applyMarkdownFontSize, fontSizeMarkdownPlugin } from './fontSizeMarkdown' import { applyMarkdownFontSize, fontSizeMarkdownPlugin } from './fontSizeMarkdown'
import '@milkdown/crepe/theme/common/style.css' import '@milkdown/crepe/theme/common/style.css'
import '@milkdown/crepe/theme/frame.css' import '@milkdown/crepe/theme/frame.css'
@@ -26,6 +28,7 @@ import '@milkdown/crepe/theme/frame.css'
const props = defineProps<{ initialContent: string }>() const props = defineProps<{ initialContent: string }>()
const editorStore = useEditorStore() const editorStore = useEditorStore()
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const themeStore = useThemeStore()
const editorRoot = ref<HTMLElement | null>(null) const editorRoot = ref<HTMLElement | null>(null)
const loading = ref(true) const loading = ref(true)
const fontSizeInput = ref(16) const fontSizeInput = ref(16)
@@ -104,6 +107,7 @@ onMounted(async () => {
featureConfigs: { featureConfigs: {
[Crepe.Feature.Placeholder]: { text: '开始记录你的想法…' }, [Crepe.Feature.Placeholder]: { text: '开始记录你的想法…' },
[Crepe.Feature.CodeMirror]: { [Crepe.Feature.CodeMirror]: {
theme: themeStore.resolvedCodeBlockTheme === 'github-dark' ? oneDark : [],
previewOnlyByDefault: false, previewOnlyByDefault: false,
searchPlaceholder: '搜索语言', searchPlaceholder: '搜索语言',
noResultText: '没有匹配的语言', noResultText: '没有匹配的语言',
@@ -250,7 +254,7 @@ defineExpose({ getEditor: () => crepe?.editor })
--crepe-color-surface-low: var(--color-background-secondary); --crepe-color-surface-low: var(--color-background-secondary);
--crepe-color-on-surface: var(--color-text-primary); --crepe-color-on-surface: var(--color-text-primary);
--crepe-color-on-surface-variant: var(--color-text-secondary); --crepe-color-on-surface-variant: var(--color-text-secondary);
--crepe-color-outline: var(--color-border-default); --crepe-color-outline: var(--color-markdown-grid);
--crepe-color-primary: var(--color-accent-primary); --crepe-color-primary: var(--color-accent-primary);
--crepe-color-secondary: var(--color-accent-soft); --crepe-color-secondary: var(--color-accent-soft);
--crepe-color-on-secondary: var(--color-text-primary); --crepe-color-on-secondary: var(--color-text-primary);
@@ -269,14 +273,21 @@ defineExpose({ getEditor: () => crepe?.editor })
.milkdown-host :deep(.ProseMirror p) { font-weight: 400; } .milkdown-host :deep(.ProseMirror p) { font-weight: 400; }
.milkdown-host :deep(.ProseMirror h1), .milkdown-host :deep(.ProseMirror h2), .milkdown-host :deep(.ProseMirror h3), .milkdown-host :deep(.ProseMirror h4), .milkdown-host :deep(.ProseMirror h5), .milkdown-host :deep(.ProseMirror h6) { font-weight: 700; } .milkdown-host :deep(.ProseMirror h1), .milkdown-host :deep(.ProseMirror h2), .milkdown-host :deep(.ProseMirror h3), .milkdown-host :deep(.ProseMirror h4), .milkdown-host :deep(.ProseMirror h5), .milkdown-host :deep(.ProseMirror h6) { font-weight: 700; }
.milkdown-host :deep(.font-size-marker) { display: none; } .milkdown-host :deep(.font-size-marker) { display: none; }
.milkdown-host :deep(.milkdown-code-block) { overflow: hidden; border: 1px solid var(--color-code-border); border-radius: 6px; background: var(--color-code-background); color: var(--color-code-text); }
.milkdown-host :deep(.milkdown-code-block .cm-editor),
.milkdown-host :deep(.milkdown-code-block .cm-gutters),
.milkdown-host :deep(.milkdown-code-block .cm-panel) { background: var(--color-code-background); }
.milkdown-host :deep(.milkdown-code-block .cm-content) { caret-color: var(--color-code-text); font-family: var(--font-editor-mono); }
.milkdown-host :deep(.milkdown-code-block .language-button) { color: var(--color-code-muted); }
:global(.milkdown-toolbar) { border: 1px solid var(--color-border-default) !important; background: var(--color-surface-elevated) !important; box-shadow: var(--shadow-md) !important; } :global(.milkdown-toolbar) { border: 1px solid var(--color-border-default) !important; background: var(--color-surface-elevated) !important; box-shadow: var(--shadow-md) !important; }
:global(.milkdown-toolbar .toolbar-item svg), :global(.milkdown-toolbar .toolbar-item.active svg) { color: var(--color-text-primary) !important; fill: var(--color-text-primary) !important; opacity: 1 !important; } :global(.milkdown-toolbar .toolbar-item svg), :global(.milkdown-toolbar .toolbar-item.active svg) { color: var(--color-text-primary) !important; fill: var(--color-text-primary) !important; opacity: 1 !important; }
:global(.milkdown-toolbar .toolbar-item:hover svg), :global(.milkdown-toolbar .toolbar-item.active svg) { color: var(--color-accent-primary) !important; fill: var(--color-accent-primary) !important; } :global(.milkdown-toolbar .toolbar-item:hover svg), :global(.milkdown-toolbar .toolbar-item.active svg) { color: var(--color-accent-primary) !important; fill: var(--color-accent-primary) !important; }
:global([data-theme='light']) .milkdown-host :deep(.milkdown-table-block th), .milkdown-host :deep(.milkdown-table-block th),
:global([data-theme='light']) .milkdown-host :deep(.milkdown-table-block td) { border-color: var(--color-text-tertiary); } .milkdown-host :deep(.milkdown-table-block td) { border-color: var(--color-markdown-grid); }
:global([data-theme='light']) .milkdown-host :deep(.milkdown-list-item-block li .label-wrapper) { color: var(--color-text-secondary); font-weight: 600; } .milkdown-host :deep(.milkdown-table-block th) { background: var(--color-markdown-table-header); font-weight: 700; }
:global([data-theme='light']) .milkdown-host :deep(.milkdown-list-item-block li .label-wrapper svg) { fill: var(--color-text-secondary); } .milkdown-host :deep(.milkdown-list-item-block li .label-wrapper) { color: var(--color-markdown-marker); font-weight: 700; }
.milkdown-host :deep(.milkdown-list-item-block li .label-wrapper svg) { fill: var(--color-markdown-marker); }
.milkdown-host :deep(code) { font-family: var(--font-editor-mono); } .milkdown-host :deep(code) { font-family: var(--font-editor-mono); }
:global([data-theme='dark']) .milkdown-host :deep(.milkdown) { color-scheme: dark; } :global([data-theme='dark'] .milkdown-host .milkdown) { color-scheme: dark; }
@media (max-width: 680px) { .toolbar-select select { min-width: 46px; width: 46px; } } @media (max-width: 680px) { .toolbar-select select { min-width: 46px; width: 46px; } }
</style> </style>
+4 -1
View File
@@ -66,13 +66,16 @@ async function openResult(result: SearchResult) {
</template> </template>
<style scoped> <style scoped>
.search-page > * { width: min(100%, 1040px); margin-inline: auto; }
.search-form { display: grid; grid-template-columns: 1fr auto; gap: var(--space-md); margin-bottom: var(--space-lg); } .search-form { display: grid; grid-template-columns: 1fr auto; gap: var(--space-md); margin-bottom: var(--space-lg); }
.search-input { height: 44px; font-size: var(--font-size-lg); } .search-input { height: 44px; font-size: var(--font-size-lg); }
.advanced { grid-column: 1 / -1; } .advanced { grid-column: 1 / -1; }
.results-header, .result-title, .result-meta { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); } .results-header, .result-title, .result-meta { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); }
.results-header { margin: var(--space-xl) 0 var(--space-md); color: var(--color-text-secondary); } .results-header { margin: var(--space-xl) 0 var(--space-md); color: var(--color-text-secondary); }
.result-list { display: grid; gap: var(--space-md); } .result-list { display: grid; gap: var(--space-md); }
.result-card { cursor: pointer; } .result-card { position: relative; cursor: pointer; overflow: hidden; }
.result-card::before { content: ''; position: absolute; inset: 0 auto 0 0; width: 3px; background: var(--color-accent-primary); opacity: 0; transform: scaleY(.45); transition: opacity var(--motion-fast), transform var(--motion-fast); }
.result-card:hover::before { opacity: 1; transform: scaleY(1); }
.snippet { margin: var(--space-md) 0; line-height: var(--line-height-relaxed); } .snippet { margin: var(--space-md) 0; line-height: var(--line-height-relaxed); }
.result-meta { color: var(--color-text-tertiary); font-size: var(--font-size-xs); } .result-meta { color: var(--color-text-tertiary); font-size: var(--font-size-xs); }
@media (max-width: 700px) { .search-form { grid-template-columns: 1fr; } .advanced { grid-column: auto; } } @media (max-width: 700px) { .search-form { grid-template-columns: 1fr; } .advanced { grid-column: auto; } }
@@ -181,11 +181,12 @@ async function chooseDefaultModel(provider: ProviderConfig, event: Event) {
.settings-page { max-width: 1120px; margin: 0 auto; } .settings-page { max-width: 1120px; margin: 0 auto; }
.settings-section { display: grid; gap: var(--space-md); } .settings-section { display: grid; gap: var(--space-md); }
.settings-section h2 { margin-bottom: var(--space-sm); } .settings-section h2 { margin-bottom: var(--space-sm); }
.setting-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-xl); min-height: 54px; padding: var(--space-sm) 0; border-bottom: 1px solid var(--color-border-subtle); } .setting-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-xl); min-height: 58px; padding: var(--space-sm) var(--space-md); border-bottom: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); transition: background-color var(--motion-fast); }
.setting-row:hover { background: var(--color-background-secondary); }
.setting-row small { display: block; color: var(--color-text-tertiary); }.short { width: min(220px, 45%); } .setting-row small { display: block; color: var(--color-text-tertiary); }.short { width: min(220px, 45%); }
.section-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-lg); } .section-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-lg); }
.provider-list { display: grid; gap: var(--space-md); }.provider-card { display: flex; align-items: center; justify-content: space-between; gap: var(--space-xl); }.provider-main { min-width: 0; flex: 1; }.provider-card p, .provider-card .tag-list { margin-top: var(--space-sm); } .provider-list { display: grid; gap: var(--space-md); }.provider-card { display: flex; align-items: center; justify-content: space-between; gap: var(--space-xl); }.provider-main { min-width: 0; flex: 1; }.provider-card p, .provider-card .tag-list { margin-top: var(--space-sm); }
.model-picker { display: flex; align-items: center; gap: var(--space-sm); margin-top: var(--space-md); }.model-picker label { white-space: nowrap; font-weight: 600; }.model-picker .select { width: min(360px, 100%); }.provider-actions { flex-wrap: wrap; justify-content: flex-end; }.error-text { color: var(--color-danger, #d33); } .model-picker { display: flex; align-items: center; gap: var(--space-sm); margin-top: var(--space-md); }.model-picker label { white-space: nowrap; font-weight: 600; }.model-picker .select { width: min(360px, 100%); }.provider-actions { flex-wrap: wrap; justify-content: flex-end; }.error-text { color: var(--color-error); }
.test-result { color: var(--color-info); }.index-summary, .diagnostic-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-md); }.index-summary > div { padding: var(--space-lg); border-radius: var(--radius-md); background: var(--color-background-secondary); }.index-summary strong, .index-summary small { display: block; }.index-summary strong { font-size: var(--font-size-3xl); } .test-result { color: var(--color-info); }.index-summary, .diagnostic-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-md); }.index-summary > div { padding: var(--space-lg); border-radius: var(--radius-md); background: var(--color-background-secondary); }.index-summary strong, .index-summary small { display: block; }.index-summary strong { font-size: var(--font-size-3xl); }
.section-description { margin-top: calc(-1 * var(--space-md)); }.diagnostic-grid { grid-template-columns: repeat(2, 1fr); }.diagnostic-grid h3 { margin: var(--space-md) 0 var(--space-xs); }.diagnostic-actions { margin-top: var(--space-md); } .section-description { margin-top: calc(-1 * var(--space-md)); }.diagnostic-grid { grid-template-columns: repeat(2, 1fr); }.diagnostic-grid h3 { margin: var(--space-md) 0 var(--space-xs); }.diagnostic-actions { margin-top: var(--space-md); }
@media (max-width: 700px) { .provider-card, .setting-row, .model-picker { align-items: flex-start; flex-direction: column; }.short, .model-picker .select { width: 100%; }.index-summary, .diagnostic-grid { grid-template-columns: 1fr; }.provider-actions { justify-content: flex-start; } } @media (max-width: 700px) { .provider-card, .setting-row, .model-picker { align-items: flex-start; flex-direction: column; }.short, .model-picker .select { width: 100%; }.index-summary, .diagnostic-grid { grid-template-columns: 1fr; }.provider-actions { justify-content: flex-start; } }
+4 -3
View File
@@ -50,10 +50,11 @@ async function remove(task: TaskItem) {
</template> </template>
<style scoped> <style scoped>
.task-list { display: grid; gap: var(--space-md); } .task-list { display: grid; gap: var(--space-md); width: min(100%, 980px); margin-inline: auto; }
.task-card { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: var(--space-md); } .task-card { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: var(--space-md); }
.status-check { width: 26px; height: 26px; border: 2px solid var(--color-border-default); border-radius: var(--radius-full); } .status-check { width: 28px; height: 28px; border: 2px solid var(--color-border-default); border-radius: var(--radius-full); transition: border-color var(--motion-fast), background-color var(--motion-fast), color var(--motion-fast), transform var(--motion-fast); }
.status-check.done { border-color: var(--color-success); background: var(--color-success); color: white; } .status-check:hover { border-color: var(--color-success); transform: scale(1.06); }
.status-check.done { border-color: var(--color-success); background: var(--color-success); color: white; box-shadow: 0 3px 10px color-mix(in srgb, var(--color-success) 24%, transparent); }
.task-title { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-sm); } .task-title { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-sm); }
.task-content p { margin: var(--space-xs) 0; } .task-content p { margin: var(--space-xs) 0; }
.task-content .subtle { display: flex; flex-wrap: wrap; gap: var(--space-md); } .task-content .subtle { display: flex; flex-wrap: wrap; gap: var(--space-md); }
+27 -2
View File
@@ -1,6 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import MarkdownContent from '@/components/common/MarkdownContent.vue'
import { useThemeStore } from '@/stores/theme' import { useThemeStore } from '@/stores/theme'
const themeStore = useThemeStore() const themeStore = useThemeStore()
const shikiPreview = `\`\`\`typescript
const notes = await search('本地优先')
\`\`\``
const codeThemeLabel = computed(() => themeStore.resolvedCodeBlockTheme === 'github-dark'
? 'Shiki · GitHub Dark'
: 'Shiki · GitHub Light')
</script> </script>
<template> <template>
@@ -13,7 +22,20 @@ const themeStore = useThemeStore()
<p class="subtle">v{{ theme.version }} · {{ theme.builtin ? '内置主题' : theme.author }}</p> <p class="subtle">v{{ theme.version }} · {{ theme.builtin ? '内置主题' : theme.author }}</p>
</button> </button>
</div> </div>
<div class="panel preference-panel"><h2 class="panel-title">编辑器外观</h2><div class="form-grid"><div class="field"><label>字号{{ themeStore.fontEditorSize }}px</label><input v-model.number="themeStore.fontEditorSize" type="range" min="12" max="24" /></div><div class="field"><label>行高{{ themeStore.lineHeight }}</label><input v-model.number="themeStore.lineHeight" type="range" min="1.2" max="2.2" step="0.1" /></div><div class="field"><label>字体</label><select v-model="themeStore.fontEditorFamily" class="select"><option value="system-ui">系统字体</option><option value="serif">衬线字体</option><option value="var(--font-ui-mono)">等宽字体</option></select></div></div><div class="editor-preview" :style="{ fontSize: `${themeStore.fontEditorSize}px`, lineHeight: themeStore.lineHeight, fontFamily: themeStore.fontEditorFamily }"><h3>主题预览</h3><p>知识的价值不只在于保存更在于被重新发现和使用</p><code>const notes = await search('本地优先')</code></div></div> <div class="panel preference-panel">
<h2 class="panel-title">编辑器外观</h2>
<div class="form-grid">
<div class="field"><label>字号{{ themeStore.fontEditorSize }}px</label><input v-model.number="themeStore.fontEditorSize" type="range" min="12" max="24" /></div>
<div class="field"><label>行高{{ themeStore.lineHeight }}</label><input v-model.number="themeStore.lineHeight" type="range" min="1.2" max="2.2" step="0.1" /></div>
<div class="field"><label>字体</label><select v-model="themeStore.fontEditorFamily" class="select"><option value="system-ui">系统字体</option><option value="serif">衬线字体</option><option value="var(--font-ui-mono)">等宽字体</option></select></div>
<div class="field"><label>代码块样式</label><select v-model="themeStore.codeBlockTheme" class="select"><option value="auto">跟随主题</option><option value="github-light">GitHub Light</option><option value="github-dark">GitHub Dark</option></select><small>Markdown 渲染使用对应的 Shiki GitHub 主题</small></div>
</div>
<div class="editor-preview" :style="{ fontSize: `${themeStore.fontEditorSize}px`, lineHeight: themeStore.lineHeight, fontFamily: themeStore.fontEditorFamily }">
<div class="preview-heading"><h3>主题预览</h3><span class="badge info">{{ codeThemeLabel }}</span></div>
<p>知识的价值不只在于保存更在于被重新发现和使用</p>
<MarkdownContent class="code-theme-preview" :source="shikiPreview" />
</div>
</div>
</section> </section>
</template> </template>
@@ -28,5 +50,8 @@ const themeStore = useThemeStore()
.theme-info { display: flex; justify-content: space-between; gap: var(--space-md); } .theme-info { display: flex; justify-content: space-between; gap: var(--space-md); }
.preference-panel { display: grid; gap: var(--space-xl); } .preference-panel { display: grid; gap: var(--space-xl); }
.editor-preview { padding: var(--space-xl); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-background-secondary); } .editor-preview { padding: var(--space-xl); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-background-secondary); }
.editor-preview p { margin: var(--space-sm) 0; }.editor-preview code { color: var(--color-accent-primary); } .editor-preview p { margin: var(--space-sm) 0; }
.preview-heading { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); }
.field small { color: var(--color-text-tertiary); }
.code-theme-preview { margin-top: var(--space-md); }
</style> </style>
+29 -7
View File
@@ -68,7 +68,7 @@ async function createVault() {
<div class="entry-container"> <div class="entry-container">
<div class="brand-section"> <div class="brand-section">
<div class="logo"><AppIcon :icon="Document" :size="56" /></div> <div class="logo"><AppIcon :icon="Document" :size="56" /></div>
<h1 class="app-title">知笔知己</h1> <h1 class="app-title">NotesAgent</h1>
<p class="app-subtitle">本地优先的 AI 笔记软件</p> <p class="app-subtitle">本地优先的 AI 笔记软件</p>
</div> </div>
@@ -161,7 +161,7 @@ async function createVault() {
background: background:
radial-gradient(circle at 20% 30%, var(--color-accent-soft) 0%, transparent 50%), radial-gradient(circle at 20% 30%, var(--color-accent-soft) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, var(--color-info-soft) 0%, transparent 50%); radial-gradient(circle at 80% 70%, var(--color-info-soft) 0%, transparent 50%);
opacity: 0.5; opacity: 0.62;
} }
.entry-container { .entry-container {
@@ -173,6 +173,7 @@ async function createVault() {
gap: 32px; gap: 32px;
max-width: 480px; max-width: 480px;
width: 90%; width: 90%;
animation: entry-in var(--motion-slow) both;
} }
.brand-section { .brand-section {
@@ -180,8 +181,16 @@ async function createVault() {
} }
.logo { .logo {
font-size: 64px; display: inline-grid;
margin-bottom: 12px; place-items: center;
width: 84px;
height: 84px;
margin-bottom: 14px;
border: 1px solid color-mix(in srgb, var(--color-accent-primary) 18%, transparent);
border-radius: 24px;
background: var(--color-surface-primary);
color: var(--color-accent-primary);
box-shadow: var(--shadow-lg);
} }
.app-title { .app-title {
@@ -206,7 +215,7 @@ async function createVault() {
border: 1px solid var(--color-border-default); border: 1px solid var(--color-border-default);
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
padding: var(--space-2xl); padding: var(--space-2xl);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-xl);
} }
.card-title { .card-title {
@@ -247,11 +256,13 @@ async function createVault() {
border-radius: var(--radius-md); border-radius: var(--radius-md);
cursor: pointer; cursor: pointer;
text-align: left; text-align: left;
transition: all var(--motion-fast); transition: background-color var(--motion-fast), border-color var(--motion-fast), box-shadow var(--motion-fast), transform var(--motion-fast);
&:hover { &:hover {
background: var(--color-accent-soft); background: var(--color-accent-soft);
border-color: var(--color-accent-secondary); border-color: var(--color-accent-secondary);
box-shadow: var(--shadow-sm);
transform: translateY(-1px);
} }
&:disabled { &:disabled {
@@ -288,8 +299,11 @@ async function createVault() {
.vault-arrow { .vault-arrow {
color: var(--color-text-tertiary); color: var(--color-text-tertiary);
font-size: 20px; font-size: 20px;
transition: color var(--motion-fast), transform var(--motion-fast);
} }
.vault-item:hover .vault-arrow { color: var(--color-accent-primary); transform: translateX(3px); }
.actions { .actions {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -307,7 +321,7 @@ async function createVault() {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all var(--motion-fast); transition: background-color var(--motion-fast), border-color var(--motion-fast), box-shadow var(--motion-fast), transform var(--motion-fast);
border: 1px solid transparent; border: 1px solid transparent;
&:disabled { &:disabled {
@@ -321,6 +335,8 @@ async function createVault() {
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: var(--color-accent-primary-hover); background: var(--color-accent-primary-hover);
transform: translateY(-1px);
box-shadow: 0 7px 18px color-mix(in srgb, var(--color-accent-primary) 25%, transparent);
} }
} }
@@ -400,6 +416,7 @@ async function createVault() {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: var(--z-modal); z-index: var(--z-modal);
animation: dialog-backdrop-in var(--motion-fast) both;
} }
.dialog { .dialog {
@@ -409,8 +426,13 @@ async function createVault() {
width: 90%; width: 90%;
max-width: 400px; max-width: 400px;
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
animation: dialog-in var(--motion-normal) both;
} }
@keyframes entry-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@keyframes dialog-backdrop-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes dialog-in { from { opacity: 0; transform: translateY(8px) scale(.985); } to { opacity: 1; transform: translateY(0) scale(1); } }
.dialog h3 { .dialog h3 {
margin: 0 0 var(--space-lg) 0; margin: 0 0 var(--space-lg) 0;
font-size: 18px; font-size: 18px;
@@ -133,5 +133,5 @@ button:hover { background: var(--color-background-secondary); }
.tree { padding: var(--space-xs); } .tree { padding: var(--space-xs); }
.context-menu { position: fixed; z-index: 1000; display: grid; min-width: 130px; padding: var(--space-xs); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-background-primary); box-shadow: var(--shadow-md); } .context-menu { position: fixed; z-index: 1000; display: grid; min-width: 130px; padding: var(--space-xs); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-background-primary); box-shadow: var(--shadow-md); }
.context-menu button { text-align: left; } .context-menu button { text-align: left; }
.context-menu .danger { color: var(--color-danger, #d33); } .context-menu .danger { color: var(--color-error); }
</style> </style>
@@ -15,10 +15,10 @@ onMounted(() => {
// Already loaded // Already loaded
} }
if (!workspaceStore.activeFilePath && workspaceStore.fileTree.length === 0) { if (!workspaceStore.activeFilePath && workspaceStore.fileTree.length === 0) {
void editorStore.loadFile('/欢迎使用知笔知己.md').then(() => { void editorStore.loadFile('/欢迎使用 NotesAgent.md').then(() => {
// 默认文件加载期间用户可能已经点击了其他文件,不能覆盖用户的选择。 // 默认文件加载期间用户可能已经点击了其他文件,不能覆盖用户的选择。
if (!workspaceStore.activeFilePath && editorStore.currentFilePath === '/欢迎使用知笔知己.md') { if (!workspaceStore.activeFilePath && editorStore.currentFilePath === '/欢迎使用 NotesAgent.md') {
workspaceStore.openFile('/欢迎使用知笔知己.md') workspaceStore.openFile('/欢迎使用 NotesAgent.md')
} }
}) })
} }
@@ -59,6 +59,11 @@ onMounted(() => {
.empty-content { .empty-content {
text-align: center; text-align: center;
padding: var(--space-3xl);
border: 1px dashed var(--color-border-default);
border-radius: var(--radius-xl);
background: var(--color-background-secondary);
animation: workspace-empty-in var(--motion-normal) both;
h2 { h2 {
font-size: 18px; font-size: 18px;
@@ -75,4 +80,6 @@ onMounted(() => {
font-size: 48px; font-size: 48px;
opacity: 0.5; opacity: 0.5;
} }
@keyframes workspace-empty-in { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
</style> </style>
+1 -1
View File
@@ -83,7 +83,7 @@ router.beforeEach((to, _from, next) => {
}) })
router.afterEach((to) => { router.afterEach((to) => {
const baseTitle = '知笔知己' const baseTitle = 'NotesAgent'
const title = to.meta.title as string | undefined const title = to.meta.title as string | undefined
document.title = title ? `${title} · ${baseTitle}` : baseTitle document.title = title ? `${title} · ${baseTitle}` : baseTitle
}) })
+2 -2
View File
@@ -65,7 +65,7 @@ export const mockPlugins: Plugin[] = [
version: '1.3.2', version: '1.3.2',
description: '接入 GitHub API,支持搜索 Issue、查看 PR 和管理仓库', description: '接入 GitHub API,支持搜索 Issue、查看 PR 和管理仓库',
icon: '', icon: '',
author: '知笔知己团队', author: 'NotesAgent 团队',
status: 'ready', status: 'ready',
enabled: true, enabled: true,
permissions: ['notes.read', 'network.request'], permissions: ['notes.read', 'network.request'],
@@ -117,7 +117,7 @@ export const mockPlugins: Plugin[] = [
version: '2.1.0', version: '2.1.0',
description: '导入 PDF 文档,提取文本和目录结构生成笔记', description: '导入 PDF 文档,提取文本和目录结构生成笔记',
icon: '', icon: '',
author: '知笔知己团队', author: 'NotesAgent 团队',
status: 'error', status: 'error',
enabled: false, enabled: false,
permissions: ['notes.write', 'attachments.read'], permissions: ['notes.write', 'attachments.read'],
+2 -2
View File
@@ -50,7 +50,7 @@ export const mockSkills: Skill[] = [
version: '1.0.0', version: '1.0.0',
description: '根据课程笔记生成复习要点和练习题,帮助高效备考', description: '根据课程笔记生成复习要点和练习题,帮助高效备考',
icon: '', icon: '',
author: '知笔知己团队', author: 'NotesAgent 团队',
permissions: ['notes.search', 'notes.read', 'tasks.create'], permissions: ['notes.search', 'notes.read', 'tasks.create'],
tools: ['notes.search', 'notes.read', 'tasks.create'], tools: ['notes.search', 'notes.read', 'tasks.create'],
retrieval_config: { top_k: 10, rerank: true, citation: true }, retrieval_config: { top_k: 10, rerank: true, citation: true },
@@ -64,7 +64,7 @@ export const mockSkills: Skill[] = [
version: '1.1.0', version: '1.1.0',
description: '从音频或文本中提取会议要点、行动项和待办任务', description: '从音频或文本中提取会议要点、行动项和待办任务',
icon: '', icon: '',
author: '知笔知己团队', author: 'NotesAgent 团队',
permissions: ['notes.search', 'notes.write', 'tasks.write', 'attachments.read'], permissions: ['notes.search', 'notes.write', 'tasks.write', 'attachments.read'],
tools: ['notes.search', 'notes.create', 'tasks.create', 'attachments.read'], tools: ['notes.search', 'notes.create', 'tasks.create', 'attachments.read'],
retrieval_config: { top_k: 5, rerank: false, citation: true }, retrieval_config: { top_k: 5, rerank: false, citation: true },
+4 -4
View File
@@ -58,7 +58,7 @@ const MOCK_FILE_TREE: FileNode[] = [
{ id: 'n-http', name: 'HTTP协议.md', path: '/计算机网络/HTTP协议.md', type: 'file' }, { id: 'n-http', name: 'HTTP协议.md', path: '/计算机网络/HTTP协议.md', type: 'file' },
], ],
}, },
{ id: 'n-welcome', name: '欢迎使用知笔知己.md', path: '/欢迎使用知笔知己.md', type: 'file' }, { id: 'n-welcome', name: '欢迎使用 NotesAgent.md', path: '/欢迎使用 NotesAgent.md', type: 'file' },
] ]
const mockFileContents = new Map<string, string>() const mockFileContents = new Map<string, string>()
@@ -89,8 +89,8 @@ export function readFileContent(filePath: string): Promise<string> {
const saved = mockFileContents.get(filePath) const saved = mockFileContents.get(filePath)
if (saved !== undefined) return Promise.resolve(saved) if (saved !== undefined) return Promise.resolve(saved)
const name = filePath.split('/').pop() || 'Untitled' const name = filePath.split('/').pop() || 'Untitled'
if (name === '欢迎使用知笔知己.md') { if (name === '欢迎使用 NotesAgent.md') {
return rememberContent(filePath, `# 欢迎使用知笔知己 return rememberContent(filePath, `# 欢迎使用 NotesAgent
这是一款本地优先的 AI 笔记软件,支持 Markdown 编辑、智能检索、RAG 问答和 Agent 助手。 这是一款本地优先的 AI 笔记软件,支持 Markdown 编辑、智能检索、RAG 问答和 Agent 助手。
@@ -207,7 +207,7 @@ def quick_sort(arr):
> 引用内容示例 > 引用内容示例
\`\`\`javascript \`\`\`javascript
console.log('Hello, Notes Agent!'); console.log('Hello, NotesAgent!');
\`\`\` \`\`\`
`) `)
} }
+47
View File
@@ -0,0 +1,47 @@
// @vitest-environment happy-dom
import { beforeEach, describe, expect, it } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { nextTick } from 'vue'
import { useThemeStore } from './theme'
beforeEach(() => {
localStorage.clear()
document.documentElement.removeAttribute('data-theme')
document.documentElement.removeAttribute('data-code-theme')
setActivePinia(createPinia())
Object.defineProperty(window, 'matchMedia', {
configurable: true,
value: () => ({ matches: false }),
})
})
describe('代码块主题偏好', () => {
it('跟随应用主题选择对应的 GitHub 代码主题', async () => {
const store = useThemeStore()
store.applyTheme('dark')
await nextTick()
expect(store.resolvedCodeBlockTheme).toBe('github-dark')
expect(document.documentElement.dataset.codeTheme).toBe('github-dark')
})
it('允许代码块主题独立于应用主题', async () => {
const store = useThemeStore()
store.applyTheme('dark')
store.codeBlockTheme = 'github-light'
await nextTick()
expect(store.resolvedCodeBlockTheme).toBe('github-light')
expect(document.documentElement.dataset.codeTheme).toBe('github-light')
})
it('恢复持久化的代码块主题偏好', async () => {
localStorage.setItem('editor-appearance', JSON.stringify({ codeBlockTheme: 'github-dark' }))
const store = useThemeStore()
store.initTheme()
await nextTick()
expect(store.codeBlockTheme).toBe('github-dark')
expect(document.documentElement.dataset.codeTheme).toBe('github-dark')
})
})
+31 -5
View File
@@ -3,17 +3,24 @@ import { ref, computed, watch } from 'vue'
import type { ThemeConfig } from '@/contracts' import type { ThemeConfig } from '@/contracts'
const builtinThemes: ThemeConfig[] = [ const builtinThemes: ThemeConfig[] = [
{ theme_id: 'light', name: '浅色', version: '1.0.0', description: '默认浅色主题', is_dark: false, builtin: true }, { theme_id: 'light', name: '浅色', version: '1.0.0', description: '默认浅色主题', is_dark: false, builtin: true, code_theme: 'github-light' },
{ theme_id: 'dark', name: '深色', version: '1.0.0', description: '默认深色主题', is_dark: true, builtin: true }, { theme_id: 'dark', name: '深色', version: '1.0.0', description: '默认深色主题', is_dark: true, builtin: true, code_theme: 'github-dark' },
{ theme_id: 'sepia', name: '护眼', version: '1.0.0', description: '护眼暖色调', is_dark: false, builtin: true }, { theme_id: 'sepia', name: '护眼', version: '1.0.0', description: '护眼暖色调', is_dark: false, builtin: true, code_theme: 'github-light' },
] ]
export type CodeBlockThemePreference = 'auto' | 'github-light' | 'github-dark'
function isCodeBlockThemePreference(value: unknown): value is CodeBlockThemePreference {
return value === 'auto' || value === 'github-light' || value === 'github-dark'
}
export const useThemeStore = defineStore('theme', () => { export const useThemeStore = defineStore('theme', () => {
const themes = ref<ThemeConfig[]>(builtinThemes) const themes = ref<ThemeConfig[]>(builtinThemes)
const currentThemeId = ref<string>('light') const currentThemeId = ref<string>('light')
const fontEditorSize = ref(15) const fontEditorSize = ref(15)
const fontEditorFamily = ref('system-ui') const fontEditorFamily = ref('system-ui')
const lineHeight = ref(1.7) const lineHeight = ref(1.7)
const codeBlockTheme = ref<CodeBlockThemePreference>('auto')
let appearanceHydrated = false let appearanceHydrated = false
const currentTheme = computed(() => const currentTheme = computed(() =>
@@ -21,6 +28,10 @@ export const useThemeStore = defineStore('theme', () => {
) )
const isDark = computed(() => currentTheme.value?.is_dark || false) const isDark = computed(() => currentTheme.value?.is_dark || false)
const resolvedCodeBlockTheme = computed<'github-light' | 'github-dark'>(() => {
if (codeBlockTheme.value !== 'auto') return codeBlockTheme.value
return currentTheme.value?.code_theme ?? (isDark.value ? 'github-dark' : 'github-light')
})
function applyTheme(themeId: string) { function applyTheme(themeId: string) {
const theme = themes.value.find((t) => t.theme_id === themeId) const theme = themes.value.find((t) => t.theme_id === themeId)
@@ -41,10 +52,11 @@ export const useThemeStore = defineStore('theme', () => {
const savedAppearance = localStorage.getItem('editor-appearance') const savedAppearance = localStorage.getItem('editor-appearance')
if (savedAppearance) { if (savedAppearance) {
try { try {
const value = JSON.parse(savedAppearance) as { size?: number; family?: string; lineHeight?: number } const value = JSON.parse(savedAppearance) as { size?: number; family?: string; lineHeight?: number; codeBlockTheme?: unknown }
if (value.size) fontEditorSize.value = value.size if (value.size) fontEditorSize.value = value.size
if (value.family) fontEditorFamily.value = value.family if (value.family) fontEditorFamily.value = value.family
if (value.lineHeight) lineHeight.value = value.lineHeight if (value.lineHeight) lineHeight.value = value.lineHeight
if (isCodeBlockThemePreference(value.codeBlockTheme)) codeBlockTheme.value = value.codeBlockTheme
} catch { localStorage.removeItem('editor-appearance') } } catch { localStorage.removeItem('editor-appearance') }
} }
const saved = localStorage.getItem('theme') const saved = localStorage.getItem('theme')
@@ -67,12 +79,20 @@ export const useThemeStore = defineStore('theme', () => {
fontEditorSize.value = 15 fontEditorSize.value = 15
fontEditorFamily.value = 'system-ui' fontEditorFamily.value = 'system-ui'
lineHeight.value = 1.7 lineHeight.value = 1.7
codeBlockTheme.value = 'auto'
} }
const persistAppearance = () => localStorage.setItem('editor-appearance', JSON.stringify({ const persistAppearance = () => localStorage.setItem('editor-appearance', JSON.stringify({
size: fontEditorSize.value, family: fontEditorFamily.value, lineHeight: lineHeight.value, size: fontEditorSize.value,
family: fontEditorFamily.value,
lineHeight: lineHeight.value,
codeBlockTheme: codeBlockTheme.value,
})) }))
watch(resolvedCodeBlockTheme, (theme) => {
document.documentElement.setAttribute('data-code-theme', theme)
}, { immediate: true })
watch(fontEditorSize, (v) => { watch(fontEditorSize, (v) => {
document.documentElement.style.setProperty('--font-editor-size', `${v}px`) document.documentElement.style.setProperty('--font-editor-size', `${v}px`)
if (appearanceHydrated) persistAppearance() if (appearanceHydrated) persistAppearance()
@@ -88,6 +108,10 @@ export const useThemeStore = defineStore('theme', () => {
if (appearanceHydrated) persistAppearance() if (appearanceHydrated) persistAppearance()
}, { immediate: true }) }, { immediate: true })
watch(codeBlockTheme, () => {
if (appearanceHydrated) persistAppearance()
})
return { return {
themes, themes,
currentThemeId, currentThemeId,
@@ -96,6 +120,8 @@ export const useThemeStore = defineStore('theme', () => {
fontEditorSize, fontEditorSize,
fontEditorFamily, fontEditorFamily,
lineHeight, lineHeight,
codeBlockTheme,
resolvedCodeBlockTheme,
applyTheme, applyTheme,
initTheme, initTheme,
toggleTheme, toggleTheme,
+67 -26
View File
@@ -1,9 +1,18 @@
.feature-page { .feature-page {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
padding: var(--space-2xl); padding: clamp(var(--space-lg), 3vw, var(--space-3xl));
background: var(--color-background-primary); background:
radial-gradient(circle at 92% -8%, var(--color-accent-soft) 0, transparent 28%),
var(--color-background-primary);
user-select: text; user-select: text;
scrollbar-gutter: stable;
animation: page-in var(--motion-normal) both;
}
.chat-page,
.workspace-view {
animation: page-in var(--motion-normal) both;
} }
.feature-header { .feature-header {
@@ -11,13 +20,15 @@
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: var(--space-lg); gap: var(--space-lg);
margin-bottom: var(--space-xl); margin: 0 auto var(--space-2xl);
max-width: 1180px;
} }
.feature-header h1 { font-size: var(--font-size-3xl); line-height: 1.2; } .feature-header h1 { font-size: clamp(var(--font-size-2xl), 2.3vw, 30px); line-height: 1.15; letter-spacing: -.025em; }
.feature-header p { margin-top: var(--space-xs); color: var(--color-text-secondary); } .feature-header p { max-width: 680px; margin-top: var(--space-sm); color: var(--color-text-secondary); }
.feature-actions, .inline-actions { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-sm); } .feature-actions, .inline-actions { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-sm); }
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-lg); } .feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-lg); max-width: 1180px; margin-inline: auto; }
.feature-page > .panel { max-width: 1180px; margin-inline: auto; }
.split-view { display: grid; grid-template-columns: minmax(260px, .8fr) minmax(360px, 1.7fr); gap: var(--space-lg); min-height: 0; } .split-view { display: grid; grid-template-columns: minmax(260px, .8fr) minmax(360px, 1.7fr); gap: var(--space-lg); min-height: 0; }
.panel, .item-card { .panel, .item-card {
@@ -27,10 +38,13 @@
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
} }
.panel { padding: var(--space-xl); } .panel { padding: clamp(var(--space-lg), 2.2vw, var(--space-2xl)); }
.item-card { padding: var(--space-lg); transition: border-color var(--motion-fast), transform var(--motion-fast); } .item-card {
.item-card:hover { border-color: var(--color-accent-secondary); } padding: var(--space-lg);
.item-card.selected { border-color: var(--color-accent-primary); box-shadow: 0 0 0 2px var(--color-accent-soft); } transition: border-color var(--motion-fast), box-shadow var(--motion-fast), transform var(--motion-fast), background-color var(--motion-fast);
}
.item-card:hover { border-color: var(--color-accent-secondary); box-shadow: var(--shadow-md); transform: translateY(-2px); }
.item-card.selected { border-color: var(--color-accent-primary); box-shadow: 0 0 0 3px var(--color-accent-soft), var(--shadow-md); }
.panel-title { margin-bottom: var(--space-md); font-size: var(--font-size-xl); } .panel-title { margin-bottom: var(--space-md); font-size: var(--font-size-xl); }
.muted { color: var(--color-text-secondary); } .muted { color: var(--color-text-secondary); }
.subtle { color: var(--color-text-tertiary); font-size: var(--font-size-sm); } .subtle { color: var(--color-text-tertiary); font-size: var(--font-size-sm); }
@@ -41,14 +55,18 @@
border: 1px solid transparent; border: 1px solid transparent;
border-radius: var(--radius-md); border-radius: var(--radius-md);
font-weight: 600; font-weight: 600;
line-height: 1;
transition: background-color var(--motion-fast), border-color var(--motion-fast), color var(--motion-fast), box-shadow var(--motion-fast), transform var(--motion-fast);
} }
.button-primary { background: var(--color-accent-primary); color: var(--color-text-inverse); } .button-primary { background: var(--color-accent-primary); color: var(--color-text-inverse); box-shadow: 0 4px 12px color-mix(in srgb, var(--color-accent-primary) 24%, transparent); }
.button-primary:hover { background: var(--color-accent-primary-hover); } .button-primary:hover { background: var(--color-accent-primary-hover); transform: translateY(-1px); box-shadow: 0 7px 18px color-mix(in srgb, var(--color-accent-primary) 28%, transparent); }
.button-secondary { border-color: var(--color-border-default); background: var(--color-surface-primary); } .button-secondary { border-color: var(--color-border-default); background: var(--color-surface-primary); }
.button-secondary:hover, .icon-button:hover { background: var(--color-background-hover); } .button-secondary:hover, .icon-button:hover { border-color: var(--color-accent-secondary); background: var(--color-background-hover); color: var(--color-accent-primary); transform: translateY(-1px); }
.button-danger { background: var(--color-error-soft); color: var(--color-error); } .button-danger { background: var(--color-error-soft); color: var(--color-error); }
button:disabled { cursor: not-allowed; opacity: .55; } .button-danger:hover { transform: translateY(-1px); box-shadow: 0 5px 14px color-mix(in srgb, var(--color-error) 18%, transparent); }
button:active:not(:disabled) { transform: translateY(0); }
button:disabled { cursor: not-allowed; opacity: .55; box-shadow: none; transform: none; }
.field { display: grid; gap: var(--space-xs); } .field { display: grid; gap: var(--space-xs); }
.field label { color: var(--color-text-secondary); font-size: var(--font-size-sm); font-weight: 600; } .field label { color: var(--color-text-secondary); font-size: var(--font-size-sm); font-weight: 600; }
@@ -59,10 +77,12 @@ button:disabled { cursor: not-allowed; opacity: .55; }
outline: none; outline: none;
background: var(--color-background-primary); background: var(--color-background-primary);
color: var(--color-text-primary); color: var(--color-text-primary);
transition: border-color var(--motion-fast), box-shadow var(--motion-fast), background-color var(--motion-fast);
} }
.input, .select { height: 36px; padding: 0 var(--space-md); } .input, .select { height: 38px; padding: 0 var(--space-md); }
.textarea { min-height: 100px; padding: var(--space-md); resize: vertical; } .textarea { min-height: 100px; padding: var(--space-md); resize: vertical; }
.input:focus, .select:focus, .textarea:focus { border-color: var(--color-border-focus); box-shadow: 0 0 0 2px var(--color-accent-soft); } .input:hover, .select:hover, .textarea:hover { border-color: var(--color-text-tertiary); }
.input:focus, .select:focus, .textarea:focus { border-color: var(--color-border-focus); box-shadow: 0 0 0 3px var(--color-accent-soft); background: var(--color-surface-primary); }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--space-md); } .form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--space-md); }
.badge { .badge {
@@ -75,6 +95,7 @@ button:disabled { cursor: not-allowed; opacity: .55; }
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
font-weight: 600; font-weight: 600;
letter-spacing: .01em;
} }
.badge.success { background: var(--color-success-soft); color: var(--color-success); } .badge.success { background: var(--color-success-soft); color: var(--color-success); }
.badge.warning { background: var(--color-warning-soft); color: var(--color-warning); } .badge.warning { background: var(--color-warning-soft); color: var(--color-warning); }
@@ -82,25 +103,45 @@ button:disabled { cursor: not-allowed; opacity: .55; }
.badge.info { background: var(--color-info-soft); color: var(--color-info); } .badge.info { background: var(--color-info-soft); color: var(--color-info); }
.tag-list { display: flex; flex-wrap: wrap; gap: var(--space-xs); } .tag-list { display: flex; flex-wrap: wrap; gap: var(--space-xs); }
.empty-state { display: grid; place-items: center; min-height: 220px; padding: var(--space-2xl); text-align: center; color: var(--color-text-secondary); } .empty-state { display: grid; place-items: center; min-height: 220px; padding: var(--space-3xl); border: 1px dashed var(--color-border-default); border-radius: var(--radius-xl); background: color-mix(in srgb, var(--color-background-secondary) 74%, transparent); text-align: center; color: var(--color-text-secondary); }
.empty-state strong { display: block; margin-bottom: var(--space-xs); color: var(--color-text-primary); font-size: var(--font-size-xl); } .empty-state strong { display: block; margin-bottom: var(--space-xs); color: var(--color-text-primary); font-size: var(--font-size-xl); }
.error-banner { margin-bottom: var(--space-lg); padding: var(--space-md); border-radius: var(--radius-md); background: var(--color-error-soft); color: var(--color-error); } .error-banner, .notice-banner { border: 1px solid transparent; animation: notice-in var(--motion-normal) both; }
.notice-banner { margin-bottom: var(--space-lg); padding: var(--space-md); border-radius: var(--radius-md); background: var(--color-info-soft); color: var(--color-info); } .error-banner { margin-bottom: var(--space-lg); padding: var(--space-md) var(--space-lg); border-color: color-mix(in srgb, var(--color-error) 22%, transparent); border-radius: var(--radius-md); background: var(--color-error-soft); color: var(--color-error); }
.notice-banner { margin-bottom: var(--space-lg); padding: var(--space-md) var(--space-lg); border-color: color-mix(in srgb, var(--color-info) 22%, transparent); border-radius: var(--radius-md); background: var(--color-info-soft); color: var(--color-info); }
.sidebar-panel { padding: var(--space-md); } .sidebar-panel { padding: var(--space-md); }
.sidebar-panel .input, .sidebar-panel .select { margin-bottom: var(--space-sm); } .sidebar-panel .input, .sidebar-panel .select { margin-bottom: var(--space-sm); }
.sidebar-list { display: grid; gap: var(--space-xs); } .sidebar-list { display: grid; gap: var(--space-xs); }
.sidebar-list-item { padding: var(--space-sm); border-radius: var(--radius-md); cursor: pointer; } .sidebar-list-item { padding: 9px var(--space-sm); border: 1px solid transparent; border-radius: var(--radius-md); cursor: pointer; transition: color var(--motion-fast), background-color var(--motion-fast), border-color var(--motion-fast), transform var(--motion-fast); }
.sidebar-list-item:hover, .sidebar-list-item.active { background: var(--color-background-hover); } .sidebar-list-item:hover { background: var(--color-background-hover); transform: translateX(2px); }
.sidebar-list-item.active { border-color: color-mix(in srgb, var(--color-accent-primary) 18%, transparent); background: var(--color-accent-soft); color: var(--color-accent-primary); }
.modal-backdrop { position: fixed; inset: 0; z-index: var(--z-modal); display: grid; place-items: center; padding: var(--space-xl); background: var(--color-background-overlay); } .modal-backdrop { position: fixed; inset: 0; z-index: var(--z-modal); display: grid; place-items: center; padding: var(--space-xl); background: var(--color-background-overlay); animation: backdrop-in var(--motion-fast) both; }
.modal { width: min(560px, 100%); max-height: 85vh; overflow: auto; padding: var(--space-xl); border-radius: var(--radius-lg); background: var(--color-surface-elevated); box-shadow: var(--shadow-xl); } .modal { width: min(560px, 100%); max-height: 85vh; overflow: auto; padding: var(--space-2xl); border: 1px solid var(--color-border-default); border-radius: var(--radius-xl); background: var(--color-surface-elevated); box-shadow: var(--shadow-xl); animation: modal-in var(--motion-normal) both; }
.modal h2 { margin-bottom: var(--space-lg); } .modal h2 { margin-bottom: var(--space-lg); }
.modal form { display: grid; gap: var(--space-md); } .modal form { display: grid; gap: var(--space-md); }
.settings-nav { display: flex; flex-wrap: wrap; gap: var(--space-xs); margin-bottom: var(--space-xl); border-bottom: 1px solid var(--color-border-default); } .settings-nav { display: flex; flex-wrap: wrap; gap: var(--space-xs); margin-bottom: var(--space-xl); padding: var(--space-xs); border: 1px solid var(--color-border-default); border-radius: var(--radius-lg); background: var(--color-background-secondary); }
.settings-nav button { padding: var(--space-sm) var(--space-md); border-bottom: 2px solid transparent; color: var(--color-text-secondary); } .settings-nav button { padding: 8px var(--space-md); border-radius: var(--radius-md); color: var(--color-text-secondary); transition: color var(--motion-fast), background-color var(--motion-fast), box-shadow var(--motion-fast); }
.settings-nav button.active { border-color: var(--color-accent-primary); color: var(--color-accent-primary); } .settings-nav button:hover { color: var(--color-text-primary); background: var(--color-background-hover); }
.settings-nav button.active { background: var(--color-surface-primary); color: var(--color-accent-primary); box-shadow: var(--shadow-sm); }
@keyframes page-in {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes notice-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes backdrop-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes modal-in {
from { opacity: 0; transform: translateY(8px) scale(.985); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@media (max-width: 900px) { @media (max-width: 900px) {
.feature-page { padding: var(--space-lg); } .feature-page { padding: var(--space-lg); }
+44
View File
@@ -0,0 +1,44 @@
/// <reference types="node" />
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const globalStyles = [
new URL('./tokens.css', import.meta.url),
new URL('./features.css', import.meta.url),
].map((path) => readFileSync(path, 'utf8')).join('\n')
const markdownStyles = [
new URL('../features/editor/VisualMarkdownEditor.vue', import.meta.url),
new URL('../features/themes/ThemesView.vue', import.meta.url),
new URL('../components/common/MarkdownContent.vue', import.meta.url),
].map((path) => readFileSync(path, 'utf8')).join('\n')
describe('轻量动效基线', () => {
it('为减少动态效果偏好提供全局回退', () => {
expect(globalStyles).toContain('@media (prefers-reduced-motion: reduce)')
})
it('不使用全属性过渡或高成本模糊滤镜', () => {
expect(globalStyles).not.toMatch(/transition:\s*all\b/)
expect(globalStyles).not.toMatch(/(?:backdrop-)?filter\s*:/)
})
it('页面入场只改变透明度和变换', () => {
const pageAnimation = globalStyles.match(/@keyframes page-in\s*{[\s\S]*?\n}/)?.[0] ?? ''
expect(pageAnimation).toContain('opacity')
expect(pageAnimation).toContain('transform')
expect(pageAnimation).not.toMatch(/(?:width|height|margin|padding|top|left)\s*:/)
})
it('Markdown 序号和表格使用独立的高对比度主题变量', () => {
expect(globalStyles).toContain('--color-markdown-grid:')
expect(globalStyles).toContain('--color-markdown-marker:')
expect(markdownStyles).toContain('var(--color-markdown-grid)')
expect(markdownStyles).toContain('var(--color-markdown-marker)')
})
it('不混用可能丢失后代选择器的 scoped global 写法', () => {
expect(markdownStyles).not.toMatch(/:global\([^\n]+\)\s+\./)
})
})
+78 -15
View File
@@ -44,17 +44,22 @@
--color-border-focus: #5b67f1; --color-border-focus: #5b67f1;
--color-border-disabled: #eef0f3; --color-border-disabled: #eef0f3;
/* Markdown */
--color-markdown-grid: #8b949e;
--color-markdown-marker: #343b44;
--color-markdown-table-header: #eef0f3;
/* Shadow */ /* Shadow */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04); --shadow-sm: 0 1px 2px rgba(31, 35, 40, 0.05), 0 1px 5px rgba(31, 35, 40, 0.03);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); --shadow-md: 0 8px 22px rgba(31, 35, 40, 0.08), 0 2px 6px rgba(31, 35, 40, 0.04);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); --shadow-lg: 0 16px 36px rgba(31, 35, 40, 0.11), 0 4px 12px rgba(31, 35, 40, 0.05);
--shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.16); --shadow-xl: 0 24px 64px rgba(31, 35, 40, 0.18), 0 8px 20px rgba(31, 35, 40, 0.08);
/* Radius */ /* Radius */
--radius-sm: 4px; --radius-sm: 6px;
--radius-md: 6px; --radius-md: 9px;
--radius-lg: 10px; --radius-lg: 13px;
--radius-xl: 14px; --radius-xl: 18px;
--radius-full: 9999px; --radius-full: 9999px;
/* Spacing */ /* Spacing */
@@ -100,16 +105,16 @@
--z-titlebar: 500; --z-titlebar: 500;
/* Motion */ /* Motion */
--motion-fast: 120ms cubic-bezier(0.4, 0, 0.2, 1); --motion-fast: 120ms cubic-bezier(0.2, 0, 0, 1);
--motion-normal: 200ms cubic-bezier(0.4, 0, 0.2, 1); --motion-normal: 190ms cubic-bezier(0.2, 0, 0, 1);
--motion-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1); --motion-slow: 260ms cubic-bezier(0.2, 0, 0, 1);
/* Layout */ /* Layout */
--titlebar-height: 38px; --titlebar-height: 42px;
--sidebar-primary-width: 52px; --sidebar-primary-width: 58px;
--sidebar-primary-width-expanded: 180px; --sidebar-primary-width-expanded: 180px;
--sidebar-secondary-width: 260px; --sidebar-secondary-width: 272px;
--statusbar-height: 26px; --statusbar-height: 28px;
} }
[data-theme='dark'] { [data-theme='dark'] {
@@ -152,6 +157,10 @@
--color-border-focus: #7d8bff; --color-border-focus: #7d8bff;
--color-border-disabled: #21262d; --color-border-disabled: #21262d;
--color-markdown-grid: #6e7681;
--color-markdown-marker: #c9d1d9;
--color-markdown-table-header: #21262d;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5); --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
@@ -172,12 +181,29 @@
--color-text-tertiary: #9a8a72; --color-text-tertiary: #9a8a72;
--color-border-default: #ddcfad; --color-border-default: #ddcfad;
--color-border-subtle: #eadfc4; --color-border-subtle: #eadfc4;
--color-markdown-grid: #9c8353;
--color-markdown-marker: #554735;
--color-markdown-table-header: #eadbb8;
--color-accent-primary: #8a5b32; --color-accent-primary: #8a5b32;
--color-accent-primary-hover: #704724; --color-accent-primary-hover: #704724;
--color-accent-soft: #edddbd; --color-accent-soft: #edddbd;
--color-text-link: #7b512e; --color-text-link: #7b512e;
} }
[data-code-theme='github-light'] {
--color-code-background: #f6f8fa;
--color-code-border: #d0d7de;
--color-code-text: #24292f;
--color-code-muted: #57606a;
}
[data-code-theme='github-dark'] {
--color-code-background: #161b22;
--color-code-border: #30363d;
--color-code-text: #c9d1d9;
--color-code-muted: #8b949e;
}
* { * {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
@@ -221,6 +247,25 @@ button {
color: inherit; color: inherit;
} }
button,
a,
input,
textarea,
select {
-webkit-tap-highlight-color: transparent;
}
input[type='checkbox'],
input[type='radio'],
input[type='range'] {
accent-color: var(--color-accent-primary);
}
:focus-visible {
outline: 2px solid var(--color-border-focus);
outline-offset: 2px;
}
input, input,
textarea, textarea,
select { select {
@@ -261,7 +306,25 @@ ol {
color: var(--color-accent-primary); color: var(--color-accent-primary);
} }
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
[data-theme='dark'] ::selection { [data-theme='dark'] ::selection {
background: var(--color-accent-primary); background: var(--color-accent-primary);
color: var(--color-text-inverse); color: var(--color-text-inverse);
} }
@media (max-width: 900px) {
:root {
--sidebar-primary-width-expanded: 160px;
--sidebar-secondary-width: 224px;
}
}
+13
View File
@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest'
import { highlightCode } from './markdown'
describe('Shiki GitHub 双主题', () => {
it('一次渲染同时生成 GitHub Light 和 GitHub Dark 颜色变量', async () => {
const html = await highlightCode('const answer = 42', 'typescript')
expect(html).toContain('github-light')
expect(html).toContain('github-dark')
expect(html).toContain('--shiki-light')
expect(html).toContain('--shiki-dark')
})
})