fix(dashboard): keep mobile terminal header on a single row

Previously the mobile terminal header used flex-wrap to stack tabs and
the action cluster on separate rows. The actions now stay pinned to the
right edge of the header row while the tab bar flexes to fill remaining
width and remains horizontally scrollable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-30 21:26:54 -07:00
parent 2bbcadac6f
commit 40620a9a50
5 changed files with 77 additions and 14 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Keep the terminal modal header on a single row on mobile. The tab bar now flexes to fill remaining width and stays scrollable, while the action cluster pins to the right edge of the same row instead of stacking onto a second row.

View File

@@ -0,0 +1,46 @@
import { describe, it, expect } from "vitest";
import { loadAllAppCss } from "../test/cssFixture";
const css = loadAllAppCss();
const terminalSectionStart = css.indexOf("Terminal Modal Mobile Responsive");
const terminalMobileSection =
terminalSectionStart >= 0 ? css.slice(terminalSectionStart) : "";
function findRuleBody(selector: RegExp): string {
const match = terminalMobileSection.match(
new RegExp(selector.source + /\s*\{([^}]*)\}/.source),
);
return match?.[1] ?? "";
}
describe("terminal mobile header row CSS contract", () => {
it("keeps the mobile terminal header on one row", () => {
const ruleBody = findRuleBody(/\.terminal-header/);
expect(ruleBody).toContain("flex-wrap: nowrap");
expect(ruleBody).toContain("overflow: hidden");
});
it("keeps tabs flexible instead of forcing them onto a full-width row", () => {
const ruleBody = findRuleBody(/\.terminal-tabs/);
expect(ruleBody).toContain("flex: 1 1 auto");
expect(ruleBody).toContain("min-width: 0");
expect(ruleBody).not.toContain("flex: 1 1 100%");
expect(ruleBody).not.toContain("min-width: 100%");
});
it("keeps the action cluster on the same row without a second-row divider", () => {
const ruleBody = findRuleBody(/\.terminal-actions/);
expect(ruleBody).toContain("flex: 0 0 auto");
expect(ruleBody).toContain("border-top: none");
expect(ruleBody).not.toContain("flex: 1 1 100%");
});
it("defines dedicated spacing between the clear and shortcuts buttons", () => {
const ruleBody = css.match(/\.terminal-clear-btn--shortcut\s*\{([^}]*)\}/)?.[1] ?? "";
expect(ruleBody).toContain("margin-left: var(--space-xs)");
});
});

View File

@@ -276,6 +276,10 @@
background: var(--card-hover);
}
.terminal-clear-btn--shortcut {
margin-left: var(--space-xs);
}
.terminal-log-container {
flex: 1;
overflow: hidden;
@@ -864,17 +868,17 @@
resize: none;
}
/* Stack tabs and actions on separate rows */
/* Keep tabs and header actions on a single row */
.terminal-header {
flex-wrap: wrap;
flex-wrap: nowrap;
padding-top: env(safe-area-inset-top, 0);
overflow: hidden;
}
/* Tabs get their own full-width row and remain scrollable */
/* Tabs stay scrollable and yield space to the fixed action cluster */
.terminal-tabs {
flex: 1 1 100%;
min-width: 100%;
order: 1;
flex: 1 1 auto;
min-width: 0;
}
/* Hide the redundant title/status indicator on mobile — .terminal-status-bar shows connection state */
@@ -882,13 +886,12 @@
display: none;
}
/* Actions sit on a compact second row */
/* Actions stay pinned to the right edge of the header row */
.terminal-actions {
flex: 1 1 100%;
order: 2;
flex: 0 0 auto;
justify-content: flex-end;
border-top: 1px solid var(--border);
padding: 0 4px;
border-top: none;
padding: 0 var(--space-xs) 0 0;
min-height: 36px;
}
@@ -1048,4 +1051,3 @@
}
}

View File

@@ -1132,7 +1132,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
: undefined
}
>
{/* Header — on mobile (≤768px) flex-wrap stacks tabs and actions on separate rows;
{/* Header — on mobile (≤768px) keep tabs and actions on one row;
.terminal-title is hidden; action button labels are hidden (icons only) */}
<div className="terminal-header">
{/* Tab Bar */}
@@ -1210,7 +1210,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
<span className="terminal-action-label">Clear</span>
</button>
<button
className="terminal-clear-btn"
className="terminal-clear-btn terminal-clear-btn--shortcut"
onClick={() => setShowShortcuts((current) => !current)}
data-testid="terminal-shortcut-toggle"
title="Shortcuts"

View File

@@ -1679,6 +1679,16 @@ describe("TerminalModal — mobile layout contract", () => {
});
});
it("adds the shortcut spacing hook to the shortcuts toggle", async () => {
render(<TerminalModal isOpen={true} onClose={mockOnClose} />);
await waitFor(() => {
expect(screen.getByTestId("terminal-shortcut-toggle").className).toContain(
"terminal-clear-btn--shortcut",
);
});
});
it("terminal-title section contains the status indicator for connection state", async () => {
render(<TerminalModal isOpen={true} onClose={mockOnClose} />);