FN-8557: preserve terminal geometry on tablets

Keep floating terminal controls available on touch tablets across responsive and keyboard viewport changes.

- Classify known tablet screens separately from phone fallbacks.
- Restore tablet floating and docked geometry at the 768px breakpoint with larger touch resize targets.
- Cover keyboard-shrunken and breakpoint tablet interactions, and document the responsive behavior.

Files changed:
 docs/dashboard-guide.md                            |   2 +-
 .../dashboard/app/components/TerminalModal.css     |  76 +++++++++--
 .../dashboard/app/components/TerminalModal.tsx     |  33 ++---
 .../components/__tests__/TerminalModal.test.tsx    | 150 +++++++++++++++++++++
 .../app/hooks/__tests__/useViewportMode.test.ts    |  14 ++
 packages/dashboard/app/hooks/useViewportMode.ts    |  33 ++++-
 6 files changed, 277 insertions(+), 31 deletions(-)

Fusion-Task-Id: FN-8557

Fusion-Task-Lineage: cd334682-e70a-4531-ab53-c5c0898f0bf1

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-24 00:59:42 -07:00
parent c5e9a7956a
commit 973c978f91
6 changed files with 277 additions and 31 deletions

View File

@@ -764,7 +764,7 @@ Mailbox view shows inbox/outbox communication threads and unread state. When an
## Interactive Terminal
Fusion embeds a terminal using xterm.js. Desktop and tablet use the footer status bar as the terminal launcher; mobile keeps the full-screen terminal path.
Fusion embeds a terminal using xterm.js. Desktop and tablet use the footer status bar as the terminal launcher; mobile keeps the full-screen terminal path. Known touch tablets, including at the 768px responsive boundary, retain docked/floating presentation rather than falling back to the phone sheet. Their saved floating size and position and touch drag/edge-or-corner resize controls stay available when a software keyboard shortens the visual viewport. True narrow phones, including folded panes and short phone landscapes, intentionally remain full-screen.
<!-- FNXC:Terminal 2026-07-11-18:20: FN-7824 first-launch terminal sockets auto-retry with capped backoff until the first successful open, so the manual Reconnect affordance is reserved for terminal sessions that already connected and then exhaust their mid-session reconnect budget. -->
On first launch or first open, the terminal keeps reconnecting automatically until its initial WebSocket opens; it should show **Reconnecting...** during that cold-start recovery rather than requiring a manual **Reconnect** click. If an already-connected terminal drops and exhausts its bounded reconnect budget, Fusion then parks it as **Disconnected** and surfaces the manual **Reconnect** control.

View File

