From af2556d29ec275e8706ab5a6c98894584c718d51 Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Sat, 5 Sep 2026 23:56:02 +0800 Subject: [PATCH] fix(ui): unify dialogs and complete theme component coverage --- docs/development/主题组件覆盖检查.md | 14 +++++++ .../src/assets/themes/paper-moments.theme | 6 ++- .../src/components/common/AppDialog.spec.ts | 36 +++++++++++++++++ frontend/src/components/common/AppDialog.vue | 39 +++++++++++++++++++ .../src/components/common/dialogScroll.ts | 26 +++++++++++++ frontend/src/features/agent/AgentView.vue | 5 ++- frontend/src/features/agent/ToolOption.vue | 4 +- frontend/src/features/agent/TraceTimeline.vue | 2 +- .../src/features/chat/ChatPersonaDialog.vue | 6 ++- frontend/src/features/mcp/McpServersView.vue | 7 ++-- frontend/src/features/media/MediaView.vue | 9 +++-- .../settings/ProviderPresetSelector.vue | 2 +- .../features/settings/RequestJsonEditor.vue | 4 +- frontend/src/features/tasks/TasksView.vue | 3 +- .../themes/CommunityThemePreview.spec.ts | 23 +++++++++++ .../features/themes/CommunityThemePreview.vue | 17 +++++--- .../src/features/themes/ThemesView.spec.ts | 21 ++++++++-- frontend/src/features/themes/ThemesView.vue | 5 ++- .../src/features/themes/themeSpecimen.html | 19 +++++++++ frontend/src/styles/features.css | 9 ++--- frontend/tests/visual/README.md | 9 +++++ frontend/tests/visual/index.html | 23 +++++++++++ 22 files changed, 254 insertions(+), 35 deletions(-) create mode 100644 frontend/src/components/common/AppDialog.spec.ts create mode 100644 frontend/src/components/common/AppDialog.vue create mode 100644 frontend/src/components/common/dialogScroll.ts create mode 100644 frontend/src/features/themes/CommunityThemePreview.spec.ts create mode 100644 frontend/src/features/themes/themeSpecimen.html create mode 100644 frontend/tests/visual/README.md create mode 100644 frontend/tests/visual/index.html diff --git a/docs/development/主题组件覆盖检查.md b/docs/development/主题组件覆盖检查.md index 7967612..d4a04c1 100644 --- a/docs/development/主题组件覆盖检查.md +++ b/docs/development/主题组件覆盖检查.md @@ -30,3 +30,17 @@ AI 对话 Enter 发送,Shift+Enter 换行,输入法确认和长按 Enter 不 ## 下拉与折叠控件补充 编辑器外观行统一为标签、38px 控件、说明三层,避免只有代码主题字段带说明时将其他控件拉偏。补齐所有原生 details 的 `ui-disclosure` 样式,统一折叠箭头、展开背景和边框。下拉框增加共享箭头、选项配色、焦点及禁用态;支持 `appearance: base-select` 的浏览器使用可主题化选项面板,其他浏览器保留原生选择行为并应用可支持的颜色。原生系统弹出层的完整装饰不能仅靠 CSS 在所有浏览器中保证。 + +## 6107b7f 后续遗漏修复 + +- 任务、MCP、主题导入、社区预览、Agent 权限确认接入 AppDialog:原生顶层遮罩、内部滚动、祖先滚动锁及焦点归还。滚动锁使用引用计数,嵌套弹窗关闭不会提前解锁背景。权限确认禁止 Escape/点击遮罩隐式关闭;MCP 忙碌时禁止隐式关闭。人设弹窗补用同一滚动锁。 +- 修复音视频三处、请求 JSON 一处 textarea 的单行高度覆盖;使用 textarea 样式、最小高度和纵向拉伸。单行高度规则仅匹配 input.input 与 select。 +- Agent 工具原文和 Trace 完整数据接入 ui-disclosure,并合并重复容器规则。Provider 预设选中态使用主题强调色。 +- 音视频页面标题和内容宽度对齐共享布局。纸间时光补充工具选择卡片的轻量纸张边框,版本 1.6.2;旧版更新入口及应用后 CSS 刷新已有自动测试。其余社区主题复用公共控件修复,未无意义提升包版本。 +- 安装前预览加载真实 tokens.css 与 features.css,并增加多行/单行/下拉/禁用/展开折叠/错误标签/Markdown/图表配色及长标识样例;保留无脚本 sandbox 与 CSP,不改宿主主题。 + +验证:六主题在 1280、600、360 像素浏览器窗口下检查;样例 iframe 内容宽度分别为 660、508、268px,均无水平溢出,多行框均为 100px。实际任务页弹窗矩形覆盖 1280×720 全视口,焦点进入表单,Escape 关闭后回到新建按钮;实际术语校对框为 100px 并支持纵向拉伸。 + +可重复的隔离浏览器入口与操作说明位于 `frontend/tests/visual/README.md`,支持六主题、长内容弹窗和真实编辑器输入。测试不访问用户笔记或调用模型。组件样例矩阵并不等同于所有业务数据、浏览器和完整色彩对比度验收;不声称完成未执行的全页面截图或性能优化。 + +最终验证:56 个测试文件、323 项前端测试通过;生产构建通过。复扫结果:未接入共享样式的 details 为 0,误用 input 类的 textarea 为 0。 diff --git a/frontend/src/assets/themes/paper-moments.theme b/frontend/src/assets/themes/paper-moments.theme index 964110a..0a24502 100644 --- a/frontend/src/assets/themes/paper-moments.theme +++ b/frontend/src/assets/themes/paper-moments.theme @@ -1,6 +1,6 @@ theme_id: paper-moments name: 纸间时光 · Paper Moments -version: 1.6.1 +version: 1.6.2 author: NotesAgent description: 奶油纸张、手帐虚线与粉蓝胶带,把每天的灵感好好收藏。 min_app_version: 0.2.0 @@ -318,3 +318,7 @@ license: MIT @supports (appearance: base-select) { [data-theme="paper-moments"] ::picker(select) { border: 1px solid #b5a693; outline: 1px dashed #d5c8b5; outline-offset: -4px; background: #fffdf5; box-shadow: var(--shadow-md); } } + +/* Nested choices retain a quiet paper border without repeating tape/shadows. */ +[data-theme="paper-moments"] .surface-nested { border: 1px dashed #c5b9a7; background: #fffdf5; border-radius: 6px; } +[data-theme="paper-moments"] .surface-nested.selected { border-color: var(--color-accent-primary); background: var(--color-accent-soft); } diff --git a/frontend/src/components/common/AppDialog.spec.ts b/frontend/src/components/common/AppDialog.spec.ts new file mode 100644 index 0000000..3f64e60 --- /dev/null +++ b/frontend/src/components/common/AppDialog.spec.ts @@ -0,0 +1,36 @@ +// @vitest-environment happy-dom +import { afterEach, expect, it } from 'vitest' +import { mount, type VueWrapper } from '@vue/test-utils' +import AppDialog from './AppDialog.vue' +const mounted: VueWrapper[] = [] +afterEach(() => { mounted.splice(0).reverse().forEach(w => w.unmount()); document.body.innerHTML = ''; document.body.style.cssText = ''; document.documentElement.style.cssText = '' }) +it('locks all scroll ancestors and restores focus and inline styles', async () => { + const opener = document.createElement('button'); document.body.append(opener); opener.focus() + const host = document.createElement('div'); host.style.setProperty('overflow', 'auto', 'important'); document.body.append(host) + const w = mount(AppDialog, { props:{label:'测试'}, slots:{default:''}, attachTo:host }); mounted.push(w) + expect(w.get('dialog').element.open).toBe(true) + expect(host.style.overflow).toBe('hidden') + expect(document.body.style.overflow).toBe('hidden') + await w.get('dialog').trigger('keydown', {key:'Escape'}) + expect(w.emitted('close')).toHaveLength(1) + w.unmount(); mounted.pop() + expect(host.style.overflow).toBe('auto') + expect(host.style.getPropertyPriority('overflow')).toBe('important') + expect(document.body.style.overflow).toBe('') + expect(document.activeElement).toBe(opener) +}) +it('retains scroll locks until the last nested dialog closes', () => { + const first = mount(AppDialog, {props:{label:'父弹窗'}, attachTo:document.body}); mounted.push(first) + const second = mount(AppDialog, {props:{label:'子弹窗'}, attachTo:document.body}); mounted.push(second) + first.unmount(); mounted.splice(0,1) + expect(document.body.style.overflow).toBe('hidden') + second.unmount(); mounted.pop() + expect(document.body.style.overflow).toBe('') +}) +it('does not dismiss permission or busy dialogs through Escape or backdrop', async () => { + const w = mount(AppDialog, {props:{label:'权限确认',dismissible:false},attachTo:document.body}); mounted.push(w) + await w.get('dialog').trigger('keydown',{key:'Escape'}) + await w.get('dialog').trigger('cancel') + await w.get('dialog').trigger('click') + expect(w.emitted('close')).toBeUndefined() +}) diff --git a/frontend/src/components/common/AppDialog.vue b/frontend/src/components/common/AppDialog.vue new file mode 100644 index 0000000..eb15de7 --- /dev/null +++ b/frontend/src/components/common/AppDialog.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/frontend/src/components/common/dialogScroll.ts b/frontend/src/components/common/dialogScroll.ts new file mode 100644 index 0000000..b814b35 --- /dev/null +++ b/frontend/src/components/common/dialogScroll.ts @@ -0,0 +1,26 @@ +// Reference counts keep the underlying page locked when dialogs are nested. +const locks = new WeakMap() +export function lockDialogScroll(dialog: HTMLElement): () => void { + const elements: HTMLElement[] = [] + for (let element = dialog.parentElement; element; element = element.parentElement) { + const lock = locks.get(element) + if (lock) lock.count++ + else { + locks.set(element, { count: 1, value: element.style.getPropertyValue('overflow'), priority: element.style.getPropertyPriority('overflow') }) + element.style.setProperty('overflow', 'hidden', 'important') + } + elements.push(element) + } + let released = false + return () => { + if (released) return + released = true + for (const element of elements) { + const lock = locks.get(element)! + if (--lock.count) continue + if (lock.value) element.style.setProperty('overflow', lock.value, lock.priority) + else element.style.removeProperty('overflow') + locks.delete(element) + } + } +} diff --git a/frontend/src/features/agent/AgentView.vue b/frontend/src/features/agent/AgentView.vue index 47af04d..a7048a5 100644 --- a/frontend/src/features/agent/AgentView.vue +++ b/frontend/src/features/agent/AgentView.vue @@ -1,4 +1,5 @@