diff --git a/scripts/dev-cloud-workbench-smoke.test.mjs b/scripts/dev-cloud-workbench-smoke.test.mjs index ff27e6a7..035092db 100644 --- a/scripts/dev-cloud-workbench-smoke.test.mjs +++ b/scripts/dev-cloud-workbench-smoke.test.mjs @@ -727,6 +727,7 @@ test("layout smoke verifies desktop and mobile default workbench geometry withou "layout-narrow-desktop-default", "layout-mobile-default", "layout-feedback-352-resource-explorer-removed", + "layout-left-sidebar-collapse", "layout-gate-desktop", "layout-gate-narrow-desktop", "layout-gate-mobile" @@ -791,9 +792,19 @@ test("layout smoke verifies desktop and mobile default workbench geometry withou assert.equal(mobileDefault.wiring.metadataInDetails, true); assert.equal(mobileDefault.noHorizontalOverflow.right, true); + const leftCollapse = report.checks.find((check) => check.id === "layout-left-sidebar-collapse")?.observations; + assert.equal(leftCollapse.status, "pass"); + assert.equal(leftCollapse.coverage.desktop.widthReclaimed, true); + assert.equal(leftCollapse.coverage["narrow-desktop"].widthReclaimed, true); + assert.equal(leftCollapse.coverage.mobile.widthReclaimed, true); + assert.equal(leftCollapse.coverage.desktop.collapsed.keyTargetsReachable, true); + assert.equal(leftCollapse.coverage.mobile.collapsed.keyTargetsReachable, true); + assert.equal(leftCollapse.coverage.desktop.restoredCollapsed.collapsed, true); + assert.equal(leftCollapse.coverage.mobile.expanded.routeButtonsVisible, true); + const compact = compactLayoutSmokeCliOutput(report); assert.equal(compact.status, "pass"); - assert.equal(compact.artifacts.screenshotCount, 12); + assert.equal(compact.artifacts.screenshotCount, 21); assert.equal(compact.failures.length, 0); assert.equal(compact.blockers.length, 0); assert.equal(JSON.stringify(compact).includes('"checks"'), false); diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index a1a2ab7a..1369cb2f 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -268,6 +268,7 @@ const workbenchMarkers = Object.freeze([ "data-app-shell", "workbench-shell", "activity-rail", + "left-sidebar-toggle", "center-workspace", "conversation-list", "agent-chat-status", @@ -1130,6 +1131,12 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) { summary: "#352 default workbench has no aside#resource-explorer, #explorer-resize, resource tree/capability copy, or reclaimed left panel across desktop and mobile.", observations: summarizeResourceExplorerRemovalCoverage(viewportResults) }, + { + id: "layout-left-sidebar-collapse", + status: Object.values(viewportResults).every((result) => result.leftCollapse?.pass) ? "pass" : "blocked", + summary: "#278 left activity rail collapses and expands on desktop and mobile, persists in localStorage, and gives Code Agent plus M3 panels the reclaimed space without restoring the removed resource explorer.", + observations: summarizeLeftSidebarCollapseCoverage(viewportResults) + }, { id: "layout-right-sidebar-resize-desktop", status: desktop.rightResize.pass ? "pass" : "blocked", @@ -1265,6 +1272,7 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) { screenshotDir: artifactRoot, screenshots: Object.values(viewportResults).flatMap((result) => [ ...(result.artifacts?.screenshots ?? []), + ...(result.leftCollapse?.artifacts?.screenshots ?? []), ...(result.gate?.currentRoute?.artifacts?.screenshots ?? []) ]), reportPath: args.reportPath ?? null @@ -1429,7 +1437,7 @@ function layoutSourceContract() { "docs/reference/cloud-workbench.md", "docs/reference/code-agent-chat-readiness.md" ], - summary: "Cloud Workbench layout smoke protects the #99 workbench route, #227 visible M3 control area, #352 resource-explorer removal, Code Agent input, right hardware/trusted-record containers, /gate current route, and outer-scroll lock without claiming hardware acceptance." + summary: "Cloud Workbench layout smoke protects the #99 workbench route, #227 visible M3 control area, #278 left navigation collapse/expand, #352 resource-explorer removal, Code Agent input, right hardware/trusted-record containers, /gate current route, and outer-scroll lock without claiming hardware acceptance." }; } @@ -2276,14 +2284,30 @@ function hasResourceExplorerRemovalContract({ html, app, styles }) { return ( removedSelectorsAbsent && removedTextAbsent && + /id=["']activity-rail["']/u.test(html) && + /id=["']left-sidebar-toggle["']/u.test(html) && + /aria-label=["']折叠左侧导航["']/u.test(html) && + /title=["']折叠左侧导航["']/u.test(html) && + /aria-controls=["']activity-rail["']/u.test(html) && + /aria-expanded=["']true["']/u.test(html) && /LAYOUT_STORAGE_KEY\s*=\s*["']hwlab\.workbench\.layout\.v1["']/u.test(app) && + /leftSidebarCollapsed:\s*payload\.leftSidebarCollapsed === true/u.test(app) && + /leftSidebarCollapsed:\s*state\.layout\.leftSidebarCollapsed/u.test(app) && + /function\s+initLeftSidebarToggle\s*\(/u.test(app) && + /function\s+setLeftSidebarCollapsed\s*\(/u.test(app) && + /is-left-sidebar-collapsed/u.test(app) && + /展开左侧导航/u.test(app) && + /折叠左侧导航/u.test(app) && /setPointerCapture/u.test(app) && /localStorage\?\.setItem\(\s*LAYOUT_STORAGE_KEY/u.test(app) && /event\.key === "Home"/u.test(app) && /event\.key === "End"/u.test(app) && /ArrowLeft/u.test(app) && /ArrowRight/u.test(app) && + /--rail-collapsed-width:\s*44px/u.test(styles) && /\.workbench-shell\s*\{[^}]*grid-template-columns:\s*var\(--rail-width\)\s+minmax\(420px,\s*1fr\)\s+var\(--right-width-expanded\);/su.test(styles) && + /\.workbench-shell\.is-left-sidebar-collapsed\s*\{[^}]*--rail-width:\s*var\(--rail-collapsed-width\);/su.test(styles) && + /\.workbench-shell\.is-left-sidebar-collapsed \.activity-rail \.rail-button:not\(\.sidebar-toggle\),\s*\n\.workbench-shell\.is-left-sidebar-collapsed \.rail-spacer\s*\{[^}]*display:\s*none;/su.test(styles) && /@media\s*\(max-width:\s*1240px\)[\s\S]*?\.workbench-shell\s*\{[\s\S]*?grid-template-columns:\s*var\(--rail-width\)\s+minmax\(0,\s*1fr\);/u.test(styles) && /id=["']hardware-sidebar["']/u.test(html) && /id=["']right-sidebar-resize["']/u.test(html) && @@ -6233,11 +6257,12 @@ async function inspectWorkbenchLayoutViewport(browser, url, viewport, options = compareTo: null }); artifacts.screenshots.push(...await captureLayoutScreenshots(page, options.artifactRoot, viewport, defaultLayout.mode)); + const leftCollapse = await inspectLeftSidebarCollapse(page, { viewport, artifactRoot: options.artifactRoot }); const rightResize = viewport.width > 1240 ? await inspectRightSidebarResizeDesktop(page, { viewport }) : await inspectRightSidebarResizeDisabled(page, { viewport }); const gate = await inspectWorkbenchGateLayout(browser, url, viewport, options); - return { default: defaultLayout, rightResize, gate, artifacts }; + return { default: defaultLayout, leftCollapse, rightResize, gate, artifacts }; } finally { await page.close(); } @@ -6361,6 +6386,250 @@ async function dragRightSidebarResizeHandle(page, targetWidth) { await page.mouse.up(); } +async function inspectLeftSidebarCollapse(page, { viewport, artifactRoot }) { + const before = await leftSidebarCollapseMetrics(page); + const beforeScreenshot = await captureElementScreenshot( + page, + "[data-app-shell]", + artifactRoot, + viewport, + "left-sidebar-expanded-before" + ); + await page.locator("#left-sidebar-toggle").click(); + await page.waitForTimeout(100); + const collapsed = await leftSidebarCollapseMetrics(page); + const collapsedScreenshot = await captureElementScreenshot( + page, + "[data-app-shell]", + artifactRoot, + viewport, + "left-sidebar-collapsed" + ); + await page.reload({ waitUntil: "domcontentloaded", timeout: 15000 }); + await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 }); + await page.waitForTimeout(150); + const restoredCollapsed = await leftSidebarCollapseMetrics(page); + await page.locator("#left-sidebar-toggle").click(); + await page.waitForTimeout(100); + const expanded = await leftSidebarCollapseMetrics(page); + const expandedScreenshot = await captureElementScreenshot( + page, + "[data-app-shell]", + artifactRoot, + viewport, + "left-sidebar-expanded-after" + ); + const widthReclaimed = collapsed.centerWidth >= before.centerWidth + Math.min(12, Math.max(0, before.railWidth - collapsed.railWidth - 2)); + const pass = + before.toggle.ariaExpanded === "true" && + before.toggle.ariaLabel === "折叠左侧导航" && + before.toggle.title === "折叠左侧导航" && + before.routeButtonsVisible && + collapsed.toggle.ariaExpanded === "false" && + collapsed.toggle.ariaLabel === "展开左侧导航" && + collapsed.toggle.title === "展开左侧导航" && + collapsed.railWidth < before.railWidth - 12 && + widthReclaimed && + collapsed.routeButtonsVisible === false && + collapsed.keyTargetsReachable && + collapsed.resourceExplorerRemoved && + collapsed.rootScrollLocked && + collapsed.noHorizontalOverflow && + restoredCollapsed.collapsed === true && + restoredCollapsed.storage?.leftSidebarCollapsed === true && + restoredCollapsed.keyTargetsReachable && + expanded.toggle.ariaExpanded === "true" && + expanded.toggle.ariaLabel === "折叠左侧导航" && + expanded.railWidth >= before.railWidth - 2 && + Math.abs(expanded.centerWidth - before.centerWidth) <= 3 && + expanded.routeButtonsVisible && + expanded.storage?.leftSidebarCollapsed === false && + expanded.keyTargetsReachable && + expanded.resourceExplorerRemoved && + expanded.rootScrollLocked && + expanded.noHorizontalOverflow; + return { + viewport, + pass, + widthReclaimed, + before, + collapsed, + restoredCollapsed, + expanded, + artifacts: { + screenshots: [beforeScreenshot, collapsedScreenshot, expandedScreenshot].filter(Boolean) + }, + failures: pass ? [] : [ + { + selector: "#left-sidebar-toggle", + failureType: "left-sidebar-collapse-regression", + viewport, + summary: "#278 left sidebar collapse/expand did not reclaim space, persist state, or preserve key workbench controls." + } + ] + }; +} + +async function leftSidebarCollapseMetrics(page) { + return page.evaluate(async () => { + const shell = document.querySelector("[data-app-shell]"); + const rail = document.querySelector("#activity-rail"); + const center = document.querySelector(".center-workspace"); + const right = document.querySelector("#hardware-sidebar"); + const toggle = document.querySelector("#left-sidebar-toggle"); + const routeButtons = [...document.querySelectorAll(".activity-rail [data-route]")]; + const ownsHit = (element, hit) => hit === element || element.contains(hit); + const hitLabel = (hit) => hit ? `${hit.tagName.toLowerCase()}${hit.id ? `#${hit.id}` : ""}${hit.className ? `.${String(hit.className).trim().replace(/\s+/gu, ".")}` : ""}` : "none"; + const nearestScrollableAncestor = (element) => { + let current = element.parentElement; + while (current && current !== document.documentElement) { + const style = getComputedStyle(current); + const scrollable = current.scrollHeight > current.clientHeight + 2 && !["visible", "clip"].includes(style.overflowY); + if (scrollable) return current; + current = current.parentElement; + } + return null; + }; + const scrollIntoNearestContainer = async (element) => { + const container = nearestScrollableAncestor(element); + if (!container) { + element.scrollIntoView({ block: "center", inline: "nearest" }); + await new Promise((resolve) => requestAnimationFrame(resolve)); + return null; + } + const elementBox = element.getBoundingClientRect(); + const containerBox = container.getBoundingClientRect(); + const inset = Math.max(6, Math.min(24, (container.clientHeight - Math.min(elementBox.height, container.clientHeight)) / 2)); + container.scrollTop += elementBox.top - containerBox.top - inset; + await new Promise((resolve) => requestAnimationFrame(resolve)); + return container; + }; + const clipBoxForElement = (element) => { + let visible = { + left: 0, + top: 0, + right: window.innerWidth, + bottom: window.innerHeight + }; + let current = element.parentElement; + while (current && current !== document.documentElement) { + const style = getComputedStyle(current); + if (!["visible", "clip"].includes(style.overflow) || !["visible", "clip"].includes(style.overflowY) || !["visible", "clip"].includes(style.overflowX)) { + const box = current.getBoundingClientRect(); + visible = { + left: Math.max(visible.left, box.left), + top: Math.max(visible.top, box.top), + right: Math.min(visible.right, box.right), + bottom: Math.min(visible.bottom, box.bottom) + }; + } + current = current.parentElement; + } + return visible; + }; + const inspectTarget = async (selector) => { + const element = document.querySelector(selector); + if (!element) return { selector, ok: false, missing: true }; + const scrollContainer = await scrollIntoNearestContainer(element); + const box = element.getBoundingClientRect(); + const clipBox = clipBoxForElement(element); + const visibleBox = { + left: Math.max(box.left, clipBox.left), + top: Math.max(box.top, clipBox.top), + right: Math.min(box.right, clipBox.right), + bottom: Math.min(box.bottom, clipBox.bottom) + }; + const visibleWidth = Math.max(0, visibleBox.right - visibleBox.left); + const visibleHeight = Math.max(0, visibleBox.bottom - visibleBox.top); + const x = visibleBox.left + visibleWidth / 2; + const y = visibleBox.top + visibleHeight / 2; + const stack = document.elementsFromPoint(x, y); + const style = getComputedStyle(element); + return { + selector, + ok: + style.display !== "none" && + style.visibility !== "hidden" && + box.width > 0 && + box.height > 0 && + visibleWidth > 0 && + visibleHeight > 0 && + x >= 0 && + x <= window.innerWidth && + y >= 0 && + y <= window.innerHeight && + stack.some((candidate) => ownsHit(element, candidate)), + hit: hitLabel(stack[0] ?? null), + visibleBox, + scrollContainer: scrollContainer ? scrollContainer.id ? `#${scrollContainer.id}` : scrollContainer.className ? `.${String(scrollContainer.className).trim().replace(/\s+/gu, ".")}` : scrollContainer.tagName.toLowerCase() : null + }; + }; + const keyTargets = []; + for (const selector of [ + "#left-sidebar-toggle", + "#command-input", + "#command-send", + "#m3-write-do", + "#m3-read-di", + "#tab-control", + "#tab-wiring", + "#tab-records", + "[data-hardware-tab='overview']", + "[data-hardware-tab='gateways']", + "[data-hardware-tab='box1']", + "[data-hardware-tab='box2']", + "[data-hardware-tab='patch']" + ]) { + keyTargets.push(await inspectTarget(selector)); + } + let storage = null; + try { + storage = JSON.parse(localStorage.getItem("hwlab.workbench.layout.v1") ?? "null"); + } catch { + storage = "parse_failed"; + } + const railBox = rail?.getBoundingClientRect(); + const centerBox = center?.getBoundingClientRect(); + const rightBox = right?.getBoundingClientRect(); + return { + viewport: { width: window.innerWidth, height: window.innerHeight }, + collapsed: shell?.classList.contains("is-left-sidebar-collapsed") === true, + storage, + shellColumns: shell ? getComputedStyle(shell).gridTemplateColumns : "", + railWidth: railBox?.width ?? 0, + centerWidth: centerBox?.width ?? 0, + rightWidth: rightBox?.width ?? 0, + routeButtonsVisible: routeButtons.some((button) => getComputedStyle(button).display !== "none" && button.getBoundingClientRect().width > 0 && button.getBoundingClientRect().height > 0), + toggle: { + text: toggle?.textContent?.trim() ?? "", + ariaExpanded: toggle?.getAttribute("aria-expanded") ?? "", + ariaLabel: toggle?.getAttribute("aria-label") ?? "", + title: toggle?.getAttribute("title") ?? "", + ariaControls: toggle?.getAttribute("aria-controls") ?? "" + }, + keyTargets, + keyTargetsReachable: keyTargets.every((target) => target.ok), + resourceExplorerRemoved: + document.querySelector("#resource-explorer") === null && + document.querySelector("#explorer-resize") === null && + document.querySelector("#resource-tree") === null && + !document.body.textContent.includes("资源浏览与硬件资源树") && + !document.body.textContent.includes("只读工作区") && + !document.body.textContent.includes("常用能力"), + noHorizontalOverflow: + document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2 && + document.body.scrollWidth <= document.body.clientWidth + 2 && + (!shell || shell.scrollWidth <= shell.clientWidth + 2) && + (!right || right.scrollWidth <= right.clientWidth + 2), + rootScrollLocked: + getComputedStyle(document.documentElement).overflow === "hidden" && + getComputedStyle(document.body).overflow === "hidden" && + document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 && + document.body.scrollHeight <= document.body.clientHeight + 2 + }; + }); +} + async function rightSidebarResizeMetrics(page) { return page.evaluate(async () => { const shell = document.querySelector("[data-app-shell]"); @@ -7482,6 +7751,76 @@ function summarizeResourceExplorerRemovalCoverage(viewportResults) { }; } +function summarizeLeftSidebarCollapseCoverage(viewportResults) { + const coverageEntries = Object.entries(viewportResults).map(([id, result]) => { + const observation = result.leftCollapse; + return [ + id, + { + viewport: observation?.viewport ?? null, + covered: observation?.pass === true, + widthReclaimed: observation?.widthReclaimed === true, + before: observation?.before + ? { + railWidth: observation.before.railWidth, + centerWidth: observation.before.centerWidth, + toggle: observation.before.toggle, + routeButtonsVisible: observation.before.routeButtonsVisible + } + : null, + collapsed: observation?.collapsed + ? { + railWidth: observation.collapsed.railWidth, + centerWidth: observation.collapsed.centerWidth, + toggle: observation.collapsed.toggle, + routeButtonsVisible: observation.collapsed.routeButtonsVisible, + keyTargetsReachable: observation.collapsed.keyTargetsReachable, + resourceExplorerRemoved: observation.collapsed.resourceExplorerRemoved, + rootScrollLocked: observation.collapsed.rootScrollLocked, + noHorizontalOverflow: observation.collapsed.noHorizontalOverflow + } + : null, + restoredCollapsed: observation?.restoredCollapsed + ? { + collapsed: observation.restoredCollapsed.collapsed, + storage: observation.restoredCollapsed.storage, + keyTargetsReachable: observation.restoredCollapsed.keyTargetsReachable + } + : null, + expanded: observation?.expanded + ? { + railWidth: observation.expanded.railWidth, + centerWidth: observation.expanded.centerWidth, + toggle: observation.expanded.toggle, + routeButtonsVisible: observation.expanded.routeButtonsVisible, + storage: observation.expanded.storage, + keyTargetsReachable: observation.expanded.keyTargetsReachable, + resourceExplorerRemoved: observation.expanded.resourceExplorerRemoved, + rootScrollLocked: observation.expanded.rootScrollLocked, + noHorizontalOverflow: observation.expanded.noHorizontalOverflow + } + : null + } + ]; + }); + const coverage = Object.fromEntries(coverageEntries); + const failures = coverageEntries + .filter(([, item]) => !item.covered) + .map(([id, item]) => ({ + checkId: "layout-left-sidebar-collapse", + viewport: item.viewport, + selector: "#left-sidebar-toggle", + failureType: "left-sidebar-collapse-regression", + summary: `#278 left activity rail collapse guard failed in ${id} viewport.`, + observation: item + })); + return { + status: failures.length === 0 ? "pass" : "blocked", + coverage, + failures + }; +} + function summarizeCoverageGap(viewportResults, issueKey) { const fieldName = { issue288: "issue288SingleTableReady", diff --git a/scripts/validate-dev-gate-report.mjs b/scripts/validate-dev-gate-report.mjs index f30ed292..bade1f85 100644 --- a/scripts/validate-dev-gate-report.mjs +++ b/scripts/validate-dev-gate-report.mjs @@ -2225,14 +2225,14 @@ async function validateDevCloudWorkbenchLayoutReport(report, label) { if (report.status !== "skip") { for (const requiredCheck of [ - "layout-desktop-expanded", - "layout-desktop-collapsed", - "layout-desktop-restored", - "layout-narrow-desktop-expanded", - "layout-narrow-desktop-collapsed", - "layout-narrow-desktop-restored", - "layout-mobile-collapsed", - "layout-mobile-drawer", + "layout-desktop-default", + "layout-narrow-desktop-default", + "layout-mobile-default", + "layout-feedback-352-resource-explorer-removed", + "layout-left-sidebar-collapse", + "layout-right-sidebar-resize-desktop", + "layout-right-sidebar-resize-narrow-desktop", + "layout-right-sidebar-resize-mobile", "layout-gate-desktop", "layout-gate-narrow-desktop", "layout-gate-mobile" diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index 4f37965d..d83395c5 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -95,6 +95,7 @@ const el = { loginShell: byId("login-shell"), shell: query("[data-app-shell]"), logoutButton: byId("logout-button"), + leftSidebarToggle: byId("left-sidebar-toggle"), rightSidebarResize: byId("right-sidebar-resize"), routePath: byId("route-path"), liveStatus: byId("live-status"), @@ -164,6 +165,7 @@ const state = { pending: false }, layout: { + leftSidebarCollapsed: false, rightSidebarWidth: RIGHT_SIDEBAR_DEFAULT_WIDTH, rightDrag: null } @@ -171,6 +173,7 @@ const state = { initRoutes(); initLayoutSizing(); +initLeftSidebarToggle(); initRightSidebarResize(); initSideTabs(); initHardwareTabs(); @@ -221,6 +224,7 @@ function initRoutes() { function initLayoutSizing() { const persisted = readPersistedLayout(); const rightSidebarWidth = clampRightSidebarWidth(persisted?.rightSidebarWidth ?? RIGHT_SIDEBAR_DEFAULT_WIDTH); + setLeftSidebarCollapsed(persisted?.leftSidebarCollapsed === true, { persist: false }); setRightSidebarWidth(rightSidebarWidth, { persist: false }); } @@ -231,6 +235,7 @@ function readPersistedLayout() { const payload = JSON.parse(raw); if (payload?.version !== 1) return null; return { + leftSidebarCollapsed: payload.leftSidebarCollapsed === true, rightSidebarWidth: Number(payload.rightSidebarWidth) }; } catch { @@ -244,6 +249,7 @@ function persistLayout() { LAYOUT_STORAGE_KEY, JSON.stringify({ version: 1, + leftSidebarCollapsed: state.layout.leftSidebarCollapsed, rightSidebarWidth: state.layout.rightSidebarWidth }) ); @@ -252,6 +258,25 @@ function persistLayout() { } } +function initLeftSidebarToggle() { + el.leftSidebarToggle.addEventListener("click", () => { + setLeftSidebarCollapsed(!state.layout.leftSidebarCollapsed); + }); +} + +function setLeftSidebarCollapsed(collapsed, options = {}) { + state.layout.leftSidebarCollapsed = collapsed === true; + el.shell.classList.toggle("is-left-sidebar-collapsed", state.layout.leftSidebarCollapsed); + el.leftSidebarToggle.setAttribute("aria-expanded", state.layout.leftSidebarCollapsed ? "false" : "true"); + el.leftSidebarToggle.setAttribute( + "aria-label", + state.layout.leftSidebarCollapsed ? "展开左侧导航" : "折叠左侧导航" + ); + el.leftSidebarToggle.title = state.layout.leftSidebarCollapsed ? "展开左侧导航" : "折叠左侧导航"; + el.leftSidebarToggle.textContent = state.layout.leftSidebarCollapsed ? "展开" : "收起"; + if (options.persist !== false) persistLayout(); +} + function setRightSidebarWidth(width, options = {}) { const clamped = clampRightSidebarWidth(width); state.layout.rightSidebarWidth = clamped; diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html index 577318e4..8bb62102 100644 --- a/web/hwlab-cloud-web/index.html +++ b/web/hwlab-cloud-web/index.html @@ -31,11 +31,20 @@
-