feat(ui): split-screen auth + enterprise CTA + dark theme default

- AuthShell: dark two-column auth layout (brand/value panel with CSS-only
  waveform motif + proof points + Bland-style enterprise CTA block on the
  left, zinc-900 form card on the right; single-column on mobile).
- AuthEnterpriseCTA: "Talk to our team" → dograh.com/contact?intent=enterprise.
- stack-theme: dark StackTheme token overrides synced to globals.css.
- page.tsx: wrap StackHandler (non-fullPage) in AuthShell + StackTheme;
  local-auth fallback preserved inside the shell. BackButton slimmed for the card.
- Dark locked as default: <html className="dark">, next-themes ThemeProvider
  (defaultTheme="dark", enableSystem=false); inline no-FOUC script defaults dark.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pritesh 2026-06-09 13:49:22 +05:30
parent d694a81f0a
commit 0662a1770f
7 changed files with 212 additions and 51 deletions

View file

@ -0,0 +1,25 @@
"use client";
// Bland-style enterprise call-to-action rendered inside the auth brand panel.
// Links out to the main marketing site's enterprise intake form rather than the
// in-app modal, since the visitor is not yet authenticated here.
import { Button } from "@/components/ui/button";
export function AuthEnterpriseCTA() {
return (
<a
href="https://dograh.com/contact?intent=enterprise"
target="_blank"
rel="noopener noreferrer"
className="block"
>
<Button
variant="outline"
className="w-full border-white/20 bg-white/5 text-zinc-100 hover:bg-white/10 hover:text-white"
>
Talk to our team
</Button>
</a>
);
}

View file

@ -0,0 +1,82 @@
// Dark two-column auth shell. LEFT (lg+ only): a brand/value panel with a
// CSS-only audio-waveform motif, proof points, and a Bland-style enterprise CTA
// block at the bottom (passed in as `enterpriseSlot`). RIGHT: a centered
// zinc-900 card that wraps the Stack Auth form (`children`). Mobile collapses to
// the single card column. Palette is the app's blacks/greys with one warm CTA
// accent on the waveform + focus.
import type { ReactNode } from "react";
const PROOF_POINTS = [
"Open source",
"7+ telephony providers",
"Open architecture",
];
export function AuthShell({
children,
enterpriseSlot,
}: {
children: ReactNode;
enterpriseSlot?: ReactNode;
}) {
return (
<div className="grid min-h-screen w-full bg-background lg:grid-cols-[45%_55%]">
{/* Brand / value panel — hidden on mobile */}
<aside className="relative hidden flex-col justify-between overflow-hidden border-r border-border/60 bg-zinc-950 p-10 lg:flex xl:p-14">
{/* Ambient depth: soft radial glow behind the content */}
<div
aria-hidden
className="pointer-events-none absolute -left-24 top-1/3 size-[28rem] rounded-full opacity-20 blur-3xl"
style={{ background: "radial-gradient(circle, var(--cta), transparent 70%)" }}
/>
<div className="relative flex items-center gap-3">
<div className="auth-waveform" aria-hidden>
<span /><span /><span /><span /><span /><span /><span /><span />
</div>
<span className="text-xl font-semibold tracking-tight text-zinc-50">Dograh</span>
</div>
<div className="relative max-w-md space-y-6">
<h1 className="text-3xl font-semibold leading-tight tracking-tight text-zinc-50 xl:text-4xl">
Voice AI for outbound calling, built in the open.
</h1>
<ul className="flex flex-wrap gap-x-3 gap-y-2 text-sm text-zinc-400">
{PROOF_POINTS.map((point, i) => (
<li key={point} className="flex items-center gap-3">
{i > 0 && <span aria-hidden className="text-zinc-700">·</span>}
<span>{point}</span>
</li>
))}
</ul>
</div>
{/* Enterprise CTA block (Bland-style) */}
<div className="relative max-w-md space-y-3 rounded-xl border border-white/10 bg-white/[0.03] p-5">
<h2 className="text-sm font-semibold text-zinc-100">
Need on-prem, data residency &amp; a data perimeter?
</h2>
<p className="text-sm text-zinc-400">
We deploy Dograh inside your environment for regulated and high-scale teams.
</p>
{enterpriseSlot}
</div>
</aside>
{/* Form column */}
<main className="flex items-center justify-center p-6 sm:p-10">
<div className="w-full max-w-md space-y-6 rounded-2xl border border-border/60 bg-card p-6 shadow-lg sm:p-8">
{/* Mobile-only wordmark (brand panel is hidden) */}
<div className="flex items-center gap-3 lg:hidden">
<div className="auth-waveform" aria-hidden>
<span /><span /><span /><span /><span /><span /><span /><span />
</div>
<span className="text-lg font-semibold tracking-tight">Dograh</span>
</div>
{children}
</div>
</main>
</div>
);
}

View file

@ -9,16 +9,14 @@ export function BackButton() {
const router = useRouter();
return (
<header className="flex items-center border-b px-4 py-3">
<Button
variant="ghost"
size="sm"
onClick={() => router.back()}
className="gap-2"
>
<ArrowLeft className="h-4 w-4" />
Go Back
</Button>
</header>
<Button
variant="ghost"
size="sm"
onClick={() => router.back()}
className="-ml-2 gap-2 text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="h-4 w-4" />
Go Back
</Button>
);
}

View file

