From e0eb6a721e84cd17aa385b4ee6991a89a60a88d4 Mon Sep 17 00:00:00 2001 From: Huarch Date: Sat, 10 Oct 2026 16:30:20 +0800 Subject: [PATCH] fix(workbench): align investigation diagram controls --- src/styles.css | 38 ++++++++++++++- .../browser/dynamic-investigation-flow.e2e.ts | 47 ++++++++++++++++++- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/styles.css b/src/styles.css index 495b5ac..1a77c5f 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1353,6 +1353,12 @@ button.agent-history-session-trigger:not(:disabled):active { .investigation-diagram .node { cursor: pointer; } .investigation-diagram .node:hover { filter: brightness(0.97); } +.investigation-diagram [data-streamdown="mermaid-block"] { + --diagram-control-rail-inset: 1.0625rem; + --diagram-control-group-gap: 0.5rem; + --diagram-zoom-rail-height: 7.625rem; +} + /* Match Button's compact icon-sm size, including controls portalled to body. */ button[type="button"]:has(> .investigation-diagram-control-icon) { --diagram-control-size: 2rem; @@ -1398,14 +1404,37 @@ div:has(> button > [data-investigation-diagram-control="zoom-in"]), .investigation-diagram [data-streamdown="mermaid-block"] > div:first-child { display: none; } .investigation-diagram [data-streamdown="mermaid-block"] > div:has(> [data-streamdown="mermaid-block-actions"]) { + position: absolute; + z-index: 20; + inset-block-start: auto; + inset-block-end: calc( + var(--diagram-control-rail-inset) + var(--diagram-zoom-rail-height) + + var(--diagram-control-group-gap) + ); + inset-inline-start: auto; + inset-inline-end: var(--diagram-control-rail-inset); height: auto; margin-top: 0; } +.investigation-diagram div:has(> button > [data-investigation-diagram-control="zoom-in"]) { + inset-inline-start: auto; + inset-inline-end: 0.5rem; +} + button[type="button"]:has(> [data-investigation-diagram-control="close"]) { position: absolute; - border-color: var(--surface-border); + inset-block-start: 1.3125rem; + inset-inline-end: 1.3125rem; + border-color: transparent; background: var(--surface-control); + box-shadow: + 0 0 0 4px var(--surface-control), + 0 0 0 5px var(--surface-border); +} + +button[type="button"]:has(> [data-investigation-diagram-control="close"])::after { + inset: -5px; } @media (hover: hover) { @@ -1424,6 +1453,13 @@ button[type="button"]:has(> .investigation-diagram-control-icon):active:not(:dis transform: scale(0.98); } +button[type="button"]:has(> [data-investigation-diagram-control="close"]):active:not(:disabled) { + box-shadow: + 0 0 0 4px var(--surface-control), + 0 0 0 5px var(--action-border-pressed), + var(--action-shadow-secondary-pressed); +} + button[type="button"]:has(> .investigation-diagram-control-icon):focus-visible { outline: 2px solid var(--action-focus-ring); outline-offset: 2px; diff --git a/tests/browser/dynamic-investigation-flow.e2e.ts b/tests/browser/dynamic-investigation-flow.e2e.ts index 2f828f1..34373f5 100644 --- a/tests/browser/dynamic-investigation-flow.e2e.ts +++ b/tests/browser/dynamic-investigation-flow.e2e.ts @@ -202,7 +202,34 @@ test("调查报告展示 SVG 脉络、处置进展和报告版本,缺数正常 }); } }; + const assertInlineControlRail = async (surface: typeof diagram) => { + const mermaidBlock = surface.locator('[data-streamdown="mermaid-block"]'); + const graph = surface.locator('[data-streamdown="mermaid"]'); + const fullscreenButton = surface.getByRole("button", { name: "全屏查看", exact: true }); + const zoomInButton = surface.getByRole("button", { name: "Zoom in", exact: true }); + const fullscreenGroup = surface.locator('[data-streamdown="mermaid-block-actions"]'); + const zoomGroup = zoomInButton.locator(".."); + const [blockBox, graphBox, fullscreenBox, zoomInBox, fullscreenGroupBox, zoomGroupBox] = await Promise.all([ + mermaidBlock.boundingBox(), graph.boundingBox(), fullscreenButton.boundingBox(), zoomInButton.boundingBox(), + fullscreenGroup.boundingBox(), zoomGroup.boundingBox() + ]); + expect(blockBox).not.toBeNull(); + expect(graphBox).not.toBeNull(); + expect(fullscreenBox).not.toBeNull(); + expect(zoomInBox).not.toBeNull(); + expect(fullscreenGroupBox).not.toBeNull(); + expect(zoomGroupBox).not.toBeNull(); + expect(Math.round(graphBox!.y - blockBox!.y)).toBeLessThanOrEqual(10); + expect(Math.round(fullscreenBox!.x)).toBe(Math.round(zoomInBox!.x)); + expect(Math.round(zoomInBox!.y - fullscreenBox!.y - fullscreenBox!.height)).toBe(18); + expect(Math.round(fullscreenGroupBox!.x)).toBe(Math.round(zoomGroupBox!.x)); + expect(Math.round(fullscreenGroupBox!.width)).toBe(Math.round(zoomGroupBox!.width)); + expect(Math.round(zoomGroupBox!.y - fullscreenGroupBox!.y - fullscreenGroupBox!.height)).toBe(8); + expect(Math.round(blockBox!.x + blockBox!.width - fullscreenGroupBox!.x - fullscreenGroupBox!.width)).toBe(18); + expect(Math.round(graphBox!.x + graphBox!.width - zoomGroupBox!.x - zoomGroupBox!.width)).toBe(8); + }; assertControlGeometry(inlineControls); + await assertInlineControlRail(diagram); const zoomIn = diagram.getByRole("button", { name: "Zoom in", exact: true }); const zoomOut = diagram.getByRole("button", { name: "Zoom out", exact: true }); const resetZoom = diagram.getByRole("button", { name: "Reset zoom and pan", exact: true }); @@ -237,6 +264,21 @@ test("调查报告展示 SVG 脉络、处置进展和报告版本,缺数正常 await expect(fullscreen.locator("svg[id]")).toHaveCount(1); const fullscreenControls = await captureDiagramControls(fullscreen); assertControlGeometry(fullscreenControls); + const assertFullscreenCloseFrame = async () => { + const close = fullscreen.getByRole("button", { name: "退出全屏", exact: true }); + const frame = await close.evaluate((button) => { + const rect = button.getBoundingClientRect(); + return { + top: rect.top, + right: window.innerWidth - rect.right, + boxShadow: getComputedStyle(button).boxShadow + }; + }); + expect(Math.round(frame.top)).toBe(21); + expect(Math.round(frame.right)).toBe(21); + expect(frame.boxShadow).not.toBe("none"); + }; + await assertFullscreenCloseFrame(); await writeFile(testInfo.outputPath("diagram-control-geometry.json"), JSON.stringify({ viewport: page.viewportSize(), inline: inlineControls, fullscreen: fullscreenControls }, null, 2)); await testInfo.attach("styled-investigation-fullscreen", { body: await page.screenshot({ path: testInfo.outputPath("styled-investigation-fullscreen.png") }), contentType: "image/png" }); await fullscreen.getByRole("button", { name: "退出全屏", exact: true }).click(); @@ -345,12 +387,15 @@ test("调查报告展示 SVG 脉络、处置进展和报告版本,缺数正常 await expect(mobilePanel.getByLabel("调查脉络", { exact: true }).locator("svg[id]")).toHaveCount(1); const mobileDiagram = mobilePanel.getByLabel("调查脉络", { exact: true }); assertControlGeometry(await captureDiagramControls(mobileDiagram)); + await assertInlineControlRail(mobileDiagram); if (width === 320) { await mobileDiagram.getByRole("button", { name: "全屏查看", exact: true }).click(); await expect(fullscreen.locator("svg[id]")).toHaveCount(1); assertControlGeometry(await captureDiagramControls(fullscreen)); + await assertFullscreenCloseFrame(); await testInfo.attach("diagram-fullscreen-320", { body: await page.screenshot({ path: testInfo.outputPath("diagram-fullscreen-320.png") }), contentType: "image/png" }); - await fullscreen.getByRole("button", { name: "退出全屏", exact: true }).click(); + await fullscreen.getByRole("button", { name: "退出全屏", exact: true }).evaluate((button) => button.click()); + await expect(fullscreen).toHaveCount(0); } expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width); await mobilePanel.getByLabel("任务报告", { exact: true }).scrollIntoViewIfNeeded();