From 40cb0d3d45c94a380fa578b0dada66cb94a5df29 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 11 Jun 2026 20:14:54 -0700 Subject: [PATCH] FN-6223: keep usage dialog near the top Keep usage dialog positioning consistently top-biased across viewport surfaces. - Clamp desktop popover placement to the upper viewport while preserving anchor alignment and saved sizing. - Add overlay styling so modal and mobile sheet presentations start near the top. - Extend UsageIndicator tests across desktop, mobile, saved-size, and data-state surfaces. - Add a patch changeset for the published Fusion package. Files changed: .changeset/top-usage-dialog.md | 5 + .../dashboard/app/components/UsageIndicator.css | 8 ++ .../dashboard/app/components/UsageIndicator.tsx | 18 ++- .../components/__tests__/UsageIndicator.test.tsx | 139 ++++++++++++++++++++- 4 files changed, 162 insertions(+), 8 deletions(-) Fusion-Task-Id: FN-6223 Fusion-Task-Lineage: a35e82f4-ddef-4d68-81f0-6fee420f0392 --- .changeset/top-usage-dialog.md | 5 + .../app/components/UsageIndicator.css | 8 + .../app/components/UsageIndicator.tsx | 18 ++- .../__tests__/UsageIndicator.test.tsx | 139 +++++++++++++++++- 4 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 .changeset/top-usage-dialog.md diff --git a/.changeset/top-usage-dialog.md b/.changeset/top-usage-dialog.md new file mode 100644 index 0000000000..c638aa43b0 --- /dev/null +++ b/.changeset/top-usage-dialog.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Keep the dashboard usage dialog near the top of the viewport across desktop popover, modal, and mobile presentations. diff --git a/packages/dashboard/app/components/UsageIndicator.css b/packages/dashboard/app/components/UsageIndicator.css index c8e10600e8..e8eb63b9bf 100644 --- a/packages/dashboard/app/components/UsageIndicator.css +++ b/packages/dashboard/app/components/UsageIndicator.css @@ -621,12 +621,20 @@ resize: both; } +.usage-modal-overlay { + --overlay-padding-top: var(--space-lg); +} + .usage-modal.modal { resize: both; overflow: hidden; } @media (max-width: 768px) { + .usage-modal-overlay { + --overlay-padding-top: 0; + } + .usage-modal--popover, .usage-modal.modal { resize: none; diff --git a/packages/dashboard/app/components/UsageIndicator.tsx b/packages/dashboard/app/components/UsageIndicator.tsx index 2d219780cc..f0d9d581df 100644 --- a/packages/dashboard/app/components/UsageIndicator.tsx +++ b/packages/dashboard/app/components/UsageIndicator.tsx @@ -85,6 +85,9 @@ function getUsageColorClass(percentUsed: number): string { const HIDDEN_WINDOWS_STORAGE_KEY = "kb-usage-hidden-windows"; const MODAL_SIZE_STORAGE_KEY = "kb-usage-modal-size"; const PROVIDER_ORDER_KEY = "kb-usage-provider-order"; +const DESKTOP_POPOVER_GAP = 8; +const DESKTOP_POPOVER_TOP_INSET = DESKTOP_POPOVER_GAP * 2; +const DESKTOP_POPOVER_MAX_TOP_VIEWPORT_RATIO = 0.25; interface ModalSize { width: number; @@ -890,12 +893,19 @@ export function UsageIndicator({ isOpen, onClose, projectId, anchorRect }: Usage if (!isOpen) return null; const showDesktopPopover = Boolean(anchorRect && isDesktopViewport); - const desktopGap = 8; - const maxTopPadding = 12; const defaultPopoverWidth = 420; const popoverWidth = savedSize?.width ?? defaultPopoverWidth; const desktopTop = showDesktopPopover - ? Math.min((anchorRect?.bottom ?? 0) + desktopGap, window.innerHeight - maxTopPadding) + ? Math.max( + DESKTOP_POPOVER_TOP_INSET, + Math.min( + (anchorRect?.bottom ?? 0) + DESKTOP_POPOVER_GAP, + Math.max( + DESKTOP_POPOVER_TOP_INSET, + window.innerHeight * DESKTOP_POPOVER_MAX_TOP_VIEWPORT_RATIO + ) + ) + ) : undefined; // Anchor popover so its right edge aligns with the anchor button's right edge, // but use `left` positioning so native resize (bottom-right handle) feels natural. @@ -1052,7 +1062,7 @@ export function UsageIndicator({ isOpen, onClose, projectId, anchorRect }: Usage } return ( -
+
{usageContent}
); diff --git a/packages/dashboard/app/components/__tests__/UsageIndicator.test.tsx b/packages/dashboard/app/components/__tests__/UsageIndicator.test.tsx index a6cd1992fb..c918acd394 100644 --- a/packages/dashboard/app/components/__tests__/UsageIndicator.test.tsx +++ b/packages/dashboard/app/components/__tests__/UsageIndicator.test.tsx @@ -44,6 +44,21 @@ function createAnchorRect(partial: Partial = {}): DOMRect { const USAGE_VIEW_MODE_KEY = scopedKey("kb-usage-view-mode", TEST_PROJECT_ID); const USAGE_HIDDEN_WINDOWS_KEY = scopedKey("kb-usage-hidden-windows", TEST_PROJECT_ID); const USAGE_PROVIDER_ORDER_KEY = scopedKey("kb-usage-provider-order", TEST_PROJECT_ID); +const USAGE_MODAL_SIZE_KEY = scopedKey("kb-usage-modal-size", TEST_PROJECT_ID); + +function setViewportSize({ width, height }: { width: number; height: number }) { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: width, + }); + Object.defineProperty(window, "innerHeight", { + writable: true, + configurable: true, + value: height, + }); + window.dispatchEvent(new Event("resize")); +} function getWindowIdentity(label: string, index: number): string { return `${index}::${label}`; @@ -105,6 +120,8 @@ describe("UsageIndicator", () => { localStorage.removeItem(USAGE_VIEW_MODE_KEY); localStorage.removeItem(USAGE_HIDDEN_WINDOWS_KEY); localStorage.removeItem(USAGE_PROVIDER_ORDER_KEY); + localStorage.removeItem(USAGE_MODAL_SIZE_KEY); + setViewportSize({ width: 1024, height: 768 }); }); it("renders nothing when isOpen is false", () => { @@ -438,7 +455,7 @@ describe("UsageIndicator", () => { expect(mockOnClose).toHaveBeenCalledTimes(1); }); - it("renders as popover below anchor on desktop when anchorRect provided", () => { + it("renders as top-biased popover below a high desktop anchor", () => { mockUseUsageData.mockReturnValue(createUsageDataState({ providers: mockProviders, loading: false, @@ -459,10 +476,65 @@ describe("UsageIndicator", () => { const modal = screen.getByTestId("usage-modal") as HTMLElement; expect(modal).toHaveClass("usage-modal--popover"); expect(modal.style.top).toBe("88px"); + expect(Number.parseFloat(modal.style.top)).toBeLessThan(window.innerHeight / 2); expect(modal.style.left).toBe("520px"); }); - it("renders as full-screen modal when anchorRect is null", () => { + it("keeps the desktop popover near the top on a short viewport", () => { + setViewportSize({ width: 1024, height: 240 }); + mockUseUsageData.mockReturnValue(createUsageDataState({ + providers: mockProviders, + loading: false, + error: null, + lastUpdated: new Date(), + refresh: mockRefresh, + })); + + render( + + ); + + const modal = screen.getByTestId("usage-modal") as HTMLElement; + expect(modal).toHaveClass("usage-modal--popover"); + expect(modal.style.top).toBe("60px"); + expect(Number.parseFloat(modal.style.top)).toBeLessThan(window.innerHeight / 2); + }); + + it("clamps the desktop popover near the top for a low anchor while preserving saved size", () => { + setViewportSize({ width: 1024, height: 800 }); + localStorage.setItem(USAGE_MODAL_SIZE_KEY, JSON.stringify({ width: 600, height: 500 })); + mockUseUsageData.mockReturnValue(createUsageDataState({ + providers: mockProviders, + loading: false, + error: null, + lastUpdated: new Date(), + refresh: mockRefresh, + })); + + render( + + ); + + const modal = screen.getByTestId("usage-modal") as HTMLElement; + expect(modal).toHaveClass("usage-modal--popover"); + expect(modal.style.top).toBe("200px"); + expect(Number.parseFloat(modal.style.top)).toBeLessThan(window.innerHeight / 2); + expect(modal.style.left).toBe("340px"); + expect(modal.style.width).toBe("600px"); + expect(modal.style.height).toBe("500px"); + }); + + it("renders as top-aligned full-screen modal when anchorRect is null", () => { mockUseUsageData.mockReturnValue(createUsageDataState({ providers: mockProviders, loading: false, @@ -473,8 +545,67 @@ describe("UsageIndicator", () => { render(); - expect(screen.getByTestId("usage-modal")).toHaveClass("modal"); - expect(screen.getByTestId("usage-modal")).not.toHaveClass("usage-modal--popover"); + const overlay = screen.getByTestId("usage-modal-overlay"); + const modal = screen.getByTestId("usage-modal"); + expect(overlay).toHaveClass("modal-overlay", "open", "usage-modal-overlay"); + expect(modal).toHaveClass("modal"); + expect(modal).not.toHaveClass("usage-modal--popover"); + expect(modal.parentElement).toBe(overlay); + }); + + it("uses the top-aligned mobile sheet surface instead of the desktop popover", () => { + setViewportSize({ width: 768, height: 600 }); + mockUseUsageData.mockReturnValue(createUsageDataState({ + providers: mockProviders, + loading: false, + error: null, + lastUpdated: new Date(), + refresh: mockRefresh, + })); + + render( + + ); + + const overlay = screen.getByTestId("usage-modal-overlay"); + const modal = screen.getByTestId("usage-modal") as HTMLElement; + expect(overlay).toHaveClass("usage-modal-overlay"); + expect(modal).toHaveClass("usage-modal", "modal"); + expect(modal).not.toHaveClass("usage-modal--popover"); + expect(modal.style.top).toBe(""); + }); + + it.each([ + ["populated", createUsageDataState({ providers: mockProviders, loading: false, error: null, lastUpdated: new Date(), refresh: mockRefresh }), "Anthropic"], + ["empty", createUsageDataState({ providers: [], loading: false, error: null, lastUpdated: null, refresh: mockRefresh }), "No AI providers configured"], + ["loading", createUsageDataState({ providers: [], loading: true, error: null, lastUpdated: null, hasFetched: false, refresh: mockRefresh }), null], + ["error", createUsageDataState({ providers: [], loading: false, error: "Failed to fetch usage data", lastUpdated: null, refresh: mockRefresh }), "Failed to load usage data"], + ])("keeps top-biased popover positioning for %s content", (_stateName, usageState, expectedText) => { + setViewportSize({ width: 1024, height: 240 }); + mockUseUsageData.mockReturnValue(usageState); + + render( + + ); + + const modal = screen.getByTestId("usage-modal") as HTMLElement; + expect(modal).toHaveClass("usage-modal--popover"); + expect(modal.style.top).toBe("60px"); + if (expectedText) { + expect(screen.getByText(expectedText)).toBeInTheDocument(); + } else { + expect(document.querySelector(".usage-skeleton")).toBeInTheDocument(); + } }); it("calls onClose when overlay is clicked", () => {