feat: tighten mobile-first workbench chrome

This commit is contained in:
kris
2026-03-30 00:59:07 +08:00
parent 0466f5b672
commit 25a050453e
3 changed files with 47 additions and 5 deletions

View File

@@ -778,6 +778,7 @@ function ensureAuthUi() {
</div> </div>
<div class="helper-text" data-role="auth-message"></div> <div class="helper-text" data-role="auth-message"></div>
<div class="auth-actions"> <div class="auth-actions">
<button class="btn btn-secondary" type="button" data-action="logout-session">退出工作区</button>
<button class="btn btn-secondary" type="button" data-action="auth-refresh">重新加载</button> <button class="btn btn-secondary" type="button" data-action="auth-refresh">重新加载</button>
<button class="btn btn-primary" type="submit" data-action="submit-auth">自动连接</button> <button class="btn btn-primary" type="submit" data-action="submit-auth">自动连接</button>
</div> </div>
@@ -792,11 +793,13 @@ function renderAuthUi() {
ensureAuthUi(); ensureAuthUi();
const session = appState.session; const session = appState.session;
const openButton = document.querySelector('[data-action="open-auth"]'); const openButton = document.querySelector('[data-action="open-auth"]');
const logoutButton = document.querySelector('[data-action="logout-session"]'); const logoutButtons = document.querySelectorAll('[data-action="logout-session"]');
const status = document.querySelector(".auth-status"); const status = document.querySelector(".auth-status");
const message = document.querySelector('[data-role="auth-message"]'); const message = document.querySelector('[data-role="auth-message"]');
if (openButton) openButton.textContent = session ? "连接状态" : "自动连接"; if (openButton) openButton.textContent = session ? "连接状态" : "自动连接";
if (logoutButton) logoutButton.hidden = !session; logoutButtons.forEach((button) => {
button.hidden = !session;
});
if (status) { if (status) {
status.textContent = appState.busy status.textContent = appState.busy
? appState.message || "正在加载..." ? appState.message || "正在加载..."
@@ -5348,7 +5351,7 @@ function renderDiscoveryScreen() {
]; ];
const activeTab = getActiveDetailTab("discoveryDetailTab", detailTabs); const activeTab = getActiveDetailTab("discoveryDetailTab", detailTabs);
const selectedSummaryHtml = ` const selectedSummaryHtml = `
<div class="hero-card" style="padding:18px;"> <div class="hero-card discovery-selected-hero" style="padding:18px;">
<div class="entity-cell"> <div class="entity-cell">
<div class="avatar-lg">${escapeHtml(initials(getAccountName(selected) || "SF"))}</div> <div class="avatar-lg">${escapeHtml(initials(getAccountName(selected) || "SF"))}</div>
<div> <div>
@@ -6035,7 +6038,7 @@ function renderProductionScreen() {
${renderMainAgentLandingNotice("production")} ${renderMainAgentLandingNotice("production")}
<div class="panel pad"> <div class="panel pad">
<div class="panel-head"><div><h3>生产队列</h3><div class="panel-subtitle"></div></div></div> <div class="panel-head"><div><h3>生产队列</h3><div class="panel-subtitle"></div></div></div>
<div class="layout-grid grid-4" style="margin-top:16px;"> <div class="layout-grid grid-4 production-queue-grid" style="margin-top:16px;">
<div class="queue-card"><h4>分析任务</h4><p> ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "analysis").length))} </p></div> <div class="queue-card"><h4>分析任务</h4><p> ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "analysis").length))} </p></div>
<div class="queue-card"><h4>实拍剪辑</h4><p> ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "real_cut").length))} </p></div> <div class="queue-card"><h4>实拍剪辑</h4><p> ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "real_cut").length))} </p></div>
<div class="queue-card"><h4>AI 视频</h4><p> ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "ai_video").length))} </p></div> <div class="queue-card"><h4>AI 视频</h4><p> ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "ai_video").length))} </p></div>

View File

@@ -1930,6 +1930,7 @@ tbody tr:hover {
} }
.topbar { .topbar {
display: none;
margin-top: 6px; margin-top: 6px;
padding: 14px; padding: 14px;
border-radius: 18px; border-radius: 18px;
@@ -2193,6 +2194,11 @@ tbody tr:hover {
text-align: center; text-align: center;
} }
.discovery-selected-hero .mini-grid,
.production-queue-grid {
display: none;
}
.mobile-flow-focus-card { .mobile-flow-focus-card {
display: grid; display: grid;
gap: 10px; gap: 10px;
@@ -2263,7 +2269,16 @@ tbody tr:hover {
.oneliner-fab { .oneliner-fab {
right: 14px; right: 14px;
bottom: calc(96px + env(safe-area-inset-bottom)); 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 { table {

View File

@@ -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, /data-action="open-mobile-sidebar"/);
assert.match(HTML, /class="mobile-tabbar"/); assert.match(HTML, /class="mobile-tabbar"/);
assert.match(HTML, /class="mobile-sidebar-backdrop"/); 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="dashboard"[\s\S]*mobile-tabbar-item/);
assert.match(HTML, /data-screen-target="intake"[\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/); 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, /\.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, /\.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, /\.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", () => { 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 setMobileSidebarOpen\(next\)/);
assert.match(APP, /function getScreenLabel\(screenId = appState\.screen\)/); assert.match(APP, /function getScreenLabel\(screenId = appState\.screen\)/);
assert.match(APP, /function syncMobileShell\(\)/); assert.match(APP, /function syncMobileShell\(\)/);
assert.match(APP, /const mobileWorkspaceSummary = document\.querySelector\("\.mobile-workspace-summary"\)/);
assert.match(shell, /syncMobileShell\(\);/); assert.match(shell, /syncMobileShell\(\);/);
assert.match(APP, /setMobileSidebarOpen\(false\);[\s\S]*appState\.screen = resolvedId;/); assert.match(APP, /setMobileSidebarOpen\(false\);[\s\S]*appState\.screen = resolvedId;/);
assert.match(clicks, /name === "open-mobile-sidebar"/); 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/); 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", () => { test("detail tab buttons expose the active state for touch navigation", () => {
const detailTabs = extractBetween(APP, "function renderDetailTabs(stateKey, tabs) {", "function renderDiscoveryOverviewSection("); const detailTabs = extractBetween(APP, "function renderDetailTabs(stateKey, tabs) {", "function renderDiscoveryOverviewSection(");
assert.match(detailTabs, /aria-pressed="\$\{tab\.value === active \? "true" : "false"\}"/); 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/); 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", () => { test("projects screen uses an adaptive project grid instead of a fixed three-column squeeze", () => {
const projects = extractBetween(APP, "function renderProjectsScreen()", "function getActiveDetailTab("); const projects = extractBetween(APP, "function renderProjectsScreen()", "function getActiveDetailTab(");
assert.match(projects, /project-status-grid/); assert.match(projects, /project-status-grid/);