diff --git a/apps/compliance-portal/src/_locales/en-US.json b/apps/compliance-portal/src/_locales/en-US.json
index 8abdfd02b..29f9a0cb2 100644
--- a/apps/compliance-portal/src/_locales/en-US.json
+++ b/apps/compliance-portal/src/_locales/en-US.json
@@ -15,6 +15,58 @@
"userMenu": {
"signOut": "Sign out"
},
+ "common": {
+ "cancel": "Cancel",
+ "error": "Something went wrong"
+ },
+ "auth": {
+ "backToPortal": "Back to portal",
+ "signIn": {
+ "title": "Sign in to continue",
+ "description": "Access protected resources or submit a request",
+ "or": "OR",
+ "withProvider": "Sign in with {{provider}}",
+ "emailLabel": "Email Address",
+ "emailPlaceholder": "you@company.com",
+ "emailRequired": "Email is required",
+ "emailInvalid": "Enter a valid email address",
+ "sendMagicLink": "Send Magic Link",
+ "resend": "Resend link",
+ "resendIn": "Resend link in {{seconds}}s",
+ "magicLinkSent": "Magic link sent!",
+ "magicLinkSentNote": "Check your email and use the link to sign in."
+ },
+ "requestAccess": {
+ "success": "Access requested"
+ },
+ "verify": {
+ "title": "Confirming your email",
+ "description": "Please wait while we sign you in\u2026"
+ },
+ "fullName": {
+ "title": "Add your name",
+ "label": "Full Name",
+ "placeholder": "John Doe",
+ "required": "Full name is required",
+ "tooShort": "Full name is too short",
+ "submit": "Continue"
+ },
+ "magicLinkExpired": {
+ "title": "Link expired",
+ "description": "This magic link has expired. Please request a new one."
+ },
+ "magicLinkAlreadyUsed": {
+ "title": "Link already used",
+ "description": "This magic link has already been used. Please request a new one."
+ },
+ "errors": {
+ "magicLinkFailed": "Couldn't send the magic link. Please try again.",
+ "verifyFailed": "Couldn't verify the link. Please try again.",
+ "fullNameFailed": "Couldn't save your name. Please try again.",
+ "requestFailed": "Couldn't complete your access request. Please try again.",
+ "ndaRequired": "You need to sign the NDA before requesting access."
+ }
+ },
"home": {
"heroTitle": "Trust at {{name}}.",
"heroDescription": "Welcome to our Compliance Portal. Find our security documentation and certifications here.",
diff --git a/apps/compliance-portal/src/_locales/fr-FR.json b/apps/compliance-portal/src/_locales/fr-FR.json
index 03fcd2d1c..10244333c 100644
--- a/apps/compliance-portal/src/_locales/fr-FR.json
+++ b/apps/compliance-portal/src/_locales/fr-FR.json
@@ -15,6 +15,58 @@
"userMenu": {
"signOut": "Se déconnecter"
},
+ "common": {
+ "cancel": "Annuler",
+ "error": "Une erreur est survenue"
+ },
+ "auth": {
+ "backToPortal": "Retour au portail",
+ "signIn": {
+ "title": "Connectez-vous pour continuer",
+ "description": "Accédez aux ressources protégées ou soumettez une demande",
+ "or": "OU",
+ "withProvider": "Se connecter avec {{provider}}",
+ "emailLabel": "Adresse e-mail",
+ "emailPlaceholder": "vous@entreprise.com",
+ "emailRequired": "L'e-mail est requis",
+ "emailInvalid": "Saisissez une adresse e-mail valide",
+ "sendMagicLink": "Envoyer le lien magique",
+ "resend": "Renvoyer le lien",
+ "resendIn": "Renvoyer le lien dans {{seconds}}s",
+ "magicLinkSent": "Lien magique envoyé !",
+ "magicLinkSentNote": "Consultez votre e-mail et utilisez le lien pour vous connecter."
+ },
+ "requestAccess": {
+ "success": "Accès demandé"
+ },
+ "verify": {
+ "title": "Confirmation de votre e-mail",
+ "description": "Veuillez patienter pendant que nous vous connectons\u2026"
+ },
+ "fullName": {
+ "title": "Ajoutez votre nom",
+ "label": "Nom complet",
+ "placeholder": "Jean Dupont",
+ "required": "Le nom complet est requis",
+ "tooShort": "Le nom complet est trop court",
+ "submit": "Continuer"
+ },
+ "magicLinkExpired": {
+ "title": "Lien expiré",
+ "description": "Ce lien magique a expiré. Veuillez en demander un nouveau."
+ },
+ "magicLinkAlreadyUsed": {
+ "title": "Lien déjà utilisé",
+ "description": "Ce lien magique a déjà été utilisé. Veuillez en demander un nouveau."
+ },
+ "errors": {
+ "magicLinkFailed": "Impossible d'envoyer le lien magique. Veuillez réessayer.",
+ "verifyFailed": "Impossible de vérifier le lien. Veuillez réessayer.",
+ "fullNameFailed": "Impossible d'enregistrer votre nom. Veuillez réessayer.",
+ "requestFailed": "Impossible de finaliser votre demande d'accès. Veuillez réessayer.",
+ "ndaRequired": "Vous devez signer l'accord de confidentialité avant de demander l'accès."
+ }
+ },
"home": {
"heroTitle": "La confiance chez {{name}}.",
"heroDescription": "Bienvenue dans notre Compliance Portal. Retrouvez ici notre documentation et nos certifications de sécurité.",
diff --git a/apps/compliance-portal/src/components/TopBar/TopBar.tsx b/apps/compliance-portal/src/components/TopBar/TopBar.tsx
index e1a85785b..18ce8f00a 100644
--- a/apps/compliance-portal/src/components/TopBar/TopBar.tsx
+++ b/apps/compliance-portal/src/components/TopBar/TopBar.tsx
@@ -27,6 +27,9 @@ import { useTranslation } from "react-i18next";
import { graphql, useFragment } from "react-relay";
import { Link as RouterLink, useLocation } from "react-router";
+import { buildRequestAllContinueUrl } from "#/lib/auth/continueUrl";
+import { useSignInDialog } from "#/lib/auth/signInDialogContext";
+
import type { TopBar_query$key } from "./__generated__/TopBar_query.graphql";
import { TopBarUserMenu } from "./TopBarUserMenu";
import { topBar } from "./variants";
@@ -64,6 +67,7 @@ export function TopBar({ queryKey }: TopBarProps) {
const { t } = useTranslation();
const data = useFragment(topBarFragment, queryKey);
const { pathname } = useLocation();
+ const { openSignIn } = useSignInDialog();
const { currentTrustCenter } = data;
const organizationName = currentTrustCenter.organization.name;
@@ -109,7 +113,13 @@ export function TopBar({ queryKey }: TopBarProps) {
))}
{data.viewer == null
? (
- }>
+ }
+ onClick={() => openSignIn({ continueTo: buildRequestAllContinueUrl() })}
+ >
{t("topBar.getAccess")}
)
diff --git a/apps/compliance-portal/src/components/auth/LoginForm.tsx b/apps/compliance-portal/src/components/auth/LoginForm.tsx
new file mode 100644
index 000000000..dd2e3f877
--- /dev/null
+++ b/apps/compliance-portal/src/components/auth/LoginForm.tsx
@@ -0,0 +1,168 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { Field } from "@base-ui/react/field";
+import { Form } from "@base-ui/react/form";
+import { Toast } from "@base-ui/react/toast";
+import type { GraphQLError } from "@probo/helpers";
+import { Button } from "@probo/ui/src/v2/Button/Button";
+import { DialogBody } from "@probo/ui/src/v2/Dialog/DialogBody";
+import { DialogFooter } from "@probo/ui/src/v2/Dialog/DialogFooter";
+import { TextField } from "@probo/ui/src/v2/form/TextField";
+import { Text } from "@probo/ui/src/v2/typography/Text";
+import { useEffect, useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { graphql } from "relay-runtime";
+
+import { getSafeContinueUrl } from "#/lib/auth/continueUrl";
+import { useMutation } from "#/lib/relay/useMutation";
+
+import type { LoginFormMutation } from "./__generated__/LoginFormMutation.graphql";
+import { OIDCProviders } from "./OIDCProviders";
+
+const RESEND_COOLDOWN_SECONDS = 60;
+
+const sendMagicLinkMutation = graphql`
+ mutation LoginFormMutation($input: SendMagicLinkInput!) {
+ sendMagicLink(input: $input) {
+ success
+ }
+ }
+`;
+
+interface LoginFormProps {
+ // Absolute URL to return to after authentication (carries the request-all
+ // marker so an access request resumes once signed in).
+ continueTo: string;
+ onCancel: () => void;
+}
+
+// Sign-in form used inside the dialog: SSO providers, then a magic-link email
+// flow. On success it flips to a "check your email" state with a resend timer.
+export function LoginForm({ continueTo, onCancel }: LoginFormProps) {
+ const { t } = useTranslation();
+ const toast = Toast.useToastManager();
+ const [magicLinkSent, setMagicLinkSent] = useState(false);
+ const [secondsLeft, setSecondsLeft] = useState(RESEND_COOLDOWN_SECONDS);
+ const intervalRef = useRef>(undefined);
+
+ const [sendMagicLink, isSending] = useMutation(
+ sendMagicLinkMutation,
+ { errorToast: false },
+ );
+
+ useEffect(() => {
+ if (!magicLinkSent) {
+ return;
+ }
+ intervalRef.current = setInterval(() => {
+ setSecondsLeft(seconds => Math.max(seconds - 1, 0));
+ }, 1000);
+ return () => clearInterval(intervalRef.current);
+ }, [magicLinkSent]);
+
+ const handleSend = (email: string) => {
+ void sendMagicLink({
+ variables: { input: { email, continue: continueTo } },
+ onCompleted: (_response, errors) => {
+ const code = (errors?.[0] as GraphQLError | undefined)?.extensions?.code;
+
+ // Already signed in elsewhere: jump straight to the return URL so any
+ // pending access request resumes.
+ if (code === "ALREADY_AUTHENTICATED") {
+ window.location.href = getSafeContinueUrl(continueTo);
+ return;
+ }
+
+ if (errors && errors.length > 0) {
+ toast.add({ title: t("auth.errors.magicLinkFailed"), type: "error" });
+ return;
+ }
+
+ setSecondsLeft(RESEND_COOLDOWN_SECONDS);
+ setMagicLinkSent(true);
+ toast.add({ title: t("auth.signIn.magicLinkSent"), type: "success" });
+ },
+ onError: () => {
+ toast.add({ title: t("auth.errors.magicLinkFailed"), type: "error" });
+ },
+ }).catch(() => {});
+ };
+
+ const resendDisabled = magicLinkSent && secondsLeft > 0;
+ const submitLabel = magicLinkSent
+ ? secondsLeft > 0
+ ? t("auth.signIn.resendIn", { seconds: secondsLeft })
+ : t("auth.signIn.resend")
+ : t("auth.signIn.sendMagicLink");
+
+ return (
+
+ );
+}
diff --git a/apps/compliance-portal/src/components/auth/OIDCButton.tsx b/apps/compliance-portal/src/components/auth/OIDCButton.tsx
new file mode 100644
index 000000000..ccd6e5814
--- /dev/null
+++ b/apps/compliance-portal/src/components/auth/OIDCButton.tsx
@@ -0,0 +1,75 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { Google } from "@probo/ui/src/Atoms/ThirdParties/Google";
+import { Microsoft } from "@probo/ui/src/Atoms/ThirdParties/Microsoft";
+import { Button } from "@probo/ui/src/v2/Button/Button";
+import type { ComponentProps } from "react";
+import { useTranslation } from "react-i18next";
+import { graphql, useFragment } from "react-relay";
+
+import type { OIDCButton_provider$key } from "./__generated__/OIDCButton_provider.graphql";
+
+const providerFragment = graphql`
+ fragment OIDCButton_provider on OIDCProviderInfo {
+ name
+ loginURL
+ }
+`;
+
+const providerIcons: Record) => React.ReactNode> = {
+ google: Google,
+ microsoft: Microsoft,
+};
+
+interface OIDCButtonProps {
+ providerKey: OIDCButton_provider$key;
+ // Absolute URL to return to after the provider completes authentication.
+ continueTo: string;
+}
+
+// Redirects the whole window to the provider's hosted login, carrying the
+// `continue` target so the portal resumes the pending flow on return.
+export function OIDCButton({ providerKey, continueTo }: OIDCButtonProps) {
+ const { t } = useTranslation();
+ const provider = useFragment(providerFragment, providerKey);
+ const Icon = providerIcons[provider.name];
+ const label = t("auth.signIn.withProvider", {
+ provider: provider.name.charAt(0).toUpperCase() + provider.name.slice(1),
+ });
+
+ return (
+ : undefined}
+ onClick={() => {
+ const loginURL = new URL(provider.loginURL, window.location.origin);
+ loginURL.searchParams.set("continue", continueTo);
+ window.location.href = loginURL.toString();
+ }}
+ >
+ {label}
+
+ );
+}
diff --git a/apps/compliance-portal/src/components/auth/OIDCProviders.tsx b/apps/compliance-portal/src/components/auth/OIDCProviders.tsx
new file mode 100644
index 000000000..f916d4c14
--- /dev/null
+++ b/apps/compliance-portal/src/components/auth/OIDCProviders.tsx
@@ -0,0 +1,94 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { ButtonSkeleton } from "@probo/ui/src/v2/Button/ButtonSkeleton";
+import { ErrorBoundary } from "@probo/ui/src/v2/ErrorBoundary/ErrorBoundary";
+import { Text } from "@probo/ui/src/v2/typography/Text";
+import { Suspense } from "react";
+import { useTranslation } from "react-i18next";
+import { graphql, useLazyLoadQuery } from "react-relay";
+
+import type { OIDCProvidersQuery } from "./__generated__/OIDCProvidersQuery.graphql";
+import { OIDCButton } from "./OIDCButton";
+
+const oidcProvidersQuery = graphql`
+ query OIDCProvidersQuery {
+ oidcProviders {
+ ...OIDCButton_provider
+ }
+ }
+`;
+
+interface OIDCProvidersProps {
+ continueTo: string;
+}
+
+function Divider() {
+ const { t } = useTranslation();
+ return (
+
+
+ >
+ );
+}
+
+// SSO buttons for the sign-in dialog. Providers load lazily on open; if the
+// query fails or the trust center has none, the section renders nothing so the
+// email flow stays usable.
+export function OIDCProviders({ continueTo }: OIDCProvidersProps) {
+ return (
+ console.error("Failed to load SSO providers", error)}
+ >
+
+
+
+
+ )}
+ >
+
+
+
+ );
+}
diff --git a/apps/compliance-portal/src/components/auth/SignInDialog.tsx b/apps/compliance-portal/src/components/auth/SignInDialog.tsx
new file mode 100644
index 000000000..ac97d1fc8
--- /dev/null
+++ b/apps/compliance-portal/src/components/auth/SignInDialog.tsx
@@ -0,0 +1,53 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { Dialog } from "@probo/ui/src/v2/Dialog/Dialog";
+import { DialogDescription } from "@probo/ui/src/v2/Dialog/DialogDescription";
+import { DialogHeader } from "@probo/ui/src/v2/Dialog/DialogHeader";
+import { DialogPopup } from "@probo/ui/src/v2/Dialog/DialogPopup";
+import { DialogTitle } from "@probo/ui/src/v2/Dialog/DialogTitle";
+import { useTranslation } from "react-i18next";
+
+import { LoginForm } from "./LoginForm";
+
+interface SignInDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ // Absolute URL to return to after authentication.
+ continueTo: string;
+}
+
+// The reusable "Login Dialog" from the design: a modal sign-in gate composed of
+// the kit Dialog plus the magic-link / SSO login form.
+export function SignInDialog({ open, onOpenChange, continueTo }: SignInDialogProps) {
+ const { t } = useTranslation();
+
+ return (
+
+ );
+}
diff --git a/apps/compliance-portal/src/lib/auth/SignInDialogProvider.tsx b/apps/compliance-portal/src/lib/auth/SignInDialogProvider.tsx
new file mode 100644
index 000000000..622473fc1
--- /dev/null
+++ b/apps/compliance-portal/src/lib/auth/SignInDialogProvider.tsx
@@ -0,0 +1,53 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { type ReactNode, useCallback, useMemo, useState } from "react";
+
+import { SignInDialog } from "#/components/auth/SignInDialog";
+import {
+ type OpenSignInOptions,
+ SignInDialogContextProvider,
+} from "#/lib/auth/signInDialogContext";
+
+interface SignInDialogProviderProps {
+ children: ReactNode;
+}
+
+// Owns the single sign-in dialog instance and exposes `openSignIn` so any
+// descendant (top bar, resource rows, …) can prompt authentication and pass the
+// URL to return to afterwards.
+export function SignInDialogProvider({ children }: SignInDialogProviderProps) {
+ const [open, setOpen] = useState(false);
+ const [continueTo, setContinueTo] = useState(() => window.location.href);
+
+ const openSignIn = useCallback((options?: OpenSignInOptions) => {
+ setContinueTo(options?.continueTo ?? window.location.href);
+ setOpen(true);
+ }, []);
+
+ const value = useMemo(() => ({ openSignIn }), [openSignIn]);
+
+ return (
+
+ {children}
+
+
+ );
+}
diff --git a/apps/compliance-portal/src/lib/auth/continueUrl.ts b/apps/compliance-portal/src/lib/auth/continueUrl.ts
new file mode 100644
index 000000000..cbbeb8e21
--- /dev/null
+++ b/apps/compliance-portal/src/lib/auth/continueUrl.ts
@@ -0,0 +1,59 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { getPathPrefix } from "#/lib/http/pathPrefix";
+
+// Marker appended to a post-auth `continue` URL so the portal fires the pending
+// "request access" mutation once the user lands back authenticated.
+export const REQUEST_ALL_PARAM = "request-all";
+
+// Validates a `continue` target before we navigate to it. Only same-origin URLs
+// under the portal's path prefix are accepted; anything else falls back to the
+// portal home, so a crafted `?continue=` can never bounce the user off-site.
+export function getSafeContinueUrl(param: string | null | undefined): string {
+ const prefix = getPathPrefix();
+ const fallback = window.location.origin + (prefix || "/");
+
+ if (!param) {
+ return fallback;
+ }
+
+ try {
+ const url = new URL(param, window.location.origin);
+ if (
+ url.origin === window.location.origin
+ && url.pathname.startsWith(`${prefix}/`)
+ ) {
+ return window.location.origin + url.pathname + url.search;
+ }
+ } catch {
+ return fallback;
+ }
+
+ return fallback;
+}
+
+// Absolute URL of the current page with the request-all marker set, used as the
+// `continue` target so the access request resumes after sign-in.
+export function buildRequestAllContinueUrl(): string {
+ const url = new URL(window.location.href);
+ url.searchParams.set(REQUEST_ALL_PARAM, "true");
+ return url.toString();
+}
diff --git a/apps/compliance-portal/src/lib/auth/signInDialogContext.ts b/apps/compliance-portal/src/lib/auth/signInDialogContext.ts
new file mode 100644
index 000000000..bb9860aaf
--- /dev/null
+++ b/apps/compliance-portal/src/lib/auth/signInDialogContext.ts
@@ -0,0 +1,43 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { createContext, useContext } from "react";
+
+export type OpenSignInOptions = {
+ // Absolute URL the user should return to once authenticated. Defaults to the
+ // current page. Include the request-all marker to resume an access request.
+ continueTo?: string;
+};
+
+export type SignInDialogContextValue = {
+ openSignIn: (options?: OpenSignInOptions) => void;
+};
+
+const SignInDialogContext = createContext(null);
+
+export const SignInDialogContextProvider = SignInDialogContext.Provider;
+
+export function useSignInDialog(): SignInDialogContextValue {
+ const context = useContext(SignInDialogContext);
+ if (context === null) {
+ throw new Error("useSignInDialog must be used within a SignInDialogProvider");
+ }
+ return context;
+}
diff --git a/apps/compliance-portal/src/lib/auth/useResumeAccessRequest.ts b/apps/compliance-portal/src/lib/auth/useResumeAccessRequest.ts
new file mode 100644
index 000000000..8f8f41b9e
--- /dev/null
+++ b/apps/compliance-portal/src/lib/auth/useResumeAccessRequest.ts
@@ -0,0 +1,117 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { Toast } from "@base-ui/react/toast";
+import type { GraphQLError } from "@probo/helpers";
+import { useEffect, useRef } from "react";
+import { useTranslation } from "react-i18next";
+import { useNavigate, useSearchParams } from "react-router";
+import { graphql } from "relay-runtime";
+
+import {
+ buildRequestAllContinueUrl,
+ REQUEST_ALL_PARAM,
+} from "#/lib/auth/continueUrl";
+import { useMutation } from "#/lib/relay/useMutation";
+
+import type { useResumeAccessRequestMutation } from "./__generated__/useResumeAccessRequestMutation.graphql";
+
+const requestAllAccessesMutation = graphql`
+ mutation useResumeAccessRequestMutation {
+ requestAllAccesses {
+ trustCenterAccess {
+ id
+ }
+ }
+ }
+`;
+
+// After a user signs in through the dialog, they land back on the page that
+// carried the request-all marker. This hook fires the deferred
+// `requestAllAccesses` mutation once (when authenticated), routes to the
+// full-name gate when the backend asks for it, and clears the marker so a
+// refresh never re-triggers it.
+export function useResumeAccessRequest(isAuthenticated: boolean) {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const navigate = useNavigate();
+ const toast = Toast.useToastManager();
+ const { t } = useTranslation();
+ const firedRef = useRef(false);
+
+ const [requestAllAccesses] = useMutation(
+ requestAllAccessesMutation,
+ { errorToast: false },
+ );
+
+ const shouldResume
+ = isAuthenticated && searchParams.get(REQUEST_ALL_PARAM) === "true";
+
+ useEffect(() => {
+ if (!shouldResume || firedRef.current) {
+ return;
+ }
+ firedRef.current = true;
+
+ // Drop the marker up front so a reload can't queue a second request.
+ searchParams.delete(REQUEST_ALL_PARAM);
+ setSearchParams(searchParams, { replace: true });
+
+ void requestAllAccesses({
+ variables: {},
+ onCompleted: (_response, errors) => {
+ const code = (errors?.[0] as GraphQLError | undefined)?.extensions?.code;
+
+ // The backend gates access behind a completed profile; send the user to
+ // the full-name step, preserving the marker so the request resumes.
+ if (code === "FULL_NAME_REQUIRED") {
+ const continueUrl = buildRequestAllContinueUrl();
+ void navigate(`/full-name?continue=${encodeURIComponent(continueUrl)}`);
+ return;
+ }
+
+ if (errors && errors.length > 0) {
+ toast.add({
+ title:
+ code === "NDA_SIGNATURE_REQUIRED"
+ ? t("auth.errors.ndaRequired")
+ : t("auth.errors.requestFailed"),
+ type: "error",
+ });
+ return;
+ }
+
+ toast.add({ title: t("auth.requestAccess.success"), type: "success" });
+ },
+ onError: () => {
+ toast.add({ title: t("auth.errors.requestFailed"), type: "error" });
+ },
+ // The awaitable wrapper rejects on failure; toasts are handled above, so
+ // swallow the rejection to avoid an unhandled promise.
+ }).catch(() => {});
+ }, [
+ shouldResume,
+ navigate,
+ requestAllAccesses,
+ searchParams,
+ setSearchParams,
+ t,
+ toast,
+ ]);
+}
diff --git a/apps/compliance-portal/src/lib/auth/useSafeContinueUrl.ts b/apps/compliance-portal/src/lib/auth/useSafeContinueUrl.ts
new file mode 100644
index 000000000..d5374c053
--- /dev/null
+++ b/apps/compliance-portal/src/lib/auth/useSafeContinueUrl.ts
@@ -0,0 +1,33 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { useMemo } from "react";
+import { useSearchParams } from "react-router";
+
+import { getSafeContinueUrl } from "#/lib/auth/continueUrl";
+
+// Reactive, validated `continue` URL from the current `?continue=` param. Falls
+// back to the portal home when the param is absent or points off-site.
+export function useSafeContinueUrl(): string {
+ const [searchParams] = useSearchParams();
+ const param = searchParams.get("continue");
+
+ return useMemo(() => getSafeContinueUrl(param), [param]);
+}
diff --git a/apps/compliance-portal/src/pages/MainLayout.tsx b/apps/compliance-portal/src/pages/MainLayout.tsx
index d04cc51a3..47206e37c 100644
--- a/apps/compliance-portal/src/pages/MainLayout.tsx
+++ b/apps/compliance-portal/src/pages/MainLayout.tsx
@@ -25,11 +25,16 @@ import { Outlet } from "react-router";
import { PoweredBy } from "#/components/PoweredBy/PoweredBy";
import { TopBar } from "#/components/TopBar/TopBar";
+import { SignInDialogProvider } from "#/lib/auth/SignInDialogProvider";
+import { useResumeAccessRequest } from "#/lib/auth/useResumeAccessRequest";
import type { MainLayoutQuery } from "./__generated__/MainLayoutQuery.graphql";
export const mainLayoutQuery = graphql`
query MainLayoutQuery {
+ viewer {
+ __typename
+ }
...TopBar_query
}
`;
@@ -42,16 +47,21 @@ export function MainLayout({ queryRef }: MainLayoutProps) {
const { t } = useTranslation();
const data = usePreloadedQuery(mainLayoutQuery, queryRef);
+ // Resume a deferred "request access" once the user lands back authenticated.
+ useResumeAccessRequest(data.viewer != null);
+
return (
- // Bound the shell to the viewport so the TopBar and footer stay fixed and the
- // page area scrolls on its own. Pages that fill the height (the document
- // viewer) then scroll their own body while their toolbar stays put.
-
-
-
-
+ // Bound the shell to the viewport so the TopBar and footer stay fixed and the
+ // page area scrolls on its own. Pages that fill the height (the document
+ // viewer) then scroll their own body while their toolbar stays put.
+
+
+
+
+
+
+
-
-
+
);
}
diff --git a/apps/compliance-portal/src/pages/auth/AuthLayout.tsx b/apps/compliance-portal/src/pages/auth/AuthLayout.tsx
new file mode 100644
index 000000000..7d55fedac
--- /dev/null
+++ b/apps/compliance-portal/src/pages/auth/AuthLayout.tsx
@@ -0,0 +1,34 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { Card } from "@probo/ui/src/v2/Card/Card";
+import { Outlet } from "react-router";
+
+// Minimal centered chrome for the standalone auth steps that a magic-link email
+// or a full-name gate lands on (these cannot live inside the portal shell).
+export default function AuthLayout() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/apps/compliance-portal/src/pages/auth/AuthLayoutSkeleton.tsx b/apps/compliance-portal/src/pages/auth/AuthLayoutSkeleton.tsx
new file mode 100644
index 000000000..079b4173e
--- /dev/null
+++ b/apps/compliance-portal/src/pages/auth/AuthLayoutSkeleton.tsx
@@ -0,0 +1,30 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { CardSkeleton } from "@probo/ui/src/v2/Card/CardSkeleton";
+
+// Loading fallback for the standalone auth routes while their chunk resolves.
+export function AuthLayoutSkeleton() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/compliance-portal/src/pages/auth/FullNamePage.tsx b/apps/compliance-portal/src/pages/auth/FullNamePage.tsx
new file mode 100644
index 000000000..a6310e64d
--- /dev/null
+++ b/apps/compliance-portal/src/pages/auth/FullNamePage.tsx
@@ -0,0 +1,112 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { Field } from "@base-ui/react/field";
+import { Form } from "@base-ui/react/form";
+import { Toast } from "@base-ui/react/toast";
+import type { GraphQLError } from "@probo/helpers";
+import { Button } from "@probo/ui/src/v2/Button/Button";
+import { TextField } from "@probo/ui/src/v2/form/TextField";
+import { Heading } from "@probo/ui/src/v2/typography/Heading";
+import { useTranslation } from "react-i18next";
+import { graphql } from "relay-runtime";
+
+import { useSafeContinueUrl } from "#/lib/auth/useSafeContinueUrl";
+import { useMutation } from "#/lib/relay/useMutation";
+
+import type { FullNamePageMutation } from "./__generated__/FullNamePageMutation.graphql";
+
+const updateFullNameMutation = graphql`
+ mutation FullNamePageMutation($input: UpdateFullNameInput!) {
+ updateFullName(input: $input) {
+ success
+ }
+ }
+`;
+
+// Post-sign-in gate collecting the user's display name when the backend reports
+// FULL_NAME_REQUIRED, then forwarding to the validated continue URL.
+export default function FullNamePage() {
+ const { t } = useTranslation();
+ const toast = Toast.useToastManager();
+ const safeContinueUrl = useSafeContinueUrl();
+
+ const [updateFullName, isUpdating] = useMutation(
+ updateFullNameMutation,
+ { errorToast: false },
+ );
+
+ const handleSubmit = (fullName: string) => {
+ void updateFullName({
+ variables: { input: { fullName } },
+ onCompleted: (_response, errors) => {
+ const code = (errors?.[0] as GraphQLError | undefined)?.extensions?.code;
+
+ if (code === "ALREADY_AUTHENTICATED" || (errors == null || errors.length === 0)) {
+ window.location.href = safeContinueUrl;
+ return;
+ }
+
+ toast.add({ title: t("auth.errors.fullNameFailed"), type: "error" });
+ },
+ onError: () => {
+ toast.add({ title: t("auth.errors.fullNameFailed"), type: "error" });
+ },
+ }).catch(() => {});
+ };
+
+ return (
+
+
+ {t("auth.fullName.title")}
+
+
+
+
+ );
+}
diff --git a/apps/compliance-portal/src/pages/auth/MagicLinkAlreadyUsedPage.tsx b/apps/compliance-portal/src/pages/auth/MagicLinkAlreadyUsedPage.tsx
new file mode 100644
index 000000000..1bb46be38
--- /dev/null
+++ b/apps/compliance-portal/src/pages/auth/MagicLinkAlreadyUsedPage.tsx
@@ -0,0 +1,39 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { Link } from "@probo/ui/src/v2/Button/Link";
+import { Heading } from "@probo/ui/src/v2/typography/Heading";
+import { Text } from "@probo/ui/src/v2/typography/Text";
+import { useTranslation } from "react-i18next";
+
+// Shown when a magic link was already consumed; the user restarts sign-in.
+export default function MagicLinkAlreadyUsedPage() {
+ const { t } = useTranslation();
+
+ return (
+
+ );
+}
diff --git a/apps/compliance-portal/src/pages/auth/MagicLinkExpiredPage.tsx b/apps/compliance-portal/src/pages/auth/MagicLinkExpiredPage.tsx
new file mode 100644
index 000000000..34c8d7b24
--- /dev/null
+++ b/apps/compliance-portal/src/pages/auth/MagicLinkExpiredPage.tsx
@@ -0,0 +1,39 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { Link } from "@probo/ui/src/v2/Button/Link";
+import { Heading } from "@probo/ui/src/v2/typography/Heading";
+import { Text } from "@probo/ui/src/v2/typography/Text";
+import { useTranslation } from "react-i18next";
+
+// Shown when a magic link has expired; the user restarts sign-in from home.
+export default function MagicLinkExpiredPage() {
+ const { t } = useTranslation();
+
+ return (
+
+ );
+}
diff --git a/apps/compliance-portal/src/pages/auth/VerifyMagicLinkPage.tsx b/apps/compliance-portal/src/pages/auth/VerifyMagicLinkPage.tsx
new file mode 100644
index 000000000..fc627d41e
--- /dev/null
+++ b/apps/compliance-portal/src/pages/auth/VerifyMagicLinkPage.tsx
@@ -0,0 +1,101 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { Toast } from "@base-ui/react/toast";
+import type { GraphQLError } from "@probo/helpers";
+import { Heading } from "@probo/ui/src/v2/typography/Heading";
+import { Text } from "@probo/ui/src/v2/typography/Text";
+import { useEffect, useRef } from "react";
+import { useTranslation } from "react-i18next";
+import { useNavigate, useSearchParams } from "react-router";
+import { graphql } from "relay-runtime";
+
+import { getSafeContinueUrl } from "#/lib/auth/continueUrl";
+import { useMutation } from "#/lib/relay/useMutation";
+
+import type { VerifyMagicLinkPageMutation } from "./__generated__/VerifyMagicLinkPageMutation.graphql";
+
+const verifyMagicLinkMutation = graphql`
+ mutation VerifyMagicLinkPageMutation($input: VerifyMagicLinkInput!) {
+ verifyMagicLink(input: $input) {
+ continue
+ }
+ }
+`;
+
+// Landing page for the magic-link email. It verifies the token on mount and
+// forwards to the (validated) continue URL, where any pending access request
+// resumes.
+export default function VerifyMagicLinkPage() {
+ const { t } = useTranslation();
+ const toast = Toast.useToastManager();
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const submittedRef = useRef(false);
+
+ const [verifyMagicLink] = useMutation(
+ verifyMagicLinkMutation,
+ { errorToast: false },
+ );
+
+ useEffect(() => {
+ const token = searchParams.get("token");
+ if (!token || submittedRef.current) {
+ return;
+ }
+ submittedRef.current = true;
+
+ void verifyMagicLink({
+ variables: { input: { token: token.trim() } },
+ onCompleted: (response, errors) => {
+ const code = (errors?.[0] as GraphQLError | undefined)?.extensions?.code;
+
+ if (code === "ALREADY_AUTHENTICATED") {
+ window.location.href = getSafeContinueUrl(null);
+ return;
+ }
+ if (code === "TOKEN_EXPIRED") {
+ void navigate("/magic-link-expired");
+ return;
+ }
+ if (code === "TOKEN_ALREADY_USED") {
+ void navigate("/magic-link-already-used");
+ return;
+ }
+ if (errors && errors.length > 0) {
+ toast.add({ title: t("auth.errors.verifyFailed"), type: "error" });
+ return;
+ }
+
+ window.location.href = getSafeContinueUrl(response.verifyMagicLink?.continue);
+ },
+ onError: () => {
+ toast.add({ title: t("auth.errors.verifyFailed"), type: "error" });
+ },
+ }).catch(() => {});
+ }, [navigate, searchParams, t, toast, verifyMagicLink]);
+
+ return (
+
+ );
+}
diff --git a/apps/compliance-portal/src/pages/auth/routes.ts b/apps/compliance-portal/src/pages/auth/routes.ts
new file mode 100644
index 000000000..79dc993ec
--- /dev/null
+++ b/apps/compliance-portal/src/pages/auth/routes.ts
@@ -0,0 +1,51 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { lazy } from "@probo/react-lazy";
+import type { AppRoute } from "@probo/routes";
+
+import { AuthLayoutSkeleton } from "./AuthLayoutSkeleton";
+
+// Standalone auth steps that a magic-link email or a full-name gate lands on.
+// These sit outside the portal shell, under a minimal centered layout.
+export const authRoutes = [
+ {
+ Fallback: AuthLayoutSkeleton,
+ Component: lazy(() => import("#/pages/auth/AuthLayout")),
+ children: [
+ {
+ path: "verify-magic-link",
+ Component: lazy(() => import("#/pages/auth/VerifyMagicLinkPage")),
+ },
+ {
+ path: "full-name",
+ Component: lazy(() => import("#/pages/auth/FullNamePage")),
+ },
+ {
+ path: "magic-link-expired",
+ Component: lazy(() => import("#/pages/auth/MagicLinkExpiredPage")),
+ },
+ {
+ path: "magic-link-already-used",
+ Component: lazy(() => import("#/pages/auth/MagicLinkAlreadyUsedPage")),
+ },
+ ],
+ },
+] satisfies AppRoute[];
diff --git a/apps/compliance-portal/src/routes.tsx b/apps/compliance-portal/src/routes.tsx
index 6453462ec..a82da7eba 100644
--- a/apps/compliance-portal/src/routes.tsx
+++ b/apps/compliance-portal/src/routes.tsx
@@ -25,6 +25,7 @@ import { createBrowserRouter } from "react-router";
import { PageErrorBoundary } from "#/components/errors/PageErrorBoundary";
import { RootErrorBoundary } from "#/components/errors/RootErrorBoundary";
import { getPathPrefix } from "#/lib/http/pathPrefix";
+import { authRoutes } from "#/pages/auth/routes";
import { documentRoutes } from "#/pages/documents/routes";
import { HomePageSkeleton } from "#/pages/HomePageSkeleton";
import { MainLayoutSkeleton } from "#/pages/MainLayoutSkeleton";
@@ -64,6 +65,7 @@ const routes = [
},
],
},
+ ...authRoutes,
] satisfies AppRoute[];
// The portal is served under a /trust/{slug} path prefix (or a bare custom
diff --git a/packages/ui/src/v2/Dialog/Dialog.tsx b/packages/ui/src/v2/Dialog/Dialog.tsx
new file mode 100644
index 000000000..f2a966020
--- /dev/null
+++ b/packages/ui/src/v2/Dialog/Dialog.tsx
@@ -0,0 +1,31 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { Dialog as BaseDialog } from "@base-ui/react/dialog";
+import type { ComponentProps } from "react";
+
+// Root of the modal dialog (Radix "Dialog"). Controlled the same way as Base
+// UI's Dialog: `open` / `onOpenChange`, or left uncontrolled. See
+// contrib/claude/ui.md.
+export type DialogProps = ComponentProps;
+
+export function Dialog(props: DialogProps) {
+ return ;
+}
diff --git a/packages/ui/src/v2/Dialog/DialogBody.tsx b/packages/ui/src/v2/Dialog/DialogBody.tsx
new file mode 100644
index 000000000..3fd742b42
--- /dev/null
+++ b/packages/ui/src/v2/Dialog/DialogBody.tsx
@@ -0,0 +1,32 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import type { ComponentProps } from "react";
+
+import { dialog } from "./variants";
+
+// Content region between the header and footer, carrying the dialog's horizontal
+// padding.
+export function DialogBody(props: ComponentProps<"div">) {
+ const { className, ...rest } = props;
+ const { body } = dialog();
+
+ return ;
+}
diff --git a/packages/ui/src/v2/Dialog/DialogClose.tsx b/packages/ui/src/v2/Dialog/DialogClose.tsx
new file mode 100644
index 000000000..92f814617
--- /dev/null
+++ b/packages/ui/src/v2/Dialog/DialogClose.tsx
@@ -0,0 +1,30 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { Dialog as BaseDialog } from "@base-ui/react/dialog";
+import type { ComponentProps } from "react";
+
+// Closes the dialog. Pass `render` to reuse an existing control (e.g. a Button)
+// as the close action, per Base UI's polymorphism.
+export type DialogCloseProps = ComponentProps;
+
+export function DialogClose(props: DialogCloseProps) {
+ return ;
+}
diff --git a/packages/ui/src/v2/Dialog/DialogDescription.tsx b/packages/ui/src/v2/Dialog/DialogDescription.tsx
new file mode 100644
index 000000000..1cddf473b
--- /dev/null
+++ b/packages/ui/src/v2/Dialog/DialogDescription.tsx
@@ -0,0 +1,36 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { Dialog as BaseDialog } from "@base-ui/react/dialog";
+import type { ComponentProps } from "react";
+
+import { dialog } from "./variants";
+
+// Accessible dialog description (wires `aria-describedby` via Base UI).
+export type DialogDescriptionProps = Omit, "className"> & {
+ className?: string;
+};
+
+export function DialogDescription(props: DialogDescriptionProps) {
+ const { className, ...rest } = props;
+ const { description } = dialog();
+
+ return ;
+}
diff --git a/packages/ui/src/v2/Dialog/DialogFooter.tsx b/packages/ui/src/v2/Dialog/DialogFooter.tsx
new file mode 100644
index 000000000..0e763b2f5
--- /dev/null
+++ b/packages/ui/src/v2/Dialog/DialogFooter.tsx
@@ -0,0 +1,32 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import type { ComponentProps } from "react";
+
+import { dialog } from "./variants";
+
+// Footer region for dialog actions, right-aligned with the dialog's horizontal
+// padding.
+export function DialogFooter(props: ComponentProps<"div">) {
+ const { className, ...rest } = props;
+ const { footer } = dialog();
+
+ return ;
+}
diff --git a/packages/ui/src/v2/Dialog/DialogHeader.tsx b/packages/ui/src/v2/Dialog/DialogHeader.tsx
new file mode 100644
index 000000000..a2f8cabda
--- /dev/null
+++ b/packages/ui/src/v2/Dialog/DialogHeader.tsx
@@ -0,0 +1,32 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import type { ComponentProps } from "react";
+
+import { dialog } from "./variants";
+
+// Header region grouping the title and description with the dialog's horizontal
+// padding.
+export function DialogHeader(props: ComponentProps<"div">) {
+ const { className, ...rest } = props;
+ const { header } = dialog();
+
+ return ;
+}
diff --git a/packages/ui/src/v2/Dialog/DialogPopup.tsx b/packages/ui/src/v2/Dialog/DialogPopup.tsx
new file mode 100644
index 000000000..e00a31dec
--- /dev/null
+++ b/packages/ui/src/v2/Dialog/DialogPopup.tsx
@@ -0,0 +1,46 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { Dialog as BaseDialog } from "@base-ui/react/dialog";
+import type { ComponentProps } from "react";
+
+import { dialog } from "./variants";
+
+export type DialogPopupProps
+ = & Omit, "className">
+ & {
+ className?: string;
+ };
+
+// Portal + dimmed backdrop + centered, styled popup frame. Children compose the
+// header / body / footer regions.
+export function DialogPopup(props: DialogPopupProps) {
+ const { className, children, ...popupProps } = props;
+ const { backdrop, popup } = dialog();
+
+ return (
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/packages/ui/src/v2/Dialog/DialogSkeleton.tsx b/packages/ui/src/v2/Dialog/DialogSkeleton.tsx
new file mode 100644
index 000000000..3cc37aebf
--- /dev/null
+++ b/packages/ui/src/v2/Dialog/DialogSkeleton.tsx
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { TextSkeleton } from "../typography/TextSkeleton";
+
+import { dialog, dialogSkeleton } from "./variants";
+
+// Loading placeholder matching the dialog frame (header + body lines + footer),
+// importing only variants and skeleton primitives — never Base UI.
+export function DialogSkeleton() {
+ const { header, body, footer } = dialog();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/ui/src/v2/Dialog/DialogTitle.tsx b/packages/ui/src/v2/Dialog/DialogTitle.tsx
new file mode 100644
index 000000000..c8af3237a
--- /dev/null
+++ b/packages/ui/src/v2/Dialog/DialogTitle.tsx
@@ -0,0 +1,36 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { Dialog as BaseDialog } from "@base-ui/react/dialog";
+import type { ComponentProps } from "react";
+
+import { dialog } from "./variants";
+
+// Accessible dialog title (wires `aria-labelledby` via Base UI).
+export type DialogTitleProps = Omit, "className"> & {
+ className?: string;
+};
+
+export function DialogTitle(props: DialogTitleProps) {
+ const { className, ...rest } = props;
+ const { title } = dialog();
+
+ return ;
+}
diff --git a/packages/ui/src/v2/Dialog/DialogTrigger.tsx b/packages/ui/src/v2/Dialog/DialogTrigger.tsx
new file mode 100644
index 000000000..5eaa0efda
--- /dev/null
+++ b/packages/ui/src/v2/Dialog/DialogTrigger.tsx
@@ -0,0 +1,30 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { Dialog as BaseDialog } from "@base-ui/react/dialog";
+import type { ComponentProps } from "react";
+
+// Opens the dialog. Pass `render` to use an existing control (e.g. a Button) as
+// the trigger, per Base UI's polymorphism — do not clone children by hand.
+export type DialogTriggerProps = ComponentProps;
+
+export function DialogTrigger(props: DialogTriggerProps) {
+ return ;
+}
diff --git a/packages/ui/src/v2/Dialog/variants.ts b/packages/ui/src/v2/Dialog/variants.ts
new file mode 100644
index 000000000..38abe2fd3
--- /dev/null
+++ b/packages/ui/src/v2/Dialog/variants.ts
@@ -0,0 +1,57 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { tv } from "tailwind-variants/lite";
+
+// Modal dialog (Radix "Dialog" over Base UI). The popup is centered and gets
+// vertical padding + a 16px gap; each region (header, body, footer) carries its
+// own horizontal padding so a full-bleed body slot stays possible.
+export const dialog = tv({
+ slots: {
+ backdrop: [
+ "fixed inset-0 z-50 bg-sand-12/40",
+ "transition-opacity duration-150",
+ "data-starting-style:opacity-0 data-ending-style:opacity-0",
+ ],
+ popup: [
+ "fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2",
+ "flex w-[calc(100vw-2rem)] max-w-[600px] flex-col gap-4",
+ "max-h-[calc(100vh-2rem)] overflow-y-auto overflow-x-clip",
+ "rounded-5 border border-sand-6 bg-sand-1 py-6 shadow-6 outline-none",
+ "transition-all duration-150",
+ "data-starting-style:scale-95 data-starting-style:opacity-0",
+ "data-ending-style:scale-95 data-ending-style:opacity-0",
+ ],
+ header: "flex flex-col gap-2 px-6",
+ title: "text-4 font-medium text-sand-12",
+ description: "text-2 text-sand-11",
+ body: "px-6",
+ footer: "flex items-center justify-end gap-3 px-6",
+ },
+});
+
+// Static frame matching the dialog popup, without the interactive positioning,
+// so a placeholder can render before Base UI (and the content) loads.
+export const dialogSkeleton = tv({
+ base: [
+ "flex w-full max-w-[600px] flex-col gap-4",
+ "rounded-5 border border-sand-6 bg-sand-1 py-6 shadow-6",
+ ],
+});