Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/apollo-vertex/content/docs/components/sonner.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ Add the `<Toaster />` 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: () => (
<>
<ThemeProvider>
<Outlet />
<Toaster />
</>
</ThemeProvider>
),
})
```
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="space-y-3">
<Toaster />
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
className="border-info text-info dark:text-info bg-transparent hover:bg-info/15"
onClick={() =>
toast.info("Background sync complete", {
description: "All data is up to date.",
})
}
>
<InfoIcon className="h-4 w-4" /> Info
</Button>
<Button
variant="outline"
className="border-success text-success dark:text-success bg-transparent hover:bg-success/10"
onClick={() =>
toast.success("Changes saved", {
description: "Your updates have been applied.",
})
}
>
<CircleCheckIcon className="h-4 w-4" /> Success
</Button>
<Button
variant="outline"
className="border-warning text-warning-foreground dark:text-warning bg-transparent hover:bg-warning/15"
onClick={() =>
toast.warning("Storage almost full", {
description: "Consider removing unused files.",
})
}
>
<TriangleAlertIcon className="h-4 w-4" /> Warning
</Button>
<Button
variant="outline"
className="border-destructive text-destructive dark:text-destructive bg-transparent hover:bg-destructive/10"
onClick={() =>
toast.error("Upload failed", {
description: "Please check your connection and retry.",
})
}
>
<OctagonXIcon className="h-4 w-4" /> Error
</Button>
<ShellThemeProvider>
<div className="space-y-3">
<Toaster />
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
className="border-info text-info dark:text-info bg-transparent hover:bg-info/15"
onClick={() =>
toast.info("Background sync complete", {
description: "All data is up to date.",
})
}
>
<InfoIcon className="h-4 w-4" /> Info
</Button>
<Button
variant="outline"
className="border-success text-success dark:text-success bg-transparent hover:bg-success/10"
onClick={() =>
toast.success("Changes saved", {
description: "Your updates have been applied.",
})
}
>
<CircleCheckIcon className="h-4 w-4" /> Success
</Button>
<Button
variant="outline"
className="border-warning text-warning-foreground dark:text-warning bg-transparent hover:bg-warning/15"
onClick={() =>
toast.warning("Storage almost full", {
description: "Consider removing unused files.",
})
}
>
<TriangleAlertIcon className="h-4 w-4" /> Warning
</Button>
<Button
variant="outline"
className="border-destructive text-destructive dark:text-destructive bg-transparent hover:bg-destructive/10"
onClick={() =>
toast.error("Upload failed", {
description: "Please check your connection and retry.",
})
}
>
<OctagonXIcon className="h-4 w-4" /> Error
</Button>
</div>
<p className="text-xs text-muted-foreground">
Click each button to trigger a toast at the top-center of the screen.
Toasts auto-dismiss after 5 seconds.
</p>
</div>
<p className="text-xs text-muted-foreground">
Click each button to trigger a toast at the top-center of the screen.
Toasts auto-dismiss after 5 seconds.
</p>
</div>
</ShellThemeProvider>
);
}

Expand Down
1 change: 0 additions & 1 deletion packages/apollo-vertex/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion packages/apollo-vertex/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
28 changes: 18 additions & 10 deletions packages/apollo-vertex/registry/shell/shell-theme-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down
75 changes: 75 additions & 0 deletions packages/apollo-vertex/registry/sonner/sonner.test.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<div data-testid="sonner" data-theme={theme} />
),
}));

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 (
<button type="button" onClick={() => setTheme("dark")}>
{darkLabel}
</button>
);
}

describe("Toaster", () => {
beforeEach(() => {
installMatchMedia(false);
localStorage.clear();
});

afterEach(() => {
cleanup();
localStorage.clear();
});

it("passes the shell resolved theme to Sonner", () => {
const { getByRole, getByTestId } = render(
<ThemeProvider>
<ThemeToggle />
<Toaster />
</ThemeProvider>,
);

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(
<ThemeProvider>
<Toaster theme="light" />
</ThemeProvider>,
);

expect(getByTestId("sonner").dataset.theme).toBe("dark");
});
});
13 changes: 5 additions & 8 deletions packages/apollo-vertex/registry/sonner/sonner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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 (
<Sonner
theme={
theme === "light" || theme === "dark" || theme === "system"
? theme
: "system"
}
theme={resolvedTheme}
className="toaster group"
position="top-center"
duration={5000}
Expand Down
3 changes: 0 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading