diff --git a/apps/apollo-vertex/content/docs/components/sonner.mdx b/apps/apollo-vertex/content/docs/components/sonner.mdx index 88aaf3620f..db2ab19ff4 100644 --- a/apps/apollo-vertex/content/docs/components/sonner.mdx +++ b/apps/apollo-vertex/content/docs/components/sonner.mdx @@ -19,14 +19,14 @@ Add the `` component once to your root route. Status colors, icons, c ```tsx import { createRootRoute, Outlet } from "@tanstack/react-router" -import { Toaster } from "@uipath/apollo-vertex" +import { ThemeProvider, Toaster } from "@uipath/apollo-vertex" export const Route = createRootRoute({ component: () => ( - <> + - + ), }) ``` @@ -66,7 +66,7 @@ toast.error("Upload failed", { | `position` | `ToasterProps["position"]` | `"top-center"` | Position of the toast stack on screen | | `duration` | `number` | `5000` | Auto-dismiss delay in milliseconds | | `closeButton` | `boolean` | `true` | Show a close button on each toast | -| `theme` | `"light" \| "dark" \| "system"` | `"system"` | Color theme. Defaults to the current app theme via `next-themes` | +| `theme` | `"light" \| "dark" \| "system"` | shell `resolvedTheme` | Ignored. The toaster always uses the shell resolved theme from `ThemeProvider`. | | `toastOptions` | `ToasterProps["toastOptions"]` | — | Global options applied to all toasts (classNames, style, etc.) | ## Toast function options diff --git a/apps/apollo-vertex/content/docs/guidelines/notifications/notification-examples.tsx b/apps/apollo-vertex/content/docs/guidelines/notifications/notification-examples.tsx index 936d8df888..05021079e9 100644 --- a/apps/apollo-vertex/content/docs/guidelines/notifications/notification-examples.tsx +++ b/apps/apollo-vertex/content/docs/guidelines/notifications/notification-examples.tsx @@ -13,62 +13,65 @@ import { Alert, AlertDescription, AlertTitle } from "@/registry/alert/alert"; import { Badge } from "@/registry/badge/badge"; import { Button } from "@/registry/button/button"; import { Toaster } from "@/registry/sonner/sonner"; +import { ShellThemeProvider } from "../../../../templates/shell/ShellThemeProvider"; export function SonnerExamples() { return ( -
- -
- - - - + +
+ +
+ + + + +
+

+ Click each button to trigger a toast at the top-center of the screen. + Toasts auto-dismiss after 5 seconds. +

-

- Click each button to trigger a toast at the top-center of the screen. - Toasts auto-dismiss after 5 seconds. -

-
+ ); } diff --git a/packages/apollo-vertex/package.json b/packages/apollo-vertex/package.json index 52def91279..62fa2115d6 100644 --- a/packages/apollo-vertex/package.json +++ b/packages/apollo-vertex/package.json @@ -85,7 +85,6 @@ "input-otp": "^1.4.2", "lucide-react": "^0.577.0", "luxon": "^3.7.1", - "next-themes": "^0.4.6", "react-day-picker": "^9.13.0", "react-joyride": "^3.0.2", "react-resizable-panels": "^4.7.3", diff --git a/packages/apollo-vertex/registry.json b/packages/apollo-vertex/registry.json index f211814906..9b06396052 100644 --- a/packages/apollo-vertex/registry.json +++ b/packages/apollo-vertex/registry.json @@ -2704,7 +2704,8 @@ "type": "registry:ui", "title": "Sonner", "description": "An opinionated toast component for React.", - "dependencies": ["sonner", "next-themes"], + "dependencies": ["sonner"], + "registryDependencies": ["@uipath/shell"], "cssVars": { "theme": { "color-info": "var(--info)", diff --git a/packages/apollo-vertex/registry/shell/shell-theme-provider.tsx b/packages/apollo-vertex/registry/shell/shell-theme-provider.tsx index d312ab0f1f..ecbca6168d 100644 --- a/packages/apollo-vertex/registry/shell/shell-theme-provider.tsx +++ b/packages/apollo-vertex/registry/shell/shell-theme-provider.tsx @@ -72,6 +72,22 @@ function isValidTheme(value: string | null): value is Theme { return value === "light" || value === "dark" || value === "system"; } +function useSystemDark() { + return useSyncExternalStore( + (cb) => { + const mq = window.matchMedia("(prefers-color-scheme: dark)"); + mq.addEventListener("change", cb); + return () => mq.removeEventListener("change", cb); + }, + () => window.matchMedia("(prefers-color-scheme: dark)").matches, + () => false, + ); +} + +function resolveSystemTheme(systemDark: boolean): "light" | "dark" { + return systemDark ? "dark" : "light"; +} + function applyThemeClass(resolved: "light" | "dark") { const root = window.document.documentElement; root.classList.remove("light", "dark"); @@ -117,18 +133,10 @@ export function ThemeProvider({ return isValidTheme(stored) ? stored : defaultTheme; }); - const systemDark = useSyncExternalStore( - (cb) => { - const mq = window.matchMedia("(prefers-color-scheme: dark)"); - mq.addEventListener("change", cb); - return () => mq.removeEventListener("change", cb); - }, - () => window.matchMedia("(prefers-color-scheme: dark)").matches, - () => false, - ); + const systemDark = useSystemDark(); const resolvedTheme = - theme === "system" ? (systemDark ? "dark" : "light") : theme; + theme === "system" ? resolveSystemTheme(systemDark) : theme; const setTheme = (newTheme: Theme) => { localStorage.setItem(storageKey, newTheme); diff --git a/packages/apollo-vertex/registry/sonner/sonner.test.tsx b/packages/apollo-vertex/registry/sonner/sonner.test.tsx new file mode 100644 index 0000000000..edc0eeeb58 --- /dev/null +++ b/packages/apollo-vertex/registry/sonner/sonner.test.tsx @@ -0,0 +1,75 @@ +import { cleanup, fireEvent, render } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { THEME_STORAGE_KEY } from "@/registry/shell/shell-constants"; +import { ThemeProvider, useTheme } from "@/registry/shell/shell-theme-provider"; + +import { Toaster } from "./sonner"; + +vi.mock("sonner", () => ({ + Toaster: ({ theme }: { theme?: string }) => ( +
+ ), +})); + +const darkLabel = "use dark"; + +function installMatchMedia(matches: boolean) { + Object.defineProperty(window, "matchMedia", { + writable: true, + configurable: true, + value: () => ({ + matches, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + }), + }); +} + +function ThemeToggle() { + const { setTheme } = useTheme(); + return ( + + ); +} + +describe("Toaster", () => { + beforeEach(() => { + installMatchMedia(false); + localStorage.clear(); + }); + + afterEach(() => { + cleanup(); + localStorage.clear(); + }); + + it("passes the shell resolved theme to Sonner", () => { + const { getByRole, getByTestId } = render( + + + + , + ); + + expect(getByTestId("sonner").dataset.theme).toBe("light"); + + fireEvent.click(getByRole("button", { name: darkLabel })); + + expect(getByTestId("sonner").dataset.theme).toBe("dark"); + }); + + it("resolves a system shell theme to light or dark", () => { + installMatchMedia(true); + localStorage.setItem(THEME_STORAGE_KEY, "system"); + + const { getByTestId } = render( + + + , + ); + + expect(getByTestId("sonner").dataset.theme).toBe("dark"); + }); +}); diff --git a/packages/apollo-vertex/registry/sonner/sonner.tsx b/packages/apollo-vertex/registry/sonner/sonner.tsx index 0f4e7af995..2a73a7c22f 100644 --- a/packages/apollo-vertex/registry/sonner/sonner.tsx +++ b/packages/apollo-vertex/registry/sonner/sonner.tsx @@ -7,9 +7,10 @@ import { OctagonXIcon, TriangleAlertIcon, } from "lucide-react"; -import { useTheme } from "next-themes"; import { Toaster as Sonner, type ToasterProps } from "sonner"; +import { useTheme } from "@/registry/shell/shell-theme-provider"; + // Shared icon alignment: top-align icons with the first line of toast text. const iconAlign = "[&>[data-icon]]:!items-start [&>[data-icon]>svg]:!mt-0.5"; @@ -35,16 +36,12 @@ export const apolloToastClassNames: NonNullable< "!left-auto !right-2 !top-2 !transform-none !border-none !bg-transparent !rounded-sm", }; -const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme(); +const Toaster = ({ theme: _theme, ...props }: ToasterProps) => { + const { resolvedTheme } = useTheme(); return (