diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx
index a84316f2..1bcc3613 100644
--- a/src/app/(auth)/login/page.tsx
+++ b/src/app/(auth)/login/page.tsx
@@ -81,7 +81,6 @@ export default function LoginPage() {
autoComplete="username"
autoCapitalize="none"
spellCheck={false}
- placeholder="you@example.com or yourname"
disabled={isLoading}
className={cn(errors.identifier && 'border-destructive focus-visible:ring-destructive')}
{...register('identifier')}
diff --git a/src/app/api/auth/sign-in-by-identifier/route.ts b/src/app/api/auth/sign-in-by-identifier/route.ts
index 00cdcea4..4a618658 100644
--- a/src/app/api/auth/sign-in-by-identifier/route.ts
+++ b/src/app/api/auth/sign-in-by-identifier/route.ts
@@ -93,6 +93,14 @@ export async function POST(req: NextRequest) {
'x-forwarded-for': req.headers.get('x-forwarded-for') ?? ip,
'user-agent': req.headers.get('user-agent') ?? '',
cookie: req.headers.get('cookie') ?? '',
+ // CRITICAL: forward Origin + Referer so better-auth's CSRF check
+ // passes. Without these the internal call lands as a cross-origin
+ // request with no Origin → 403 MISSING_OR_NULL_ORIGIN, and the
+ // user sees a generic "Invalid credentials" toast even though
+ // the password is right. (Bug surfaced 2026-05-13 testing on
+ // 192.168.1.17:3000 from an iPad.)
+ ...(req.headers.get('origin') ? { origin: req.headers.get('origin')! } : {}),
+ ...(req.headers.get('referer') ? { referer: req.headers.get('referer')! } : {}),
},
body: forwardBody,
});
diff --git a/src/components/layout/user-menu.tsx b/src/components/layout/user-menu.tsx
index bddd1d2a..173540d9 100644
--- a/src/components/layout/user-menu.tsx
+++ b/src/components/layout/user-menu.tsx
@@ -15,7 +15,7 @@
import { useRouter } from 'next/navigation';
import { useQueryClient } from '@tanstack/react-query';
-import { Moon, Sun, LogOut, User, Settings, Bell, Check, Building2 } from 'lucide-react';
+import { LogOut, User, Settings, Bell, Check, Building2 } from 'lucide-react';
import { type ReactNode } from 'react';
import { useUIStore } from '@/stores/ui-store';
@@ -51,17 +51,10 @@ export function UserMenu({ trigger, align = 'end', user, ports }: UserMenuProps)
const currentPortId = useUIStore((s) => s.currentPortId);
const currentPortSlug = useUIStore((s) => s.currentPortSlug);
const setPort = useUIStore((s) => s.setPort);
- const darkMode = useUIStore((s) => s.darkMode);
- const toggleDarkMode = useUIStore((s) => s.toggleDarkMode);
const base = currentPortSlug ? `/${currentPortSlug}` : '';
const showPortSwitcher = ports && ports.length > 1;
- function handleToggleDarkMode() {
- toggleDarkMode();
- document.documentElement.classList.toggle('dark');
- }
-
function handlePortChange(port: Port) {
if (port.id === currentPortId) return;
setPort(port.id, port.slug);
@@ -132,20 +125,6 @@ export function UserMenu({ trigger, align = 'end', user, ports }: UserMenuProps)
Notification preferences
-
- {darkMode ? (
- <>
-
- Light Mode
- >
- ) : (
- <>
-
- Dark Mode
- >
- )}
-
-
router.push('/api/auth/sign-out')}
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
index 4662e1d6..2b6bac97 100644
--- a/src/components/ui/dialog.tsx
+++ b/src/components/ui/dialog.tsx
@@ -51,7 +51,12 @@ const DialogContent = React.forwardRef<
'max-h-dvh overflow-y-auto sm:max-h-[calc(100dvh-2rem)]',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'sm:top-[50%] sm:right-auto sm:bottom-auto sm:left-[50%] sm:max-w-lg sm:translate-x-[-50%] sm:translate-y-[-50%] sm:border sm:rounded-lg',
- 'sm:data-[state=closed]:zoom-out-95 sm:data-[state=open]:zoom-in-95 sm:data-[state=closed]:slide-out-to-left-1/2 sm:data-[state=closed]:slide-out-to-top-[48%] sm:data-[state=open]:slide-in-from-left-1/2 sm:data-[state=open]:slide-in-from-top-[48%]',
+ // Desktop animation: subtle centered fade + zoom (no slide-from-
+ // corner so the dialog appears in place rather than flying in
+ // from top-right). The base fade-in/out classes above provide
+ // the opacity transition; zoom-95 adds a 5% scale-in for
+ // depth without feeling jarring.
+ 'sm:data-[state=closed]:zoom-out-95 sm:data-[state=open]:zoom-in-95',
className,
)}
{...props}
diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx
index 584f52a7..17a6ba0d 100644
--- a/src/components/ui/sonner.tsx
+++ b/src/components/ui/sonner.tsx
@@ -1,16 +1,16 @@
'use client';
-import { useTheme } from 'next-themes';
import { Toaster as Sonner } from 'sonner';
type ToasterProps = React.ComponentProps;
+// Dark mode is disabled across the app — hardcode the light theme so
+// sonner doesn't pick up a `prefers-color-scheme: dark` system hint and
+// render against a dark background that the rest of the UI doesn't use.
const Toaster = ({ ...props }: ToasterProps) => {
- const { theme = 'system' } = useTheme();
-
return (
void;
setPort: (portId: string, portSlug: string) => void;
- toggleDarkMode: () => void;
}
export const useUIStore = create()(
@@ -17,10 +15,8 @@ export const useUIStore = create()(
sidebarCollapsed: false,
currentPortId: null,
currentPortSlug: null,
- darkMode: false,
toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),
setPort: (portId, portSlug) => set({ currentPortId: portId, currentPortSlug: portSlug }),
- toggleDarkMode: () => set((s) => ({ darkMode: !s.darkMode })),
}),
{
name: 'pn-crm-ui',
@@ -30,7 +26,6 @@ export const useUIStore = create()(
// the previous session before the URL-derived effect runs.
partialize: (state) => ({
sidebarCollapsed: state.sidebarCollapsed,
- darkMode: state.darkMode,
}),
},
),