HWLAB Cloud
diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs
index 25965015..b4f8379a 100644
--- a/web/hwlab-cloud-web/scripts/check.mjs
+++ b/web/hwlab-cloud-web/scripts/check.mjs
@@ -3,7 +3,10 @@ import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { loadMvpGateSummary } from "../../../internal/mvp-gate/summary.mjs";
-import { runDevCloudWorkbenchStaticSmoke } from "../../../scripts/src/dev-cloud-workbench-smoke-lib.mjs";
+import {
+ runDevCloudWorkbenchMobileSmoke,
+ runDevCloudWorkbenchStaticSmoke
+} from "../../../scripts/src/dev-cloud-workbench-smoke-lib.mjs";
import { gateSummary } from "../gate-summary.mjs";
import { runCloudWebM3ReadonlyContract } from "./m3-readonly-contract.mjs";
import { runWorkbenchHardwarePanelContract } from "./workbench-hardware-panel-contract.mjs";
@@ -54,12 +57,23 @@ const requiredTrustedRecordTerms = Object.freeze([
]);
const workbenchSmoke = runDevCloudWorkbenchStaticSmoke();
const helpSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "help-md-contract");
+const mobileLayoutSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "mobile-workbench-layout-contract");
+const mobileWorkbenchSmoke = await runDevCloudWorkbenchMobileSmoke();
assert.equal(workbenchSmoke.status, "pass", JSON.stringify(workbenchSmoke.blockers, null, 2));
assert.equal(workbenchSmoke.evidenceLevel, "SOURCE");
assert.equal(workbenchSmoke.devLive, false);
assert.equal(helpSmokeCheck?.status, "pass", "PR #114 help Markdown route must be present and ready");
+assert.equal(mobileLayoutSmokeCheck?.status, "pass", "mobile 390x844 workbench layout contract must be present");
assert.equal(workbenchSmoke.help.status, "pass", "smoke report must not leave #114 help contract pending");
+assert.notEqual(mobileWorkbenchSmoke.status, "blocked", JSON.stringify(mobileWorkbenchSmoke.blockers, null, 2));
+assert.equal(mobileWorkbenchSmoke.evidenceLevel, "SOURCE");
+assert.equal(mobileWorkbenchSmoke.devLive, false);
+if (mobileWorkbenchSmoke.status === "pass") {
+ assert.equal(mobileWorkbenchSmoke.viewport.width, 390);
+ assert.equal(mobileWorkbenchSmoke.viewport.height, 844);
+ assert.equal(mobileWorkbenchSmoke.checks.every((check) => check.status === "pass"), true);
+}
assert.match(html, /HWLAB 云工作台/);
for (const chineseWorkbenchText of [
@@ -150,6 +164,7 @@ assert.match(html, /src="\/app\.mjs"/);
for (const workbenchElement of [
"activity-rail",
"explorer",
+ "resource-explorer",
"resource-tree",
"conversation-list",
"agent-chat-status",
@@ -165,6 +180,8 @@ for (const workbenchElement of [
]) {
assert.match(html, new RegExp(workbenchElement));
}
+assert.match(html, /aria-controls="resource-explorer"/);
+assert.match(html, /aria-expanded="true"/);
for (const viewId of ["workspace", "gate"]) {
assert.match(html, new RegExp(`data-view="${viewId}"`));
}
@@ -286,6 +303,17 @@ assert.match(styles, /body\s*>\s*\[data-app-shell\]\s*{[^}]*min-height:\s*0;/s);
assert.match(styles, /\.workbench-shell\s*{[^}]*height:\s*100(?:d)?vh;[^}]*overflow:\s*hidden;/s);
assert.match(styles, /\.view\s*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s);
assert.match(styles, /\.(?:resource-tree|compact-list|conversation-list|trace-list|task-list|hardware-list)[^{]*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s);
+assert.match(app, /function syncMobileExplorer/);
+assert.match(app, /window\.matchMedia\("\(max-width: 860px\)"\)/);
+assert.match(app, /setExplorerCollapsed\(true\)/);
+assert.match(app, /function collapseExplorerAfterMobileAction/);
+assert.match(functionBody(app, "setExplorerCollapsed"), /aria-expanded/);
+assert.match(styles, /\.explorer-collapsed \.explorer\s*{[^}]*pointer-events:\s*none;/s);
+assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.activity-rail\s*{[\s\S]*?grid-row:\s*1 \/ 3;/);
+assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.explorer\s*{[\s\S]*?grid-row:\s*1 \/ 3;[\s\S]*?z-index:\s*4;/);
+assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.center-workspace\s*{[\s\S]*?grid-row:\s*1;/);
+assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.right-sidebar\s*{[\s\S]*?grid-row:\s*2;/);
+assert.match(styles, /@media \(max-width: 520px\)[\s\S]*?\.command-bar\s*{[\s\S]*?grid-template-columns:\s*minmax\(0, 1fr\) auto auto;/);
assert.match(app, /fetchJson\("\/v1"\)/);
assert.match(app, /fetchJson\("\/health\/live"\)/);
assert.match(app, /callRpc\("system\.health"\)/);
diff --git a/web/hwlab-cloud-web/styles.css b/web/hwlab-cloud-web/styles.css
index 587a4059..153f6e5d 100644
--- a/web/hwlab-cloud-web/styles.css
+++ b/web/hwlab-cloud-web/styles.css
@@ -147,6 +147,10 @@ ul {
display: none;
}
+.explorer-collapsed .explorer {
+ pointer-events: none;
+}
+
.explorer-head,
.topbar,
.hardware-status,
@@ -1129,34 +1133,89 @@ tbody tr:last-child td {
@media (max-width: 860px) {
.workbench-shell,
.explorer-collapsed {
- grid-template-columns: 76px minmax(0, 1fr);
- grid-template-rows: clamp(150px, 26dvh, 240px) minmax(0, 1fr) clamp(180px, 32dvh, 280px);
+ grid-template-columns: 68px minmax(0, 1fr);
+ grid-template-rows: minmax(0, 1fr) clamp(214px, 30dvh, 280px);
+ }
+
+ .activity-rail {
+ grid-column: 1;
+ grid-row: 1 / 3;
+ grid-template-rows: repeat(3, minmax(42px, auto)) minmax(12px, 1fr) minmax(42px, auto);
+ overflow: hidden;
+ }
+
+ .rail-button {
+ min-height: 42px;
}
.explorer {
grid-column: 2;
+ grid-row: 1 / 3;
+ z-index: 4;
+ max-height: 100%;
overflow: auto;
+ box-shadow: -1px 0 0 var(--line), 12px 0 24px rgba(0, 0, 0, 0.24);
}
.center-workspace,
.right-sidebar {
- grid-column: 1 / 3;
+ grid-column: 2;
}
.center-workspace {
- grid-row: 2;
+ grid-row: 1;
}
.right-sidebar {
- grid-row: 3;
+ grid-row: 2;
}
.explorer-collapsed .explorer {
display: none;
}
+ .topbar {
+ min-height: 0;
+ gap: 8px;
+ padding: 10px;
+ }
+
+ .topbar-main h2 {
+ font-size: 16px;
+ }
+
+ .probe-card {
+ padding: 8px 10px;
+ }
+
+ .workbench-view,
+ .gate-view,
+ .help-view,
+ .right-sidebar {
+ padding: 8px;
+ }
+
+ .conversation-column {
+ grid-template-rows: minmax(210px, 1fr) minmax(150px, 0.45fr);
+ gap: 8px;
+ }
+
+ .command-bar {
+ grid-template-columns: minmax(0, 1fr) auto auto;
+ align-items: stretch;
+ padding: 8px;
+ }
+
+ .input-shell {
+ padding: 0 8px;
+ }
+
+ .command-button {
+ min-width: 48px;
+ padding: 0 9px;
+ }
+
.topbar,
- .command-bar,
.gate-split {
grid-template-columns: 1fr;
}
@@ -1173,3 +1232,45 @@ tbody tr:last-child td {
grid-template-columns: 1fr;
}
}
+
+@media (max-width: 520px) {
+ .workbench-shell,
+ .explorer-collapsed {
+ grid-template-columns: 64px minmax(0, 1fr);
+ grid-template-rows: minmax(0, 1fr) clamp(200px, 28dvh, 250px);
+ }
+
+ .activity-rail {
+ padding: 7px;
+ }
+
+ .rail-button {
+ padding: 6px 3px;
+ font-size: 11px;
+ }
+
+ .topbar {
+ display: block;
+ }
+
+ .topbar .probe-card {
+ margin-top: 8px;
+ }
+
+ .command-bar {
+ grid-template-columns: minmax(0, 1fr) auto auto;
+ }
+
+ .prompt-mark {
+ display: none;
+ }
+
+ .input-shell {
+ grid-template-columns: minmax(0, 1fr);
+ }
+
+ .command-button {
+ min-width: 45px;
+ padding: 0 8px;
+ }
+}