From 944d4628cf021b70e0a339817aa178218c2273bb Mon Sep 17 00:00:00 2001
From: ComputelessComputer
<63365510+ComputelessComputer@users.noreply.github.com>
Date: Mon, 1 Jun 2026 12:17:56 +0900
Subject: [PATCH] fix(toast): raise sidebar toast placement
Move the upgrade toast higher when the left sidebar timeline is active and cover the placement behavior with focused tests.
---
apps/desktop/src/main/shell-frame.test.tsx | 14 +-
apps/desktop/src/main/shell-frame.tsx | 2 +-
apps/desktop/src/sidebar/toast/index.test.tsx | 214 ++++++++++++++++++
apps/desktop/src/sidebar/toast/index.tsx | 98 +++++++-
4 files changed, 318 insertions(+), 10 deletions(-)
create mode 100644 apps/desktop/src/sidebar/toast/index.test.tsx
diff --git a/apps/desktop/src/main/shell-frame.test.tsx b/apps/desktop/src/main/shell-frame.test.tsx
index aeeaa0294d..7faa4b5dcf 100644
--- a/apps/desktop/src/main/shell-frame.test.tsx
+++ b/apps/desktop/src/main/shell-frame.test.tsx
@@ -47,7 +47,9 @@ vi.mock("~/shared/config", () => ({
}));
vi.mock("~/sidebar/toast", () => ({
- ToastArea: () =>
,
+ ToastArea: ({ placement }: { placement?: "default" | "left-sidebar" }) => (
+
+ ),
}));
vi.mock("~/store/zustand/tabs", () => ({
@@ -72,7 +74,9 @@ describe("ClassicMainShellFrame", () => {
it("uses top-edge main surface chrome in top timeline mode", () => {
render();
- expect(screen.getByTestId("toast-area")).toBeTruthy();
+ expect(
+ screen.getByTestId("toast-area").getAttribute("data-placement"),
+ ).toBe("default");
expect(
screen
.getByTestId("main-shell-scaffold")
@@ -85,6 +89,9 @@ describe("ClassicMainShellFrame", () => {
render();
+ expect(
+ screen.getByTestId("toast-area").getAttribute("data-placement"),
+ ).toBe("left-sidebar");
expect(
screen
.getByTestId("main-shell-scaffold")
@@ -98,6 +105,9 @@ describe("ClassicMainShellFrame", () => {
render();
+ expect(
+ screen.getByTestId("toast-area").getAttribute("data-placement"),
+ ).toBe("default");
expect(
screen
.getByTestId("main-shell-scaffold")
diff --git a/apps/desktop/src/main/shell-frame.tsx b/apps/desktop/src/main/shell-frame.tsx
index 7571219824..d352b2f2d4 100644
--- a/apps/desktop/src/main/shell-frame.tsx
+++ b/apps/desktop/src/main/shell-frame.tsx
@@ -53,7 +53,7 @@ export function ClassicMainShellFrame() {
-
+
);
}
diff --git a/apps/desktop/src/sidebar/toast/index.test.tsx b/apps/desktop/src/sidebar/toast/index.test.tsx
new file mode 100644
index 0000000000..94556d7404
--- /dev/null
+++ b/apps/desktop/src/sidebar/toast/index.test.tsx
@@ -0,0 +1,214 @@
+import { act, cleanup, render, screen } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+const mocks = vi.hoisted(() => ({
+ signIn: vi.fn(),
+ dismissToast: vi.fn(),
+ openNew: vi.fn(),
+ updateSettingsTabState: vi.fn(),
+ clearDevtoolsPreview: vi.fn(),
+ setToastActionTarget: vi.fn(),
+}));
+
+vi.mock("~/auth", () => ({
+ useAuth: () => ({
+ session: null,
+ signIn: mocks.signIn,
+ }),
+}));
+
+vi.mock("~/contexts/notifications", () => ({
+ useNotifications: () => ({
+ hasActiveDownload: false,
+ downloadProgress: null,
+ downloadingModel: null,
+ activeDownloads: [],
+ localSttStatus: null,
+ isLocalSttModel: false,
+ }),
+}));
+
+vi.mock("~/shared/config", () => ({
+ useConfigValues: () => ({
+ current_llm_provider: "local",
+ current_llm_model: "model",
+ current_stt_provider: "local",
+ current_stt_model: "model",
+ }),
+}));
+
+vi.mock("~/store/zustand/devtools-toast-preview", () => ({
+ useDevtoolsToastPreview: (
+ selector: (state: { preview: null; clearPreview: () => void }) => unknown,
+ ) =>
+ selector({
+ preview: null,
+ clearPreview: mocks.clearDevtoolsPreview,
+ }),
+}));
+
+vi.mock("~/store/zustand/tabs", () => ({
+ useTabs: (
+ selector: (state: {
+ currentTab: { type: string };
+ openNew: () => void;
+ updateSettingsTabState: () => void;
+ }) => unknown,
+ ) =>
+ selector({
+ currentTab: { type: "empty" },
+ openNew: mocks.openNew,
+ updateSettingsTabState: mocks.updateSettingsTabState,
+ }),
+}));
+
+vi.mock("~/store/zustand/toast-action", () => ({
+ useToastAction: (
+ selector: (state: { setTarget: (target: "stt" | null) => void }) => unknown,
+ ) => selector({ setTarget: mocks.setToastActionTarget }),
+}));
+
+vi.mock("./useDismissedToasts", () => ({
+ useDismissedToasts: () => ({
+ dismissToast: mocks.dismissToast,
+ isDismissed: () => false,
+ }),
+}));
+
+import { ToastArea } from "./index";
+
+describe("ToastArea", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ mocks.signIn.mockClear();
+ mocks.dismissToast.mockClear();
+ mocks.openNew.mockClear();
+ mocks.updateSettingsTabState.mockClear();
+ mocks.clearDevtoolsPreview.mockClear();
+ mocks.setToastActionTarget.mockClear();
+ });
+
+ afterEach(() => {
+ cleanup();
+ document.body.innerHTML = "";
+ vi.useRealTimers();
+ });
+
+ it("keeps the default toast placement fixed to the existing top timeline position", () => {
+ render();
+
+ act(() => {
+ vi.advanceTimersByTime(500);
+ });
+
+ const toastContainer = screen
+ .getByText("Pro features available")
+ .closest(".fixed") as HTMLElement | null;
+
+ expect(toastContainer?.style.left).toBe("calc(50% + 0px)");
+ expect(toastContainer?.style.top).toBe("56px");
+ });
+
+ it("positions the left sidebar toast relative to the main white surface", () => {
+ const mainSurface = document.createElement("div");
+ mainSurface.setAttribute("data-chat-floating-anchor", "");
+ vi.spyOn(mainSurface, "getBoundingClientRect").mockReturnValue({
+ bottom: 520,
+ height: 500,
+ left: 200,
+ right: 800,
+ top: 20,
+ width: 600,
+ x: 200,
+ y: 20,
+ toJSON: () => ({}),
+ });
+ document.body.appendChild(mainSurface);
+
+ render();
+
+ act(() => {
+ vi.advanceTimersByTime(500);
+ });
+
+ const toastContainer = screen
+ .getByText("Pro features available")
+ .closest(".fixed") as HTMLElement | null;
+
+ expect(toastContainer?.style.left).toBe("500px");
+ expect(toastContainer?.style.top).toBe("56px");
+ });
+
+ it("repositions the left sidebar toast when the main surface scrolls", () => {
+ const mainSurface = document.createElement("div");
+ mainSurface.setAttribute("data-chat-floating-anchor", "");
+ let top = 20;
+
+ vi.spyOn(mainSurface, "getBoundingClientRect").mockImplementation(() => ({
+ bottom: top + 500,
+ height: 500,
+ left: 200,
+ right: 800,
+ top,
+ width: 600,
+ x: 200,
+ y: top,
+ toJSON: () => ({}),
+ }));
+ document.body.appendChild(mainSurface);
+
+ render();
+
+ act(() => {
+ vi.advanceTimersByTime(500);
+ });
+
+ const toastContainer = screen
+ .getByText("Pro features available")
+ .closest(".fixed") as HTMLElement | null;
+
+ expect(toastContainer?.style.top).toBe("56px");
+
+ act(() => {
+ top = 52;
+ window.dispatchEvent(new Event("scroll"));
+ });
+
+ expect(toastContainer?.style.top).toBe("88px");
+ });
+
+ it("uses fallback placement immediately when left sidebar anchoring is disabled", () => {
+ const mainSurface = document.createElement("div");
+ mainSurface.setAttribute("data-chat-floating-anchor", "");
+ vi.spyOn(mainSurface, "getBoundingClientRect").mockReturnValue({
+ bottom: 520,
+ height: 500,
+ left: 200,
+ right: 800,
+ top: 20,
+ width: 600,
+ x: 200,
+ y: 20,
+ toJSON: () => ({}),
+ });
+ document.body.appendChild(mainSurface);
+
+ const { rerender } = render();
+
+ act(() => {
+ vi.advanceTimersByTime(500);
+ });
+
+ const toastContainer = screen
+ .getByText("Pro features available")
+ .closest(".fixed") as HTMLElement | null;
+
+ expect(toastContainer?.style.left).toBe("500px");
+ expect(toastContainer?.style.top).toBe("56px");
+
+ rerender();
+
+ expect(toastContainer?.style.left).toBe("calc(50% + 0px)");
+ expect(toastContainer?.style.top).toBe("56px");
+ });
+});
diff --git a/apps/desktop/src/sidebar/toast/index.tsx b/apps/desktop/src/sidebar/toast/index.tsx
index 5bf4220200..dcd34d197f 100644
--- a/apps/desktop/src/sidebar/toast/index.tsx
+++ b/apps/desktop/src/sidebar/toast/index.tsx
@@ -1,5 +1,5 @@
import { AnimatePresence, motion } from "motion/react";
-import { useCallback, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { cn } from "@hypr/utils";
@@ -20,11 +20,28 @@ import { useDevtoolsToastPreview } from "~/store/zustand/devtools-toast-preview"
import { useTabs } from "~/store/zustand/tabs";
import { useToastAction } from "~/store/zustand/toast-action";
-export function ToastArea() {
+type ToastAreaPlacement = "default" | "left-sidebar";
+type ToastAreaPosition = {
+ left: number | string;
+ top: number;
+};
+
+const DEFAULT_TOP_OFFSET_PX = 56;
+const LEFT_SIDEBAR_TOP_OFFSET_PX = 36;
+const MAIN_SURFACE_SELECTOR = "[data-chat-floating-anchor]";
+
+export function ToastArea({
+ placement = "default",
+}: {
+ placement?: ToastAreaPlacement;
+}) {
const auth = useAuth();
const { dismissToast, isDismissed } = useDismissedToasts();
const shouldShowToast = useShouldShowToast();
const contentOffset = useMainContentCenterOffset();
+ const mainSurfacePosition = useMainSurfaceToastPosition(
+ placement === "left-sidebar",
+ );
const {
hasActiveDownload,
downloadProgress,
@@ -177,6 +194,12 @@ export function ToastArea() {
: displayToast?.id;
const dismissAction = displayToast?.dismissible ? handleDismiss : undefined;
+ const position =
+ mainSurfacePosition ??
+ getFallbackPosition({
+ contentOffset,
+ placement,
+ });
if (!shouldShowToast || !displayToast) {
return null;
@@ -191,12 +214,10 @@ export function ToastArea() {
exit={{ opacity: 0, y: -20, scale: 0.95 }}
transition={{ duration: 0.2, ease: "easeOut" }}
style={{
- left: `calc(50% + ${contentOffset}px)`,
+ left: position.left,
+ top: position.top,
}}
- className={cn([
- "fixed top-14 z-40 -translate-x-1/2",
- "pointer-events-none",
- ])}
+ className={cn(["fixed z-40 -translate-x-1/2", "pointer-events-none"])}
>
@@ -261,3 +282,66 @@ function useMainContentCenterOffset() {
return contentOffset;
}
+
+function useMainSurfaceToastPosition(enabled: boolean) {
+ const [position, setPosition] = useState(null);
+
+ useEffect(() => {
+ if (!enabled) {
+ setPosition(null);
+ return;
+ }
+
+ const computePosition = () => {
+ const mainSurface = document.querySelector(MAIN_SURFACE_SELECTOR);
+ if (!mainSurface) {
+ setPosition(null);
+ return;
+ }
+
+ const rect = mainSurface.getBoundingClientRect();
+ setPosition({
+ left: rect.left + rect.width / 2,
+ top: rect.top + LEFT_SIDEBAR_TOP_OFFSET_PX,
+ });
+ };
+
+ computePosition();
+ window.addEventListener("resize", computePosition);
+ window.addEventListener("scroll", computePosition, true);
+
+ const resizeObserver =
+ typeof ResizeObserver !== "undefined"
+ ? new ResizeObserver(computePosition)
+ : null;
+
+ const mainSurface = document.querySelector(MAIN_SURFACE_SELECTOR);
+ if (mainSurface) {
+ resizeObserver?.observe(mainSurface);
+ }
+
+ return () => {
+ window.removeEventListener("resize", computePosition);
+ window.removeEventListener("scroll", computePosition, true);
+ resizeObserver?.disconnect();
+ };
+ }, [enabled]);
+
+ return enabled ? position : null;
+}
+
+function getFallbackPosition({
+ contentOffset,
+ placement,
+}: {
+ contentOffset: number;
+ placement: ToastAreaPlacement;
+}): ToastAreaPosition {
+ return {
+ left: `calc(50% + ${contentOffset}px)`,
+ top:
+ placement === "left-sidebar"
+ ? LEFT_SIDEBAR_TOP_OFFSET_PX
+ : DEFAULT_TOP_OFFSET_PX,
+ };
+}