mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
fix: theme toggle
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
@import "tailwindcss";
|
||||
@import "@edr/ui-common/theme.css" layer(theme);
|
||||
|
||||
/* The app toggles dark mode by setting the `dark` class on <html> (see
|
||||
main.tsx / FreightDashboardLayout). Without this, Tailwind v4 compiles
|
||||
`dark:` utilities to `@media (prefers-color-scheme: dark)` and they follow
|
||||
the OS setting instead of the in-app toggle. */
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
/* The backoffice is light-only — there is no theme toggle and nothing sets the
|
||||
`dark` class. This override is load-bearing: without it Tailwind v4 compiles
|
||||
`dark:` utilities to `@media (prefers-color-scheme: dark)`, so every
|
||||
leftover `dark:` class (the vendored IAM UI is full of them) would activate
|
||||
on an OS-level dark setting. Binding the variant to a class that is never
|
||||
rendered keeps those utilities inert no matter what the OS says. */
|
||||
@custom-variant dark (&:where(.edr-dark-disabled));
|
||||
|
||||
/* Bridge the central Mantine theme into Tailwind. freightMantineTheme
|
||||
(createTheme) is the single source of truth; these just alias its generated
|
||||
|
||||
@@ -144,7 +144,6 @@ const DashboardShell = () => {
|
||||
sidebarSections={sidebarSections}
|
||||
activeHref={location.pathname}
|
||||
onNavigate={navigate}
|
||||
enableThemeToggle
|
||||
userName={displayName}
|
||||
userEmail={user?.email}
|
||||
onLogout={logout}
|
||||
|
||||
@@ -15,9 +15,7 @@ import {
|
||||
FileSignature,
|
||||
Languages,
|
||||
LogOut,
|
||||
Moon,
|
||||
Search,
|
||||
Sun,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { type ReactNode } from "react";
|
||||
@@ -31,13 +29,10 @@ import type { PageMeta } from "./types";
|
||||
export interface FreightDashboardHeaderProps {
|
||||
pageMeta: PageMeta;
|
||||
headerRight?: ReactNode;
|
||||
enableThemeToggle?: boolean;
|
||||
userName?: string;
|
||||
userEmail?: string;
|
||||
userInitials?: string;
|
||||
onLogout?: () => void;
|
||||
theme: "light" | "dark";
|
||||
onToggleTheme: () => void;
|
||||
mobileOpened: boolean;
|
||||
onToggleMobile: () => void;
|
||||
/** Hide the mobile burger when the shell has no sidebar to open. */
|
||||
@@ -51,13 +46,10 @@ const ISLAND =
|
||||
|
||||
const FreightDashboardHeader = ({
|
||||
headerRight,
|
||||
enableThemeToggle = false,
|
||||
userName = "User",
|
||||
userEmail,
|
||||
userInitials,
|
||||
onLogout,
|
||||
theme,
|
||||
onToggleTheme,
|
||||
mobileOpened,
|
||||
onToggleMobile,
|
||||
hideSidebarBurger = false,
|
||||
@@ -129,26 +121,6 @@ const FreightDashboardHeader = ({
|
||||
|
||||
<NotificationBellContainer />
|
||||
|
||||
{enableThemeToggle && (
|
||||
<Tooltip
|
||||
label={theme === "dark" ? "Light mode" : "Dark mode"}
|
||||
withArrow
|
||||
openDelay={300}
|
||||
>
|
||||
<UnstyledButton
|
||||
className={ISLAND}
|
||||
onClick={onToggleTheme}
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<Sun size={17} strokeWidth={1.8} />
|
||||
) : (
|
||||
<Moon size={17} strokeWidth={1.8} />
|
||||
)}
|
||||
</UnstyledButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Avatar pill */}
|
||||
<Menu width={240} position="bottom-end" withinPortal shadow="md" offset={8} radius="md">
|
||||
<Menu.Target>
|
||||
|
||||
@@ -1,26 +1,15 @@
|
||||
import { AppShell, Box } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { type ReactNode, useEffect, useState } from "react";
|
||||
import { type ReactNode } from "react";
|
||||
|
||||
import FreightDashboardHeader from "./FreightDashboardHeader";
|
||||
import FreightSidebar from "./FreightSidebar";
|
||||
import { getPageMeta } from "./route-meta";
|
||||
import type { SidebarSection } from "./types";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
const THEME_STORAGE_KEY = "edr-theme";
|
||||
const HEADER_HEIGHT = 64;
|
||||
const NAVBAR_WIDTH = 280;
|
||||
|
||||
function getInitialTheme(): Theme {
|
||||
if (typeof window === "undefined") return "light";
|
||||
const stored = window.localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (stored === "dark" || stored === "light") return stored;
|
||||
return window.matchMedia?.("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
export interface FreightDashboardLayoutProps {
|
||||
sidebarSections: SidebarSection[];
|
||||
/** Render the shell with no navbar at all (used by GL clearance-only users). */
|
||||
@@ -28,7 +17,6 @@ export interface FreightDashboardLayoutProps {
|
||||
activeHref?: string;
|
||||
onNavigate?: (href: string) => void;
|
||||
headerRight?: ReactNode;
|
||||
enableThemeToggle?: boolean;
|
||||
userName?: string;
|
||||
userEmail?: string;
|
||||
userInitials?: string;
|
||||
@@ -42,7 +30,6 @@ const FreightDashboardLayout = ({
|
||||
activeHref = "",
|
||||
onNavigate,
|
||||
headerRight,
|
||||
enableThemeToggle = false,
|
||||
userName,
|
||||
userEmail,
|
||||
userInitials,
|
||||
@@ -53,20 +40,6 @@ const FreightDashboardLayout = ({
|
||||
const [mobileOpened, { toggle: toggleMobile, close: closeMobile }] =
|
||||
useDisclosure(false);
|
||||
|
||||
const [theme, setTheme] = useState<Theme>(() =>
|
||||
enableThemeToggle ? getInitialTheme() : "light",
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableThemeToggle) return;
|
||||
const root = document.documentElement;
|
||||
root.classList.toggle("dark", theme === "dark");
|
||||
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
|
||||
}, [theme, enableThemeToggle]);
|
||||
|
||||
const toggleTheme = () =>
|
||||
setTheme((current) => (current === "dark" ? "light" : "dark"));
|
||||
|
||||
const navigate = (href: string) => {
|
||||
closeMobile();
|
||||
onNavigate?.(href);
|
||||
@@ -93,13 +66,10 @@ const FreightDashboardLayout = ({
|
||||
<FreightDashboardHeader
|
||||
pageMeta={pageMeta}
|
||||
headerRight={headerRight}
|
||||
enableThemeToggle={enableThemeToggle}
|
||||
userName={userName}
|
||||
userEmail={userEmail}
|
||||
userInitials={userInitials}
|
||||
onLogout={onLogout}
|
||||
theme={theme}
|
||||
onToggleTheme={toggleTheme}
|
||||
mobileOpened={mobileOpened}
|
||||
onToggleMobile={toggleMobile}
|
||||
hideSidebarBurger={hideSidebar}
|
||||
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
LogOut,
|
||||
User,
|
||||
Key,
|
||||
Moon,
|
||||
Sun,
|
||||
Home,
|
||||
Menu,
|
||||
} from "lucide-react";
|
||||
@@ -25,7 +23,6 @@ import { NavLink, useNavigate, useLocation } from "react-router-dom";
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { FiBell } from "react-icons/fi";
|
||||
import { useUser } from "@/shared/context/UserContext";
|
||||
import { useDarkMode } from "@/shared/hooks/useDarkMode";
|
||||
import {
|
||||
UI_LANGUAGE_OPTIONS,
|
||||
getUiLanguageLabel,
|
||||
@@ -49,7 +46,6 @@ export const TopBar = () => {
|
||||
const { pathname } = useLocation();
|
||||
const { logout } = useAuthUser();
|
||||
const userDetails = useUser();
|
||||
const { isDarkMode, toggleDarkMode } = useDarkMode();
|
||||
const { config: tenantConfig } = useTenantConfig();
|
||||
const moduleConfig = resolveModuleConfig(tenantConfig);
|
||||
const isAdminModuleVisible = userDetails?.roles?.some(
|
||||
@@ -66,7 +62,6 @@ export const TopBar = () => {
|
||||
moduleConfig.siteManagement && isAdminModuleVisible,
|
||||
].filter(Boolean).length;
|
||||
const shouldShowHomePageLink = visibleModuleCount > 1;
|
||||
const isObjectiveManagementRoute = pathname.startsWith("/objective-management");
|
||||
const fullName = userDetails?.name?.en || t("user");
|
||||
const splittedName = fullName.trim().split(" ");
|
||||
const initials =
|
||||
@@ -269,19 +264,6 @@ export const TopBar = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={toggleDarkMode}
|
||||
aria-label="Toggle Dark Mode"
|
||||
className="p-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors duration-200"
|
||||
title={isDarkMode ? "Switch to light mode" : "Switch to dark mode"}
|
||||
style={{ display: isObjectiveManagementRoute ? "none" : "block" }}>
|
||||
{isDarkMode ? (
|
||||
<Sun className="h-5 w-5 text-yellow-500" />
|
||||
) : (
|
||||
<Moon className="h-5 w-5 text-gray-600" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
|
||||
@@ -18,11 +18,8 @@ import {
|
||||
Info,
|
||||
Menu,
|
||||
X,
|
||||
Moon,
|
||||
Sun,
|
||||
} from "lucide-react";
|
||||
import { useUser } from "@/shared/context/UserContext";
|
||||
import { useDarkMode } from "@/shared/hooks/useDarkMode";
|
||||
import { useTenantConfig, resolveModuleConfig } from "@/layout/components/TenantConfig";
|
||||
import {
|
||||
UI_LANGUAGE_OPTIONS,
|
||||
@@ -55,7 +52,6 @@ const Header = () => {
|
||||
const location = useLocation();
|
||||
const isComplaintContext = isComplaintAuthContext(location.pathname);
|
||||
const { isAuthenticated, showExternalPortalChrome } = useExternalPortalSession();
|
||||
const { isDarkMode, toggleDarkMode } = useDarkMode();
|
||||
const { config: tenantConfig } = useTenantConfig();
|
||||
const moduleConfig = resolveModuleConfig(tenantConfig);
|
||||
|
||||
@@ -480,22 +476,6 @@ const Header = () => {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* Dark Mode Toggle */}
|
||||
<motion.button
|
||||
onClick={toggleDarkMode}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="p-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors duration-200"
|
||||
title={
|
||||
isDarkMode ? "Switch to light mode" : "Switch to dark mode"
|
||||
}
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<Sun className="h-5 w-5 text-yellow-500" />
|
||||
) : (
|
||||
<Moon className="h-5 w-5 text-gray-600" />
|
||||
)}
|
||||
</motion.button>
|
||||
<motion.button
|
||||
onClick={handleSignIn}
|
||||
whileHover={{
|
||||
@@ -646,26 +626,6 @@ const Header = () => {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Dark Mode Toggle for Mobile */}
|
||||
<motion.button
|
||||
onClick={toggleDarkMode}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className="block w-full px-4 py-3 text-base font-medium text-center text-gray-700 dark:text-gray-200 bg-gray-100 dark:bg-gray-700 rounded-lg shadow hover:bg-gray-200 dark:hover:bg-gray-600 transition-all duration-200 flex items-center justify-center gap-2"
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<>
|
||||
<Sun className="h-5 w-5 text-yellow-500" />
|
||||
{t("landingPage.lightMode")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Moon className="h-5 w-5 text-gray-600" />
|
||||
{t("landingPage.darkMode")}
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
handleSignIn();
|
||||
|
||||
@@ -31,24 +31,19 @@ import { QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
const THEME_STORAGE_KEY = "edr-theme";
|
||||
|
||||
const applyStoredTheme = () => {
|
||||
const storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY);
|
||||
const prefersDark = window.matchMedia?.("(prefers-color-scheme: dark)").matches;
|
||||
const theme = storedTheme === "dark" || storedTheme === "light"
|
||||
? storedTheme
|
||||
: prefersDark
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
if (theme === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* The backoffice is light-only. This runs before render for two reasons:
|
||||
* strip the `dark` class in case anything ever adds it, and evict the
|
||||
* `edr-theme` key that earlier builds persisted — without the eviction, a user
|
||||
* who had toggled dark before this change keeps a "dark" value in
|
||||
* localStorage that any future re-read would resurrect.
|
||||
*/
|
||||
const forceLightTheme = () => {
|
||||
document.documentElement.classList.remove("dark");
|
||||
window.localStorage.removeItem(THEME_STORAGE_KEY);
|
||||
};
|
||||
|
||||
applyStoredTheme();
|
||||
forceLightTheme();
|
||||
|
||||
// Must run before render so replay and exception capture cover startup errors.
|
||||
// No-ops when VITE_POSTHOG_KEY is unset.
|
||||
@@ -63,7 +58,7 @@ if (!rootElement) {
|
||||
createRoot(rootElement).render(
|
||||
<PostHogProvider client={posthog}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MantineProvider theme={freightMantineTheme}>
|
||||
<MantineProvider theme={freightMantineTheme} forceColorScheme="light">
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
|
||||
@@ -25,8 +25,6 @@ import {
|
||||
Cog,
|
||||
Bolt,
|
||||
Globe,
|
||||
Moon,
|
||||
Sun,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -39,7 +37,6 @@ import { Button } from "@/shared/common/ui/button";
|
||||
import { useTheme } from "@/user-management/web-Management/hooks/useTheme";
|
||||
import { useLocalizedName } from "@/shared/common/localizedName";
|
||||
import { useModules } from "@/user-management/web-Management/hooks/useModules";
|
||||
import { useDarkMode } from "@/shared/hooks/useDarkMode";
|
||||
import {
|
||||
useTenantConfig,
|
||||
resolveModuleConfig,
|
||||
@@ -98,7 +95,6 @@ const Header = () => {
|
||||
logout();
|
||||
};
|
||||
|
||||
const { isDarkMode, toggleDarkMode } = useDarkMode();
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem("i18nextLng")) {
|
||||
@@ -440,22 +436,6 @@ const Header = () => {
|
||||
))}
|
||||
</div>
|
||||
<div className="hidden md:flex items-center gap-3 lg:gap-4">
|
||||
<motion.button
|
||||
onClick={toggleDarkMode}
|
||||
className="p-2 rounded-lg text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800 transition-colors"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
aria-label={
|
||||
isDarkMode ? t("header.lightMode") : t("header.darkMode")
|
||||
}
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<Sun className="w-5 h-5" />
|
||||
) : (
|
||||
<Moon className="w-5 h-5" />
|
||||
)}
|
||||
</motion.button>
|
||||
|
||||
<div className="w-32 lg:w-36">
|
||||
<Select
|
||||
value={currentLanguage}
|
||||
@@ -544,21 +524,6 @@ const Header = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center md:hidden gap-2">
|
||||
<motion.button
|
||||
onClick={toggleDarkMode}
|
||||
className="p-2 rounded-lg text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800 transition-colors"
|
||||
whileTap={{ scale: 0.9 }}
|
||||
aria-label={
|
||||
isDarkMode ? t("header.lightMode") : t("header.darkMode")
|
||||
}
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<Sun className="h-5 w-5" />
|
||||
) : (
|
||||
<Moon className="h-5 w-5" />
|
||||
)}
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
className="inline-flex items-center justify-center p-2 rounded-lg text-gray-600 hover:text-gray-900 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-all"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { Editor } from "@tinymce/tinymce-react";
|
||||
import type { Editor as TinyMCEEditor } from "tinymce";
|
||||
|
||||
@@ -34,16 +34,10 @@ export default function RichTextEditor({
|
||||
shouldClear?: boolean;
|
||||
}) {
|
||||
const editorRef = useRef<TinyMCEEditor | null>(null);
|
||||
const [isDarkMode, setIsDarkMode] = useState(() => {
|
||||
if (typeof window === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const root = document.documentElement;
|
||||
return (
|
||||
root.classList.contains("dark") || root.getAttribute("data-theme") === "dark"
|
||||
);
|
||||
});
|
||||
// The app is light-only — nothing sets `dark` / data-theme on <html>, so this
|
||||
// is a constant rather than observed state. Kept as a named flag so the
|
||||
// TinyMCE skin/content-style branches below stay readable.
|
||||
const isDarkMode = false;
|
||||
|
||||
// ✅ When shouldClear becomes true → clear editor + remove draft
|
||||
useEffect(() => {
|
||||
@@ -54,28 +48,6 @@ export default function RichTextEditor({
|
||||
}
|
||||
}, [shouldClear]);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
const updateThemeMode = () => {
|
||||
setIsDarkMode(
|
||||
root.classList.contains("dark") ||
|
||||
root.getAttribute("data-theme") === "dark",
|
||||
);
|
||||
};
|
||||
|
||||
updateThemeMode();
|
||||
|
||||
const observer = new MutationObserver(updateThemeMode);
|
||||
observer.observe(root, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class", "data-theme"],
|
||||
});
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const init = useMemo(
|
||||
() => ({
|
||||
// Point self-hosted TinyMCE at the static assets copied into /public/tinymce.
|
||||
|
||||
@@ -21,8 +21,6 @@ import {
|
||||
Key,
|
||||
LogOut,
|
||||
LucideIcon,
|
||||
Moon,
|
||||
Sun,
|
||||
User,
|
||||
UserPen,
|
||||
UsersRound,
|
||||
@@ -37,7 +35,6 @@ import UserApprovalDropdown from "../PendingUsersList";
|
||||
import { usePendingUsers } from "@/user-management/userManagement/hooks/usePendingUsersHook";
|
||||
import { useUserDetail } from "../hooks/useUserDetail";
|
||||
import { MeDto } from "@/shared/dto/user/meDto";
|
||||
import { useDarkMode } from "@/shared/hooks/useDarkMode";
|
||||
import {
|
||||
UI_LANGUAGE_OPTIONS,
|
||||
getUiLanguageLabel,
|
||||
@@ -133,7 +130,6 @@ const Top: React.FC<HeaderProps> = ({
|
||||
logout();
|
||||
};
|
||||
|
||||
const { isDarkMode, toggleDarkMode } = useDarkMode();
|
||||
const { pendingUsers } = usePendingUsers({ take: 5, skip: 0 });
|
||||
const pendingUsersCount = pendingUsers?.count || 0;
|
||||
|
||||
@@ -359,21 +355,6 @@ const Top: React.FC<HeaderProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="flex max-w-[56vw] flex-shrink-0 items-center space-x-1.5 sm:max-w-none sm:space-x-2.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 rounded-xl bg-white/90 text-gray-600 shadow-sm ring-1 ring-inset ring-black/5 transition-colors hover:bg-primary-50 hover:text-primary-700 hover:ring-primary-200 dark:bg-gray-800/90 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-primary-900/20 dark:hover:text-primary-300 dark:hover:ring-primary-700/40 sm:h-10 sm:w-10"
|
||||
aria-label={isDarkMode ? "Light mode" : "Dark mode"}
|
||||
onClick={toggleDarkMode}
|
||||
title={isDarkMode ? "Switch to light mode" : "Switch to dark mode"}
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<Sun className="h-4 w-4 text-yellow-500" />
|
||||
) : (
|
||||
<Moon className="h-4 w-4 text-gray-700 dark:text-gray-300" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{canActivateUsers && (
|
||||
<div className="relative">
|
||||
<Button
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||
|
||||
// Pinned to "light": the app is light-only. This previously read next-themes'
|
||||
// useTheme, which falls back to "system" when no ThemeProvider is mounted (and
|
||||
// none is) — sonner then resolves "system" against prefers-color-scheme and
|
||||
// rendered dark toasts over a light UI on any OS set to dark.
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
theme="light"
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
|
||||
@@ -13,8 +13,6 @@ import {
|
||||
Mail,
|
||||
Phone,
|
||||
User,
|
||||
Moon,
|
||||
Sun,
|
||||
} from "lucide-react";
|
||||
import { useAuthUser } from "@/shared/hooks/useAuthUser";
|
||||
import { useTenantConfig } from "@/layout/components/TenantConfig";
|
||||
@@ -26,13 +24,11 @@ import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAuth } from "@/shared/context/AuthContext";
|
||||
import OTPModal from "./OTPModal";
|
||||
import { useDarkMode } from "@/shared/hooks/useDarkMode";
|
||||
import { getRememberMePreference } from "@/shared/utils/authPersistence";
|
||||
|
||||
export const Login = () => {
|
||||
const { login: authLogin, isLoggingIn } = useAuthUser();
|
||||
const { config } = useTenantConfig();
|
||||
const { isDarkMode, toggleDarkMode } = useDarkMode();
|
||||
const detectLoginMethod = (value: string): "email" | "phone" | "username" => {
|
||||
if (value.includes("@") && value.includes(".com")) return "email";
|
||||
if (/^\+?\d+$/.test(value)) return "phone";
|
||||
@@ -121,19 +117,6 @@ export const Login = () => {
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Dark Mode Toggle */}
|
||||
<button
|
||||
onClick={toggleDarkMode}
|
||||
className="fixed top-4 right-4 md:top-6 md:right-6 z-50 p-2 rounded-full bg-white dark:bg-gray-800 shadow-md hover:shadow-lg transition-all duration-300 hover:scale-105 border border-gray-200 dark:border-gray-700"
|
||||
title={isDarkMode ? "Switch to light mode" : "Switch to dark mode"}
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<Sun className="w-5 h-5 text-yellow-500" />
|
||||
) : (
|
||||
<Moon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="w-full max-w-6xl bg-white dark:bg-gray-800 rounded-2xl shadow-2xl overflow-hidden flex flex-col lg:flex-row">
|
||||
{/* Left Panel - Login Form */}
|
||||
<div className="lg:w-1/2 w-full p-6 sm:p-8 md:p-10 lg:p-12 xl:p-16 flex flex-col justify-center">
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
// Same storage key/values as main.tsx and FreightDashboardLayout so the
|
||||
// vendored IAM UI toggle and the host dashboard toggle stay in sync.
|
||||
const THEME_STORAGE_KEY = "edr-theme";
|
||||
|
||||
export const useDarkMode = () => {
|
||||
const [isDarkMode, setIsDarkMode] = useState(() => {
|
||||
// Initialize from localStorage or system preference
|
||||
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (savedTheme === "dark" || savedTheme === "light") {
|
||||
return savedTheme === "dark";
|
||||
}
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Apply dark mode class on mount and when isDarkMode changes
|
||||
if (isDarkMode) {
|
||||
document.documentElement.classList.add("dark");
|
||||
localStorage.setItem(THEME_STORAGE_KEY, "dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
localStorage.setItem(THEME_STORAGE_KEY, "light");
|
||||
}
|
||||
}, [isDarkMode]);
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
setIsDarkMode((prev) => !prev);
|
||||
};
|
||||
|
||||
return { isDarkMode, toggleDarkMode };
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useState, useEffect } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { Editor as TinyMCEEditor } from "@/record-management/common/editor/rte";
|
||||
import type { Editor as TinyMCEEditorType } from "tinymce";
|
||||
|
||||
@@ -21,18 +21,11 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
||||
placeholders,
|
||||
}) => {
|
||||
const hasPlaceholders = placeholders && placeholders.length > 0;
|
||||
const [isDarkMode, setIsDarkMode] = useState(() =>
|
||||
typeof window !== "undefined" && document.documentElement.classList.contains("dark")
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new MutationObserver(() => {
|
||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||
});
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// The app is light-only — nothing sets the `dark` class on <html>, so this is
|
||||
// a constant rather than observed state. Kept as a named flag so the TinyMCE
|
||||
// skin/content-style branches below stay readable.
|
||||
const isDarkMode = false;
|
||||
|
||||
const init = useMemo(() => {
|
||||
return {
|
||||
height: 600,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
@import "tailwindcss";
|
||||
@import "@edr/ui-common/theme.css" layer(theme);
|
||||
|
||||
/* Dark mode is toggled via the `dark` class on <html>. Without this, Tailwind
|
||||
v4 compiles `dark:` utilities to `@media (prefers-color-scheme: dark)` and
|
||||
they follow the OS setting instead of the in-app toggle. */
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
/* The portal is light-only — there is no theme toggle and nothing sets the
|
||||
`dark` class. This override is load-bearing: without it Tailwind v4 compiles
|
||||
`dark:` utilities to `@media (prefers-color-scheme: dark)`, so every
|
||||
leftover `dark:` class in the tree would activate on an OS-level dark
|
||||
setting. Binding the variant to a class that is never rendered keeps those
|
||||
utilities inert no matter what the OS (or a stray `.dark` class) says. */
|
||||
@custom-variant dark (&:where(.edr-dark-disabled));
|
||||
|
||||
/* Bridge the central Mantine theme into Tailwind. Mantine (createTheme) is the
|
||||
single source of truth; these just alias its generated CSS variables so
|
||||
|
||||
@@ -317,7 +317,6 @@ const App = () => {
|
||||
sidebarItems={sidebarItems}
|
||||
activeHref={location.pathname}
|
||||
onNavigate={navigate}
|
||||
enableThemeToggle
|
||||
userName={displayName}
|
||||
userEmail={userEmail}
|
||||
companyProfiles={companyProfiles}
|
||||
|
||||
@@ -15,8 +15,6 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
UnstyledButton,
|
||||
useComputedColorScheme,
|
||||
useMantineColorScheme,
|
||||
useMantineTheme,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
@@ -26,17 +24,15 @@ import {
|
||||
FileSignature,
|
||||
LogOut,
|
||||
Menu as MenuIcon,
|
||||
Moon,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Settings,
|
||||
Sun,
|
||||
Upload,
|
||||
User,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { type CSSProperties, Fragment, type ReactNode, useState } from "react";
|
||||
import { Fragment, type ReactNode, useState } from "react";
|
||||
import { PROFILE_TYPE_LABELS } from "@/constants/profileMode";
|
||||
import NotificationBellContainer from "@/features/notifications/NotificationBellContainer";
|
||||
import SupportWidget from "@/features/support/SupportWidget";
|
||||
@@ -59,7 +55,6 @@ export interface AppLayoutProps {
|
||||
* to the new-booking wizard. Compatible with react-router's `navigate`.
|
||||
*/
|
||||
onNavigate?: (href: string, options?: { state?: unknown }) => void;
|
||||
enableThemeToggle?: boolean;
|
||||
userName?: string;
|
||||
userEmail?: string;
|
||||
/** Operational profiles for the company — surfaced as reference chips in the account menu. */
|
||||
@@ -154,7 +149,6 @@ export function AppLayout({
|
||||
sidebarItems,
|
||||
activeHref = "",
|
||||
onNavigate,
|
||||
enableThemeToggle = false,
|
||||
userName = "User",
|
||||
userEmail,
|
||||
companyProfiles = [],
|
||||
@@ -165,9 +159,6 @@ export function AppLayout({
|
||||
}: AppLayoutProps) {
|
||||
const [mobileOpen, { toggle: toggleMobile }] = useDisclosure();
|
||||
const theme = useMantineTheme();
|
||||
const { setColorScheme } = useMantineColorScheme();
|
||||
const computedColorScheme = useComputedColorScheme("light");
|
||||
|
||||
const borderColor = theme.colors["edr-border"][6];
|
||||
const mutedColor = theme.colors["edr-muted"][6];
|
||||
const textColor = theme.colors["edr-text"][6];
|
||||
@@ -178,10 +169,6 @@ export function AppLayout({
|
||||
const navigate = (href: string, options?: { state?: unknown }) =>
|
||||
onNavigate?.(href, options);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setColorScheme(computedColorScheme === "dark" ? "light" : "dark");
|
||||
};
|
||||
|
||||
const initials = getInitials(userName);
|
||||
const activePage = getActivePage(sidebarItems, activePath);
|
||||
|
||||
@@ -279,20 +266,6 @@ export function AppLayout({
|
||||
activePath === item.href.toLowerCase() ||
|
||||
activePath.startsWith(item.href.toLowerCase() + "/");
|
||||
|
||||
// Shared header "island" styles — every control is a consistent 36px chip.
|
||||
const islandStyle: CSSProperties = {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 999,
|
||||
border: `1px solid ${borderColor}`,
|
||||
backgroundColor: "#fff",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
cursor: "pointer",
|
||||
};
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
layout="alt"
|
||||
@@ -457,20 +430,6 @@ export function AppLayout({
|
||||
{/* Notifications */}
|
||||
<NotificationBellContainer />
|
||||
|
||||
{enableThemeToggle && (
|
||||
<UnstyledButton
|
||||
onClick={toggleTheme}
|
||||
style={islandStyle}
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{computedColorScheme === "dark" ? (
|
||||
<Sun size={17} color={textColor} strokeWidth={1.8} />
|
||||
) : (
|
||||
<Moon size={17} color={textColor} strokeWidth={1.8} />
|
||||
)}
|
||||
</UnstyledButton>
|
||||
)}
|
||||
|
||||
{/* Avatar pill */}
|
||||
<Menu
|
||||
width={220}
|
||||
|
||||
@@ -48,7 +48,10 @@ if (!rootElement) {
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<PostHogProvider client={posthog}>
|
||||
<MantineProvider theme={mantineTheme} defaultColorScheme="light">
|
||||
{/* forceColorScheme (not defaultColorScheme) — the app is light-only.
|
||||
`default` still lets Mantine flip to dark via useMantineColorScheme
|
||||
or a persisted color-scheme value; `force` pins it. */}
|
||||
<MantineProvider theme={mantineTheme} forceColorScheme="light">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<PostHogErrorBoundary fallback={<AppErrorFallback />}>
|
||||
|
||||
Reference in New Issue
Block a user