@ -1,18 +1,25 @@
import { StackHandler } from "@stackframe/stack";
import { StackHandler, StackTheme } from "@stackframe/stack";
import { getAuthProvider } from "@/lib/auth/config";
import { AuthEnterpriseCTA } from "./AuthEnterpriseCTA";
import { AuthShell } from "./AuthShell";
import { BackButton } from "./BackButton";
import { stackAuthDarkTheme } from "./stack-theme";
export default async function Handler(props: unknown) {
const authProvider = await getAuthProvider();
if (authProvider === "local") {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h1>Local Auth Mode</h1>
<p>Stack Auth handler is disabled when using local authentication.</p>
</div>
<AuthShell enterpriseSlot={<AuthEnterpriseCTA />}>
<div className="space-y-2 text-center text-zinc-200">
<h1 className="text-xl font-semibold">Local Auth Mode</h1>
<p className="text-sm text-muted-foreground">
Stack Auth handler is disabled when using local authentication.
</p>
</div>
</AuthShell>
);
}
@ -21,15 +28,11 @@ export default async function Handler(props: unknown) {
const app = await getStackServerApp();
return (
<div className="flex flex-col h-screen">
<AuthShell enterpriseSlot={<AuthEnterpriseCTA />}>
<BackButton />
<div className="flex-1 overflow-auto">
<StackHandler
fullPage
app={app!}
routeProps={props}
/>
</div>
</div>
<StackTheme theme={stackAuthDarkTheme}>
<StackHandler fullPage={false} app={app!} routeProps={props} />
</StackTheme>
</AuthShell>
);
}

View file

@ -0,0 +1,35 @@
// Dark token overrides for the embedded Stack Auth form so it blends into the
// auth card surface (zinc-900 background, zinc-100 foreground, the warm CTA
// accent on the primary button, zinc-800 borders/inputs). Kept in sync with the
// .dark tokens in globals.css. Values are CSS color strings; Stack applies them
// to its own CSS variables.
import type { StackTheme } from "@stackframe/stack";
import type { ComponentProps } from "react";
type ThemeConfig = NonNullable<ComponentProps<typeof StackTheme>["theme"]>;
export const stackAuthDarkTheme: ThemeConfig = {
dark: {
background: "oklch(0.205 0 0)",
foreground: "oklch(0.985 0 0)",
card: "oklch(0.205 0 0)",
cardForeground: "oklch(0.985 0 0)",
popover: "oklch(0.205 0 0)",
popoverForeground: "oklch(0.985 0 0)",
primary: "oklch(0.78 0.16 67)",
primaryForeground: "oklch(0.16 0.02 60)",
secondary: "oklch(0.269 0 0)",
secondaryForeground: "oklch(0.985 0 0)",
muted: "oklch(0.269 0 0)",
mutedForeground: "oklch(0.708 0 0)",
accent: "oklch(0.269 0 0)",
accentForeground: "oklch(0.985 0 0)",
destructive: "oklch(0.704 0.191 22.216)",
destructiveForeground: "oklch(0.985 0 0)",
border: "oklch(0.269 0 0)",
input: "oklch(0.269 0 0)",
ring: "oklch(0.78 0.16 67)",
},
radius: "0.625rem",
};

View file

@ -9,6 +9,7 @@ import AppLayout from "@/components/layout/AppLayout";
import PostHogIdentify from "@/components/PostHogIdentify";
import { SentryErrorBoundary } from "@/components/SentryErrorBoundary";
import SpinLoader from "@/components/SpinLoader";
import { ThemeProvider } from "@/components/ThemeProvider";
import { Toaster } from "@/components/ui/sonner";
import { AppConfigProvider } from "@/context/AppConfigContext";
import { OnboardingProvider } from "@/context/OnboardingContext";
@ -39,21 +40,24 @@ export default function RootLayout({
}) {
return (
<html lang="en" suppressHydrationWarning>
<html lang="en" className="dark" suppressHydrationWarning>
<head>
{/* Inline script to prevent flash of light theme - runs before React hydrates */}
{/* Inline script to prevent flash of light theme - runs before React hydrates.
Dark is the locked default: only an explicit stored 'light' opts out. */}
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
var theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
} catch (e) {}
} catch (e) {
document.documentElement.classList.add('dark');
}
})();
`,
}}
@ -61,26 +65,28 @@ export default function RootLayout({
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<SentryErrorBoundary>
<AuthProvider>
<AppConfigProvider>
<Suspense fallback={<SpinLoader />}>
<UserConfigProvider>
<TelephonyConfigWarningsProvider>
<OnboardingProvider>
<PostHogIdentify />
<AppLayout>
{children}
</AppLayout>
<Toaster />
<ChatwootWidget />
</OnboardingProvider>
</TelephonyConfigWarningsProvider>
</UserConfigProvider>
</Suspense>
</AppConfigProvider>
</AuthProvider>
</SentryErrorBoundary>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false} disableTransitionOnChange>
<SentryErrorBoundary>
<AuthProvider>
<AppConfigProvider>
<Suspense fallback={<SpinLoader />}>
<UserConfigProvider>
<TelephonyConfigWarningsProvider>
<OnboardingProvider>
<PostHogIdentify />
<AppLayout>
{children}
</AppLayout>
<Toaster />
<ChatwootWidget />
</OnboardingProvider>
</TelephonyConfigWarningsProvider>
</UserConfigProvider>
</Suspense>
</AppConfigProvider>
</AuthProvider>
</SentryErrorBoundary>
</ThemeProvider>
</body>
</html>
);

View file

@ -0,0 +1,12 @@
"use client";
// Thin wrapper around next-themes so the root (server) layout can mount a theme
// provider without pulling client-only code into the server module graph. Dark
// is the locked default; the system preference is intentionally not consulted.
import { ThemeProvider as NextThemesProvider } from "next-themes";
import type { ComponentProps } from "react";
export function ThemeProvider({ children, ...props }: ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}