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:
5
.changeset/terminal-mobile-single-row-header.md
Normal file
5
.changeset/terminal-mobile-single-row-header.md
Normal 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.
|
||||
@@ -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)");
|
||||
});
|
||||
});
|
||||
@@ -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 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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} />);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user