feat: add controlled workbench sidebar resize
Merge PR #326: controlled left workbench resize with layout smoke coverage.
This commit is contained in:
@@ -40,6 +40,17 @@ The default workbench follows the #99 VS Code-style structure:
|
||||
Unavailable or future controls must be disabled with blocker context. The
|
||||
frontend must not imply generic direct hardware control.
|
||||
|
||||
The left resource explorer is a controlled resizable column on desktop-class
|
||||
viewports. Its width defaults to `292px`, clamps inside a bounded range
|
||||
(`220px` to `420px` before viewport-specific clamping), and is changed through
|
||||
the `#explorer-resize` separator. The separator has a Chinese accessible name,
|
||||
supports pointer drag plus Arrow/Page/Home/End keyboard adjustment, and stores
|
||||
only the clamped width in versioned `localStorage` key
|
||||
`hwlab.workbench.layout.v1`. Collapsing the resource tree hides the column but
|
||||
keeps the saved width so expanding restores it. At `<=860px`, the resize handle
|
||||
is removed from the focus/hit-target flow and the mobile drawer/collapse flow
|
||||
owns the layout.
|
||||
|
||||
The `panel-wiring` / `hwlab-patch-panel` view must use a two-column long table
|
||||
instead of an eight-column wide table. The first table row/header names the two
|
||||
connected devices, for example `res_boxsimu_1` and `res_boxsimu_2`; following
|
||||
@@ -121,6 +132,12 @@ runner 入口。它使用 Playwright 覆盖 `1366x768`、`1024x768` 和 `390x844
|
||||
点击与 `document.elementsFromPoint()`;不得使用 `force: true` 或纯 DOM
|
||||
dispatch 绕过用户命中。
|
||||
|
||||
Layout smoke also covers #278 resize behavior. Desktop and narrow desktop runs
|
||||
drag `#explorer-resize`, exercise Arrow/Home/End keyboard changes, assert
|
||||
min/max/clamp and versioned storage restore, then collapse and expand the
|
||||
resource tree to prove width state is retained. The mobile run asserts the
|
||||
handle is not displayed or focusable and that no horizontal overflow appears.
|
||||
|
||||
常用命令:
|
||||
|
||||
```sh
|
||||
@@ -171,6 +188,8 @@ blockers/failures:`viewport`、`selector`、`failureType`、summary 和 artifa
|
||||
Playwright layout and overlap guardrail.
|
||||
- [pikasTech/HWLAB#276](https://github.com/pikasTech/HWLAB/issues/276):
|
||||
patch-panel wiring panel two-column long-table feedback.
|
||||
- [pikasTech/HWLAB#278](https://github.com/pikasTech/HWLAB/issues/278):
|
||||
resource explorer collapse and controlled resize feedback.
|
||||
- [code-agent-chat-readiness.md](code-agent-chat-readiness.md): Code Agent
|
||||
chat 同源 readiness 和 provider credential blocker 边界。
|
||||
- [docs/cloud-web-workbench.md](../cloud-web-workbench.md): detailed frontend
|
||||
|
||||
@@ -517,6 +517,9 @@ test("layout smoke verifies desktop and mobile sidebar collapse geometry", async
|
||||
"layout-narrow-desktop-restored",
|
||||
"layout-mobile-collapsed",
|
||||
"layout-mobile-drawer",
|
||||
"layout-explorer-resize-desktop",
|
||||
"layout-explorer-resize-narrow-desktop",
|
||||
"layout-explorer-resize-mobile",
|
||||
"layout-gate-desktop",
|
||||
"layout-gate-narrow-desktop",
|
||||
"layout-gate-mobile"
|
||||
@@ -549,6 +552,7 @@ test("layout smoke verifies desktop and mobile sidebar collapse geometry", async
|
||||
assert.equal(desktopCollapsed.wiring.headers.length, 2);
|
||||
assert.deepEqual(desktopCollapsed.wiring.legacyHeaderHits, []);
|
||||
assert.equal(desktopCollapsed.wiring.horizontalScroll.panelScrollWidth <= desktopCollapsed.wiring.horizontalScroll.panelClientWidth + 2, true);
|
||||
assert.equal(desktopCollapsed.resizeHandle.ok, false);
|
||||
|
||||
const mobileCollapsed = report.checks.find((check) => check.id === "layout-mobile-collapsed")?.observations;
|
||||
assert.equal(mobileCollapsed.explorerCollapsed, true);
|
||||
@@ -556,6 +560,7 @@ test("layout smoke verifies desktop and mobile sidebar collapse geometry", async
|
||||
assert.equal(mobileCollapsed.keyTargetsReachable, true);
|
||||
assert.equal(mobileCollapsed.wiring.longTableOk, true);
|
||||
assert.equal(mobileCollapsed.wiring.noHorizontalScroll, true);
|
||||
assert.equal(mobileCollapsed.resizeHandle.ok, false);
|
||||
|
||||
const desktopExpanded = report.checks.find((check) => check.id === "layout-desktop-expanded")?.observations;
|
||||
assert.equal(Object.hasOwn(desktopExpanded.boxes.shell, "text"), false);
|
||||
@@ -563,6 +568,29 @@ test("layout smoke verifies desktop and mobile sidebar collapse geometry", async
|
||||
assert.equal(desktopExpanded.failures.length, 0);
|
||||
assert.equal(desktopExpanded.semanticOverlapChecks.every((check) => !check.overlaps), true);
|
||||
assert.equal(desktopExpanded.overflowChecks.every((check) => check.ok), true);
|
||||
assert.equal(desktopExpanded.resizeHandle.ok, true);
|
||||
|
||||
const desktopResize = report.checks.find((check) => check.id === "layout-explorer-resize-desktop")?.observations;
|
||||
assert.equal(desktopResize.pass, true);
|
||||
assert.equal(desktopResize.before.handle.ariaLabel, "拖拽调整左侧资源树宽度");
|
||||
assert.equal(desktopResize.before.handle.role, "separator");
|
||||
assert.equal(Math.abs(desktopResize.afterDrag.explorerWidth - desktopResize.before.explorerWidth) >= 24, true);
|
||||
assert.equal(desktopResize.afterEnd.explorerWidth, desktopResize.afterEnd.bounds.max);
|
||||
assert.equal(desktopResize.afterHome.explorerWidth, desktopResize.afterHome.bounds.min);
|
||||
assert.equal(desktopResize.collapsed.explorerCollapsed, true);
|
||||
assert.equal(desktopResize.collapsed.handleTabIndex, -1);
|
||||
assert.equal(desktopResize.restored.storage.version, 1);
|
||||
assert.equal(desktopResize.restoredFromStorage.keyTargetsReachable, true);
|
||||
|
||||
const narrowResize = report.checks.find((check) => check.id === "layout-explorer-resize-narrow-desktop")?.observations;
|
||||
assert.equal(narrowResize.pass, true);
|
||||
assert.equal(narrowResize.afterEnd.explorerWidth <= narrowResize.afterEnd.bounds.max, true);
|
||||
assert.equal(narrowResize.restoredFromStorage.noHorizontalOverflow, true);
|
||||
|
||||
const mobileResize = report.checks.find((check) => check.id === "layout-explorer-resize-mobile")?.observations;
|
||||
assert.equal(mobileResize.pass, true);
|
||||
assert.equal(mobileResize.handleDisplay, "none");
|
||||
assert.equal(mobileResize.handleTabIndex, -1);
|
||||
|
||||
const compact = compactLayoutSmokeCliOutput(report);
|
||||
assert.equal(compact.status, "pass");
|
||||
|
||||
@@ -1019,6 +1019,27 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
|
||||
summary: "Mobile drawer expansion makes resource controls reachable and can collapse back through the same accessible button.",
|
||||
observations: mobile.drawer
|
||||
},
|
||||
{
|
||||
id: "layout-explorer-resize-desktop",
|
||||
status: desktop.resize?.pass ? "pass" : "blocked",
|
||||
viewport: desktop.resize?.viewport ?? desktop.expanded.viewport,
|
||||
summary: "Desktop left resource tree resize supports pointer drag, keyboard bounds, clamping, localStorage v1 restore, and collapse/expand width preservation.",
|
||||
observations: desktop.resize
|
||||
},
|
||||
{
|
||||
id: "layout-explorer-resize-narrow-desktop",
|
||||
status: narrowDesktop.resize?.pass ? "pass" : "blocked",
|
||||
viewport: narrowDesktop.resize?.viewport ?? narrowDesktop.expanded.viewport,
|
||||
summary: "Narrow desktop left resource tree resize clamps against the stacked layout so M3 controls and Code Agent input remain reachable.",
|
||||
observations: narrowDesktop.resize
|
||||
},
|
||||
{
|
||||
id: "layout-explorer-resize-mobile",
|
||||
status: mobile.resize?.pass ? "pass" : "blocked",
|
||||
viewport: mobile.resize?.viewport ?? mobile.collapsed.viewport,
|
||||
summary: "Mobile layout disables the drag handle as a hit target and keeps the drawer/collapse flow reachable without horizontal overflow.",
|
||||
observations: mobile.resize
|
||||
},
|
||||
{
|
||||
id: "layout-gate-desktop",
|
||||
status: desktop.gate.currentRoute.pass ? "pass" : "blocked",
|
||||
@@ -2086,12 +2107,32 @@ function hasSidebarCollapseContract({ html, app, styles }) {
|
||||
/aria-label=["']收起左侧资源树["']/u.test(html) &&
|
||||
/title=["']收起左侧资源树,给工作区更多宽度["']/u.test(html) &&
|
||||
/>收起资源树<\/button>/u.test(html) &&
|
||||
/id=["']explorer-resize["']/u.test(html) &&
|
||||
/role=["']separator["']/u.test(html) &&
|
||||
/aria-label=["']拖拽调整左侧资源树宽度["']/u.test(html) &&
|
||||
/aria-valuemin=["']220["']/u.test(html) &&
|
||||
/aria-valuemax=["']420["']/u.test(html) &&
|
||||
/aria-valuenow=["']292["']/u.test(html) &&
|
||||
/LAYOUT_STORAGE_KEY\s*=\s*["']hwlab\.workbench\.layout\.v1["']/u.test(app) &&
|
||||
/function\s+initExplorerResize\s*\(/u.test(app) &&
|
||||
/function\s+clampExplorerWidth\s*\(/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) &&
|
||||
/setAttribute\("aria-label", collapsed \? "展开左侧资源树" : "收起左侧资源树"\)/u.test(setCollapsedBody) &&
|
||||
/textContent = collapsed \? "展开资源树" : "收起资源树"/u.test(setCollapsedBody) &&
|
||||
/title = collapsed \? "展开左侧资源树,恢复导航和硬件资源" : "收起左侧资源树,给工作区更多宽度"/u.test(setCollapsedBody) &&
|
||||
/--explorer-width:\s*292px/u.test(styles) &&
|
||||
/--explorer-min-width:\s*220px/u.test(styles) &&
|
||||
/--explorer-max-width:\s*420px/u.test(styles) &&
|
||||
/--right-width:\s*clamp\(560px,\s*38vw,\s*740px\)/u.test(styles) &&
|
||||
/--right-width-expanded:\s*clamp\(560px,\s*45vw,\s*740px\)/u.test(styles) &&
|
||||
/\.explorer-resize\s*\{[^}]*cursor:\s*col-resize;/su.test(styles) &&
|
||||
/\.explorer-collapsed\s+\.explorer-resize\s*\{[^}]*display:\s*none;/su.test(styles) &&
|
||||
/@media\s*\(max-width:\s*860px\)[\s\S]*?\.explorer-resize\s*\{[\s\S]*?display:\s*none;/u.test(styles) &&
|
||||
/\.explorer-collapsed\s*\{[^}]*--explorer-width:\s*0px;[^}]*grid-template-columns:\s*var\(--rail-width\)\s+0\s+minmax\(420px,\s*1fr\)\s+var\(--right-width\);/su.test(styles) &&
|
||||
/\.side-panel\s*\{[^}]*overflow:\s*auto;[^}]*overscroll-behavior:\s*contain;/su.test(styles)
|
||||
);
|
||||
@@ -4861,8 +4902,9 @@ async function inspectWorkbenchLayoutViewport(browser, url, viewport, options =
|
||||
keyTargetsReachable: recollapsed.keyTargetsReachable
|
||||
};
|
||||
drawer.pass = drawer.pass && recollapsed.explorerCollapsed === true && recollapsed.keyTargetsReachable;
|
||||
const resize = await inspectExplorerResizeMobile(page, { viewport });
|
||||
const gate = await inspectWorkbenchGateLayout(browser, url, viewport, options);
|
||||
return { collapsed, drawer, gate, artifacts };
|
||||
return { collapsed, drawer, resize, gate, artifacts };
|
||||
}
|
||||
|
||||
const expanded = await inspectLayoutState(page, {
|
||||
@@ -4886,8 +4928,9 @@ async function inspectWorkbenchLayoutViewport(browser, url, viewport, options =
|
||||
viewport,
|
||||
compareTo: expanded
|
||||
});
|
||||
const resize = await inspectExplorerResizeDesktop(page, { viewport });
|
||||
const gate = await inspectWorkbenchGateLayout(browser, url, viewport, options);
|
||||
return { expanded, collapsed, restored, gate, artifacts };
|
||||
return { expanded, collapsed, restored, resize, gate, artifacts };
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
@@ -4913,6 +4956,292 @@ async function inspectWorkbenchGateLayout(browser, url, viewport, options = {})
|
||||
}
|
||||
}
|
||||
|
||||
async function inspectExplorerResizeDesktop(page, { viewport }) {
|
||||
const before = await page.evaluate(() => {
|
||||
const shell = document.querySelector("[data-app-shell]");
|
||||
const explorer = document.querySelector("#resource-explorer");
|
||||
const center = document.querySelector(".center-workspace");
|
||||
const right = document.querySelector(".right-sidebar");
|
||||
const handle = document.querySelector("#explorer-resize");
|
||||
return {
|
||||
explorerWidth: explorer?.getBoundingClientRect().width ?? 0,
|
||||
centerWidth: center?.getBoundingClientRect().width ?? 0,
|
||||
rightWidth: right?.getBoundingClientRect().width ?? 0,
|
||||
cssWidth: Number.parseFloat(getComputedStyle(shell).getPropertyValue("--explorer-width")),
|
||||
storage: localStorage.getItem("hwlab.workbench.layout.v1"),
|
||||
handle: {
|
||||
ariaLabel: handle?.getAttribute("aria-label") ?? "",
|
||||
title: handle?.getAttribute("title") ?? "",
|
||||
role: handle?.getAttribute("role") ?? "",
|
||||
tabIndex: handle?.tabIndex ?? null,
|
||||
valueMin: handle?.getAttribute("aria-valuemin") ?? "",
|
||||
valueMax: handle?.getAttribute("aria-valuemax") ?? "",
|
||||
valueNow: handle?.getAttribute("aria-valuenow") ?? "",
|
||||
valueText: handle?.getAttribute("aria-valuetext") ?? ""
|
||||
}
|
||||
};
|
||||
});
|
||||
const min = Number(before.handle.valueMin);
|
||||
const max = Number(before.handle.valueMax);
|
||||
const growTarget = Math.min(max, before.explorerWidth + 72);
|
||||
const shrinkTarget = Math.max(min, before.explorerWidth - 48);
|
||||
const dragTarget = growTarget - before.explorerWidth >= 32 ? growTarget : shrinkTarget;
|
||||
await dragExplorerResizeHandle(page, dragTarget);
|
||||
const afterDrag = await explorerResizeMetrics(page);
|
||||
await page.locator("#explorer-resize").press("ArrowLeft");
|
||||
const afterArrowLeft = await explorerResizeMetrics(page);
|
||||
await page.locator("#explorer-resize").press("End");
|
||||
const afterEnd = await explorerResizeMetrics(page);
|
||||
await page.locator("#explorer-resize").press("Home");
|
||||
const afterHome = await explorerResizeMetrics(page);
|
||||
await page.locator("#explorer-resize").press("ArrowRight");
|
||||
const afterArrowRight = await explorerResizeMetrics(page);
|
||||
await page.locator("#explorer-toggle").click();
|
||||
await page.waitForFunction(() => document.querySelector("[data-app-shell]")?.classList.contains("explorer-collapsed"), null, { timeout: 8000 });
|
||||
const collapsed = await explorerResizeMetrics(page);
|
||||
await page.locator("#explorer-toggle").click();
|
||||
await page.waitForFunction(() => !document.querySelector("[data-app-shell]")?.classList.contains("explorer-collapsed"), null, { timeout: 8000 });
|
||||
const restored = await explorerResizeMetrics(page);
|
||||
await page.reload({ waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
await page.locator("#explorer-toggle").waitFor({ state: "visible", timeout: 12000 });
|
||||
await page.waitForTimeout(150);
|
||||
const restoredFromStorage = await explorerResizeMetrics(page);
|
||||
const expectedArrow = Math.min(afterHome.bounds.max, afterHome.bounds.min + 16);
|
||||
const pass =
|
||||
before.handle.role === "separator" &&
|
||||
before.handle.ariaLabel === "拖拽调整左侧资源树宽度" &&
|
||||
before.handle.title.includes("左右方向键") &&
|
||||
before.handle.tabIndex === 0 &&
|
||||
Number.isFinite(min) &&
|
||||
Number.isFinite(max) &&
|
||||
max >= min &&
|
||||
Math.abs(afterDrag.explorerWidth - before.explorerWidth) >= 24 &&
|
||||
afterDrag.explorerWidth >= min &&
|
||||
afterDrag.explorerWidth <= max &&
|
||||
afterArrowLeft.explorerWidth <= afterDrag.explorerWidth - 12 &&
|
||||
Math.abs(afterEnd.explorerWidth - afterEnd.bounds.max) <= 2 &&
|
||||
Math.abs(afterHome.explorerWidth - afterHome.bounds.min) <= 2 &&
|
||||
Math.abs(afterArrowRight.explorerWidth - expectedArrow) <= 2 &&
|
||||
collapsed.explorerCollapsed === true &&
|
||||
collapsed.explorerWidth <= 1 &&
|
||||
collapsed.handleTabIndex === -1 &&
|
||||
Math.abs(restored.explorerWidth - afterArrowRight.explorerWidth) <= 2 &&
|
||||
restored.storage?.version === 1 &&
|
||||
restored.storage?.explorerWidth === afterArrowRight.explorerWidth &&
|
||||
Math.abs(restoredFromStorage.explorerWidth - afterArrowRight.explorerWidth) <= 2 &&
|
||||
restoredFromStorage.keyTargetsReachable &&
|
||||
restoredFromStorage.rootScrollLocked &&
|
||||
restoredFromStorage.noHorizontalOverflow;
|
||||
return {
|
||||
viewport,
|
||||
pass,
|
||||
targetWidth: dragTarget,
|
||||
before,
|
||||
afterDrag,
|
||||
afterArrowLeft,
|
||||
afterEnd,
|
||||
afterHome,
|
||||
afterArrowRight,
|
||||
collapsed,
|
||||
restored,
|
||||
restoredFromStorage
|
||||
};
|
||||
}
|
||||
|
||||
async function dragExplorerResizeHandle(page, targetWidth) {
|
||||
const start = await page.evaluate(() => {
|
||||
const explorer = document.querySelector("#resource-explorer");
|
||||
const handle = document.querySelector("#explorer-resize");
|
||||
const explorerBox = explorer.getBoundingClientRect();
|
||||
const handleBox = handle.getBoundingClientRect();
|
||||
return {
|
||||
startX: handleBox.left + handleBox.width / 2,
|
||||
startY: handleBox.top + Math.min(80, handleBox.height / 2),
|
||||
currentWidth: explorerBox.width
|
||||
};
|
||||
});
|
||||
const targetX = start.startX + targetWidth - start.currentWidth;
|
||||
await page.mouse.move(start.startX, start.startY);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(targetX, start.startY, { steps: 8 });
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
async function explorerResizeMetrics(page) {
|
||||
return page.evaluate(async () => {
|
||||
const shell = document.querySelector("[data-app-shell]");
|
||||
const explorer = document.querySelector("#resource-explorer");
|
||||
const center = document.querySelector(".center-workspace");
|
||||
const right = document.querySelector(".right-sidebar");
|
||||
const handle = document.querySelector("#explorer-resize");
|
||||
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 targetSelectors = [
|
||||
"#command-input",
|
||||
"#command-send",
|
||||
"#m3-write-do",
|
||||
"#m3-read-di",
|
||||
"[data-hardware-tab='overview']",
|
||||
"[data-hardware-tab='box1']",
|
||||
"[data-hardware-tab='box2']",
|
||||
"[data-hardware-tab='patch']"
|
||||
];
|
||||
const keyTargets = [];
|
||||
for (const selector of targetSelectors) {
|
||||
const element = document.querySelector(selector);
|
||||
if (!element) {
|
||||
keyTargets.push({ selector, ok: false, missing: true });
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
keyTargets.push({
|
||||
selector,
|
||||
ok:
|
||||
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),
|
||||
center: { x, y },
|
||||
visibleBox,
|
||||
scrollContainer: scrollContainer ? scrollContainer.id ? `#${scrollContainer.id}` : scrollContainer.className ? `.${String(scrollContainer.className).trim().replace(/\s+/gu, ".")}` : scrollContainer.tagName.toLowerCase() : null
|
||||
});
|
||||
}
|
||||
let storage = null;
|
||||
try {
|
||||
storage = JSON.parse(localStorage.getItem("hwlab.workbench.layout.v1") ?? "null");
|
||||
} catch {
|
||||
storage = "parse_failed";
|
||||
}
|
||||
return {
|
||||
explorerCollapsed: shell?.classList.contains("explorer-collapsed") === true,
|
||||
explorerWidth: explorer?.getBoundingClientRect().width ?? 0,
|
||||
centerWidth: center?.getBoundingClientRect().width ?? 0,
|
||||
rightWidth: right?.getBoundingClientRect().width ?? 0,
|
||||
cssWidth: Number.parseFloat(getComputedStyle(shell).getPropertyValue("--explorer-width")),
|
||||
bounds: {
|
||||
min: Number(handle?.getAttribute("aria-valuemin")),
|
||||
max: Number(handle?.getAttribute("aria-valuemax"))
|
||||
},
|
||||
handleTabIndex: handle?.tabIndex ?? null,
|
||||
handleAriaDisabled: handle?.getAttribute("aria-disabled") ?? "",
|
||||
handleAriaValueNow: Number(handle?.getAttribute("aria-valuenow")),
|
||||
handleAriaValueText: handle?.getAttribute("aria-valuetext") ?? "",
|
||||
storage,
|
||||
keyTargets,
|
||||
keyTargetsReachable: keyTargets.every((target) => target.ok),
|
||||
noHorizontalOverflow:
|
||||
document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2 &&
|
||||
document.body.scrollWidth <= document.body.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 inspectExplorerResizeMobile(page, { viewport }) {
|
||||
const observation = await page.evaluate(() => {
|
||||
const handle = document.querySelector("#explorer-resize");
|
||||
const box = handle?.getBoundingClientRect();
|
||||
const style = handle ? getComputedStyle(handle) : null;
|
||||
const shell = document.querySelector("[data-app-shell]");
|
||||
const right = document.querySelector(".right-sidebar");
|
||||
return {
|
||||
viewport: { width: window.innerWidth, height: window.innerHeight },
|
||||
explorerCollapsed: shell?.classList.contains("explorer-collapsed") === true,
|
||||
handlePresent: Boolean(handle),
|
||||
handleDisplay: style?.display ?? "missing",
|
||||
handleWidth: box?.width ?? 0,
|
||||
handleTabIndex: handle?.tabIndex ?? null,
|
||||
handleAriaDisabled: handle?.getAttribute("aria-disabled") ?? "",
|
||||
noHorizontalOverflow:
|
||||
document.documentElement.scrollWidth <= document.documentElement.clientWidth + 2 &&
|
||||
document.body.scrollWidth <= document.body.clientWidth + 2 &&
|
||||
(!right || right.scrollWidth <= right.clientWidth + 2)
|
||||
};
|
||||
});
|
||||
return {
|
||||
viewport,
|
||||
pass:
|
||||
observation.explorerCollapsed === true &&
|
||||
observation.handlePresent &&
|
||||
observation.handleDisplay === "none" &&
|
||||
observation.handleTabIndex === -1 &&
|
||||
observation.handleAriaDisabled === "true" &&
|
||||
observation.noHorizontalOverflow,
|
||||
...observation
|
||||
};
|
||||
}
|
||||
|
||||
async function inspectLayoutState(page, { mode, viewport, compareTo }) {
|
||||
return page.evaluate(async ({ mode, viewport, compareTo }) => {
|
||||
const failures = [];
|
||||
@@ -5173,6 +5502,7 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
|
||||
for (const [selector, label] of selectorLabels) {
|
||||
keyTargets.push(await inspectHitTarget(selector, label));
|
||||
}
|
||||
const resizeHandle = await inspectHitTarget("#explorer-resize", "左侧资源树宽度调整手柄");
|
||||
const drawerTargets = [];
|
||||
for (const [selector, label] of drawerSelectorLabels) {
|
||||
drawerTargets.push(await inspectFirstHit(selector, label));
|
||||
@@ -5554,6 +5884,7 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
|
||||
keyTargetsReachable,
|
||||
keyTargetsRequirement,
|
||||
blockedKeyTargets,
|
||||
resizeHandle,
|
||||
drawerReachable,
|
||||
blockedDrawerTargets,
|
||||
sidePanelScroll,
|
||||
|
||||
@@ -55,6 +55,10 @@ const NON_LIVE_ID_VALUES = Object.freeze(["", "n/a", "none", "null", "undefined"
|
||||
const TRUSTED_CODE_AGENT_PROVIDERS = Object.freeze(["openai-responses", "codex-cli", "codex-readonly-runner"]);
|
||||
const CODEX_RUNNER_CAPABLE_PROVIDERS = Object.freeze(["codex-readonly-runner"]);
|
||||
const UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN = /\b(?:echo|mock|fixture|stub|sample)\b/iu;
|
||||
const LAYOUT_STORAGE_KEY = "hwlab.workbench.layout.v1";
|
||||
const EXPLORER_DEFAULT_WIDTH = 292;
|
||||
const EXPLORER_MIN_WIDTH = 220;
|
||||
const EXPLORER_MAX_WIDTH = 420;
|
||||
|
||||
const viewIds = new Set([...document.querySelectorAll("[data-view]")].map((view) => view.dataset.view));
|
||||
let rpcSequence = 0;
|
||||
@@ -62,6 +66,7 @@ let rpcSequence = 0;
|
||||
const el = {
|
||||
shell: query("[data-app-shell]"),
|
||||
explorerToggle: byId("explorer-toggle"),
|
||||
explorerResize: byId("explorer-resize"),
|
||||
explorerStatus: byId("explorer-status"),
|
||||
treeCount: byId("tree-count"),
|
||||
resourceTree: byId("resource-tree"),
|
||||
@@ -119,12 +124,18 @@ const state = {
|
||||
hardwareTab: "overview",
|
||||
operation: null,
|
||||
pending: false
|
||||
},
|
||||
layout: {
|
||||
explorerWidth: EXPLORER_DEFAULT_WIDTH,
|
||||
drag: null
|
||||
}
|
||||
};
|
||||
|
||||
initRoutes();
|
||||
initLayoutSizing();
|
||||
initExplorerToggle();
|
||||
syncMobileExplorer();
|
||||
initExplorerResize();
|
||||
initSideTabs();
|
||||
initHardwareTabs();
|
||||
initQuickActions();
|
||||
@@ -169,6 +180,93 @@ function initRoutes() {
|
||||
showView(routeFromLocation());
|
||||
}
|
||||
|
||||
function initLayoutSizing() {
|
||||
const persisted = readPersistedLayout();
|
||||
const width = clampExplorerWidth(persisted?.explorerWidth ?? EXPLORER_DEFAULT_WIDTH);
|
||||
setExplorerWidth(width, { persist: false });
|
||||
}
|
||||
|
||||
function readPersistedLayout() {
|
||||
try {
|
||||
const raw = window.localStorage?.getItem(LAYOUT_STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const payload = JSON.parse(raw);
|
||||
if (payload?.version !== 1) return null;
|
||||
return {
|
||||
explorerWidth: Number(payload.explorerWidth)
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function persistLayout() {
|
||||
try {
|
||||
window.localStorage?.setItem(
|
||||
LAYOUT_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
explorerWidth: state.layout.explorerWidth
|
||||
})
|
||||
);
|
||||
} catch {
|
||||
// Browser storage can be unavailable; layout still works for the session.
|
||||
}
|
||||
}
|
||||
|
||||
function setExplorerWidth(width, options = {}) {
|
||||
const clamped = clampExplorerWidth(width);
|
||||
state.layout.explorerWidth = clamped;
|
||||
el.shell.style.setProperty("--explorer-width", `${clamped}px`);
|
||||
syncExplorerResizeA11y();
|
||||
if (options.persist !== false) persistLayout();
|
||||
return clamped;
|
||||
}
|
||||
|
||||
function clampExplorerWidth(value) {
|
||||
const numeric = Number(value);
|
||||
const width = Number.isFinite(numeric) ? numeric : EXPLORER_DEFAULT_WIDTH;
|
||||
const bounds = explorerResizeBounds();
|
||||
return Math.min(Math.max(Math.round(width), bounds.min), bounds.max);
|
||||
}
|
||||
|
||||
function explorerResizeBounds() {
|
||||
const styles = getComputedStyle(el.shell);
|
||||
const cssMin = readCssPixel(styles.getPropertyValue("--explorer-min-width"), EXPLORER_MIN_WIDTH);
|
||||
const cssMax = readCssPixel(styles.getPropertyValue("--explorer-max-width"), EXPLORER_MAX_WIDTH);
|
||||
if (window.matchMedia("(max-width: 860px)").matches) {
|
||||
return { min: EXPLORER_MIN_WIDTH, max: EXPLORER_MAX_WIDTH };
|
||||
}
|
||||
if (window.matchMedia("(max-width: 1240px)").matches) {
|
||||
const railWidth = readCssPixel(styles.getPropertyValue("--rail-width"), 86);
|
||||
const dynamicMax = window.innerWidth - railWidth - 420 - 4;
|
||||
return boundedResizeRange(cssMin, cssMax, dynamicMax);
|
||||
}
|
||||
const railWidth = readCssPixel(styles.getPropertyValue("--rail-width"), 92);
|
||||
const rightWidth = Math.min(Math.max(window.innerWidth * 0.45, 560), 740);
|
||||
const dynamicMax = window.innerWidth - railWidth - 360 - rightWidth - 4;
|
||||
return boundedResizeRange(cssMin, cssMax, dynamicMax);
|
||||
}
|
||||
|
||||
function boundedResizeRange(min, max, dynamicMax) {
|
||||
const safeMin = Math.max(0, Math.round(min));
|
||||
const safeMax = Math.max(safeMin, Math.round(Math.min(max, Number.isFinite(dynamicMax) ? dynamicMax : max)));
|
||||
return { min: safeMin, max: safeMax };
|
||||
}
|
||||
|
||||
function readCssPixel(value, fallback) {
|
||||
const parsed = Number.parseFloat(String(value ?? "").trim());
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
|
||||
function syncExplorerResizeA11y() {
|
||||
const bounds = explorerResizeBounds();
|
||||
el.explorerResize.setAttribute("aria-valuemin", String(bounds.min));
|
||||
el.explorerResize.setAttribute("aria-valuemax", String(bounds.max));
|
||||
el.explorerResize.setAttribute("aria-valuenow", String(state.layout.explorerWidth));
|
||||
el.explorerResize.setAttribute("aria-valuetext", `左侧资源树宽度 ${state.layout.explorerWidth} 像素`);
|
||||
}
|
||||
|
||||
function routeFromLocation() {
|
||||
const hashRoute = window.location.hash.replace(/^#\/?/, "");
|
||||
if (viewIds.has(hashRoute)) return hashRoute;
|
||||
@@ -199,13 +297,75 @@ function initExplorerToggle() {
|
||||
});
|
||||
}
|
||||
|
||||
function initExplorerResize() {
|
||||
window.addEventListener("resize", () => {
|
||||
if (canResizeExplorer()) setExplorerWidth(state.layout.explorerWidth, { persist: false });
|
||||
syncExplorerResizeA11y();
|
||||
});
|
||||
el.explorerResize.addEventListener("pointerdown", (event) => {
|
||||
if (!canResizeExplorer()) return;
|
||||
event.preventDefault();
|
||||
el.explorerResize.setPointerCapture?.(event.pointerId);
|
||||
state.layout.drag = {
|
||||
pointerId: event.pointerId,
|
||||
startX: event.clientX,
|
||||
startWidth: state.layout.explorerWidth
|
||||
};
|
||||
el.shell.classList.add("is-resizing");
|
||||
});
|
||||
el.explorerResize.addEventListener("pointermove", (event) => {
|
||||
const drag = state.layout.drag;
|
||||
if (!drag || drag.pointerId !== event.pointerId) return;
|
||||
setExplorerWidth(drag.startWidth + event.clientX - drag.startX, { persist: false });
|
||||
});
|
||||
el.explorerResize.addEventListener("pointerup", finishExplorerResize);
|
||||
el.explorerResize.addEventListener("pointercancel", finishExplorerResize);
|
||||
el.explorerResize.addEventListener("keydown", (event) => {
|
||||
if (!canResizeExplorer()) return;
|
||||
const keySteps = {
|
||||
ArrowLeft: -16,
|
||||
ArrowRight: 16,
|
||||
PageDown: -48,
|
||||
PageUp: 48
|
||||
};
|
||||
if (Object.hasOwn(keySteps, event.key)) {
|
||||
event.preventDefault();
|
||||
setExplorerWidth(state.layout.explorerWidth + keySteps[event.key]);
|
||||
} else if (event.key === "Home") {
|
||||
event.preventDefault();
|
||||
setExplorerWidth(explorerResizeBounds().min);
|
||||
} else if (event.key === "End") {
|
||||
event.preventDefault();
|
||||
setExplorerWidth(explorerResizeBounds().max);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function finishExplorerResize(event) {
|
||||
const drag = state.layout.drag;
|
||||
if (!drag || drag.pointerId !== event.pointerId) return;
|
||||
state.layout.drag = null;
|
||||
el.shell.classList.remove("is-resizing");
|
||||
persistLayout();
|
||||
}
|
||||
|
||||
function canResizeExplorer() {
|
||||
return !window.matchMedia("(max-width: 860px)").matches && !el.shell.classList.contains("explorer-collapsed");
|
||||
}
|
||||
|
||||
function setExplorerCollapsed(collapsed) {
|
||||
if (!collapsed) {
|
||||
setExplorerWidth(state.layout.explorerWidth, { persist: false });
|
||||
}
|
||||
el.shell.classList.toggle("explorer-collapsed", collapsed);
|
||||
el.explorerToggle.setAttribute("aria-pressed", collapsed ? "true" : "false");
|
||||
el.explorerToggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||||
el.explorerToggle.setAttribute("aria-label", collapsed ? "展开左侧资源树" : "收起左侧资源树");
|
||||
el.explorerToggle.title = collapsed ? "展开左侧资源树,恢复导航和硬件资源" : "收起左侧资源树,给工作区更多宽度";
|
||||
el.explorerToggle.textContent = collapsed ? "展开资源树" : "收起资源树";
|
||||
const resizeDisabled = collapsed || window.matchMedia("(max-width: 860px)").matches;
|
||||
el.explorerResize.tabIndex = resizeDisabled ? -1 : 0;
|
||||
el.explorerResize.setAttribute("aria-disabled", resizeDisabled ? "true" : "false");
|
||||
}
|
||||
|
||||
function syncMobileExplorer() {
|
||||
@@ -215,6 +375,7 @@ function syncMobileExplorer() {
|
||||
setExplorerCollapsed(true);
|
||||
} else {
|
||||
setExplorerCollapsed(false);
|
||||
setExplorerWidth(state.layout.explorerWidth, { persist: false });
|
||||
}
|
||||
};
|
||||
apply();
|
||||
|
||||
@@ -44,6 +44,19 @@
|
||||
</div>
|
||||
<div id="explorer-capabilities" class="compact-list"></div>
|
||||
</section>
|
||||
<div
|
||||
class="resize-handle explorer-resize"
|
||||
id="explorer-resize"
|
||||
role="separator"
|
||||
tabindex="0"
|
||||
aria-label="拖拽调整左侧资源树宽度"
|
||||
aria-controls="resource-explorer"
|
||||
aria-orientation="vertical"
|
||||
aria-valuemin="220"
|
||||
aria-valuemax="420"
|
||||
aria-valuenow="292"
|
||||
title="拖拽调整左侧资源树宽度;用左右方向键微调,Home/End 到最小或最大。"
|
||||
></div>
|
||||
</aside>
|
||||
|
||||
<section class="center-workspace">
|
||||
|
||||
@@ -271,6 +271,7 @@ assert.match(html, /src="\/app\.mjs"/);
|
||||
for (const workbenchElement of [
|
||||
"activity-rail",
|
||||
"explorer",
|
||||
"explorer-resize",
|
||||
"resource-explorer",
|
||||
"resource-tree",
|
||||
"conversation-list",
|
||||
@@ -293,11 +294,31 @@ assert.match(html, /aria-expanded="true"/);
|
||||
assert.match(html, /aria-pressed="false"/);
|
||||
assert.match(html, /aria-label="收起左侧资源树"/);
|
||||
assert.match(html, />收起资源树<\/button>/);
|
||||
assert.match(html, /id="explorer-resize"/);
|
||||
assert.match(html, /role="separator"/);
|
||||
assert.match(html, /aria-label="拖拽调整左侧资源树宽度"/);
|
||||
assert.match(html, /aria-valuemin="220"/);
|
||||
assert.match(html, /aria-valuemax="420"/);
|
||||
assert.match(html, /aria-valuenow="292"/);
|
||||
assert.match(app, /setAttribute\("aria-label", collapsed \? "展开左侧资源树" : "收起左侧资源树"\)/);
|
||||
assert.match(app, /textContent = collapsed \? "展开资源树" : "收起资源树"/);
|
||||
assert.match(app, /LAYOUT_STORAGE_KEY\s*=\s*"hwlab\.workbench\.layout\.v1"/);
|
||||
assert.match(app, /function initExplorerResize/);
|
||||
assert.match(app, /function clampExplorerWidth/);
|
||||
assert.match(app, /setPointerCapture/);
|
||||
assert.match(app, /localStorage\?\.setItem\(\s*LAYOUT_STORAGE_KEY/);
|
||||
assert.match(app, /ArrowLeft/);
|
||||
assert.match(app, /ArrowRight/);
|
||||
assert.match(app, /event\.key === "Home"/);
|
||||
assert.match(app, /event\.key === "End"/);
|
||||
assert.match(styles, /--explorer-width:\s*292px/);
|
||||
assert.match(styles, /--explorer-min-width:\s*220px/);
|
||||
assert.match(styles, /--explorer-max-width:\s*420px/);
|
||||
assert.match(styles, /--right-width:\s*clamp\(560px,\s*38vw,\s*740px\)/);
|
||||
assert.match(styles, /--right-width-expanded:\s*clamp\(560px,\s*45vw,\s*740px\)/);
|
||||
assert.match(styles, /\.explorer-resize\s*{[^}]*cursor:\s*col-resize;/s);
|
||||
assert.match(styles, /\.explorer-collapsed \.explorer-resize\s*{[^}]*display:\s*none;/s);
|
||||
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.explorer-resize\s*{[\s\S]*?display:\s*none;/);
|
||||
assert.match(styles, /\.explorer-collapsed\s*\{[^}]*--explorer-width:\s*0px;[^}]*grid-template-columns:\s*var\(--rail-width\)\s+0\s+minmax\(420px,\s*1fr\)\s+var\(--right-width\);/s);
|
||||
for (const viewId of ["workspace", "gate"]) {
|
||||
assert.match(html, new RegExp(`data-view="${viewId}"`));
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
--info: #7ea6c9;
|
||||
--rail-width: 92px;
|
||||
--explorer-width: 292px;
|
||||
--explorer-min-width: 220px;
|
||||
--explorer-max-width: 420px;
|
||||
--right-width: clamp(560px, 38vw, 740px);
|
||||
--right-width-expanded: clamp(560px, 45vw, 740px);
|
||||
--mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
@@ -140,10 +142,11 @@ ul {
|
||||
}
|
||||
|
||||
.explorer {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
padding: 10px 14px 10px 10px;
|
||||
background: rgba(22, 25, 22, 0.98);
|
||||
border-right: 1px solid var(--line);
|
||||
overflow: hidden;
|
||||
@@ -154,6 +157,52 @@ ul {
|
||||
grid-template-columns: var(--rail-width) 0 minmax(420px, 1fr) var(--right-width);
|
||||
}
|
||||
|
||||
.workbench-shell.is-resizing {
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
background: transparent;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.resize-handle::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: var(--line-strong);
|
||||
opacity: 0.56;
|
||||
}
|
||||
|
||||
.explorer-resize {
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 12px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.explorer-resize::before {
|
||||
top: 10px;
|
||||
bottom: 10px;
|
||||
left: 5px;
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
.resize-handle:hover::before,
|
||||
.resize-handle:focus-visible::before,
|
||||
.workbench-shell.is-resizing .resize-handle::before {
|
||||
background: var(--accent);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.resize-handle:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.explorer-collapsed .explorer {
|
||||
padding: 0;
|
||||
border-right: 0;
|
||||
@@ -168,6 +217,10 @@ ul {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.explorer-collapsed .explorer-resize {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.explorer-head,
|
||||
.topbar,
|
||||
.hardware-status,
|
||||
@@ -1346,6 +1399,8 @@ tbody tr:last-child td {
|
||||
.workbench-shell {
|
||||
--rail-width: 86px;
|
||||
--explorer-width: 260px;
|
||||
--explorer-min-width: 210px;
|
||||
--explorer-max-width: 320px;
|
||||
grid-template-columns: var(--rail-width) var(--explorer-width) minmax(420px, 1fr);
|
||||
grid-template-rows: minmax(0, 1fr) clamp(220px, 36dvh, 360px);
|
||||
}
|
||||
@@ -1368,6 +1423,8 @@ tbody tr:last-child td {
|
||||
.workbench-shell,
|
||||
.explorer-collapsed {
|
||||
--rail-width: 68px;
|
||||
--explorer-min-width: 0px;
|
||||
--explorer-max-width: 0px;
|
||||
grid-template-columns: var(--rail-width) minmax(0, 1fr);
|
||||
grid-template-rows: minmax(0, 1fr) clamp(214px, 30dvh, 280px);
|
||||
}
|
||||
@@ -1389,10 +1446,15 @@ tbody tr:last-child td {
|
||||
grid-row: 1 / 3;
|
||||
z-index: 4;
|
||||
max-height: 100%;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
box-shadow: -1px 0 0 var(--line), 12px 0 24px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.explorer-resize {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.center-workspace,
|
||||
.right-sidebar {
|
||||
grid-column: 2;
|
||||
|
||||
Reference in New Issue
Block a user