Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 86 additions & 69 deletions ee/apps/den-web/app/(den)/_components/auth-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import { isSamePathname } from "../_lib/client-route";
import type { AuthMode } from "../_lib/den-flow";
import { useDenFlow } from "../_providers/den-flow-provider";

type PanelContent = {
title: string;
copy: string;
submitLabel: string;
togglePrompt?: string;
toggleActionLabel?: string;
};

function getDesktopGrant(url: string | null) {
if (!url) return null;
try {
Expand Down Expand Up @@ -52,7 +60,7 @@ function SocialButton({
return (
<button
type="button"
className="flex w-full items-center justify-center gap-3 rounded-xl border border-gray-200 bg-white px-4 py-3 text-[14px] font-medium text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60"
className="den-button-secondary den-social-button"
onClick={onClick}
disabled={disabled}
>
Expand All @@ -62,25 +70,27 @@ function SocialButton({
}

export function AuthPanel({
panelTitle,
panelCopy,
prefilledEmail,
prefillKey,
initialMode = "sign-up",
lockEmail = false,
hideSocialAuth = false,
hideEmailField = false,
eyebrow = "Account",
signUpContent,
signInContent,
verificationContent,
}: {
panelTitle?: string;
panelCopy?: string;
prefilledEmail?: string;
prefillKey?: string;
initialMode?: AuthMode;
lockEmail?: boolean;
hideSocialAuth?: boolean;
hideEmailField?: boolean;
eyebrow?: string;
signUpContent?: Partial<PanelContent>;
signInContent?: Partial<PanelContent>;
verificationContent?: Partial<PanelContent>;
}) {
const router = useRouter();
const pathname = usePathname();
Expand Down Expand Up @@ -111,17 +121,37 @@ export function AuthPanel({
resolveUserLandingRoute,
} = useDenFlow();

const resolvedSignUpContent: PanelContent = {
title: "Get started.",
copy: "Free to try. Team plans from $50/mo.",
submitLabel: "Create account",
togglePrompt: "Have an account?",
toggleActionLabel: "Sign in",
...signUpContent,
};

const resolvedSignInContent: PanelContent = {
title: "Welcome back.",
copy: "Sign in to open your team workspace.",
submitLabel: "Sign in",
togglePrompt: "Need an account?",
toggleActionLabel: "Create one",
...signInContent,
};

const resolvedVerificationContent: PanelContent = {
title: "Verify your email.",
copy: "Enter the six-digit code from your inbox.",
submitLabel: "Verify email",
...verificationContent,
};

const desktopGrant = getDesktopGrant(desktopRedirectUrl);
const resolvedPanelTitle = verificationRequired
? "Verify your email."
: panelTitle ?? (authMode === "sign-up" ? "Create your Cloud account." : "Sign in to Cloud.");
const resolvedPanelCopy = verificationRequired
? "Enter the six-digit code from your inbox to finish setup."
: panelCopy ?? (
authMode === "sign-up"
? "Start with email, GitHub, or Google."
: "Welcome back. Keep your team setup in sync across Cloud and desktop."
);
const activeContent = verificationRequired
? resolvedVerificationContent
: authMode === "sign-in"
? resolvedSignInContent
: resolvedSignUpContent;
const showLockedEmailSummary = Boolean(prefilledEmail && lockEmail && hideEmailField);

useEffect(() => {
Expand All @@ -147,46 +177,46 @@ export function AuthPanel({
};

return (
<div className="rounded-[28px] border border-gray-100 bg-white p-6 shadow-[0_10px_30px_-24px_rgba(15,23,42,0.22)] md:p-7">
<div className="grid gap-2">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-400">
{eyebrow}
</p>
<h2 className="text-[28px] font-semibold tracking-[-0.04em] text-gray-900">{resolvedPanelTitle}</h2>
<p className="text-[14px] leading-relaxed text-gray-500">{resolvedPanelCopy}</p>
<div className="den-frame grid gap-5 p-6 md:p-7">
<div className="grid gap-3">
<p className="den-eyebrow">{eyebrow}</p>
<div className="grid gap-2">
<h2 className="den-title-lg">{activeContent.title}</h2>
<p className="den-copy">{activeContent.copy}</p>
</div>
</div>

{desktopAuthRequested ? (
<div className="mt-5 rounded-2xl border border-sky-100 bg-sky-50 p-4 text-[13px] text-sky-900">
Finish auth here and we&apos;ll send you back into the OpenWork desktop app.
<div className="den-notice is-info grid gap-3 text-[13px]">
<p className="m-0">Finish sign-in here, then jump back into the OpenWork desktop app.</p>
{desktopRedirectUrl ? (
<div className="mt-4 grid gap-2">
<div className="flex flex-wrap gap-2">
<div className="grid gap-3">
<div className="flex flex-wrap gap-3">
<button
type="button"
className="rounded-full border border-sky-200 bg-white px-4 py-2 text-xs font-medium text-sky-900 transition-colors hover:bg-sky-100"
className="den-button-secondary w-full sm:w-auto"
onClick={() => window.location.assign(desktopRedirectUrl)}
>
Open OpenWork
</button>
<button
type="button"
className="rounded-full border border-sky-200 bg-white px-4 py-2 text-xs font-medium text-sky-900 transition-colors hover:bg-sky-100"
className="den-button-secondary w-full sm:w-auto"
onClick={() => void copyDesktopValue("link", desktopRedirectUrl)}
>
{copiedDesktopField === "link" ? "Copied link" : "Copy sign-in link"}
</button>
{desktopGrant ? (
<button
type="button"
className="rounded-full border border-sky-200 bg-white px-4 py-2 text-xs font-medium text-sky-900 transition-colors hover:bg-sky-100"
className="den-button-secondary w-full sm:w-auto"
onClick={() => void copyDesktopValue("code", desktopGrant)}
>
{copiedDesktopField === "code" ? "Copied code" : "Copy one-time code"}
</button>
) : null}
</div>
<p className="text-xs leading-5 text-sky-800/80">
<p className="m-0 text-xs leading-5 text-sky-800/80">
If OpenWork does not open automatically, copy the sign-in link or one-time code and paste it into the OpenWork desktop app.
</p>
</div>
Expand All @@ -195,7 +225,7 @@ export function AuthPanel({
) : null}

<form
className="mt-5 grid gap-3"
className="grid gap-4"
onSubmit={async (event) => {
const next = verificationRequired
? await submitVerificationCode(event)
Expand Down Expand Up @@ -228,33 +258,24 @@ export function AuthPanel({
<span>Continue with Google</span>
</SocialButton>

<div
className="flex items-center gap-3 text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-400"
aria-hidden="true"
>
<span className="h-px flex-1 bg-gray-200" />
<div className="den-divider" aria-hidden="true">
<span>or</span>
<span className="h-px flex-1 bg-gray-200" />
</div>
</>
) : null}

{showLockedEmailSummary ? (
<div className="rounded-2xl border border-gray-100 bg-gray-50 px-4 py-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-400">
Invited email
</p>
<p className="mt-1 text-[14px] font-medium text-gray-900">{prefilledEmail}</p>
<div className="den-frame-inset grid gap-1 rounded-[1.5rem] px-4 py-3">
<p className="den-label">Invited email</p>
<p className="m-0 text-sm font-medium text-[var(--dls-text-primary)]">{prefilledEmail}</p>
</div>
) : null}

{!hideEmailField ? (
<label className="grid gap-2">
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-400">
Email
</span>
<span className="den-label">Email</span>
<input
className="w-full rounded-xl border border-gray-200 bg-white px-4 py-3 text-[14px] text-gray-900 outline-none transition focus:border-gray-300 focus:ring-4 focus:ring-gray-900/5 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500"
className="den-input disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500"
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
Expand All @@ -268,11 +289,9 @@ export function AuthPanel({

{!verificationRequired ? (
<label className="grid gap-2">
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-400">
Password
</span>
<span className="den-label">Password</span>
<input
className="w-full rounded-xl border border-gray-200 bg-white px-4 py-3 text-[14px] text-gray-900 outline-none transition focus:border-gray-300 focus:ring-4 focus:ring-gray-900/5"
className="den-input"
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
Expand All @@ -282,11 +301,9 @@ export function AuthPanel({
</label>
) : (
<label className="grid gap-2">
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-400">
Verification code
</span>
<span className="den-label">Verification code</span>
<input
className="w-full rounded-xl border border-gray-200 bg-white px-4 py-3 text-center text-[18px] font-semibold tracking-[0.35em] text-gray-900 outline-none transition focus:border-gray-300 focus:ring-4 focus:ring-gray-900/5"
className="den-input text-center text-[18px] font-semibold tracking-[0.35em]"
type="text"
inputMode="numeric"
pattern="[0-9]*"
Expand All @@ -302,32 +319,26 @@ export function AuthPanel({

<button
type="submit"
className="flex w-full items-center justify-center gap-2 rounded-full bg-gray-900 px-5 py-3 text-[14px] font-medium text-white transition-colors hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-60"
className="den-button-primary w-full"
disabled={authBusy || desktopRedirectBusy}
>
{authBusy || desktopRedirectBusy
? "Working..."
: verificationRequired
? "Verify email"
: authMode === "sign-in"
? "Sign in to Cloud"
: "Create Cloud account"}
{authBusy || desktopRedirectBusy ? "Working..." : activeContent.submitLabel}
{!authBusy && !desktopRedirectBusy ? <ArrowRight className="h-4 w-4" /> : null}
</button>

{verificationRequired ? (
<div className="flex flex-col gap-3 sm:flex-row">
<button
type="button"
className="w-full rounded-full border border-gray-200 bg-white px-4 py-3 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60"
className="den-button-secondary w-full"
onClick={() => void resendVerificationCode()}
disabled={authBusy || desktopRedirectBusy}
>
Resend code
</button>
<button
type="button"
className="w-full rounded-full border border-gray-200 bg-white px-4 py-3 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60"
className="den-button-secondary w-full"
onClick={() => cancelVerification()}
disabled={authBusy || desktopRedirectBusy}
>
Expand All @@ -338,21 +349,27 @@ export function AuthPanel({
</form>

{!verificationRequired ? (
<div className="mt-4 flex items-center justify-between gap-3 border-t border-gray-200 pt-4 text-sm text-gray-500">
<p>{authMode === "sign-in" ? "Need an account?" : "Already have an account?"}</p>
<div className="flex items-center justify-between gap-3 border-t border-[var(--dls-border)] pt-4 text-sm text-[var(--dls-text-secondary)]">
<p className="m-0">
{authMode === "sign-in"
? resolvedSignInContent.togglePrompt
: resolvedSignUpContent.togglePrompt}
</p>
<button
type="button"
className="font-medium text-gray-900 transition hover:opacity-70"
className="font-medium text-[var(--dls-text-primary)] transition hover:opacity-70"
onClick={() => setAuthMode(authMode === "sign-in" ? "sign-up" : "sign-in")}
>
{authMode === "sign-in" ? "Create account" : "Switch to sign in"}
{authMode === "sign-in"
? resolvedSignInContent.toggleActionLabel
: resolvedSignUpContent.toggleActionLabel}
</button>
</div>
) : null}

{showAuthFeedback ? (
<div
className="mt-4 grid gap-1 rounded-2xl border border-gray-100 bg-gray-50 px-4 py-3 text-center text-[13px] text-gray-500"
className="den-frame-inset grid gap-1 rounded-[1.5rem] px-4 py-3 text-center text-[13px] text-[var(--dls-text-secondary)]"
aria-live="polite"
>
<p>{authInfo}</p>
Expand Down
40 changes: 19 additions & 21 deletions ee/apps/den-web/app/(den)/_components/auth-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,23 @@ import { AuthPanel } from "./auth-panel";

function FeatureCard({ title, body }: { title: string; body: string }) {
return (
<div className="rounded-2xl border border-gray-100 bg-white p-5">
<p className="mb-2 text-[14px] font-medium text-gray-900">{title}</p>
<p className="text-[13px] leading-[1.6] text-gray-500">{body}</p>
<div className="den-stat-card grid gap-2">
<p className="m-0 text-[14px] font-medium text-[var(--dls-text-primary)]">{title}</p>
<p className="m-0 text-[13px] leading-[1.6] text-[var(--dls-text-secondary)]">{body}</p>
</div>
);
}

function LoadingPanel({ title, body }: { title: string; body: string }) {
return (
<div className="rounded-[28px] border border-gray-100 bg-white p-6 shadow-[0_10px_30px_-24px_rgba(15,23,42,0.22)] md:p-7">
<div className="den-frame grid gap-3 p-6 md:p-7">
<div className="grid gap-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-400">
OpenWork Cloud
</p>
<h2 className="text-[28px] font-semibold tracking-[-0.04em] text-gray-900">{title}</h2>
<p className="text-[14px] leading-relaxed text-gray-500">{body}</p>
<p className="den-eyebrow">OpenWork Cloud</p>
<h2 className="den-title-lg">{title}</h2>
<p className="den-copy">{body}</p>
</div>
<div className="mt-6 h-2 overflow-hidden rounded-full bg-gray-100">
<div className="h-full w-1/3 animate-pulse rounded-full bg-gray-900/80" />
<div className="h-2 overflow-hidden rounded-full bg-[var(--dls-hover)]">
<div className="h-full w-1/3 animate-pulse rounded-full bg-[var(--dls-accent)]" />
</div>
</div>
);
Expand Down Expand Up @@ -70,7 +68,7 @@ export function AuthScreen() {
<section className="den-page flex w-full items-center py-4 lg:min-h-[calc(100vh-2.5rem)]">
<div className="grid w-full gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(360px,440px)]">
<div className="order-2 flex flex-col gap-6 lg:order-1">
<div className="relative min-h-[300px] overflow-hidden rounded-[32px] border border-gray-100 px-7 py-8 md:px-10 md:py-10">
<div className="den-frame relative min-h-[300px] overflow-hidden px-7 py-8 md:px-10 md:py-10">
<div className="absolute inset-0 z-0">
<Dithering
speed={0}
Expand Down Expand Up @@ -104,30 +102,30 @@ export function AuthScreen() {

<div className="grid gap-4">
<span className="inline-flex w-fit rounded-full border border-white/20 bg-white/15 px-3 py-1 text-[10px] font-medium uppercase tracking-[0.18em] text-white backdrop-blur-md">
Shared setups
OpenWork Cloud
</span>
<h1 className="max-w-[12ch] text-[2.25rem] font-semibold leading-[0.95] tracking-[-0.06em] text-white md:text-[3rem]">
Share your OpenWork setup with your team.
One setup, every seat.
</h1>
<p className="max-w-[34rem] text-[15px] leading-7 text-white/80">
Provision shared setups, invite your org, and keep background workspaces available across Cloud and desktop.
Configure once. Your whole team gets the same tools, agents, and providers.
</p>
</div>
</div>
</div>

<div className="grid gap-4 md:grid-cols-3">
<FeatureCard
title="Team sharing"
body="Package skills, MCPs, plugins, and config once so the whole org can use the same setup."
title="Shared config"
body="Set it up once, then push it to the org."
/>
<FeatureCard
title="Cloud Hosted Agents"
body="Keep selected workflows running in the cloud without asking each teammate to run them locally."
title="Cloud agents"
body="Workflows that keep running while your team is away."
/>
<FeatureCard
title="Custom LLM Providers"
body="Whether you want to use LiteLLM, Azure, or any other provider, you can use OpenWork to provision your team."
title="Your models"
body="Bring your own provider when the team is ready."
/>
</div>
</div>
Expand Down
Loading
Loading