diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js
index e4653ac..bfdd8d7 100644
--- a/web/storyforge-web-v4/assets/app.js
+++ b/web/storyforge-web-v4/assets/app.js
@@ -4499,8 +4499,12 @@ async function saveCandidateAsBenchmark(candidateIndex, relationType = "benchmar
function screenShell(title, subtitle, actionsHtml, bodyHtml) {
const actionLayout = splitPrimaryAction(actionsHtml);
+ const body = String(bodyHtml || "");
+ const hasMobileFocusCard = body.includes("mobile-flow-focus-card");
+ const headClassName = ["screen-head"];
+ if (hasMobileFocusCard) headClassName.push("screen-head-has-mobile-focus");
return `
-
+
${escapeHtml(title)}
${escapeHtml(subtitle)}
@@ -4510,7 +4514,7 @@ function screenShell(title, subtitle, actionsHtml, bodyHtml) {
${actionLayout.secondary ? `
${actionLayout.secondary}
` : ""}
- ${bodyHtml}
+ ${body}
`;
}
diff --git a/web/storyforge-web-v4/assets/styles.css b/web/storyforge-web-v4/assets/styles.css
index bd67f1b..a306c76 100644
--- a/web/storyforge-web-v4/assets/styles.css
+++ b/web/storyforge-web-v4/assets/styles.css
@@ -42,6 +42,11 @@ body {
overflow-x: hidden;
}
+body.sheet-open,
+body.mobile-sidebar-open {
+ overflow: hidden;
+}
+
a {
color: inherit;
text-decoration: none;
@@ -2187,6 +2192,10 @@ tbody tr:hover {
overflow: hidden;
}
+ .screen-head.screen-head-has-mobile-focus p {
+ -webkit-line-clamp: 1;
+ }
+
.panel-subtitle {
display: -webkit-box;
-webkit-line-clamp: 2;
@@ -2232,6 +2241,10 @@ tbody tr:hover {
display: none;
}
+ .screen-head.screen-head-has-mobile-focus .action-row-secondary {
+ display: none;
+ }
+
.btn,
.tab,
.tag.clickable-tag {
@@ -2289,6 +2302,24 @@ tbody tr:hover {
min-width: 0;
}
+ .compact-summary-row {
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ overscroll-behavior-x: contain;
+ scrollbar-width: none;
+ padding-bottom: 2px;
+ }
+
+ .compact-summary-row::-webkit-scrollbar {
+ display: none;
+ }
+
+ .compact-summary-row .tag {
+ flex: 0 0 auto;
+ white-space: nowrap;
+ text-align: center;
+ }
+
.grid-4,
.grid-5,
.mini-grid,
@@ -2620,7 +2651,7 @@ tbody tr:hover {
}
.compact-summary-row .tag {
- width: calc(50% - 4px);
+ width: auto;
text-align: center;
}
diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs
index f9d20d4..9a03a9e 100644
--- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs
+++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs
@@ -109,6 +109,7 @@ test("mobile action sheets and oneliner runtime behave like bottom sheets", () =
assert.match(CSS, /\.sheet-handle\s*\{/);
assert.match(CSS, /\.sheet-open \.mobile-tabbar\s*\{[\s\S]*opacity:\s*0/);
assert.match(CSS, /\.oneliner-open \.oneliner-fab\s*\{[\s\S]*opacity:\s*0/);
+ assert.match(CSS, /body\.sheet-open,\s*body\.mobile-sidebar-open\s*\{[\s\S]*overflow:\s*hidden/);
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/);
@@ -155,6 +156,13 @@ test("mobile touch targets raise tappable buttons, tabs, and action tags closer
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/);
});
+test("mobile compact summaries scroll horizontally instead of stacking into a second row", () => {
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.compact-summary-row\s*\{[\s\S]*flex-wrap:\s*nowrap/);
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.compact-summary-row\s*\{[\s\S]*overflow-x:\s*auto/);
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.compact-summary-row::-webkit-scrollbar\s*\{[\s\S]*display:\s*none/);
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.compact-summary-row \.tag\s*\{[\s\S]*flex:\s*0 0 auto/);
+});
+
test("action tags render interactive controls as buttons instead of passive spans", () => {
const actionTagSource = extractBetween(APP, "function actionTag(", "function renderPipelineButton(");
assert.match(actionTagSource, /if \(targetAction\) \{/);
@@ -172,6 +180,13 @@ test("mobile screen heads split the first action into a primary rail and keep th
assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.action-row-secondary\s*\{[\s\S]*overflow-x:\s*auto/);
});
+test("mobile screens with a focus card collapse the secondary action rail", () => {
+ assert.match(APP, /const hasMobileFocusCard = body\.includes\("mobile-flow-focus-card"\)/);
+ assert.match(APP, /screen-head-has-mobile-focus/);
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.screen-head\.screen-head-has-mobile-focus \.action-row-secondary\s*\{[\s\S]*display:\s*none/);
+ assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.screen-head\.screen-head-has-mobile-focus p\s*\{[\s\S]*-webkit-line-clamp:\s*1/);
+});
+
test("mobile bottom navigation stays highlighted for grouped related screens", () => {
assert.match(APP, /function getMobileTabGroup\(screenId = appState\.screen\)/);
assert.match(APP, /tracking:\s*"discovery"/);