FN-6502: make Quick Chat default taller on tablets

Quick Chat now opens with more vertical room on tablet-sized floating panels without overwriting saved desktop sizes.

- Compute a taller default panel height for tablet/mobile floating viewports while preserving desktop and full-screen portrait mobile behavior.
- Track explicit resize activity before persisting panel size so computed defaults do not pollute stored preferences.
- Add coverage for tablet defaults, persisted desktop sizes, tablet persistence preservation, and portrait mobile full-screen sizing.

Files changed:
 packages/dashboard/app/components/QuickChatFAB.tsx | 37 ++++++++---
 .../app/components/__tests__/QuickChatFAB.test.tsx | 73 ++++++++++++++++++++++
 2 files changed, 102 insertions(+), 8 deletions(-)

Fusion-Task-Id: FN-6502
Fusion-Task-Lineage: 3b8af4e2-64f9-42b8-8373-feab1e8d77ae
This commit is contained in:
gsxdsm
2026-06-16 23:14:46 -07:00
parent c073fdc85c
commit 4e0a860ee8
2 changed files with 102 additions and 8 deletions

View File

@@ -340,6 +340,21 @@ const QUICK_CHAT_DEFAULT_PANEL_SIZE: PanelSize = {
height: 400,
};
/**
* FNXC:QuickChatPanelSize 2026-06-16-23:03:
* FN-6502 requires Quick Chat to open taller by default on floating-panel mobile/tablet viewports while portrait mobile stays full-screen through CSS and desktop defaults plus persisted sizes remain unchanged.
*/
function getDefaultQuickChatPanelSize(): PanelSize {
if (typeof window === "undefined" || window.innerWidth <= QUICK_CHAT_DESKTOP_BREAKPOINT || window.innerWidth > 1024) {
return QUICK_CHAT_DEFAULT_PANEL_SIZE;
}
return {
width: QUICK_CHAT_DEFAULT_PANEL_SIZE.width,
height: Math.max(QUICK_CHAT_DEFAULT_PANEL_SIZE.height, Math.floor(window.innerHeight * 0.8)),
};
}
const ALLOWED_ATTACHMENT_TYPES = new Set([
"image/png",
"image/jpeg",
@@ -650,23 +665,30 @@ function usePanelResize(projectId: string | undefined, fabRight: number, fabBott
);
const loadPersistedSize = useCallback((): PanelSize => {
const defaultSize = getDefaultQuickChatPanelSize();
if (typeof window === "undefined" || window.innerWidth <= QUICK_CHAT_DESKTOP_BREAKPOINT) {
return QUICK_CHAT_DEFAULT_PANEL_SIZE;
return defaultSize;
}
try {
const raw = localStorage.getItem(storageKey);
if (!raw) return QUICK_CHAT_DEFAULT_PANEL_SIZE;
if (!raw) return defaultSize;
const parsed = JSON.parse(raw) as Partial<PanelSize>;
if (typeof parsed.width !== "number" || typeof parsed.height !== "number") {
return QUICK_CHAT_DEFAULT_PANEL_SIZE;
return defaultSize;
}
return { width: parsed.width, height: parsed.height };
} catch {
return QUICK_CHAT_DEFAULT_PANEL_SIZE;
return defaultSize;
}
}, [storageKey]);
const [panelSize, setPanelSize] = useState<PanelSize>(loadPersistedSize);
const panelSizeRef = useRef(panelSize);
const hasUserResizedPanelRef = useRef(false);
useEffect(() => {
panelSizeRef.current = panelSize;
}, [panelSize]);
/**
* Anchor offset relative to the FAB position.
@@ -682,7 +704,7 @@ function usePanelResize(projectId: string | undefined, fabRight: number, fabBott
}, [anchorOffset, clampPanelSize, fabBottom, fabRight, isDesktopViewport, isOpen]);
useEffect(() => {
if (!isOpen || !isDesktopViewport()) return;
if (!isOpen || !isDesktopViewport() || !hasUserResizedPanelRef.current) return;
try {
localStorage.setItem(storageKey, JSON.stringify(panelSize));
} catch {
@@ -772,6 +794,7 @@ function usePanelResize(projectId: string | undefined, fabRight: number, fabBott
),
);
hasUserResizedPanelRef.current = true;
setPanelSize(clamped);
setAnchorOffset({ right: clampedAnchorRight, bottom: clampedAnchorBottom });
};
@@ -786,7 +809,7 @@ function usePanelResize(projectId: string | undefined, fabRight: number, fabBott
// Persist final size.
try {
localStorage.setItem(storageKey, JSON.stringify({ width: panelSize.width, height: panelSize.height }));
localStorage.setItem(storageKey, JSON.stringify(panelSizeRef.current));
} catch {
// Best-effort
}
@@ -802,8 +825,6 @@ function usePanelResize(projectId: string | undefined, fabRight: number, fabBott
fabBottom,
fabRight,
isDesktopViewport,
panelSize.height,
panelSize.width,
storageKey,
],
);

View File

@@ -1455,6 +1455,79 @@ describe("QuickChatFAB session-first UX", () => {
expect(panel).toHaveStyle({ width: "420px", height: "360px" });
});
it("FN-6502: opens taller by default on tablet without persisting the computed size", async () => {
Object.defineProperty(window, "innerWidth", { configurable: true, value: 800 });
Object.defineProperty(window, "innerHeight", { configurable: true, value: 900 });
window.dispatchEvent(new Event("resize"));
mockUseViewportMode.mockReturnValue("tablet");
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
const panel = await screen.findByTestId("quick-chat-panel");
expect(panel).toHaveStyle({ width: "320px", height: "720px" });
expect(localStorage.getItem("fusion:quick-chat-size-proj-1")).toBeNull();
});
it("FN-6502: keeps the desktop default size unchanged when no persisted size exists", async () => {
Object.defineProperty(window, "innerWidth", { configurable: true, value: 1440 });
Object.defineProperty(window, "innerHeight", { configurable: true, value: 900 });
window.dispatchEvent(new Event("resize"));
mockUseViewportMode.mockReturnValue("desktop");
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
const panel = await screen.findByTestId("quick-chat-panel");
expect(panel).toHaveStyle({ width: "320px", height: "400px" });
});
it("FN-6502: restores an existing desktop persisted size on desktop", async () => {
Object.defineProperty(window, "innerWidth", { configurable: true, value: 1440 });
Object.defineProperty(window, "innerHeight", { configurable: true, value: 900 });
window.dispatchEvent(new Event("resize"));
mockUseViewportMode.mockReturnValue("desktop");
localStorage.setItem("fusion:quick-chat-size-proj-1", JSON.stringify({ width: 500, height: 520 }));
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
const panel = await screen.findByTestId("quick-chat-panel");
expect(panel).toHaveStyle({ width: "500px", height: "520px" });
});
it("FN-6502: tablet open does not overwrite a pre-existing desktop persisted size", async () => {
Object.defineProperty(window, "innerWidth", { configurable: true, value: 800 });
Object.defineProperty(window, "innerHeight", { configurable: true, value: 900 });
window.dispatchEvent(new Event("resize"));
mockUseViewportMode.mockReturnValue("tablet");
const persistedSize = { width: 500, height: 520 };
localStorage.setItem("fusion:quick-chat-size-proj-1", JSON.stringify(persistedSize));
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
const panel = await screen.findByTestId("quick-chat-panel");
expect(panel).toHaveStyle({ width: "500px", height: "520px" });
expect(JSON.parse(localStorage.getItem("fusion:quick-chat-size-proj-1") || "null")).toEqual(persistedSize);
});
it("FN-6502: portrait mobile keeps inline panel sizing disabled for the full-screen CSS sheet", async () => {
Object.defineProperty(window, "innerWidth", { configurable: true, value: 375 });
Object.defineProperty(window, "innerHeight", { configurable: true, value: 800 });
window.dispatchEvent(new Event("resize"));
mockUseViewportMode.mockReturnValue("mobile");
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
const panel = await screen.findByTestId("quick-chat-panel");
expect(panel.style.width).toBe("");
expect(panel.style.height).toBe("");
expect(panel.style.right).toBe("");
expect(panel.style.bottom).toBe("");
});
it("shows jump-to-latest only after leaving live tail and scrolls back on click", async () => {
mockFetchChatMessages.mockResolvedValueOnce({
messages: [