分析任务
最近 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "analysis").length))} 条
实拍剪辑
最近 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "real_cut").length))} 条
AI 视频
最近 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "ai_video").length))} 条
diff --git a/web/storyforge-web-v4/assets/styles.css b/web/storyforge-web-v4/assets/styles.css
index 5f65c16..1805d6f 100644
--- a/web/storyforge-web-v4/assets/styles.css
+++ b/web/storyforge-web-v4/assets/styles.css
@@ -1930,6 +1930,7 @@ tbody tr:hover {
}
.topbar {
+ display: none;
margin-top: 6px;
padding: 14px;
border-radius: 18px;
@@ -2193,6 +2194,11 @@ tbody tr:hover {
text-align: center;
}
+ .discovery-selected-hero .mini-grid,
+ .production-queue-grid {
+ display: none;
+ }
+
.mobile-flow-focus-card {
display: grid;
gap: 10px;
@@ -2263,7 +2269,16 @@ tbody tr:hover {
.oneliner-fab {
right: 14px;
bottom: calc(96px + env(safe-area-inset-bottom));
- padding: 11px 12px;
+ width: 52px;
+ height: 52px;
+ padding: 0;
+ gap: 0;
+ justify-content: center;
+ border-radius: 18px;
+ }
+
+ .oneliner-fab-text {
+ display: none;
}
table {
diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs
index 9b8f3dc..bfd70eb 100644
--- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs
+++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs
@@ -30,6 +30,7 @@ test("mobile shell includes a native-like header, drawer toggle, and bottom tab
assert.match(HTML, /data-action="open-mobile-sidebar"/);
assert.match(HTML, /class="mobile-tabbar"/);
assert.match(HTML, /class="mobile-sidebar-backdrop"/);
+ assert.match(HTML, /class="mobile-workspace-summary"/);
assert.match(HTML, /data-screen-target="dashboard"[\s\S]*mobile-tabbar-item/);
assert.match(HTML, /data-screen-target="intake"[\s\S]*mobile-tabbar-item/);
assert.match(HTML, /data-screen-target="discovery"[\s\S]*mobile-tabbar-item/);
@@ -45,6 +46,10 @@ test("mobile shell styling uses safe-area padding, drawer navigation, and fixed
assert.match(CSS, /\.mobile-sidebar-open\s+\.sidebar\s*\{[\s\S]*transform:\s*translateX\(0\)/);
assert.match(CSS, /\.content\s*\{[\s\S]*padding-bottom:\s*calc\(110px \+ env\(safe-area-inset-bottom\)\)/);
assert.match(CSS, /\.oneliner-fab\s*\{[\s\S]*bottom:\s*calc\(96px \+ env\(safe-area-inset-bottom\)\)/);
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.mobile-workspace-summary\s*\{[\s\S]*display:\s*flex/);
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.workspace-switch span\s*\{[\s\S]*display:\s*none/);
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.auth-status\s*\{[\s\S]*display:\s*none/);
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.oneliner-fab-text\s*\{[\s\S]*display:\s*none/);
});
test("mobile shell javascript syncs drawer state and active labels with the current screen", () => {
@@ -54,6 +59,7 @@ test("mobile shell javascript syncs drawer state and active labels with the curr
assert.match(APP, /function setMobileSidebarOpen\(next\)/);
assert.match(APP, /function getScreenLabel\(screenId = appState\.screen\)/);
assert.match(APP, /function syncMobileShell\(\)/);
+ assert.match(APP, /const mobileWorkspaceSummary = document\.querySelector\("\.mobile-workspace-summary"\)/);
assert.match(shell, /syncMobileShell\(\);/);
assert.match(APP, /setMobileSidebarOpen\(false\);[\s\S]*appState\.screen = resolvedId;/);
assert.match(clicks, /name === "open-mobile-sidebar"/);
@@ -69,6 +75,13 @@ test("mobile layout turns screen actions and page tabs into native-like horizont
assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.page-detail-tabs \.tab\s*\{[\s\S]*flex:\s*0 0 auto/);
});
+test("mobile shell removes duplicated desktop topbar and collapses the main agent fab", () => {
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.topbar\s*\{[\s\S]*display:\s*none/);
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.oneliner-fab\s*\{[\s\S]*width:\s*52px/);
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.oneliner-fab\s*\{[\s\S]*justify-content:\s*center/);
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.oneliner-fab-text\s*\{[\s\S]*display:\s*none/);
+});
+
test("detail tab buttons expose the active state for touch navigation", () => {
const detailTabs = extractBetween(APP, "function renderDetailTabs(stateKey, tabs) {", "function renderDiscoveryOverviewSection(");
assert.match(detailTabs, /aria-pressed="\$\{tab\.value === active \? "true" : "false"\}"/);
@@ -150,6 +163,17 @@ test("discovery and production screens expose mobile focus cards with next-step
assert.match(cssMobile, /\.mobile-flow-focus-card/);
});
+test("mobile heavy screens mark redundant desktop metric blocks for compact hiding", () => {
+ const discovery = extractBetween(APP, "function renderDiscoveryScreen()", "function renderTrackingScreen()");
+ const production = extractBetween(APP, "function renderProductionScreen()", "function renderReviewScreen()");
+ const cssMobile = extractBetween(CSS, "@media (max-width: 760px) {", "@media (max-width: 560px) {");
+
+ assert.match(discovery, /discovery-selected-hero/);
+ assert.match(production, /production-queue-grid/);
+ assert.match(cssMobile, /\.discovery-selected-hero \.mini-grid/);
+ assert.match(cssMobile, /\.production-queue-grid/);
+});
+
test("projects screen uses an adaptive project grid instead of a fixed three-column squeeze", () => {
const projects = extractBetween(APP, "function renderProjectsScreen()", "function getActiveDetailTab(");
assert.match(projects, /project-status-grid/);