diff --git a/web/storyforge-web-v4/assets/styles.css b/web/storyforge-web-v4/assets/styles.css
index 1184075..c3c6edb 100644
--- a/web/storyforge-web-v4/assets/styles.css
+++ b/web/storyforge-web-v4/assets/styles.css
@@ -533,6 +533,14 @@ select {
padding: 22px;
}
+.sheet-handle {
+ width: 48px;
+ height: 5px;
+ border-radius: 999px;
+ background: rgba(133, 155, 189, 0.34);
+ margin: 0 auto 6px;
+}
+
.auth-head {
display: flex;
align-items: start;
@@ -2068,17 +2076,21 @@ tbody tr:hover {
.action-modal {
width: 100%;
max-height: min(90vh, 100%);
- border-radius: 20px;
padding: 18px;
}
.oneliner-panel {
width: 100%;
height: min(88vh, 100%);
- border-radius: 22px;
padding: 18px 18px calc(18px + env(safe-area-inset-bottom));
}
+ .auth-modal,
+ .action-modal,
+ .oneliner-panel {
+ border-radius: 24px 24px 0 0;
+ }
+
.oneliner-head {
flex-direction: column;
align-items: flex-start;
@@ -2100,6 +2112,12 @@ tbody tr:hover {
.auth-actions {
flex-direction: column-reverse;
+ position: sticky;
+ bottom: calc(env(safe-area-inset-bottom) * -1);
+ margin: 8px -18px -18px;
+ padding: 12px 18px calc(18px + env(safe-area-inset-bottom));
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.78) 0%, rgba(255, 255, 255, 0.98) 28%);
+ backdrop-filter: blur(10px);
}
.auth-actions .btn {
@@ -2107,6 +2125,15 @@ tbody tr:hover {
justify-content: center;
}
+ .oneliner-composer {
+ position: sticky;
+ bottom: calc(env(safe-area-inset-bottom) * -1);
+ margin: 0 -18px -18px;
+ padding: 12px 18px calc(18px + env(safe-area-inset-bottom));
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.76) 0%, rgba(255, 255, 255, 0.985) 26%);
+ backdrop-filter: blur(10px);
+ }
+
.screen {
margin-top: 14px;
}
diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs
index c2ced6e..62dc455 100644
--- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs
+++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs
@@ -95,6 +95,14 @@ test("mobile shell removes duplicated desktop topbar and collapses the main agen
assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.oneliner-fab-text\s*\{[\s\S]*display:\s*none/);
});
+test("mobile action sheets and oneliner runtime behave like bottom sheets", () => {
+ assert.match(APP, /class="sheet-handle"/);
+ assert.match(CSS, /\.sheet-handle\s*\{/);
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.auth-modal,\s*[\s\S]*\.action-modal,\s*[\s\S]*\.oneliner-panel\s*\{[\s\S]*border-radius:\s*24px 24px 0 0/);
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.auth-actions\s*\{[\s\S]*position:\s*sticky/);
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.oneliner-composer\s*\{[\s\S]*position:\s*sticky/);
+});
+
test("mobile touch targets raise tappable buttons, tabs, and action tags closer to native sizes", () => {
assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.btn,\s*[\s\S]*\.tab,\s*[\s\S]*\.tag\.clickable-tag\s*\{[\s\S]*min-height:\s*44px/);
assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.btn,\s*[\s\S]*\.tab,\s*[\s\S]*\.tag\.clickable-tag\s*\{[\s\S]*display:\s*inline-flex/);
@@ -218,13 +226,23 @@ test("discovery and production screens expose mobile focus cards with next-step
test("mobile heavy screens mark redundant desktop metric blocks for compact hiding", () => {
const discovery = extractBetween(APP, "function renderDiscoveryScreen()", "function renderTrackingScreen()");
+ const tracking = extractBetween(APP, "function renderTrackingScreen()", "function renderAutomationScreen()");
+ const automation = extractBetween(APP, "function renderAutomationScreen()", "function renderOwnedScreen()");
+ const owned = extractBetween(APP, "function renderOwnedScreen()", "function renderPlaybookScreen()");
+ const projects = extractBetween(APP, "function renderProjectsScreen()", "function getActiveDetailTab(");
const production = extractBetween(APP, "function renderProductionScreen()", "function renderReviewScreen()");
+ const review = extractBetween(APP, "function renderReviewScreen()", "function renderStrategyScreen()");
const playbook = extractBetween(APP, "function renderPlaybookScreen()", "function renderProductionScreen()");
const strategy = extractBetween(APP, "function renderStrategyScreen()", "function renderCreditsScreen()");
const cssMobile = extractBetween(CSS, "@media (max-width: 760px) {", "@media (max-width: 560px) {");
assert.match(discovery, /discovery-selected-hero mobile-secondary-card/);
+ assert.match(tracking, /hero-card mobile-secondary-card/);
+ assert.match(automation, /hero-card mobile-secondary-card/);
+ assert.match(owned, /hero-card mobile-secondary-card/);
+ assert.match(projects, /hero-card mobile-secondary-card/);
assert.match(production, /production-queue-grid/);
+ assert.match(review, /hero-card mobile-secondary-card/);
assert.match(playbook, /hero-card mobile-secondary-card/);
assert.match(strategy, /hero-card mobile-secondary-card/);
assert.match(cssMobile, /\.discovery-selected-hero \.mini-grid/);
@@ -233,6 +251,39 @@ test("mobile heavy screens mark redundant desktop metric blocks for compact hidi
assert.match(cssMobile, /\.mobile-secondary-card/);
});
+test("remaining mobile workbench screens expose focus cards and compact summaries", () => {
+ const projects = extractBetween(APP, "function renderProjectsScreen()", "function getActiveDetailTab(");
+ const tracking = extractBetween(APP, "function renderTrackingScreen()", "function renderAutomationScreen()");
+ const automation = extractBetween(APP, "function renderAutomationScreen()", "function renderOwnedScreen()");
+ const owned = extractBetween(APP, "function renderOwnedScreen()", "function renderPlaybookScreen()");
+ const review = extractBetween(APP, "function renderReviewScreen()", "function renderStrategyScreen()");
+
+ assert.match(projects, /mobile-only mobile-flow-focus-card/);
+ assert.match(projects, /当前项目任务/);
+ assert.match(projects, /mobile-only compact-summary-row/);
+ assert.match(projects, /当前项目/);
+
+ assert.match(tracking, /mobile-only mobile-flow-focus-card/);
+ assert.match(tracking, /当前跟踪任务/);
+ assert.match(tracking, /mobile-only compact-summary-row/);
+ assert.match(tracking, /日报/);
+
+ assert.match(automation, /mobile-only mobile-flow-focus-card/);
+ assert.match(automation, /当前自动流程任务/);
+ assert.match(automation, /mobile-only compact-summary-row/);
+ assert.match(automation, /AI 视频/);
+
+ assert.match(owned, /mobile-only mobile-flow-focus-card/);
+ assert.match(owned, /当前账号任务/);
+ assert.match(owned, /mobile-only compact-summary-row/);
+ assert.match(owned, /当前项目/);
+
+ assert.match(review, /mobile-only mobile-flow-focus-card/);
+ assert.match(review, /当前复盘任务/);
+ assert.match(review, /mobile-only compact-summary-row/);
+ assert.match(review, /已保存/);
+});
+
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/);