diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index fefa9c7e3f..4bb5ae928b 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -4702,6 +4702,7 @@ login = "Login" magicLinkSent = "Magic link sent to {{email}}! Check your email and click the link to sign in." mfaCode = "Authentication Code" mfaRequired = "Two-factor code required" +noAccount = "Don't have an account?" or = "Or" password = "Password" passwordChangedSuccess = "Password changed successfully! Please sign in with your new password." @@ -4726,21 +4727,6 @@ useMagicLink = "Use magic link instead" username = "Username" youAreLoggedIn = "You are logged in!" -[login.slides.edit] -alt = "Edit PDFs" -subtitle = "With over a dozen tools to help you redact, sign, read and manipulate PDFs, you will be sure to find what you are looking for." -title = "Edit PDFs to display/secure the information you want" - -[login.slides.overview] -alt = "Stirling PDF overview" -subtitle = "A privacy-first cloud suite for PDFs that lets you convert, sign, redact, and manage documents, along with 50+ other powerful tools." -title = "Your one-stop-shop for all your PDF needs." - -[login.slides.secure] -alt = "Secure PDFs" -subtitle = "Add passwords, redact content, and manage certificates with ease." -title = "Protect sensitive information in your PDFs" - [margin] large = "Large" medium = "Medium" diff --git a/frontend/editor/src/core/assets/login/AddToPDF.png b/frontend/editor/src/core/assets/login/AddToPDF.png deleted file mode 100644 index 94e9a0dedf..0000000000 Binary files a/frontend/editor/src/core/assets/login/AddToPDF.png and /dev/null differ diff --git a/frontend/editor/src/core/assets/login/Firstpage.png b/frontend/editor/src/core/assets/login/Firstpage.png deleted file mode 100644 index f12133f4f7..0000000000 Binary files a/frontend/editor/src/core/assets/login/Firstpage.png and /dev/null differ diff --git a/frontend/editor/src/core/assets/login/LoginBackgroundPanel.png b/frontend/editor/src/core/assets/login/LoginBackgroundPanel.png deleted file mode 100644 index 4ea0e0ccf1..0000000000 Binary files a/frontend/editor/src/core/assets/login/LoginBackgroundPanel.png and /dev/null differ diff --git a/frontend/editor/src/core/assets/login/SecurePDF.png b/frontend/editor/src/core/assets/login/SecurePDF.png deleted file mode 100644 index 6184440e92..0000000000 Binary files a/frontend/editor/src/core/assets/login/SecurePDF.png and /dev/null differ diff --git a/frontend/editor/src/core/styles/theme.css b/frontend/editor/src/core/styles/theme.css index 11d4ab217b..18955b438f 100644 --- a/frontend/editor/src/core/styles/theme.css +++ b/frontend/editor/src/core/styles/theme.css @@ -786,22 +786,20 @@ .wordmark-dark-only { display: none; } -[data-mantine-color-scheme="dark"] .wordmark-light-only { +[data-mantine-color-scheme="dark"] .wordmark { display: none; } [data-mantine-color-scheme="dark"] .wordmark-dark-only { display: block; } -/* Theme-aware image display utilities: .theme-img-light-only shows in light - mode only, .theme-img-dark-only shows in dark mode only. */ -.theme-img-light-only { +.theme-img { display: inline; } .theme-img-dark-only { display: none; } -[data-mantine-color-scheme="dark"] .theme-img-light-only { +[data-mantine-color-scheme="dark"] .theme-img { display: none; } [data-mantine-color-scheme="dark"] .theme-img-dark-only { diff --git a/frontend/editor/src/core/tests/live/authentication-login.spec.ts b/frontend/editor/src/core/tests/live/authentication-login.spec.ts index af1733ac42..f9d0f6e628 100644 --- a/frontend/editor/src/core/tests/live/authentication-login.spec.ts +++ b/frontend/editor/src/core/tests/live/authentication-login.spec.ts @@ -162,34 +162,4 @@ test.describe("1. Authentication and Login", () => { }); }); }); - - test.describe("1.6 Login Page - Carousel/Slideshow", () => { - test("should navigate between carousel slides", async ({ page }) => { - // Carousel is hidden on small viewports (< 940px wide), ensure desktop size - await page.setViewportSize({ width: 1920, height: 1080 }); - - // Starting state: User is logged out; browser on /login - await page.goto("/login"); - await page.waitForLoadState("domcontentloaded"); - - // Step 1: Verify slide indicator dots are present (carousel uses aria-label "Go to slide N") - const slideButtons = page.getByRole("button", { name: /Go to slide/i }); - const count = await slideButtons.count(); - test.skip(count === 0, "No carousel slides configured on this instance"); - - // Step 2: Click through slides - if (count >= 2) { - await slideButtons.nth(1).click(); - await page.waitForTimeout(500); - } - if (count >= 3) { - await slideButtons.nth(2).click(); - await page.waitForTimeout(500); - } - - // Step 3: Click back to slide 1 - await slideButtons.nth(0).click(); - await page.waitForTimeout(500); - }); - }); }); diff --git a/frontend/editor/src/desktop/components/SetupWizard/DesktopAuthLayout.tsx b/frontend/editor/src/desktop/components/SetupWizard/DesktopAuthLayout.tsx index f5e64f2927..7d4af4fa81 100644 --- a/frontend/editor/src/desktop/components/SetupWizard/DesktopAuthLayout.tsx +++ b/frontend/editor/src/desktop/components/SetupWizard/DesktopAuthLayout.tsx @@ -1,9 +1,5 @@ -import React, { useEffect, useMemo, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import LoginRightCarousel from "@app/auth/ui/LoginRightCarousel"; -import buildLoginSlides from "@app/components/shared/loginSlides"; -import styles from "@app/auth/ui/AuthShell.module.css"; -import { useLogoVariant } from "@app/hooks/useLogoVariant"; +import React from "react"; +import { AuthShell } from "@app/auth/ui/AuthShell"; interface DesktopAuthLayoutProps { children: React.ReactNode; @@ -12,52 +8,5 @@ interface DesktopAuthLayoutProps { export const DesktopAuthLayout: React.FC = ({ children, }) => { - const { t } = useTranslation(); - const cardRef = useRef(null); - const [hideRightPanel, setHideRightPanel] = useState(false); - const logoVariant = useLogoVariant(); - const imageSlides = useMemo( - () => buildLoginSlides(logoVariant, t), - [logoVariant, t], - ); - - useEffect(() => { - const update = () => { - // Use viewport to avoid hysteresis when the card is already in single-column mode - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - const cardWidthIfTwoCols = Math.min(1180, viewportWidth * 0.96); // matches min(73.75rem, 96vw) - const columnWidth = cardWidthIfTwoCols / 2; - const tooNarrow = columnWidth < 470; - const tooShort = viewportHeight < 740; - setHideRightPanel(tooNarrow || tooShort); - }; - update(); - window.addEventListener("resize", update); - window.addEventListener("orientationchange", update); - return () => { - window.removeEventListener("resize", update); - window.removeEventListener("orientationchange", update); - }; - }, []); - - return ( -
-
-
-
{children}
-
- {!hideRightPanel && ( - - )} -
-
- ); + return {children}; }; diff --git a/frontend/editor/src/desktop/components/SetupWizard/desktopOAuth.css b/frontend/editor/src/desktop/components/SetupWizard/desktopOAuth.css index 6661c6a99b..55ab193a4c 100644 --- a/frontend/editor/src/desktop/components/SetupWizard/desktopOAuth.css +++ b/frontend/editor/src/desktop/components/SetupWizard/desktopOAuth.css @@ -17,12 +17,12 @@ own padding-block); horizontal stays in the shorthand. */ --sui-btn-py: 1rem; /* 16px (md) */ padding: 1rem 1rem; /* 16px */ - border: 1px solid var(--auth-input-border-light-only); + border: 1px solid var(--auth-input-border); border-radius: 0.75rem; /* 12px */ - background-color: var(--auth-card-bg-light-only); + background-color: var(--auth-card-bg); font-size: 1rem; /* 16px */ font-weight: 500; - color: var(--auth-text-primary-light-only); + color: var(--auth-text-primary); cursor: pointer; gap: 0.75rem; /* 12px */ font-family: inherit; @@ -39,7 +39,7 @@ } .oauth-button-vertical-desktop:focus-visible { - outline: 2px solid var(--auth-border-focus-light-only); + outline: 2px solid var(--auth-border-focus); outline-offset: 2px; } diff --git a/frontend/editor/src/portal/components/LoginScreen.tsx b/frontend/editor/src/portal/components/LoginScreen.tsx index f16553d56d..ac69040c19 100644 --- a/frontend/editor/src/portal/components/LoginScreen.tsx +++ b/frontend/editor/src/portal/components/LoginScreen.tsx @@ -1,8 +1,4 @@ -import { useMemo } from "react"; -import { useTranslation } from "react-i18next"; import { AuthShell } from "@app/auth/ui/AuthShell"; -import LoginRightCarousel from "@app/auth/ui/LoginRightCarousel"; -import { buildDefaultLoginSlides } from "@app/auth/ui/loginSlides"; import SpringLoginForm from "@app/auth/ui/SpringLoginForm"; import { useSpringLogin } from "@app/auth/ui/useSpringLogin"; import { withBasePath } from "@app/constants/app"; @@ -12,7 +8,7 @@ import loginHeader from "@app/assets/brand/modern-logo/LoginLightModeHeader.svg" /** * Full-screen login shown by the portal's auth gate. Renders the shared - * AuthShell + carousel with the Spring form/auth wiring from @app/auth/ui. + * AuthShell with the Spring form/auth wiring from @app/auth/ui. * * It follows the user's light/dark theme (AuthShell is theme-aware — the same * screen the editor login uses; passing both logo variants keeps the header @@ -20,23 +16,10 @@ import loginHeader from "@app/assets/brand/modern-logo/LoginLightModeHeader.svg" * needs to collect credentials. */ export function LoginScreen() { - const { t } = useTranslation(); const login = useSpringLogin(); - const slides = useMemo( - () => buildDefaultLoginSlides((key, fallback) => t(key, fallback)), - [t], - ); return ( - - } - > + + + + {t("login.defaultCredentials", "Default Login Credentials")} + + + + {t("login.username", "Username")}: + {" "} + admin + + + + {t("login.password", "Password")}: + {" "} + stirling + + + {t( + "login.changePasswordWarning", + "Please change your password after logging in for the first time", + )} + + + + ); +} diff --git a/frontend/editor/src/proprietary/auth/ui/AuthScreens.stories.tsx b/frontend/editor/src/proprietary/auth/ui/AuthScreens.stories.tsx new file mode 100644 index 0000000000..9100892fee --- /dev/null +++ b/frontend/editor/src/proprietary/auth/ui/AuthScreens.stories.tsx @@ -0,0 +1,203 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { AuthShell } from "@app/auth/ui/AuthShell"; +import SpringLoginForm from "@app/auth/ui/SpringLoginForm"; +import AuthSignupPrompt from "@app/auth/ui/AuthSignupPrompt"; +import AuthDefaultCredentials from "@app/auth/ui/AuthDefaultCredentials"; +import type { SpringLoginState } from "@app/auth/ui/useSpringLogin"; +import ErrorMessage from "@app/auth/ui/ErrorMessage"; +import SignupForm from "@app/routes/signup/SignupForm"; +import DividerWithText from "@app/components/shared/DividerWithText"; +import { Button } from "@app/ui/Button"; +import loginHeader from "@app/assets/brand/modern-logo/LoginLightModeHeader.svg"; +import "@app/auth/ui/auth-theme.css"; +import "@app/auth/ui/auth.css"; + +const darkLogo = "/modern-logo/LoginDarkModeHeader.svg"; + +/** Every provider the OAuth buttons know how to render, for stress testing. */ +const ALL_PROVIDERS = [ + "google", + "github", + "apple", + "azure", + "keycloak", + "cloudron", + "authentik", + "oidc", +]; + +type LoginMethod = "all" | "normal" | "oauth2"; + +interface LoginArgs { + /** OAuth providers to render (stress test the button stack here). */ + providers: string[]; + /** all = OAuth + email · normal = email only · oauth2 = SSO only. */ + loginMethod: LoginMethod; + /** Show the "Don't have an account? Sign up" prompt below the form. */ + showSignupPrompt: boolean; + /** Show the first-time-setup default admin credentials card. */ + showDefaultCredentials: boolean; +} + +/** Interactive SpringLoginState without the network/config fetch. */ +function useFakeSpringLogin( + providers: string[], + loginMethod: LoginMethod, +): SpringLoginState { + // Prefill so the submit CTA renders in its enabled (filled) state. + const [email, setEmail] = useState("you@company.com"); + const [password, setPassword] = useState("password"); + const [mfaCode, setMfaCode] = useState(""); + const isUserPassAllowed = loginMethod === "all" || loginMethod === "normal"; + return { + email, + setEmail, + password, + setPassword, + mfaCode, + setMfaCode, + requiresMfa: false, + error: null, + setError: () => {}, + isSubmitting: false, + providers, + loginMethod, + isUserPassAllowed, + hasProviders: providers.length > 0, + signInWithEmail: async () => {}, + signInWithProvider: async () => {}, + }; +} + +function LoginPreview({ + providers, + loginMethod, + showSignupPrompt, + showDefaultCredentials, +}: LoginArgs) { + const login = useFakeSpringLogin(providers, loginMethod); + return ( + + + {showDefaultCredentials && } + {showSignupPrompt && {}} />} + + } + /> + + ); +} + +function SignupPreview() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + return ( + +
+ Stirling PDF + Stirling PDF +
+ + {}} + isSubmitting={false} + /> + +
+ +
+
+ ); +} + +/** + * The slim single-column auth screens: one narrow card centered on the page. + * Uses a fake login state so the full screen renders without a backend. The + * Login stories expose provider/login-method controls in the playground so the + * OAuth button stack can be stress tested with any number of providers. + */ +const meta: Meta = { + title: "Auth/Auth Screens", + parameters: { layout: "fullscreen" }, + argTypes: { + providers: { + control: "check", + options: ALL_PROVIDERS, + description: "OAuth providers rendered above the email form", + }, + loginMethod: { + control: "inline-radio", + options: ["all", "normal", "oauth2"], + description: + "all = OAuth + email · normal = email only · oauth2 = SSO only", + }, + showSignupPrompt: { + control: "boolean", + description: "Show the sign-up prompt below the form", + }, + showDefaultCredentials: { + control: "boolean", + description: "Show the first-time-setup default admin credentials card", + }, + }, + args: { + providers: ["google", "github", "oidc"], + loginMethod: "all", + showSignupPrompt: true, + showDefaultCredentials: false, + }, +}; +export default meta; +type Story = StoryObj; + +export const Login: Story = { + render: (args) => , +}; + +export const AllProviders: Story = { + args: { providers: ALL_PROVIDERS }, + render: (args) => , +}; + +export const LoginSsoOnly: Story = { + args: { loginMethod: "oauth2" }, + render: (args) => , +}; + +export const LoginEmailOnly: Story = { + args: { providers: [] }, + render: (args) => , +}; + +export const FirstTimeSetup: Story = { + args: { showDefaultCredentials: true }, + render: (args) => , +}; + +export const Signup: Story = { + render: () => , +}; diff --git a/frontend/editor/src/proprietary/auth/ui/AuthShell.module.css b/frontend/editor/src/proprietary/auth/ui/AuthShell.module.css index 3e5e5247ea..bb0e418d88 100644 --- a/frontend/editor/src/proprietary/auth/ui/AuthShell.module.css +++ b/frontend/editor/src/proprietary/auth/ui/AuthShell.module.css @@ -5,7 +5,7 @@ flex-direction: column; align-items: center; justify-content: center; - background-color: var(--auth-bg-color-light-only); + background-color: var(--auth-bg-color); padding: 1.5rem 1.5rem 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; @@ -13,37 +13,23 @@ } .authCard { - width: min(45rem, 96vw); - height: min(50.875rem, 96vh); - display: grid; - grid-template-columns: 1fr; + width: min(26rem, 96vw); + max-height: 96vh; + padding: 2.5rem 2rem; + display: flex; + justify-content: center; background-color: var(--auth-card-bg); border-radius: 1.25rem; box-shadow: 0 1.25rem 3.75rem rgba(0, 0, 0, 0.12); - overflow: hidden; - min-height: 0; + overflow-y: auto; + overflow-x: hidden; } -.authCardTwoColumns { - width: min(73.75rem, 96vw); - grid-template-columns: 1fr 1fr; -} - -.authLeftPanel { - display: flex; - align-items: center; - justify-content: center; - padding: 2rem; - overflow: hidden; - min-height: 0; - height: 100%; -} - -.authLeftPanel::-webkit-scrollbar { +.authCard::-webkit-scrollbar { display: none; /* WebKit browsers (Chrome, Safari, Edge) */ } .authContent { - max-width: 26.25rem; /* 420px */ + max-width: 22rem; /* 352px */ width: 100%; } diff --git a/frontend/editor/src/proprietary/auth/ui/AuthShell.tsx b/frontend/editor/src/proprietary/auth/ui/AuthShell.tsx index 13f9e7d80b..2b1f8f705c 100644 --- a/frontend/editor/src/proprietary/auth/ui/AuthShell.tsx +++ b/frontend/editor/src/proprietary/auth/ui/AuthShell.tsx @@ -1,56 +1,22 @@ -import { useEffect, useRef, useState, type ReactNode } from "react"; +import type { ReactNode } from "react"; import styles from "@app/auth/ui/AuthShell.module.css"; export interface AuthShellProps { children: ReactNode; - /** Optional panel shown beside the form on wide/tall viewports (the carousel). */ - rightPanel?: ReactNode; /** Optional fixed footer slot (the editor passes its legal/cookie footer). */ footer?: ReactNode; } /** - * The login card shell shared by the editor and the portal: a centered card - * that expands to two columns (form + right panel) on wide/tall viewports and - * collapses to a single column otherwise. Purely presentational - callers - * provide the form (children), the right panel, and an optional footer. + * The login card shell shared by the editor and the portal: a single narrow + * card centered on the screen. Purely presentational - callers provide the + * form (children) and an optional footer. */ -export function AuthShell({ children, rightPanel, footer }: AuthShellProps) { - const cardRef = useRef(null); - const [hideRightPanel, setHideRightPanel] = useState(false); - - useEffect(() => { - const update = () => { - // Use viewport to avoid hysteresis when the card is already single-column. - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - const cardWidthIfTwoCols = Math.min(1180, viewportWidth * 0.96); // min(73.75rem, 96vw) - const columnWidth = cardWidthIfTwoCols / 2; - const tooNarrow = columnWidth < 470; - const tooShort = viewportHeight < 740; - setHideRightPanel(tooNarrow || tooShort); - }; - update(); - window.addEventListener("resize", update); - window.addEventListener("orientationchange", update); - return () => { - window.removeEventListener("resize", update); - window.removeEventListener("orientationchange", update); - }; - }, []); - - const showRightPanel = Boolean(rightPanel) && !hideRightPanel; - +export function AuthShell({ children, footer }: AuthShellProps) { return (
-
-
-
{children}
-
- {showRightPanel && rightPanel} +
+
{children}
{footer && (
void; +} + +/** + * "Don't have an account? Sign up" row shown beneath the login form. The prompt + * is muted; the action reads as a brand-coloured link. + */ +export default function AuthSignupPrompt({ onSignUp }: AuthSignupPromptProps) { + const { t } = useTranslation(); + return ( +
+ {t("login.noAccount", "Don't have an account?")} + +
+ ); +} diff --git a/frontend/editor/src/proprietary/auth/ui/EmailPasswordForm.tsx b/frontend/editor/src/proprietary/auth/ui/EmailPasswordForm.tsx index 08236c12c4..e83715a802 100644 --- a/frontend/editor/src/proprietary/auth/ui/EmailPasswordForm.tsx +++ b/frontend/editor/src/proprietary/auth/ui/EmailPasswordForm.tsx @@ -3,18 +3,20 @@ import { Button } from "@app/ui/Button"; import "@app/auth/ui/auth.css"; import { TextInput, PasswordInput } from "@mantine/core"; -// Force light mode styles for auth inputs -const authInputStyles = { +// Theme-aware auth input colours (the --auth-* vars flip in dark mode via +// auth-theme.css). Exported so other auth screens (e.g. invite accept) render +// their Mantine inputs identically to login. +export const authInputStyles = { input: { - backgroundColor: "var(--auth-input-bg-light-only)", - color: "var(--auth-input-text-light-only)", - borderColor: "var(--auth-input-border-light-only)", + backgroundColor: "var(--auth-input-bg)", + color: "var(--auth-input-text)", + borderColor: "var(--auth-input-border)", "&:focus": { - borderColor: "var(--auth-border-focus-light-only)", + borderColor: "var(--auth-border-focus)", }, }, label: { - color: "var(--auth-label-text-light-only)", + color: "var(--auth-label-text)", }, }; @@ -130,7 +132,10 @@ export default function EmailPasswordForm({ (requiresMfa && !mfaCode.trim()) } fullWidth + size="lg" + fontSize="sm" loading={isSubmitting} + className="auth-submit" // Stirling-red brand CTA; the brand accent sets the colour inline so the // host app's Mantine primaryColor can't win (editor vs portal differ). accent="brand" diff --git a/frontend/editor/src/proprietary/auth/ui/LoginRightCarousel.tsx b/frontend/editor/src/proprietary/auth/ui/LoginRightCarousel.tsx deleted file mode 100644 index 490aea923c..0000000000 --- a/frontend/editor/src/proprietary/auth/ui/LoginRightCarousel.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import { memo, useEffect, useMemo, useRef, useState } from "react"; -import { CarouselDots } from "@app/ui/CarouselDots"; -import bgDefault from "@app/assets/login/LoginBackgroundPanel.png"; - -export type ImageSlide = { - src: string; - alt?: string; - cornerModelUrl?: string; - title?: string; - subtitle?: string; - followMouseTilt?: boolean; - tiltMaxDeg?: number; -}; - -function LoginRightCarousel({ - imageSlides = [], - showBackground = true, - backgroundSrc = bgDefault, - initialSeconds = 5, - slideSeconds = 8, -}: { - imageSlides?: ImageSlide[]; - showBackground?: boolean; - /** Background panel image; defaults to the bundled shared asset. */ - backgroundSrc?: string; - initialSeconds?: number; - slideSeconds?: number; -}) { - const totalSlides = imageSlides.length; - const [index, setIndex] = useState(0); - const mouse = useRef({ x: 0, y: 0 }); - - const durationsMs = useMemo(() => { - if (imageSlides.length === 0) return []; - return imageSlides.map( - (_, i) => - (i === 0 ? (initialSeconds ?? slideSeconds) : slideSeconds) * 1000, - ); - }, [imageSlides, initialSeconds, slideSeconds]); - - useEffect(() => { - if (totalSlides <= 1) return; - const timeout = setTimeout( - () => { - setIndex((i) => (i + 1) % totalSlides); - }, - durationsMs[index] ?? slideSeconds * 1000, - ); - return () => clearTimeout(timeout); - }, [index, totalSlides, durationsMs, slideSeconds]); - - useEffect(() => { - const onMove = (e: MouseEvent) => { - mouse.current.x = (e.clientX / window.innerWidth) * 2 - 1; - mouse.current.y = (e.clientY / window.innerHeight) * 2 - 1; - }; - window.addEventListener("mousemove", onMove); - return () => window.removeEventListener("mousemove", onMove); - }, []); - - function TiltImage({ - src, - alt, - enabled, - maxDeg = 6, - }: { - src: string; - alt?: string; - enabled: boolean; - maxDeg?: number; - }) { - const imgRef = useRef(null); - - useEffect(() => { - const el = imgRef.current; - if (!el) return; - - let raf = 0; - const tick = () => { - if (enabled) { - const rotY = (mouse.current.x || 0) * maxDeg; - const rotX = -(mouse.current.y || 0) * maxDeg; - el.style.transform = `translateY(-2rem) rotateX(${rotX.toFixed(2)}deg) rotateY(${rotY.toFixed(2)}deg)`; - } else { - el.style.transform = "translateY(-2rem)"; - } - raf = requestAnimationFrame(tick); - }; - raf = requestAnimationFrame(tick); - return () => cancelAnimationFrame(raf); - }, [enabled, maxDeg]); - - return ( - {alt - ); - } - - return ( -
- {showBackground && ( - Background panel - )} - - {/* Image slides */} - {imageSlides.map((s, idx) => ( -
- {(s.title || s.subtitle) && ( -
- {s.title && ( -
- {s.title} -
- )} - {s.subtitle && ( -
- {s.subtitle} -
- )} -
- )} - -
- ))} - - {/* Dot navigation */} - -
- ); -} - -export default memo(LoginRightCarousel); diff --git a/frontend/editor/src/proprietary/auth/ui/SpringLoginForm.tsx b/frontend/editor/src/proprietary/auth/ui/SpringLoginForm.tsx index ca8deb3c32..82d05a45ac 100644 --- a/frontend/editor/src/proprietary/auth/ui/SpringLoginForm.tsx +++ b/frontend/editor/src/proprietary/auth/ui/SpringLoginForm.tsx @@ -120,21 +120,19 @@ export default function SpringLoginForm({ {beforeEmailForm} {renderEmailForm && ( -
- -
+ )} {footer} diff --git a/frontend/editor/src/proprietary/auth/ui/auth-theme.css b/frontend/editor/src/proprietary/auth/ui/auth-theme.css index 1da8ba13e8..c1b843e804 100644 --- a/frontend/editor/src/proprietary/auth/ui/auth-theme.css +++ b/frontend/editor/src/proprietary/auth/ui/auth-theme.css @@ -2,25 +2,30 @@ :root { /* Auth page colors (light mode) */ - --auth-bg-color-light-only: var(--p-gray-100); + --auth-bg-color: var(--p-gray-100); --auth-card-bg: #ffffff; - --auth-card-bg-light-only: #ffffff; - --auth-label-text-light-only: var(--p-gray-700); - --auth-input-border-light-only: var(--p-gray-300); - --auth-input-bg-light-only: #ffffff; - --auth-input-text-light-only: var(--p-gray-900); - --auth-border-focus-light-only: var(--p-blue-500); - --auth-focus-ring-light-only: color-mix( + --auth-label-text: var(--p-gray-700); + --auth-input-border: var(--p-gray-300); + --auth-input-bg: #ffffff; + --auth-input-text: var(--p-gray-900); + --auth-border-focus: var(--p-blue-500); + --auth-focus-ring: color-mix(in srgb, var(--p-blue-500) 10%, transparent); + --auth-button-bg: var(--p-red-600); + --auth-button-text: #ffffff; + --auth-magic-button-bg: var(--p-gray-200); + --auth-magic-button-text: var(--p-gray-700); + --auth-text-primary: var(--p-gray-900); + --auth-text-secondary: var(--p-gray-500); + --auth-error-bg: color-mix(in srgb, var(--p-red-500) 8%, transparent); + --auth-error-border: color-mix(in srgb, var(--p-red-500) 25%, transparent); + --auth-error-text: var(--p-red-600); + --auth-success-bg: color-mix(in srgb, var(--p-green-500) 10%, transparent); + --auth-success-border: color-mix( in srgb, - var(--p-blue-500) 10%, + var(--p-green-500) 30%, transparent ); - --auth-button-bg-light-only: var(--p-red-600); - --auth-button-text-light-only: #ffffff; - --auth-magic-button-bg-light-only: var(--p-gray-200); - --auth-magic-button-text-light-only: var(--p-gray-700); - --auth-text-primary-light-only: var(--p-gray-900); - --auth-text-secondary-light-only: var(--p-gray-500); + --auth-success-text: var(--p-green-600); --text-divider-rule-rgb-light: 229, 231, 235; --text-divider-label-rgb-light: 156, 163, 175; --tool-subcategory-rule-color-light: var(--p-gray-200); @@ -28,25 +33,30 @@ } [data-mantine-color-scheme="dark"] { - --auth-bg-color-light-only: var(--bg-muted); + --auth-bg-color: var(--bg-muted); --auth-card-bg: var(--bg-surface); - --auth-card-bg-light-only: var(--bg-surface); - --auth-label-text-light-only: var(--text-secondary); - --auth-input-border-light-only: var(--border-default); - --auth-input-bg-light-only: var(--bg-raised); - --auth-input-text-light-only: var(--text-primary); - --auth-border-focus-light-only: var(--p-blue-500); - --auth-focus-ring-light-only: color-mix( + --auth-label-text: var(--text-secondary); + --auth-input-border: var(--border-default); + --auth-input-bg: var(--bg-raised); + --auth-input-text: var(--text-primary); + --auth-border-focus: var(--p-blue-500); + --auth-focus-ring: color-mix(in srgb, var(--p-blue-500) 20%, transparent); + --auth-button-bg: var(--p-red-600); + --auth-button-text: #ffffff; + --auth-magic-button-bg: var(--bg-raised); + --auth-magic-button-text: var(--text-primary); + --auth-text-primary: var(--text-primary); + --auth-text-secondary: var(--text-secondary); + --auth-error-bg: color-mix(in srgb, var(--p-red-500) 12%, transparent); + --auth-error-border: color-mix(in srgb, var(--p-red-500) 35%, transparent); + --auth-error-text: var(--p-red-400); + --auth-success-bg: color-mix(in srgb, var(--p-green-500) 12%, transparent); + --auth-success-border: color-mix( in srgb, - var(--p-blue-500) 20%, + var(--p-green-500) 35%, transparent ); - --auth-button-bg-light-only: var(--p-red-600); - --auth-button-text-light-only: #ffffff; - --auth-magic-button-bg-light-only: var(--bg-raised); - --auth-magic-button-text-light-only: var(--text-primary); - --auth-text-primary-light-only: var(--text-primary); - --auth-text-secondary-light-only: var(--text-secondary); + --auth-success-text: var(--p-green-500); --text-divider-rule-rgb-light: 28, 35, 64; --text-divider-label-rgb-light: 91, 98, 128; --tool-subcategory-rule-color-light: var(--border-default); diff --git a/frontend/editor/src/proprietary/auth/ui/auth.css b/frontend/editor/src/proprietary/auth/ui/auth.css index 31f1eb480a..1745e1d6b3 100644 --- a/frontend/editor/src/proprietary/auth/ui/auth.css +++ b/frontend/editor/src/proprietary/auth/ui/auth.css @@ -10,7 +10,10 @@ display: flex; align-items: center; gap: 0.75rem; /* 12px */ - margin: 0.375rem 0 0.5rem; /* 6px 0 8px */ + /* Center "or" between the last OAuth button and the input box: the larger top + margin balances the field label + input that sit below the smaller bottom + margin, so the gap to the button ≈ the gap to the input. */ + margin: 2.25rem 0 0.75rem; /* 36px top, 12px bottom */ } .auth-or-divider__rule { @@ -33,24 +36,33 @@ .auth-label { font-size: 0.875rem; /* 14px */ - color: var(--auth-label-text-light-only); + color: var(--auth-label-text); font-weight: 500; } +/* Normalize the Mantine inputs used by the login/signup forms so they match the + OAuth buttons and submit CTA: same height, radius, and font size. */ +.auth-fields .mantine-Input-input { + min-height: 2.625rem; /* 42px */ + height: 2.625rem; + border-radius: 0.625rem; /* 10px */ + font-size: 0.9375rem; /* 15px */ +} + .auth-input { width: 100%; padding: 0.625rem 0.75rem; /* 10px 12px */ - border: 1px solid var(--auth-input-border-light-only); + border: 1px solid var(--auth-input-border); border-radius: 0.625rem; /* 10px */ font-size: 0.875rem; /* 14px */ - background-color: var(--auth-input-bg-light-only); - color: var(--auth-input-text-light-only); + background-color: var(--auth-input-bg); + color: var(--auth-input-text); outline: none; } .auth-input:focus { - border-color: var(--auth-border-focus-light-only); - box-shadow: 0 0 0 3px var(--auth-focus-ring-light-only); + border-color: var(--auth-border-focus); + box-shadow: 0 0 0 3px var(--auth-focus-ring); } .auth-button { @@ -58,8 +70,8 @@ padding: 0.625rem 0.75rem; /* 10px 12px */ border: none; border-radius: 0.625rem; /* 10px */ - background-color: var(--auth-button-bg-light-only); - color: var(--auth-button-text-light-only); + background-color: var(--auth-button-bg); + color: var(--auth-button-text); font-size: 0.875rem; /* 14px */ font-weight: 600; margin-bottom: 0.75rem; /* 12px */ @@ -79,7 +91,7 @@ .auth-toggle-link { background: transparent; border: 0; - color: var(--auth-label-text-light-only); + color: var(--auth-label-text); font-size: 0.875rem; /* 14px */ text-decoration: underline; cursor: pointer; @@ -104,8 +116,8 @@ padding: 0.875rem 1rem; /* 14px 16px */ border: none; border-radius: 0.625rem; /* 10px */ - background-color: var(--auth-magic-button-bg-light-only); - color: var(--auth-magic-button-text-light-only); + background-color: var(--auth-magic-button-bg); + color: var(--auth-magic-button-text); font-size: 0.875rem; /* 14px */ font-weight: 600; white-space: nowrap; @@ -132,7 +144,7 @@ .auth-terms-label { font-size: 0.75rem; /* 12px */ - color: var(--auth-label-text-light-only); + color: var(--auth-label-text); } .auth-terms-label a { @@ -182,15 +194,21 @@ min-height: 3.5rem; /* 56px */ } .oauth-button-fullwidth.sui-btn.mantine-Button-root { - border-radius: 100px; + border-radius: 0.625rem; /* 10px */ + min-height: 0; +} + +/* Match the submit CTA's corners to the OAuth buttons and inputs (10px). */ +.auth-submit.sui-btn.mantine-Button-root { + border-radius: 0.625rem; /* 10px */ } .oauth-button-icon { width: 3.75rem; /* 60px */ height: 3.75rem; /* 60px */ border-radius: 0.875rem; /* 14px */ - border: 1px solid var(--auth-input-border-light-only); - background: var(--auth-card-bg-light-only); + border: 1px solid var(--auth-input-border); + background: var(--auth-card-bg); cursor: pointer; box-shadow: 0 0.125rem 0.375rem rgba(0, 0, 0, 0.04); /* 0 2px 6px */ display: flex; @@ -207,8 +225,8 @@ width: 100%; padding: 1rem; /* 16px */ border-radius: 0.875rem; /* 14px */ - border: 1px solid var(--auth-input-border-light-only); - background: var(--auth-card-bg-light-only); + border: 1px solid var(--auth-input-border); + background: var(--auth-card-bg); cursor: pointer; box-shadow: 0 0.125rem 0.375rem rgba(0, 0, 0, 0.04); /* 0 2px 6px */ display: flex; @@ -231,12 +249,12 @@ the Button's own padding-block; horizontal stays in the shorthand. */ --sui-btn-py: 1rem; /* 16px (md) */ padding: 1rem 1.5rem; /* 16px 24px */ - border: 1px solid var(--auth-input-border-light-only); + border: 1px solid var(--auth-input-border); border-radius: 999px; - background-color: var(--auth-card-bg-light-only); + background-color: var(--auth-card-bg); font-size: 1rem; /* 16px */ font-weight: 600; - color: var(--auth-text-primary-light-only); + color: var(--auth-text-primary); cursor: pointer; gap: 1rem; /* 16px */ font-family: inherit; @@ -277,15 +295,15 @@ padding: 0.75rem 1rem; /* 12px 16px */ border: 1px solid #d1d5db; border-radius: 0.75rem; /* 12px */ - background-color: var(--auth-card-bg-light-only); + background-color: var(--auth-card-bg); font-weight: 500; - color: var(--auth-text-primary-light-only); + color: var(--auth-text-primary); box-shadow: none; } .oauth-button-vertical-legacy:hover:not(:disabled) { background-color: #f3f4f6; - color: var(--auth-text-primary-light-only); + color: var(--auth-text-primary); box-shadow: none; } @@ -378,6 +396,12 @@ display: block; } +/* Fullwidth OAuth rows use a smaller icon so button height matches the inputs. */ +.oauth-button-fullwidth .oauth-icon-medium { + width: 1.25rem; /* 20px */ + height: 1.25rem; /* 20px */ +} + .oauth-icon-tiny { width: 1.25rem; /* 20px */ height: 1.25rem; /* 20px */ @@ -418,7 +442,7 @@ align-items: center; justify-content: center; background: var(--bg-muted); - border: 1px solid var(--auth-input-border-light-only); + border: 1px solid var(--auth-input-border); } .oauth-button-vertical-tinted .oauth-icon-wrapper { @@ -465,17 +489,17 @@ } .sso-demo-card { - border: 1px solid var(--auth-input-border-light-only); + border: 1px solid var(--auth-input-border); border-radius: 1rem; padding: 1rem; - background: var(--auth-card-bg-light-only); + background: var(--auth-card-bg); box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.06); } .sso-demo-title { font-size: 0.875rem; font-weight: 700; - color: var(--auth-text-primary-light-only); + color: var(--auth-text-primary); margin-bottom: 0.75rem; letter-spacing: 0.01em; text-transform: uppercase; @@ -520,12 +544,12 @@ .login-title { font-size: 2rem; /* 32px */ font-weight: 800; - color: var(--auth-text-primary-light-only); + color: var(--auth-text-primary); margin: 0 0 0.375rem; /* 0 0 6px */ } .login-subtitle { - color: var(--auth-text-secondary-light-only); + color: var(--auth-text-secondary); font-size: 0.875rem; /* 14px */ margin: 0; } @@ -538,7 +562,7 @@ .navigation-link-button { background: none; border: none; - color: var(--auth-label-text-light-only); + color: var(--auth-label-text); font-size: 0.875rem; /* 14px */ cursor: pointer; text-decoration: underline; @@ -552,46 +576,46 @@ /* Message Styles */ .error-message { padding: 1rem; /* 16px */ - background-color: #fef2f2; - border: 1px solid #fecaca; + background-color: var(--auth-error-bg); + border: 1px solid var(--auth-error-border); border-radius: 0.5rem; /* 8px */ margin-bottom: 1.5rem; /* 24px */ } .error-message-text { - color: #dc2626; + color: var(--auth-error-text); font-size: 0.875rem; /* 14px */ margin: 0; } .success-message { padding: 1rem; /* 16px */ - background-color: #f0fdf4; - border: 1px solid #bbf7d0; + background-color: var(--auth-success-bg); + border: 1px solid var(--auth-success-border); border-radius: 0.5rem; /* 8px */ margin-bottom: 1.5rem; /* 24px */ } .success-message-text { - color: #059669; + color: var(--auth-success-text); font-size: 0.875rem; /* 14px */ margin: 0; } /* Field-level error styles */ .auth-field-error { - color: #dc2626; + color: var(--auth-error-text); font-size: 0.6875rem; /* 11px */ margin-top: 0.125rem; /* 2px */ line-height: 1.1; } .auth-input-error { - border-color: #dc2626 !important; + border-color: var(--auth-error-text) !important; } .auth-input-error:focus { - border-color: #dc2626 !important; + border-color: var(--auth-error-text) !important; box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1) !important; } @@ -626,13 +650,32 @@ text-decoration: underline; cursor: pointer; font-size: 0.875rem; /* 14px */ - color: var(--auth-text-primary-light-only); + color: var(--auth-text-primary); } .auth-dot-black { opacity: 0.5; padding: 0 0.5rem; - color: var(--auth-text-primary-light-only); + color: var(--auth-text-primary); +} + +/* "Don't have an account? Sign up" row beneath the login form. */ +.auth-signup-prompt { + display: flex; + align-items: center; + justify-content: center; + gap: 0.375rem; /* 6px */ + margin-top: 1.5rem; /* 24px */ + font-size: 0.875rem; /* 14px */ + color: var(--auth-label-text); +} + +.auth-signup-link.sui-btn.mantine-Button-root { + padding: 0; + min-height: 0; + height: auto; + font-size: 0.875rem; /* 14px */ + font-weight: 700; } /* Email login button - red CTA style matching SaaS version */ @@ -663,7 +706,7 @@ } .auth-logo-header { - height: 8rem; + height: 6.5rem; /* 104px */ width: auto; } @@ -736,12 +779,12 @@ justify-content: center; /* Vertical padding via the shared Button's --sui-btn-py (beats the Button's own padding-block); horizontal stays in the shorthand. */ - --sui-btn-py: 1rem; /* 16px (md) */ - padding: 1rem 1rem; /* 16px */ + --sui-btn-py: 0.625rem; /* 10px */ + padding: 0.625rem 1rem; /* 10px 16px */ border: 1px solid #d1d5db; - border-radius: 100px; + border-radius: 0.625rem; /* 10px */ background-color: #ffffff; - font-size: 1rem; + font-size: 0.9375rem; /* 15px */ font-weight: 600; color: #000000; cursor: pointer; @@ -811,50 +854,3 @@ flex-direction: column; gap: 0.75rem; } - -.oauth-button-fullwidth { - width: 100%; - display: flex; - align-items: center; - justify-content: center; - /* Vertical padding via the shared Button's --sui-btn-py (beats the Button's - own padding-block); horizontal stays in the shorthand. */ - --sui-btn-py: 1rem; /* 16px (md) */ - padding: 1rem 1rem; /* 16px */ - border: 1px solid #d1d5db; - border-radius: 100px; - background-color: #ffffff; - font-size: 1rem; - font-weight: 600; - color: #000000; - cursor: pointer; - gap: 0.5rem; - box-shadow: 0 0.125rem 0.375rem rgba(0, 0, 0, 0.04); - transition: - background-color 150ms ease, - box-shadow 150ms ease, - border-color 150ms ease; -} - -.oauth-button-fullwidth:disabled { - cursor: not-allowed; - opacity: 0.6; -} - -.oauth-button-fullwidth:hover:not(:disabled) { - background-color: #fafafa; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); -} - -[data-mantine-color-scheme="dark"] .oauth-button-fullwidth { - background-color: var(--bg-surface); - color: var(--text-primary); - border-color: var(--border-default); - box-shadow: none; -} - -[data-mantine-color-scheme="dark"] - .oauth-button-fullwidth:hover:not(:disabled) { - background-color: var(--bg-raised); - box-shadow: none; -} diff --git a/frontend/editor/src/proprietary/auth/ui/loginSlideText.ts b/frontend/editor/src/proprietary/auth/ui/loginSlideText.ts deleted file mode 100644 index d8bc1e68c2..0000000000 --- a/frontend/editor/src/proprietary/auth/ui/loginSlideText.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { defaultTranslate, type AuthTranslate } from "@app/auth/types"; - -export type LoginSlideText = { - alt: string; - title: string; - subtitle: string; -}; - -/** - * Shared carousel copy (i18n-optional, English default). The editor and portal - * both build their slides from this so the text stays in sync; only the images - * differ (the editor swaps the hero for its logo-variant image). Order matches - * buildDefaultLoginSlides: [overview, edit, secure]. - * - * Kept image-free on purpose: the editor imports this without dragging the - * bundled slide images into its build. - */ -export function loginSlideText( - translate: AuthTranslate = defaultTranslate, -): LoginSlideText[] { - return [ - { - alt: translate("login.slides.overview.alt", "Stirling PDF overview"), - title: translate( - "login.slides.overview.title", - "Your one-stop-shop for all your PDF needs.", - ), - subtitle: translate( - "login.slides.overview.subtitle", - "A privacy-first cloud suite for PDFs that lets you convert, sign, redact, and manage documents, along with 50+ other powerful tools.", - ), - }, - { - alt: translate("login.slides.edit.alt", "Edit PDFs"), - title: translate( - "login.slides.edit.title", - "Edit PDFs to display/secure the information you want", - ), - subtitle: translate( - "login.slides.edit.subtitle", - "With over a dozen tools to help you redact, sign, read and manipulate PDFs, you will be sure to find what you are looking for.", - ), - }, - { - alt: translate("login.slides.secure.alt", "Secure PDFs"), - title: translate( - "login.slides.secure.title", - "Protect sensitive information in your PDFs", - ), - subtitle: translate( - "login.slides.secure.subtitle", - "Add passwords, redact content, and manage certificates with ease.", - ), - }, - ]; -} diff --git a/frontend/editor/src/proprietary/auth/ui/loginSlides.ts b/frontend/editor/src/proprietary/auth/ui/loginSlides.ts deleted file mode 100644 index 86565006d8..0000000000 --- a/frontend/editor/src/proprietary/auth/ui/loginSlides.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defaultTranslate, type AuthTranslate } from "@app/auth/types"; -import type { ImageSlide } from "@app/auth/ui/LoginRightCarousel"; -import { loginSlideText } from "@app/auth/ui/loginSlideText"; -import firstPage from "@app/assets/login/Firstpage.png"; -import addToPdf from "@app/assets/login/AddToPDF.png"; -import securePdf from "@app/assets/login/SecurePDF.png"; - -const SLIDE_TILT = { followMouseTilt: true, tiltMaxDeg: 5 } as const; - -/** - * Default login carousel slides using bundled images. The portal uses this set; - * the editor builds its own (logo-variant hero) from loginSlideText. - */ -export function buildDefaultLoginSlides( - translate: AuthTranslate = defaultTranslate, -): ImageSlide[] { - const text = loginSlideText(translate); - return [firstPage, addToPdf, securePdf].map((src, i) => ({ - src, - ...text[i], - ...SLIDE_TILT, - })); -} diff --git a/frontend/editor/src/proprietary/components/shared/loginSlides.ts b/frontend/editor/src/proprietary/components/shared/loginSlides.ts deleted file mode 100644 index 1099330f44..0000000000 --- a/frontend/editor/src/proprietary/components/shared/loginSlides.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BASE_PATH } from "@app/constants/app"; -import { getLogoFolder } from "@app/constants/logo"; -import type { LogoVariant } from "@app/services/preferencesService"; -import type { TFunction } from "i18next"; -import { loginSlideText } from "@app/auth/ui/loginSlideText"; -import type { ImageSlide } from "@app/auth/ui/LoginRightCarousel"; -import addToPdf from "@app/assets/login/AddToPDF.png"; -import securePdf from "@app/assets/login/SecurePDF.png"; - -const SLIDE_TILT = { followMouseTilt: true, tiltMaxDeg: 5 } as const; - -/** - * Editor login carousel slides. Copy comes from the shared set (so the editor - * and portal carousels stay in sync) and the edit/secure images are the shared - * bundled assets; only the hero is the logo-variant image the editor serves - * from /public. - */ -export const buildLoginSlides = ( - variant: LogoVariant | null | undefined, - t: TFunction, -): ImageSlide[] => { - const folder = getLogoFolder(variant); - const text = loginSlideText((key, fallback) => t(key, fallback)); - const srcs = [`${BASE_PATH}/${folder}/Firstpage.png`, addToPdf, securePdf]; - return srcs.map((src, i) => ({ src, ...text[i], ...SLIDE_TILT })); -}; - -export default buildLoginSlides; diff --git a/frontend/editor/src/proprietary/routes/AuthCallback.module.css b/frontend/editor/src/proprietary/routes/AuthCallback.module.css deleted file mode 100644 index f3bc4e7ecf..0000000000 --- a/frontend/editor/src/proprietary/routes/AuthCallback.module.css +++ /dev/null @@ -1,131 +0,0 @@ -.page { - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - padding: 50px 20px; - background: #f5f5f5; - font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", - Arial, sans-serif; -} - -.card { - background: #ffffff; - border-radius: 12px; - padding: 40px; - max-width: 420px; - width: 100%; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - text-align: center; - color: #1a1a1a; - border: 1px solid #e5e7eb; -} - -.icon { - font-size: 48px; - margin-bottom: 16px; -} - -.iconSuccess { - color: #2e7d32; -} - -.iconError { - color: #d32f2f; -} - -.iconNeutral { - color: #4b5563; -} - -.title { - font-size: 24px; - font-weight: 600; - margin-bottom: 12px; - color: #1a1a1a; -} - -.message { - color: #666; - line-height: 1.6; - font-size: 15px; -} - -.loadingExtra { - color: #6b7280; - margin-top: 12px; - font-size: 14px; -} - -.errorBox { - margin-top: 20px; - background: #ffebee; - border: 1px solid #ffcdd2; - border-radius: 8px; - padding: 16px; - color: #c62828; - font-size: 14px; - line-height: 1.5; - word-break: break-word; - text-align: left; -} - -@media (prefers-color-scheme: dark) { - .page { - background: #1a1a1a; - color: #e0e0e0; - } - - .card { - background: #2d2d2d; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); - color: #e5e7eb; - border-color: #374151; - } - - .iconSuccess { - color: #66bb6a; - } - - .iconError { - color: #ef5350; - } - - .iconNeutral { - color: #9ca3af; - } - - .title { - color: #f5f5f5; - } - - .message, - .loadingExtra { - color: #b0b0b0; - } - - .errorBox { - background: #3d2020; - border: 1px solid #5d3030; - color: #ef9a9a; - } -} - -@media (max-width: 480px) { - .page { - padding: 20px 16px; - } - - .card { - padding: 32px 24px; - } - - .title { - font-size: 20px; - } - - .icon { - font-size: 40px; - } -} diff --git a/frontend/editor/src/proprietary/routes/AuthCallback.tsx b/frontend/editor/src/proprietary/routes/AuthCallback.tsx index 5d09776df4..4ce0e4b7aa 100644 --- a/frontend/editor/src/proprietary/routes/AuthCallback.tsx +++ b/frontend/editor/src/proprietary/routes/AuthCallback.tsx @@ -7,7 +7,11 @@ import { } from "@app/auth/spring/springAuthClient"; import { markLoginLandingPending } from "@app/utils/loginLanding"; import { handleAuthCallbackSuccess } from "@app/extensions/authCallback"; -import styles from "@app/routes/AuthCallback.module.css"; +import { AuthShell } from "@app/auth/ui/AuthShell"; +import { Spinner } from "@app/ui/Spinner"; +import { withBasePath } from "@app/constants/app"; +import "@app/auth/ui/auth.css"; +import loginHeader from "@app/assets/brand/modern-logo/LoginLightModeHeader.svg"; import i18n from "@app/i18n"; /** @@ -125,25 +129,39 @@ export default function AuthCallback() { }, []); // Empty deps - only run once on mount. navigate is stable, processingRef prevents double execution return ( -
-
-
...
-
- {t("auth.callback.completing", "Completing authentication")} -
-
- {t( - "auth.callback.pleaseWait", - "Please wait while we finish signing you in.", - )} -
-
- {t( - "auth.callback.windowMayClose", - "You can close this window once it completes.", - )} -
+ +
+ Stirling PDF + Stirling PDF
-
+

+ {t("auth.callback.completing", "Completing authentication")} +

+

+ {t( + "auth.callback.pleaseWait", + "Please wait while we finish signing you in.", + )} +

+
+ +
+

+ {t( + "auth.callback.windowMayClose", + "You can close this window once it completes.", + )} +

+ ); } diff --git a/frontend/editor/src/proprietary/routes/InviteAccept.tsx b/frontend/editor/src/proprietary/routes/InviteAccept.tsx index 080cf4275a..46431c3039 100644 --- a/frontend/editor/src/proprietary/routes/InviteAccept.tsx +++ b/frontend/editor/src/proprietary/routes/InviteAccept.tsx @@ -16,6 +16,8 @@ import { useDocumentMeta } from "@app/hooks/useDocumentMeta"; import AuthLayout from "@app/routes/authShared/AuthLayout"; import LoginHeader from "@app/routes/login/LoginHeader"; import ErrorMessage from "@app/auth/ui/ErrorMessage"; +import { authInputStyles } from "@app/auth/ui/EmailPasswordForm"; +import "@app/auth/ui/auth.css"; import { BASE_PATH } from "@app/constants/app"; import apiClient from "@app/services/apiClient"; import { Button } from "@app/ui/Button"; @@ -162,15 +164,17 @@ export default function InviteAccept() { title={t("invite.invalidInvitation", "Invalid Invitation")} /> -
- -
+ ); } @@ -186,13 +190,7 @@ export default function InviteAccept() { /> {inviteData && !inviteData.emailRequired && ( - +
- +
{inviteData?.emailRequired && ( - setEmail(e.target.value)} +
+ setEmail(e.target.value)} + placeholder={t( + "invite.emailPlaceholder", + "Enter your email address", + )} + disabled={submitting} + required + autoComplete="email" + classNames={{ label: "auth-label" }} + styles={authInputStyles} + /> +
+ )} + +
+ setPassword(e.target.value)} placeholder={t( - "invite.emailPlaceholder", - "Enter your email address", + "invite.passwordPlaceholder", + "Enter your password", )} disabled={submitting} required - autoComplete="email" + autoComplete="new-password" + classNames={{ label: "auth-label" }} + styles={authInputStyles} /> - )} - - setPassword(e.target.value)} - placeholder={t("invite.passwordPlaceholder", "Enter your password")} - disabled={submitting} - required - autoComplete="new-password" - /> - - setConfirmPassword(e.target.value)} - placeholder={t( - "invite.confirmPasswordPlaceholder", - "Re-enter your password", - )} - disabled={submitting} - required - autoComplete="new-password" - /> - -
-
- + +
+ setConfirmPassword(e.target.value)} + placeholder={t( + "invite.confirmPasswordPlaceholder", + "Re-enter your password", + )} + disabled={submitting} + required + autoComplete="new-password" + classNames={{ label: "auth-label" }} + styles={authInputStyles} + /> +
+
+ +
diff --git a/frontend/editor/src/proprietary/routes/Login.tsx b/frontend/editor/src/proprietary/routes/Login.tsx index a59072d5f4..b21b9e1dd6 100644 --- a/frontend/editor/src/proprietary/routes/Login.tsx +++ b/frontend/editor/src/proprietary/routes/Login.tsx @@ -5,7 +5,6 @@ import { useNavigate, useSearchParams, } from "react-router-dom"; -import { Text, Stack, Alert } from "@mantine/core"; import { Button } from "@app/ui/Button"; import { setPostLoginRedirectPath } from "@app/auth/spring/springAuthClient"; import { markLoginLandingPending } from "@app/utils/loginLanding"; @@ -22,6 +21,8 @@ import { oauthProviderConfig, } from "@app/auth/ui/OAuthButtons"; import SpringLoginForm from "@app/auth/ui/SpringLoginForm"; +import AuthSignupPrompt from "@app/auth/ui/AuthSignupPrompt"; +import AuthDefaultCredentials from "@app/auth/ui/AuthDefaultCredentials"; import { useSpringLogin } from "@app/auth/ui/useSpringLogin"; import LoggedInState from "@app/routes/login/LoggedInState"; import loginHeader from "@app/assets/brand/modern-logo/LoginLightModeHeader.svg"; @@ -514,59 +515,14 @@ export default function Login() { ) : undefined } footer={ - isFirstTimeSetup && showDefaultCredentials && isUserPassAllowed ? ( - - - - {t("login.defaultCredentials", "Default Login Credentials")} - - - - {t("login.username", "Username")}: - {" "} - admin - - - - {t("login.password", "Password")}: - {" "} - stirling - - - {t( - "login.changePasswordWarning", - "Please change your password after logging in for the first time", - )} - - - - ) : undefined + <> + {isFirstTimeSetup && + showDefaultCredentials && + isUserPassAllowed && } + {isUserPassAllowed && ( + navigate("/signup")} /> + )} + } /> diff --git a/frontend/editor/src/proprietary/routes/authShared/AuthLayout.tsx b/frontend/editor/src/proprietary/routes/authShared/AuthLayout.tsx index 69229639e4..5078de9242 100644 --- a/frontend/editor/src/proprietary/routes/authShared/AuthLayout.tsx +++ b/frontend/editor/src/proprietary/routes/authShared/AuthLayout.tsx @@ -1,9 +1,5 @@ -import React, { useMemo } from "react"; -import { useTranslation } from "react-i18next"; +import React from "react"; import { AuthShell } from "@app/auth/ui/AuthShell"; -import LoginRightCarousel from "@app/auth/ui/LoginRightCarousel"; -import buildLoginSlides from "@app/components/shared/loginSlides"; -import { useLogoVariant } from "@app/hooks/useLogoVariant"; import Footer from "@app/components/shared/Footer"; interface AuthLayoutProps { @@ -11,30 +7,9 @@ interface AuthLayoutProps { } /** - * Editor login layout. The card shell + carousel now live in shared so the - * portal renders the identical screen; this wires the editor's logo-variant - * slides and legal/cookie footer into that shared shell. + * Editor login layout. The card shell lives in shared so the portal renders + * the identical screen; this wires the editor's legal/cookie footer into it. */ export default function AuthLayout({ children }: AuthLayoutProps) { - const { t } = useTranslation(); - const logoVariant = useLogoVariant(); - const imageSlides = useMemo( - () => buildLoginSlides(logoVariant, t), - [logoVariant, t], - ); - - return ( - - } - footer={