@@ -241,11 +241,17 @@ The floating-mode header is the move grip. `touch-action: none` is required so a
touch-action: none;
}
/*
FNXC:TerminalModalControls 2026-07-24-12:35:
Tablet operators resize the global floating terminal with the same edge and corner targets as other
floating windows. Use tokenized, larger hit regions so touch gestures are discoverable without
adding a second header or exposing controls in the fixed phone and embedded presentations.
*/
.terminal-floating-resize-handle--n,
.terminal-floating-resize-handle--s {
left: var(--space-sm);
right: var(--space-sm);
height: var(--space-sm);
left: var(--space-md);
right: var(--space-md);
height: var(--space-md);
cursor: ns-resize;
}
@@ -254,9 +260,9 @@ The floating-mode header is the move grip. `touch-action: none` is required so a
.terminal-floating-resize-handle--e,
.terminal-floating-resize-handle--w {
top: var(--space-sm);
bottom: var(--space-sm);
width: var(--space-sm);
top: var(--space-md);
bottom: var(--space-md);
width: var(--space-md);
cursor: ew-resize;
}
@@ -267,8 +273,8 @@ The floating-mode header is the move grip. `touch-action: none` is required so a
.terminal-floating-resize-handle--nw,
.terminal-floating-resize-handle--se,
.terminal-floating-resize-handle--sw {
width: var(--space-lg);
height: var(--space-lg);
width: var(--space-xl);
height: var(--space-xl);
}
.terminal-floating-resize-handle--ne { top: 0; right: 0; cursor: nesw-resize; }
@@ -1885,8 +1891,62 @@ The Android keyboard-open recurrence can start with a touch-primary visualViewpo
min-width: 36px;
min-height: 36px;
}
/*
FNXC:TerminalModalControls 2026-07-24-01:10:
A known touch tablet can share this CSS-width breakpoint with a phone. The React
classifier marks it explicitly, so restore the stored floating/docked geometry
after the phone fallback rules (which use !important to defeat stale phone sizes).
This keeps the shared header move grip and all eight resize targets reachable at
exactly 768px without exposing any geometry controls on true-phone sheets.
*/
.modal.terminal-modal.terminal-modal--tablet.terminal-modal--floating {
min-width: calc(var(--space-xl) * 20) !important;
min-height: calc(var(--space-xl) * 13.333) !important;
width: var(--terminal-float-width) !important;
height: var(--terminal-float-height) !important;
max-width: calc(100vw - (var(--space-lg) * 2)) !important;
max-height: calc(100dvh - (var(--space-lg) * 2)) !important;
border-radius: var(--radius-lg);
border: 1px solid var(--border);
}
.modal.terminal-modal.terminal-modal--tablet.terminal-modal--docked {
min-width: 0 !important;
min-height: calc(var(--space-xl) * 10) !important;
width: 100vw !important;
height: var(--terminal-docked-height) !important;
max-width: none !important;
max-height: calc(100dvh - (var(--space-xl) * 4)) !important;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
border: 1px solid var(--border);
}
.modal.terminal-modal.terminal-modal--tablet .terminal-header {
flex-wrap: nowrap;
row-gap: 0;
padding-top: 0;
overflow: visible;
}
.modal.terminal-modal.terminal-modal--tablet .terminal-tabs {
display: flex;
}
.modal.terminal-modal.terminal-modal--tablet .terminal-mobile-tabs {
display: none;
}
.modal.terminal-modal.terminal-modal--tablet .terminal-title {
display: flex;
}
.terminal-below-host:has(.terminal-modal.terminal-modal--tablet) {
display: flex;
}
}
@media (max-width: 768px) {
.terminal-below-host {
display: none;

View File

@@ -34,6 +34,7 @@ import {
import { useTerminal } from "../hooks/useTerminal";
import { useTerminalSessions } from "../hooks/useTerminalSessions";
import { useWorkspaces } from "../hooks/useWorkspaces";
import { getViewportMode, isMobileViewport } from "../hooks/useViewportMode";
import { nextFloatingZ, currentFloatingZ } from "./floatingWindowStack";
import { getPathBasename } from "../utils/pathDisplay";
import {
@@ -336,16 +337,6 @@ function getTerminalViewportWidth(hasTouchScreen = false): number {
return layoutWidth;
}
function getTerminalViewportHeight(hasTouchScreen = false): number {
if (typeof window === "undefined") return Number.POSITIVE_INFINITY;
const layoutHeight = window.innerHeight;
const visualHeight = window.visualViewport?.height;
if (hasTouchScreen && typeof visualHeight === "number" && visualHeight > 0) {
return Math.min(layoutHeight, visualHeight);
}
return layoutHeight;
}
/** Whether the current device is likely mobile (touch-primary, small viewport). */
function isMobileDevice(): boolean {
if (typeof window === "undefined") return false;
@@ -356,13 +347,15 @@ function isMobileDevice(): boolean {
}
function isTerminalMobileViewport(): boolean {
if (typeof window === "undefined") return false;
const hasTouchScreen = "ontouchstart" in window || navigator.maxTouchPoints > 0;
/*
FNXC:Terminal 2026-07-01-11:46:
Android foldables can expose a tablet-sized layout viewport while the current visualViewport is the folded phone pane. Treat touch-primary visualViewport width as the terminal mobile breakpoint so the first xterm fit uses the fullscreen/mobile shell and keyboard vars before any unfold/orientation event can repair stale desktop geometry.
FNXC:TerminalModalControls 2026-07-24-12:30:
The global terminal must use the canonical viewport contract rather than a terminal-local
visual-height shortcut. A software keyboard can shrink a tablet below the phone landscape
height without changing its physical screen, so it must retain docked/floating move and resize
controls. Canonical detection still makes true narrow phones, short phone landscapes, and folded
touch panes full-screen while preserving stored tablet/desktop geometry through transitions.
*/
return window.innerWidth <= 768 || (hasTouchScreen && (getTerminalViewportWidth(true) <= 768 || getTerminalViewportHeight(true) <= 480));
return isMobileViewport();
}
interface TabsOverflowMeasurement {
@@ -586,6 +579,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
const [floatingSize, setFloatingSize] = useState<TerminalFloatSize>(() => readTerminalFloatSize(projectId));
const [floatingPosition, setFloatingPosition] = useState<TerminalFloatPosition>(() => readTerminalFloatPosition(readTerminalFloatSize(projectId), projectId));
const [isMobileTerminal, setIsMobileTerminal] = useState(() => isTerminalMobileViewport());
const [isTabletTerminal, setIsTabletTerminal] = useState(() => getViewportMode() === "tablet");
const [tabsOverflow, setTabsOverflow] = useState(false);
/*
FNXC:Terminal 2026-07-10-00:00:
@@ -667,6 +661,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
*/
const updateViewportMode = () => {
setIsMobileTerminal(isTerminalMobileViewport());
setIsTabletTerminal(getViewportMode() === "tablet");
};
updateViewportMode();
window.addEventListener("resize", updateViewportMode);
@@ -2484,7 +2479,13 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
const showManualStart = isReady && autoCreateDisabled && !activeTab && !bootstrapError;
// FNXC:Terminal 2026-06-23-04:30: Always carry the base `terminal-modal-overlay` class so the no-dim/no-blur rule applies in EVERY mode (docked, floating, AND the mobile/default sheet that is neither) — the terminal must never dim the page behind it.
const overlayClassName = `modal-overlay open terminal-modal-overlay${isDockedMode ? " terminal-modal-overlay--docked" : ""}${isFloatingMode ? " terminal-modal-overlay--floating" : ""}`;
const modalClassName = `modal terminal-modal${isMobileTerminal && !embedded ? " terminal-modal--mobile" : ""}${isDockedMode ? " terminal-modal--docked" : ""}${isFloatingMode ? " terminal-modal--floating" : ""}${isBelowMode ? " terminal-modal--below" : ""}${embedded ? " terminal-modal--embedded" : ""}`;
/*
FNXC:TerminalModalControls 2026-07-24-01:10:
CSS still has a width-based phone media query for true-phone fallback. Mark a known tablet
explicitly so its floating/docked geometry wins at the 768px boundary rather than inheriting
the phone full-screen shell. Embedded terminals remain parent-owned and never receive this chrome.
*/
const modalClassName = `modal terminal-modal${isMobileTerminal && !embedded ? " terminal-modal--mobile" : ""}${isTabletTerminal && !isMobileTerminal && !embedded ? " terminal-modal--tablet" : ""}${isDockedMode ? " terminal-modal--docked" : ""}${isFloatingMode ? " terminal-modal--floating" : ""}${isBelowMode ? " terminal-modal--below" : ""}${embedded ? " terminal-modal--embedded" : ""}`;
/*
FNXC:TerminalWorkspaces 2026-07-13-00:00:
The workspace picker menu is portaled to `document.body`, so floating terminal mode must compare it in the same root stacking context as the panel. Keep the menu one layer above the panel's shared `floatingZ`; otherwise the fixed CSS fallback band sits below the 10100+ floating stack and the menu appears invisible behind the modal.

View File

@@ -1219,6 +1219,156 @@ describe("TerminalModal", () => {
});
});
it("keeps a keyboard-shrunken touch tablet floating, movable, and resizable", async () => {
const projectId = "touch-tablet-terminal-geometry";
const previousInnerWidth = window.innerWidth;
const previousInnerHeight = window.innerHeight;
const previousScreen = Object.getOwnPropertyDescriptor(window, "screen");
const previousMaxTouchPoints = Object.getOwnPropertyDescriptor(navigator, "maxTouchPoints");
const previousVisualViewport = window.visualViewport;
Object.defineProperty(window, "innerWidth", { configurable: true, value: 1200 });
Object.defineProperty(window, "innerHeight", { configurable: true, value: 900 });
Object.defineProperty(window, "screen", { configurable: true, value: { width: 1024, height: 768 } });
Object.defineProperty(navigator, "maxTouchPoints", { configurable: true, value: 1 });
Object.defineProperty(window, "visualViewport", {
configurable: true,
value: { width: 900, height: 400, addEventListener: vi.fn(), removeEventListener: vi.fn() },
});
vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: query === "(max-height: 480px)",
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}));
window.localStorage.setItem(`fusion:terminal-display-mode-${projectId}`, "floating");
try {
render(<TerminalModal isOpen={true} onClose={mockOnClose} projectId={projectId} />);
const modal = await screen.findByTestId("terminal-modal");
await waitFor(() => expect(mockTerminalInstance.open).toHaveBeenCalled());
expect(modal).toHaveClass("terminal-modal--floating");
expect(screen.getByTestId("terminal-floating-resize-se")).toHaveAttribute("aria-label", "Resize terminal window");
const fitCallBaseline = mockFitAddonFit.mock.calls.length;
const resizeHandle = screen.getByTestId("terminal-floating-resize-se") as HTMLElement & {
setPointerCapture: (pointerId: number) => void;
releasePointerCapture: (pointerId: number) => void;
};
resizeHandle.setPointerCapture = vi.fn();
resizeHandle.releasePointerCapture = vi.fn();
fireEvent.pointerDown(resizeHandle, { pointerId: 41, pointerType: "touch", clientX: 100, clientY: 100 });
fireEvent.pointerMove(resizeHandle, { pointerId: 99, pointerType: "touch", clientX: 300, clientY: 300 });
fireEvent.pointerMove(resizeHandle, { pointerId: 41, pointerType: "touch", clientX: 180, clientY: 170 });
fireEvent.pointerUp(resizeHandle, { pointerId: 41, pointerType: "touch" });
await waitFor(() => {
expect(window.localStorage.getItem(`fusion:terminal-modal-size-${projectId}`)).toBe(JSON.stringify({ width: 1040, height: 630 }));
expect(mockFitAddonFit.mock.calls.length).toBeGreaterThan(fitCallBaseline);
});
const header = modal.querySelector(".terminal-header") as HTMLElement & {
setPointerCapture: (pointerId: number) => void;
releasePointerCapture: (pointerId: number) => void;
};
header.setPointerCapture = vi.fn();
header.releasePointerCapture = vi.fn();
fireEvent.pointerDown(header, { pointerId: 42, pointerType: "touch", clientX: 300, clientY: 100 });
fireEvent.pointerMove(header, { pointerId: 42, pointerType: "touch", clientX: 200, clientY: 140 });
fireEvent.pointerUp(header, { pointerId: 42, pointerType: "touch" });
await waitFor(() => {
expect(JSON.parse(window.localStorage.getItem(`fusion:terminal-float-pos-${projectId}`) ?? "{}")).toEqual({ x: 44, y: 56 });
});
fireEvent.pointerDown(header, { pointerId: 43, pointerType: "touch", clientX: 200, clientY: 140 });
fireEvent.pointerMove(header, { pointerId: 43, pointerType: "touch", clientX: 100, clientY: 140 });
fireEvent.pointerCancel(header, { pointerId: 43, pointerType: "touch" });
expect(JSON.parse(window.localStorage.getItem(`fusion:terminal-float-pos-${projectId}`) ?? "{}")).toEqual({ x: 16, y: 56 });
expect(header.releasePointerCapture).toHaveBeenCalledWith(43);
} finally {
Object.defineProperty(window, "innerWidth", { configurable: true, value: previousInnerWidth });
Object.defineProperty(window, "innerHeight", { configurable: true, value: previousInnerHeight });
if (previousScreen) Object.defineProperty(window, "screen", previousScreen);
if (previousMaxTouchPoints) Object.defineProperty(navigator, "maxTouchPoints", previousMaxTouchPoints);
Object.defineProperty(window, "visualViewport", { configurable: true, value: previousVisualViewport });
}
});
it("keeps a touch tablet at the 768px boundary floating, movable, and resizable", async () => {
const projectId = "tablet-boundary-terminal-geometry";
const previousInnerWidth = window.innerWidth;
const previousInnerHeight = window.innerHeight;
const previousScreen = Object.getOwnPropertyDescriptor(window, "screen");
const previousMaxTouchPoints = Object.getOwnPropertyDescriptor(navigator, "maxTouchPoints");
Object.defineProperty(window, "innerWidth", { configurable: true, value: 768 });
Object.defineProperty(window, "innerHeight", { configurable: true, value: 1024 });
Object.defineProperty(window, "screen", { configurable: true, value: { width: 768, height: 1024 } });
Object.defineProperty(navigator, "maxTouchPoints", { configurable: true, value: 1 });
vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: query === "(max-width: 768px)",
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}));
window.localStorage.setItem(`fusion:terminal-display-mode-${projectId}`, "floating");
const styleEl = document.createElement("style");
styleEl.textContent = loadAllAppCss();
document.head.appendChild(styleEl);
try {
render(<TerminalModal isOpen={true} onClose={mockOnClose} projectId={projectId} />);
const modal = await screen.findByTestId("terminal-modal");
await waitFor(() => expect(mockTerminalInstance.open).toHaveBeenCalled());
expect(modal).toHaveClass("terminal-modal--tablet", "terminal-modal--floating");
// The 768px CSS fallback is full-screen only for true phones. A known
// tablet must win that cascade with its stored floating geometry.
const modalStyle = getComputedStyle(modal);
expect(modalStyle.width).toBe("var(--terminal-float-width)");
expect(modalStyle.height).toBe("var(--terminal-float-height)");
expect(modalStyle.maxWidth).toBe("calc(100vw - (var(--space-lg) * 2))");
const resizeHandle = screen.getByTestId("terminal-floating-resize-se") as HTMLElement & {
setPointerCapture: (pointerId: number) => void;
releasePointerCapture: (pointerId: number) => void;
};
resizeHandle.setPointerCapture = vi.fn();
resizeHandle.releasePointerCapture = vi.fn();
fireEvent.pointerDown(resizeHandle, { pointerId: 51, pointerType: "touch", clientX: 200, clientY: 200 });
fireEvent.pointerMove(resizeHandle, { pointerId: 51, pointerType: "touch", clientX: 120, clientY: 180 });
fireEvent.pointerUp(resizeHandle, { pointerId: 51, pointerType: "touch" });
await waitFor(() => {
expect(window.localStorage.getItem(`fusion:terminal-modal-size-${projectId}`)).toBe(JSON.stringify({ width: 656, height: 540 }));
});
const header = modal.querySelector(".terminal-header") as HTMLElement & {
setPointerCapture: (pointerId: number) => void;
releasePointerCapture: (pointerId: number) => void;
};
header.setPointerCapture = vi.fn();
header.releasePointerCapture = vi.fn();
fireEvent.pointerDown(header, { pointerId: 52, pointerType: "touch", clientX: 100, clientY: 100 });
fireEvent.pointerMove(header, { pointerId: 52, pointerType: "touch", clientX: 180, clientY: 140 });
fireEvent.pointerUp(header, { pointerId: 52, pointerType: "touch" });
await waitFor(() => {
expect(window.localStorage.getItem(`fusion:terminal-float-pos-${projectId}`)).toBe(JSON.stringify({ x: 96, y: 56 }));
});
} finally {
styleEl.remove();
Object.defineProperty(window, "innerWidth", { configurable: true, value: previousInnerWidth });
Object.defineProperty(window, "innerHeight", { configurable: true, value: previousInnerHeight });
if (previousScreen) Object.defineProperty(window, "screen", previousScreen);
if (previousMaxTouchPoints) Object.defineProperty(navigator, "maxTouchPoints", previousMaxTouchPoints);
}
});
it("keeps the floating terminal touch-draggable with theme-controlled shadow", () => {
const panelRule = terminalModalCss.match(/\.modal\.terminal-modal\.terminal-modal--floating\s*\{([^}]*)\}/)?.[1] ?? "";
const headerRule = terminalModalCss.match(/\.terminal-header--draggable\s*\{([^}]*)\}/)?.[1] ?? "";

View File

@@ -150,6 +150,20 @@ describe("useViewportMode", () => {
expect(renderHook(() => useViewportMode()).result.current).toBe("tablet");
});
it("keeps a touch tablet at the 768px boundary out of the phone presentation", () => {
const originalMaxTouchPoints = Object.getOwnPropertyDescriptor(navigator, "maxTouchPoints");
stubScreen(768, 1024);
Object.defineProperty(navigator, "maxTouchPoints", { configurable: true, value: 1 });
installViewportMedia({ width: true, height: false, tablet: false });
try {
expect(isMobileViewport()).toBe(false);
expect(getViewportMode()).toBe("tablet");
} finally {
if (originalMaxTouchPoints) Object.defineProperty(navigator, "maxTouchPoints", originalMaxTouchPoints);
}
});
it("keeps desktop mode when only the short-height clause matches on a desktop-class screen", () => {
stubScreen(1920, 1080);
installViewportMedia({ width: false, height: true, tablet: false });

View File

@@ -49,11 +49,19 @@ function getTouchVisualViewportHeight(): number | null {
// `(max-height: 480px)`) but never the device's physical screen. Only treat a
// short viewport as a landscape phone when the smaller physical screen edge is
// phone-class. Fail safe (return false) when screen data is unavailable.
function isPhoneClassScreen(): boolean {
function hasKnownPhysicalScreenSize(): boolean {
if (typeof window === "undefined" || !window.screen) return false;
const { width, height } = window.screen;
if (!width || !height) return false;
return Math.min(width, height) <= 480;
return Boolean(width && height);
}
function isPhoneClassScreen(): boolean {
if (!hasKnownPhysicalScreenSize()) return false;
return Math.min(window.screen.width, window.screen.height) <= 480;
}
function isTabletClassTouchScreen(): boolean {
return hasTouchScreen() && hasKnownPhysicalScreenSize() && !isPhoneClassScreen();
}
export function isMobileViewport(): boolean {
@@ -64,15 +72,28 @@ export function isMobileViewport(): boolean {
FNXC:ViewportMode 2026-07-01-11:56:
Android foldables can expose a wide layout viewport while visualViewport is the folded phone pane. Treat touch-primary visualViewport width as the mobile breakpoint so terminal surfaces render mobile controls and fit xterm from the initial folded geometry, not a stale desktop/tablet shell.
*/
return window.matchMedia(MOBILE_WIDTH_MEDIA_QUERY).matches ||
(visualWidth !== null && visualWidth <= 768) ||
/*
FNXC:TerminalModalControls 2026-07-24-14:15:
A touch viewport exactly at the 768px CSS boundary can be a portrait tablet, not a phone.
Keep the physical layout/visual-width fallback for delayed media-query and orientation updates,
but route a known tablet-class physical screen through tablet geometry controls. Unknown screen
dimensions retain the conservative CSS-only phone path, while narrow foldable panes remain phones.
*/
const hasNarrowWidth = window.innerWidth <= 768 ||
window.matchMedia(MOBILE_WIDTH_MEDIA_QUERY).matches ||
(visualWidth !== null && visualWidth <= 768);
return (hasNarrowWidth && !isTabletClassTouchScreen()) ||
((window.matchMedia(MOBILE_HEIGHT_MEDIA_QUERY).matches || (visualHeight !== null && visualHeight <= 480)) && isPhoneClassScreen());
}
export function getViewportMode(): ViewportMode {
if (typeof window === "undefined") return "desktop";
if (isMobileViewport()) return "mobile";
if (window.matchMedia("(min-width: 769px) and (max-width: 1024px)").matches) return "tablet";
const isTabletBoundary = window.innerWidth <= 768 ||
window.matchMedia(MOBILE_WIDTH_MEDIA_QUERY).matches ||
(getTouchVisualViewportWidth() ?? Number.POSITIVE_INFINITY) <= 768;
if (window.matchMedia("(min-width: 769px) and (max-width: 1024px)").matches ||
(isTabletBoundary && isTabletClassTouchScreen())) return "tablet";
return "desktop";
}