FN-7684: move terminal shortcuts/zoom controls into footer on tablet widths
On tablet-width viewports (769-1024px) the terminal header was overcrowded, so shortcut/zoom/preference controls now move into the bottom status-bar footer instead, mirroring the existing FN-7560 mobile layout while true desktop keeps the header controls. - Add an isTabletTerminal detection flag (769-1024px, non-mobile) alongside the existing mobile flag - Render the shared terminalActionControls fragment in the .terminal-status-bar footer for tablet widths, keeping desktop pin/pop-out toggles available there too - True desktop (>1024px) continues to render font size / clear / shortcuts / preferences controls in the header - Update TerminalModal.css with footer layout styles for the tablet action controls - Update TerminalModal tests to cover the new tablet breakpoint rendering - Update docs/dashboard-guide.md to describe the tablet footer control location - Add a changeset (@runfusion/fusion: patch) documenting the fix Files changed: .changeset/fn-7684-tablet-terminal-footer.md | 7 ++ docs/dashboard-guide.md | 6 +- .../dashboard/app/components/TerminalModal.css | 34 ++++++ .../dashboard/app/components/TerminalModal.tsx | 103 ++++++++++++----- .../components/__tests__/TerminalModal.test.tsx | 126 ++++++++++++++++++--- 5 files changed, 231 insertions(+), 45 deletions(-) Fusion-Task-Id: FN-7684 Fusion-Task-Lineage: 627c40af-a152-451a-a045-65ad8babea9e Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7684-tablet-terminal-footer.md
Normal file
7
.changeset/fn-7684-tablet-terminal-footer.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: On tablet widths, move terminal shortcuts/zoom controls into the bottom footer so they no longer overlap header icons.
|
||||||
|
category: fix
|
||||||
|
dev: Adds an isTabletTerminal flag (769–1024px, non-mobile) that renders the shared terminalActionControls fragment in the .terminal-status-bar footer (as FN-7560 did for mobile) instead of the header; true desktop (>1024px) keeps the header layout. Tablet footer keeps the desktop pin/pop-out toggles.
|
||||||
@@ -611,12 +611,12 @@ On Windows, the embedded terminal starts a supported shell inside Fusion, such a
|
|||||||
Use the terminal on desktop/tablet:
|
Use the terminal on desktop/tablet:
|
||||||
|
|
||||||
1. Select the **Terminal** button in the footer executor status bar.
|
1. Select the **Terminal** button in the footer executor status bar.
|
||||||
Expected outcome: the terminal opens as a bottom-docked overlay panel with the active shell session, header controls for font size / clear / shortcuts / preferences, and a draggable top resize handle.
|
Expected outcome: the terminal opens as a bottom-docked overlay panel with the active shell session and a draggable top resize handle. On true desktop (wider than the tablet tier) the font size / clear / shortcuts / preferences controls render in the terminal header; on tablet widths (769-1024px) those same controls render in a bottom action-control footer bar instead, so the narrower tablet header does not overflow (the tab strip, workspace picker, and close button stay in the header on both).
|
||||||
2. Select **Pin terminal (push content)** from the terminal header.
|
2. Select **Pin terminal (push content)** — from the terminal header on desktop, or the bottom action-control footer on tablet.
|
||||||
Expected outcome: the terminal moves into a persisted below-application panel that reserves space instead of covering the board, chat, or right sidebar. Select **Unpin terminal (overlay content)** to return to the overlay docked panel.
|
Expected outcome: the terminal moves into a persisted below-application panel that reserves space instead of covering the board, chat, or right sidebar. Select **Unpin terminal (overlay content)** to return to the overlay docked panel.
|
||||||
3. Drag the top edge of the docked or pinned panel.
|
3. Drag the top edge of the docked or pinned panel.
|
||||||
Expected outcome: the panel height changes within its viewport-safe bounds and persists per project, with pinned mode clamped shorter so the application remains usable.
|
Expected outcome: the panel height changes within its viewport-safe bounds and persists per project, with pinned mode clamped shorter so the application remains usable.
|
||||||
4. Select **Pop out** from the terminal header.
|
4. Select **Pop out** — from the terminal header on desktop, or the bottom action-control footer on tablet.
|
||||||
Expected outcome: the terminal switches to a floating window that can be dragged and freely resized; size, position, and display mode are saved per project.
|
Expected outcome: the terminal switches to a floating window that can be dragged and freely resized; size, position, and display mode are saved per project.
|
||||||
5. Select **Dock** in the floating terminal.
|
5. Select **Dock** in the floating terminal.
|
||||||
Expected outcome: the terminal returns to the bottom docked overlay panel using the saved docked height.
|
Expected outcome: the terminal returns to the bottom docked overlay panel using the saved docked height.
|
||||||
|
|||||||
@@ -1790,3 +1790,37 @@ The Android keyboard-open recurrence can start with a touch-primary visualViewpo
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:TerminalFooter 2026-07-08-15:00:
|
||||||
|
FN-7684: extend the FN-7560 mobile footer relocation to the project's canonical
|
||||||
|
tablet tier (769-1024px, `--tablet-breakpoint: 1024px`, matching the JS
|
||||||
|
`isTabletTerminal` flag). A tablet keeps the desktop display modes (docked/
|
||||||
|
floating/pinned-below, unlike mobile's fullscreen shell) but its header still
|
||||||
|
overflows if it renders the full `.terminal-actions` cluster — the help text
|
||||||
|
and zoom controls collide with the pin/pop-out/close icons. TerminalModal.tsx
|
||||||
|
stops rendering `.terminal-actions` in the header at <=1024px and instead
|
||||||
|
renders the SAME shared `terminalActionControls` fragment here, mirroring every
|
||||||
|
mobile footer declaration (not just a display:flex override) so the tablet
|
||||||
|
footer keeps the same min-width: 0 + overflow-x: auto horizontal-scroll safety
|
||||||
|
net as mobile (FN-7550/FN-7560) plus the FN-7621 touch-action carve-out.
|
||||||
|
Unlike mobile, the tablet footer has more room, so it intentionally does NOT
|
||||||
|
reapply the mobile-only `.terminal-shortcuts--header, .terminal-connection-status
|
||||||
|
{ display: none }` hide above — both stay visible on tablet.
|
||||||
|
*/
|
||||||
|
@media (min-width: 769px) and (max-width: 1024px) {
|
||||||
|
.terminal-status-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding: var(--space-xs);
|
||||||
|
padding-bottom: max(var(--space-xs), env(safe-area-inset-bottom, 0));
|
||||||
|
min-height: 36px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -357,6 +357,24 @@ function isTerminalMobileViewport(): boolean {
|
|||||||
return window.innerWidth <= 768 || (hasTouchScreen && (getTerminalViewportWidth(true) <= 768 || getTerminalViewportHeight(true) <= 480));
|
return window.innerWidth <= 768 || (hasTouchScreen && (getTerminalViewportWidth(true) <= 768 || getTerminalViewportHeight(true) <= 480));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:TerminalFooter 2026-07-08-15:00:
|
||||||
|
FN-7684: on a tablet-width viewport (769-1024px, the project's canonical tablet
|
||||||
|
tier — `--tablet-breakpoint: 1024px`) the terminal is NOT mobile but its header
|
||||||
|
still overflows if it renders the full desktop `.terminal-actions` cluster (the
|
||||||
|
zoom/font-size controls, Shortcuts/Preferences toggles, connection status, and
|
||||||
|
help text collide with the pin/pop-out/close icons — see attachment 1863.png).
|
||||||
|
This flag is checked ONLY when `isTerminalMobileViewport()` is false, so mobile
|
||||||
|
always wins the mobile fullscreen shell; it gates relocating the shared action-
|
||||||
|
control fragment into the `.terminal-status-bar` footer on tablet too (FN-7560
|
||||||
|
established the footer for mobile).
|
||||||
|
*/
|
||||||
|
function isTerminalTabletViewport(): boolean {
|
||||||
|
if (typeof window === "undefined") return false;
|
||||||
|
if (isTerminalMobileViewport()) return false;
|
||||||
|
return window.innerWidth >= 769 && window.innerWidth <= 1024;
|
||||||
|
}
|
||||||
|
|
||||||
function isMacPlatform(): boolean {
|
function isMacPlatform(): boolean {
|
||||||
if (typeof navigator === "undefined") {
|
if (typeof navigator === "undefined") {
|
||||||
return false;
|
return false;
|
||||||
@@ -548,6 +566,8 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
|||||||
const [floatingSize, setFloatingSize] = useState<TerminalFloatSize>(() => readTerminalFloatSize(projectId));
|
const [floatingSize, setFloatingSize] = useState<TerminalFloatSize>(() => readTerminalFloatSize(projectId));
|
||||||
const [floatingPosition, setFloatingPosition] = useState<TerminalFloatPosition>(() => readTerminalFloatPosition(readTerminalFloatSize(projectId), projectId));
|
const [floatingPosition, setFloatingPosition] = useState<TerminalFloatPosition>(() => readTerminalFloatPosition(readTerminalFloatSize(projectId), projectId));
|
||||||
const [isMobileTerminal, setIsMobileTerminal] = useState(() => isTerminalMobileViewport());
|
const [isMobileTerminal, setIsMobileTerminal] = useState(() => isTerminalMobileViewport());
|
||||||
|
// FNXC:TerminalFooter 2026-07-08-15:00: FN-7684 tablet tier (769-1024px, non-mobile) — keeps the desktop display modes (docked/floating/pinned-below), only the action-control render location changes.
|
||||||
|
const [isTabletTerminal, setIsTabletTerminal] = useState(() => isTerminalTabletViewport());
|
||||||
const isDockedMode = !isMobileTerminal && displayMode === "docked";
|
const isDockedMode = !isMobileTerminal && displayMode === "docked";
|
||||||
const isFloatingMode = !isMobileTerminal && displayMode === "floating";
|
const isFloatingMode = !isMobileTerminal && displayMode === "floating";
|
||||||
const isBelowMode = !isMobileTerminal && displayMode === "below";
|
const isBelowMode = !isMobileTerminal && displayMode === "below";
|
||||||
@@ -618,7 +638,11 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
|||||||
FNXC:Terminal 2026-06-21-22:58:
|
FNXC:Terminal 2026-06-21-22:58:
|
||||||
Viewport changes must force the terminal back onto the mobile fullscreen path at <=768px or touch-primary short landscape, then restore the stored desktop/tablet docked/floating mode when the viewport expands.
|
Viewport changes must force the terminal back onto the mobile fullscreen path at <=768px or touch-primary short landscape, then restore the stored desktop/tablet docked/floating mode when the viewport expands.
|
||||||
*/
|
*/
|
||||||
const updateViewportMode = () => setIsMobileTerminal(isTerminalMobileViewport());
|
const updateViewportMode = () => {
|
||||||
|
setIsMobileTerminal(isTerminalMobileViewport());
|
||||||
|
// FNXC:TerminalFooter 2026-07-08-15:00: FN-7684 — keep isTabletTerminal in sync from the SAME resize/visualViewport listeners as isMobileTerminal rather than adding a second competing listener.
|
||||||
|
setIsTabletTerminal(isTerminalTabletViewport());
|
||||||
|
};
|
||||||
updateViewportMode();
|
updateViewportMode();
|
||||||
window.addEventListener("resize", updateViewportMode);
|
window.addEventListener("resize", updateViewportMode);
|
||||||
window.visualViewport?.addEventListener("resize", updateViewportMode);
|
window.visualViewport?.addEventListener("resize", updateViewportMode);
|
||||||
@@ -2297,14 +2321,16 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
|||||||
} as CSSProperties;
|
} as CSSProperties;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:TerminalFooter 2026-07-04-20:00:
|
FNXC:TerminalFooter 2026-07-08-15:00:
|
||||||
Single source of truth for the terminal action-control cluster (reconnect/restart,
|
Single source of truth for the terminal action-control cluster (reconnect/restart,
|
||||||
font-size, Clear, Shortcuts toggle, Preferences toggle, connection status, exit code,
|
font-size, Clear, Shortcuts toggle, Preferences toggle, connection status, exit code,
|
||||||
help text, and the desktop-only pin/pop-out toggles). Rendered in exactly ONE place
|
help text, and the desktop-only pin/pop-out toggles). Rendered in exactly ONE place
|
||||||
per breakpoint: inside the header `.terminal-actions` on desktop/floating/pinned-below
|
per breakpoint: inside the header `.terminal-actions` ONLY on true desktop (>1024px,
|
||||||
(FN-7502), or inside a dedicated bottom `.terminal-status-bar` footer on mobile (FN-7560)
|
!isMobileTerminal && !isTabletTerminal), or inside a dedicated bottom
|
||||||
so the narrow mobile header does not crowd the tab dropdown and close button. Do not
|
`.terminal-status-bar` footer on BOTH mobile (<=768px, FN-7560) and tablet
|
||||||
duplicate these handlers elsewhere — always render this fragment.
|
(769-1024px, FN-7684) so the narrower header does not crowd the tab strip/dropdown,
|
||||||
|
workspace picker, and close button. Same fragment, one location per breakpoint —
|
||||||
|
never duplicate these handlers elsewhere.
|
||||||
*/
|
*/
|
||||||
const terminalActionControls = (
|
const terminalActionControls = (
|
||||||
<>
|
<>
|
||||||
@@ -2621,15 +2647,19 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
FNXC:TerminalFooter 2026-07-04-20:00:
|
FNXC:TerminalFooter 2026-07-08-15:00:
|
||||||
On desktop/floating/pinned-below the header keeps its FN-7502 shape:
|
The header renders `.terminal-actions` (the shared `terminalActionControls`
|
||||||
status title + `.terminal-actions` (rendering the shared
|
fragment) ONLY on true desktop (!isMobileTerminal && !isTabletTerminal,
|
||||||
`terminalActionControls` fragment) + close. On mobile (isMobileTerminal)
|
>1024px) — the FN-7502 shape. On mobile (isMobileTerminal) the header
|
||||||
the header renders ONLY the close button here — no `.terminal-actions`
|
renders ONLY the corner-pinned close button — no `.terminal-actions`
|
||||||
shell — because the mobile tab dropdown already occupies the header and
|
shell — because the mobile tab dropdown already occupies the header and
|
||||||
the action controls move into the `.terminal-status-bar` footer below
|
the action controls move into the `.terminal-status-bar` footer (FN-7560).
|
||||||
(FN-7560) so they don't crowd the dropdown/close. Both sites render the
|
On tablet (isTabletTerminal, 769-1024px, FN-7684) the header keeps the
|
||||||
SAME fragment, never a duplicated copy, so handlers cannot drift.
|
desktop tab strip + title + workspace picker but ALSO drops
|
||||||
|
`.terminal-actions` — it still overflows at that width — in favor of a
|
||||||
|
plain close button, with the same action controls relocating into the
|
||||||
|
footer alongside mobile. Both footer/header render sites use the SAME
|
||||||
|
fragment, never a duplicated copy, so handlers cannot drift.
|
||||||
|
|
||||||
FNXC:TerminalHeader 2026-07-04-20:45:
|
FNXC:TerminalHeader 2026-07-04-20:45:
|
||||||
FN-7565: on mobile, being a direct child of `.terminal-header` (not
|
FN-7565: on mobile, being a direct child of `.terminal-header` (not
|
||||||
@@ -2641,7 +2671,9 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
|||||||
expect for an app-sheet close control. The `terminal-close--corner`
|
expect for an app-sheet close control. The `terminal-close--corner`
|
||||||
class (CSS: highest `order` + `margin-inline-start: auto`) fixes this
|
class (CSS: highest `order` + `margin-inline-start: auto`) fixes this
|
||||||
so the X renders last in flex order and hugs the right edge regardless
|
so the X renders last in flex order and hugs the right edge regardless
|
||||||
of how wide the tab dropdown / workspace picker grow.
|
of how wide the tab dropdown / workspace picker grow. Tablet keeps the
|
||||||
|
desktop `.terminal-tabs` (not the mobile dropdown) ahead of title/close
|
||||||
|
in normal DOM order, so its plain close button needs no order override.
|
||||||
*/}
|
*/}
|
||||||
{isMobileTerminal ? (
|
{isMobileTerminal ? (
|
||||||
<button
|
<button
|
||||||
@@ -2660,9 +2692,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
|||||||
{getStatusIndicator()}
|
{getStatusIndicator()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions — labels hidden on mobile via .terminal-action-label */}
|
{isTabletTerminal ? (
|
||||||
<div className="terminal-actions" data-testid="terminal-actions">
|
|
||||||
{terminalActionControls}
|
|
||||||
<button
|
<button
|
||||||
className="terminal-close"
|
className="terminal-close"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@@ -2671,7 +2701,20 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
|||||||
>
|
>
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
) : (
|
||||||
|
/* Actions — labels hidden on mobile via .terminal-action-label */
|
||||||
|
<div className="terminal-actions" data-testid="terminal-actions">
|
||||||
|
{terminalActionControls}
|
||||||
|
<button
|
||||||
|
className="terminal-close"
|
||||||
|
onClick={onClose}
|
||||||
|
data-testid="terminal-close-btn"
|
||||||
|
title={t("terminal.closeTerminal", "Close terminal")}
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -2944,16 +2987,20 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
FNXC:TerminalFooter 2026-07-04-20:00:
|
FNXC:TerminalFooter 2026-07-08-15:00:
|
||||||
Mobile-only footer bar (FN-7560) restoring the pre-FN-7502 footer
|
Mobile + tablet footer bar (FN-7560 mobile, FN-7684 tablet) restoring
|
||||||
ergonomics: the same `terminalActionControls` fragment used by the
|
the pre-FN-7502 footer ergonomics on both narrower tiers: the same
|
||||||
desktop header renders here instead so font-size/Clear/Shortcuts/
|
`terminalActionControls` fragment used by the true-desktop header
|
||||||
Preferences/connection-status/exit-code stay reachable without
|
renders here instead so font-size/Clear/Shortcuts/Preferences/
|
||||||
crowding the mobile header's tab dropdown and close button. Scrolls
|
connection-status/exit-code stay reachable without crowding the
|
||||||
horizontally (min-width: 0 + overflow-x: auto) if controls exceed the
|
header's tab strip/dropdown, workspace picker, and close button.
|
||||||
viewport width, matching the .terminal-shortcut-panel (FN-7550) pattern.
|
Scrolls horizontally (min-width: 0 + overflow-x: auto) if controls
|
||||||
|
exceed the viewport width, matching the .terminal-shortcut-panel
|
||||||
|
(FN-7550) pattern. Tablet keeps the desktop display modes (docked/
|
||||||
|
floating/pinned-below), so this footer also carries the !isMobileTerminal-
|
||||||
|
gated pin/pop-out toggles the fragment already renders for tablet.
|
||||||
*/}
|
*/}
|
||||||
{isMobileTerminal && (
|
{(isMobileTerminal || isTabletTerminal) && (
|
||||||
<div className="terminal-status-bar" data-testid="terminal-footer-actions">
|
<div className="terminal-status-bar" data-testid="terminal-footer-actions">
|
||||||
{terminalActionControls}
|
{terminalActionControls}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -807,20 +807,26 @@ describe("TerminalModal", () => {
|
|||||||
expect(belowRule).not.toContain("position: fixed;");
|
expect(belowRule).not.toContain("position: fixed;");
|
||||||
expect(belowRule).toContain("height: var(--terminal-below-height);");
|
expect(belowRule).toContain("height: var(--terminal-below-height);");
|
||||||
|
|
||||||
// FN-7560: the `.terminal-status-bar` footer is a MOBILE-ONLY affordance
|
// FN-7560/FN-7684: the `.terminal-status-bar` footer is a MOBILE + TABLET-ONLY
|
||||||
// (isMobileTerminal, which itself excludes below mode) — it must exist only
|
// affordance (isMobileTerminal || isTabletTerminal, both of which exclude
|
||||||
// scoped inside a `@media (max-width: 768px)` block, never as a global/
|
// below mode's true-desktop display) — it must exist only scoped inside the
|
||||||
// unscoped rule that could leak a footer shell into desktop/floating/
|
// mobile `@media (max-width: 768px)` block or the tablet
|
||||||
// pinned-below. Strip every mobile media-query block out of the
|
// `@media (min-width: 769px) and (max-width: 1024px)` block, never as a
|
||||||
// stylesheet and confirm no `.terminal-status-bar` rule remains outside it.
|
// global/unscoped rule that could leak a footer shell into true-desktop
|
||||||
const cssWithoutMobileMediaBlocks = terminalModalCss.replace(
|
// (>1024px) docked/floating/pinned-below. Strip every mobile AND tablet
|
||||||
/@media \(max-width: 768px\) \{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/g,
|
// media-query block out of the stylesheet and confirm no `.terminal-status-bar`
|
||||||
"",
|
// rule remains outside them.
|
||||||
);
|
const cssWithoutMobileAndTabletMediaBlocks = terminalModalCss
|
||||||
expect(cssWithoutMobileMediaBlocks).not.toMatch(/\.terminal-status-bar\s*\{/);
|
.replace(/@media \(max-width: 768px\) \{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/g, "")
|
||||||
|
.replace(/@media \(min-width: 769px\) and \(max-width: 1024px\) \{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/g, "");
|
||||||
|
expect(cssWithoutMobileAndTabletMediaBlocks).not.toMatch(/\.terminal-status-bar\s*\{/);
|
||||||
const mobileFooterRule =
|
const mobileFooterRule =
|
||||||
terminalModalCss.match(/@media \(max-width: 768px\) \{[\s\S]*?\.terminal-status-bar\s*\{/);
|
terminalModalCss.match(/@media \(max-width: 768px\) \{[\s\S]*?\.terminal-status-bar\s*\{/);
|
||||||
expect(mobileFooterRule).not.toBeNull();
|
expect(mobileFooterRule).not.toBeNull();
|
||||||
|
const tabletFooterRule = terminalModalCss.match(
|
||||||
|
/@media \(min-width: 769px\) and \(max-width: 1024px\) \{[\s\S]*?\.terminal-status-bar\s*\{/,
|
||||||
|
);
|
||||||
|
expect(tabletFooterRule).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exposes floating drag and resize handles and refits after floating resize", async () => {
|
it("exposes floating drag and resize handles and refits after floating resize", async () => {
|
||||||
@@ -1497,6 +1503,30 @@ describe("TerminalModal", () => {
|
|||||||
expect(footerRule).toContain("min-width: 0;");
|
expect(footerRule).toContain("min-width: 0;");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("gives the tablet footer bar the same horizontal-scroll pattern (FN-7684)", () => {
|
||||||
|
// FN-7684: the tablet-tier action-control footer mirrors the FN-7560
|
||||||
|
// mobile footer's min-width: 0 + overflow-x: auto flex-scroll pattern.
|
||||||
|
const tabletFooterRule =
|
||||||
|
terminalModalCss.match(
|
||||||
|
/@media \(min-width: 769px\) and \(max-width: 1024px\) \{[\s\S]*?\.terminal-status-bar\s*\{([^}]*)\}/,
|
||||||
|
)?.[1] ?? "";
|
||||||
|
expect(tabletFooterRule).toContain("overflow-x: auto;");
|
||||||
|
expect(tabletFooterRule).toContain("min-width: 0;");
|
||||||
|
expect(tabletFooterRule).toContain("touch-action: pan-x pan-y;");
|
||||||
|
|
||||||
|
// Unlike mobile, the tablet-scoped mobile `display: none` hide for the
|
||||||
|
// help text and connection status must NOT apply at the tablet tier.
|
||||||
|
const mobileHideBlock = terminalModalCss.match(
|
||||||
|
/@media \(max-width: 768px\) \{[\s\S]*?\.terminal-shortcuts--header,\s*\n\s*\.terminal-connection-status \{[\s\S]*?\}\s*\n\}/,
|
||||||
|
);
|
||||||
|
expect(mobileHideBlock).not.toBeNull();
|
||||||
|
const tabletBlock = terminalModalCss.match(
|
||||||
|
/@media \(min-width: 769px\) and \(max-width: 1024px\) \{([\s\S]*?)\n\}/,
|
||||||
|
)?.[1] ?? "";
|
||||||
|
expect(tabletBlock).not.toMatch(/\.terminal-shortcuts--header/);
|
||||||
|
expect(tabletBlock).not.toMatch(/\.terminal-connection-status/);
|
||||||
|
});
|
||||||
|
|
||||||
describe("real-CSS mobile cascade (FN-7621 recurrence #3)", () => {
|
describe("real-CSS mobile cascade (FN-7621 recurrence #3)", () => {
|
||||||
// FN-7621: the FN-7550/FN-7560 tests above are leaf-rule string matches —
|
// FN-7621: the FN-7550/FN-7560 tests above are leaf-rule string matches —
|
||||||
// they proved the declarations exist, but never proved the panel actually
|
// they proved the declarations exist, but never proved the panel actually
|
||||||
@@ -3553,9 +3583,10 @@ describe("TerminalModal — mobile layout contract", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("header actions show connection state without a footer status-bar shell (desktop, FN-7502)", async () => {
|
it("header actions show connection state without a footer status-bar shell (desktop, FN-7502)", async () => {
|
||||||
// FN-7560: explicitly desktop-width — the footer only exists on the mobile
|
// FN-7560/FN-7684: explicitly TRUE-desktop-width (>1024px) — the footer
|
||||||
// (isMobileTerminal) path; desktop/floating/pinned-below keep the FN-7502
|
// only exists on the mobile (isMobileTerminal) and tablet (isTabletTerminal)
|
||||||
// header-actions contract with NO footer shell rendered.
|
// paths; true desktop/floating/pinned-below keep the FN-7502 header-actions
|
||||||
|
// contract with NO footer shell rendered.
|
||||||
const previousInnerWidth = window.innerWidth;
|
const previousInnerWidth = window.innerWidth;
|
||||||
Object.defineProperty(window, "innerWidth", { value: 1280, configurable: true });
|
Object.defineProperty(window, "innerWidth", { value: 1280, configurable: true });
|
||||||
|
|
||||||
@@ -3619,6 +3650,73 @@ describe("TerminalModal — mobile layout contract", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders terminal action controls in a tablet footer, not the header, and keeps pin/pop-out toggles (FN-7684)", async () => {
|
||||||
|
// FN-7684: tablet width (769-1024px) must relocate the same shared
|
||||||
|
// terminalActionControls fragment into the footer, exactly as FN-7560
|
||||||
|
// did for mobile — but unlike mobile, tablet keeps the desktop pin/
|
||||||
|
// pop-out toggles and the desktop tab strip / workspace picker.
|
||||||
|
const previousInnerWidth = window.innerWidth;
|
||||||
|
Object.defineProperty(window, "innerWidth", { value: 900, configurable: true });
|
||||||
|
fireEvent(window, new Event("resize"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
render(<TerminalModal isOpen={true} onClose={mockOnClose} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const footer = screen.getByTestId("terminal-footer-actions");
|
||||||
|
expect(footer.className).toContain("terminal-status-bar");
|
||||||
|
|
||||||
|
const clearBtn = screen.getByTestId("terminal-clear-btn");
|
||||||
|
const shortcutToggle = screen.getByTestId("terminal-shortcut-toggle");
|
||||||
|
const preferencesToggle = screen.getByTestId("terminal-preferences-toggle");
|
||||||
|
const fontSizeValue = screen.getByTestId("terminal-font-size-value");
|
||||||
|
const pinToggle = screen.getByTestId("terminal-pin-toggle");
|
||||||
|
const popoutToggle = screen.getByTestId("terminal-popout-toggle");
|
||||||
|
|
||||||
|
// Controls live inside the footer region, including the desktop-only
|
||||||
|
// pin/pop-out toggles (tablet keeps docked/floating/pinned-below modes)...
|
||||||
|
expect(footer.contains(clearBtn)).toBe(true);
|
||||||
|
expect(footer.contains(shortcutToggle)).toBe(true);
|
||||||
|
expect(footer.contains(preferencesToggle)).toBe(true);
|
||||||
|
expect(footer.contains(fontSizeValue)).toBe(true);
|
||||||
|
expect(footer.contains(pinToggle)).toBe(true);
|
||||||
|
expect(footer.contains(popoutToggle)).toBe(true);
|
||||||
|
|
||||||
|
// ...and NOT inside the header.
|
||||||
|
const header = document.querySelector(".terminal-header");
|
||||||
|
expect(header).toBeTruthy();
|
||||||
|
expect(header?.contains(clearBtn)).toBe(false);
|
||||||
|
expect(header?.contains(shortcutToggle)).toBe(false);
|
||||||
|
expect(header?.contains(preferencesToggle)).toBe(false);
|
||||||
|
expect(header?.contains(fontSizeValue)).toBe(false);
|
||||||
|
expect(header?.contains(pinToggle)).toBe(false);
|
||||||
|
expect(header?.contains(popoutToggle)).toBe(false);
|
||||||
|
|
||||||
|
// No empty .terminal-actions shell renders in the tablet header.
|
||||||
|
expect(header?.querySelector(".terminal-actions")).toBeNull();
|
||||||
|
|
||||||
|
// The close button, the desktop tab strip (not the mobile dropdown),
|
||||||
|
// and the workspace picker remain in the header.
|
||||||
|
const closeBtn = screen.getByTestId("terminal-close-btn");
|
||||||
|
expect(header?.contains(closeBtn)).toBe(true);
|
||||||
|
expect(footer.contains(closeBtn)).toBe(false);
|
||||||
|
expect(screen.queryByTestId("terminal-mobile-tabs")).toBeNull();
|
||||||
|
const tabs = screen.queryByTestId("terminal-tabs");
|
||||||
|
expect(tabs).toBeTruthy();
|
||||||
|
expect(header?.contains(tabs)).toBe(true);
|
||||||
|
// The workspace picker only renders when workspaces exist (default mock
|
||||||
|
// has none) — assert it stays in the header when present.
|
||||||
|
const workspacePicker = screen.queryByTestId("terminal-workspace-picker");
|
||||||
|
if (workspacePicker) {
|
||||||
|
expect(header?.contains(workspacePicker)).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(window, "innerWidth", { value: previousInnerWidth, configurable: true });
|
||||||
|
fireEvent(window, new Event("resize"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("pins the mobile close button to the top-right corner of the header, not buried in .terminal-actions (FN-7565)", async () => {
|
it("pins the mobile close button to the top-right corner of the header, not buried in .terminal-actions (FN-7565)", async () => {
|
||||||
const previousInnerWidth = window.innerWidth;
|
const previousInnerWidth = window.innerWidth;
|
||||||
Object.defineProperty(window, "innerWidth", { value: 390, configurable: true });
|
Object.defineProperty(window, "innerWidth", { value: 390, configurable: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user