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
This commit is contained in:
gsxdsm
2026-06-11 20:14:54 -07:00
parent ac342f5012
commit 40cb0d3d45
4 changed files with 162 additions and 8 deletions

View File

@@ -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.

View File

@@ -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;

View File

@@ -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 (
<div className="modal-overlay open" onClick={handleOverlayClick} data-testid="usage-modal-overlay">
<div className="modal-overlay open usage-modal-overlay" onClick={handleOverlayClick} data-testid="usage-modal-overlay">
{usageContent}
</div>
);

View File

@@ -44,6 +44,21 @@ function createAnchorRect(partial: Partial<DOMRect> = {}): 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(
<UsageIndicator
isOpen={true}
onClose={mockOnClose}
projectId={TEST_PROJECT_ID}
anchorRect={createAnchorRect()}
/>
);
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(
<UsageIndicator
isOpen={true}
onClose={mockOnClose}
projectId={TEST_PROJECT_ID}
anchorRect={createAnchorRect({ top: 620, bottom: 650 })}
/>
);
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(<UsageIndicator isOpen={true} onClose={mockOnClose} projectId={TEST_PROJECT_ID} anchorRect={null} />);
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(
<UsageIndicator
isOpen={true}
onClose={mockOnClose}
projectId={TEST_PROJECT_ID}
anchorRect={createAnchorRect()}
/>
);
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(
<UsageIndicator
isOpen={true}
onClose={mockOnClose}
projectId={TEST_PROJECT_ID}
anchorRect={createAnchorRect({ top: 180, bottom: 210 })}
/>
);
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", () => {