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"/);