wacrm/src/components/themed-toaster.tsx
Arnas Donauskas 1474657d03
fix(theme): light-mode polish from review pass
Self-review of the light/dark sweep surfaced neutral colors that lived
in raw inline-style / <style jsx> / gradient values (so the className
sweep correctly skipped them) and a few contrast/affordance nits:

- reply-quote: adapt to the outbound (primary) bubble via onPrimary so
  the quote/author stay legible on the violet fill in light mode; wire
  it from message-bubble.
- dashboard SVG chrome -> tokens: conversations-chart gridline/crosshair,
  pipeline-donut background ring, automation-builder dot-grid gradient.
- members-tab viewer chip: bg-card -> bg-transparent (outline) so it
  doesn't blend into a card surface in light mode.
- tag-manager / pipeline-settings color-swatch selection rings: raw
  slate/white -> var(--popover)/var(--foreground) so they flip.
- pipeline-board scrollbar (<style jsx>): slate rgb -> var tokens.
- activity-feed zebra stripe: bg-card/40 -> bg-muted/40 (survives light).
- forgot-password / import-modal: restore real hover (dead no-op hovers).
- themed-toaster: gate theme via useSyncExternalStore to avoid a
  hydration mismatch on sonner's theme attribute (no setState-in-effect).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 09:28:22 +03:00

52 lines
1.5 KiB
TypeScript

"use client";
import { useSyncExternalStore } from "react";
import { Toaster } from "sonner";
import { useTheme } from "@/hooks/use-theme";
import { DEFAULT_MODE } from "@/lib/themes";
// Returns false during SSR and the first hydration render, true after —
// the sanctioned (warning-free, no setState-in-effect) way to diverge
// server vs client. Lets us match the server-rendered default on first
// paint, then adopt the real mode.
const noopSubscribe = () => () => {};
function useIsClient() {
return useSyncExternalStore(
noopSubscribe,
() => true,
() => false,
);
}
/**
* Toaster wrapper that tracks the active light/dark mode.
*
* Lives inside <ThemeProvider> (see layout.tsx) so it can read the
* current mode and hand it to sonner. Colors are driven off the same
* CSS tokens as the rest of the app, so a toast looks at home in
* either mode without a second palette to maintain.
*
* The theme is gated behind `useIsClient`: the server renders
* DEFAULT_MODE, so first client paint must too, otherwise a light-mode
* user hydrates with a different sonner `theme` attribute than the
* server emitted and React logs a hydration mismatch.
*/
export function ThemedToaster() {
const { mode } = useTheme();
const isClient = useIsClient();
return (
<Toaster
theme={isClient ? mode : DEFAULT_MODE}
position="top-right"
toastOptions={{
style: {
background: "var(--popover)",
border: "1px solid var(--border)",
color: "var(--popover-foreground)",
},
}}
/>
);
}