diff --git a/studio/frontend/src/index.css b/studio/frontend/src/index.css index dc73112994..7afa87f597 100644 --- a/studio/frontend/src/index.css +++ b/studio/frontend/src/index.css @@ -1172,11 +1172,18 @@ mix-blend-mode: normal; } -/* Override sonner's hardcoded top: 0 on the toast close button. */ +/* Override sonner top: 0 and pin to theme tokens (--gray2 hover ignores data-sonner-theme). */ [data-sonner-toast][data-styled="true"] [data-close-button] { top: 8px !important; + background: var(--popover) !important; + color: var(--popover-foreground) !important; + border-color: var(--border) !important; } -/* Bump the X stroke so it stays visible against dark backgrounds. */ [data-sonner-toast][data-styled="true"] [data-close-button] svg { stroke-width: 2.25; } +[data-sonner-toast][data-styled="true"]:hover [data-close-button]:hover { + background: var(--muted) !important; + color: var(--popover-foreground) !important; + border-color: var(--border) !important; +} diff --git a/studio/frontend/src/lib/toast.ts b/studio/frontend/src/lib/toast.ts index 138ceb4c4d..6b1635b42e 100644 --- a/studio/frontend/src/lib/toast.ts +++ b/studio/frontend/src/lib/toast.ts @@ -1,59 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 -// sonner `toast` wrapper that defaults `dismissible: false` so swipe -// capture doesn't block text selection. Drop-in for `from "sonner"`. - -import { toast as sonnerToast, type ExternalToast } from "sonner"; - -type AnyFn = (...args: unknown[]) => unknown; - -function withDismissibleFalse(fn: F): F { - return ((...args: unknown[]) => { - // Branch by arity: React-element messages are objects too. - if (args.length <= 1) { - args.push({ dismissible: false } satisfies ExternalToast); - } else { - const lastIdx = args.length - 1; - const last = args[lastIdx]; - if (last && typeof last === "object" && !Array.isArray(last)) { - const opts = last as ExternalToast; - if (!("dismissible" in opts)) { - args[lastIdx] = { dismissible: false, ...opts }; - } - } - } - return fn(...args); - }) as F; -} - -const wrappedCallable = withDismissibleFalse( - sonnerToast as unknown as AnyFn, -) as typeof sonnerToast; - -// `promise(p, data?)` carries `dismissible` at the top of `data`, -// covering loading / success / error states. `dismiss`, `getHistory`, -// `getToasts` take no options. -const wrappedPromise: typeof sonnerToast.promise = ((promise, data) => { - const merged = - data && typeof data === "object" && !("dismissible" in data) - ? { dismissible: false, ...data } - : (data ?? { dismissible: false }); - return sonnerToast.promise(promise, merged); -}) as typeof sonnerToast.promise; - -export const toast: typeof sonnerToast = Object.assign(wrappedCallable, { - success: withDismissibleFalse(sonnerToast.success.bind(sonnerToast) as AnyFn) as typeof sonnerToast.success, - info: withDismissibleFalse(sonnerToast.info.bind(sonnerToast) as AnyFn) as typeof sonnerToast.info, - warning: withDismissibleFalse(sonnerToast.warning.bind(sonnerToast) as AnyFn) as typeof sonnerToast.warning, - error: withDismissibleFalse(sonnerToast.error.bind(sonnerToast) as AnyFn) as typeof sonnerToast.error, - message: withDismissibleFalse(sonnerToast.message.bind(sonnerToast) as AnyFn) as typeof sonnerToast.message, - loading: withDismissibleFalse(sonnerToast.loading.bind(sonnerToast) as AnyFn) as typeof sonnerToast.loading, - custom: withDismissibleFalse(sonnerToast.custom.bind(sonnerToast) as AnyFn) as typeof sonnerToast.custom, - promise: wrappedPromise, - dismiss: sonnerToast.dismiss.bind(sonnerToast) as typeof sonnerToast.dismiss, - getHistory: sonnerToast.getHistory.bind(sonnerToast) as typeof sonnerToast.getHistory, - getToasts: sonnerToast.getToasts.bind(sonnerToast) as typeof sonnerToast.getToasts, -}); +// Re-export of sonner. Swipe blocking lives on the Toaster via +// `swipeDirections={[]}`, so no per-toast dismissible override. +export { toast } from "sonner"; export type { ExternalToast } from "sonner";