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:
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user