From 409e8ed71ab041e96aa8a54eb73f53732ed96fed Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Fri, 14 Aug 2026 21:18:06 +0200 Subject: [PATCH] feat: unify authentication pages --- apps/docs/docs/management/users/index.mdx | 3 +- .../app/[locale]/auth/invite/[id]/page.tsx | 33 +++++-------- .../src/app/[locale]/auth/login/page.tsx | 45 +++++++---------- apps/nextjs/src/app/[locale]/layout.tsx | 4 +- packages/onboarding/src/account.tsx | 31 ++---------- packages/onboarding/src/index.ts | 1 + .../onboarding/src/onboarding-auth-shell.tsx | 48 +++++++++++++++++++ .../src/onboarding-studio.module.css | 25 ++++++++++ 8 files changed, 110 insertions(+), 80 deletions(-) create mode 100644 packages/onboarding/src/onboarding-auth-shell.tsx diff --git a/apps/docs/docs/management/users/index.mdx b/apps/docs/docs/management/users/index.mdx index a7181a1f5..dd14f4a6d 100644 --- a/apps/docs/docs/management/users/index.mdx +++ b/apps/docs/docs/management/users/index.mdx @@ -220,7 +220,8 @@ Clicking the `Copy & close` button will copy the URL to your clipboard. ### Self-onboarding -Clicking the invitation link will bring up a form with fields for the username and password: +Clicking the invitation link opens a centered registration form that uses the same visual shell as Homarr's onboarding +and sign-in pages, with fields for the username and password: ![](./img/invitation-self-onboarding.png) diff --git a/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx b/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx index dbf6306c3..475464aff 100644 --- a/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx +++ b/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx @@ -1,13 +1,13 @@ import { notFound } from "next/navigation"; -import { Card, Center, Stack, Text, Title } from "@mantine/core"; +import { IconUserPlus } from "@tabler/icons-react"; import { auth } from "@homarr/auth/next"; import { isProviderEnabled } from "@homarr/auth/server"; import { and, db, eq } from "@homarr/db"; import { invites } from "@homarr/db/schema"; +import { OnboardingAuthShell } from "@homarr/onboarding"; import { getScopedI18n } from "@homarr/translation/server"; -import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo"; import { RegistrationForm } from "./_registration-form"; interface InviteUsagePageProps { @@ -48,25 +48,14 @@ export default async function InviteUsagePage(props: InviteUsagePageProps) { const t = await getScopedI18n("user.page.invite"); return ( -
- - - - - {t("title")} - - - {t("subtitle")} - - - - - - - {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} - {t("description", { username: invite.creator.name! })} - - -
+ } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + footer={t("description", { username: invite.creator.name! })} + > + + ); } diff --git a/apps/nextjs/src/app/[locale]/auth/login/page.tsx b/apps/nextjs/src/app/[locale]/auth/login/page.tsx index 30eb88ba0..8286c6205 100644 --- a/apps/nextjs/src/app/[locale]/auth/login/page.tsx +++ b/apps/nextjs/src/app/[locale]/auth/login/page.tsx @@ -1,15 +1,15 @@ import { redirect } from "next/navigation"; -import { Alert, Card, Center, Code, Stack, Text, Title } from "@mantine/core"; +import { Alert, Code, Text } from "@mantine/core"; import { IconLogin } from "@tabler/icons-react"; import { env } from "@homarr/auth/env"; import { auth } from "@homarr/auth/next"; +import { OnboardingAuthShell } from "@homarr/onboarding"; import { getScopedI18n } from "@homarr/translation/server"; import { sanitizeRedirectionUrl } from "@homarr/validation/redirection-url"; import { env as appEnv } from "~/env"; -import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo"; import { LoginForm } from "./_login-form"; interface LoginProps { @@ -29,33 +29,20 @@ export default async function Login(props: LoginProps) { const t = await getScopedI18n("user.page.login"); return ( -
- - - - - {t("title")} - - - {t("subtitle")} + }> + {appEnv.DEMO_MODE ? ( + } color="blue" variant="light"> + + Demo mode is enabled. Sign in with username demo and password demo - - {appEnv.DEMO_MODE && ( - } color="blue" variant="light" w={64 * 6} maw="90vw"> - - Demo mode is enabled. Sign in with username demo and password demo - - - )} - - - - -
+ + ) : null} + + ); } diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx index 9ecb2f226..cbc64af1f 100644 --- a/apps/nextjs/src/app/[locale]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/layout.tsx @@ -11,6 +11,7 @@ import "~/styles/color-scheme.scss"; import "~/styles/scroll-area.scss"; import { notFound } from "next/navigation"; +import { ColorSchemeScript } from "@mantine/core"; import type { DayOfWeek } from "@mantine/dates"; import { NextIntlClientProvider } from "next-intl"; @@ -161,17 +162,16 @@ export default async function Layout(props: { ]); return ( - // Instead of ColorSchemScript we use data-mantine-color-scheme to prevent flickering + {session ? ( <> diff --git a/packages/onboarding/src/account.tsx b/packages/onboarding/src/account.tsx index 39002b765..fe31c6eaa 100644 --- a/packages/onboarding/src/account.tsx +++ b/packages/onboarding/src/account.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useRef, useState } from "react"; -import { Alert, Button, Group, Paper, Stack, Text, TextInput, ThemeIcon, Title } from "@mantine/core"; +import { Alert, Button, Stack, Text, TextInput } from "@mantine/core"; import { IconArrowRight, IconShieldCheck, IconUserPlus } from "@tabler/icons-react"; import type { z } from "zod/v4"; @@ -17,10 +17,9 @@ import { groupCreateSchema } from "@homarr/validation/group"; import { userInitSchema } from "@homarr/validation/user"; import { didCredentialsSignInFail } from "./account-recovery"; -import { OnboardingBackdrop } from "./onboarding-backdrop"; +import { OnboardingAuthShell } from "./onboarding-auth-shell"; import type { OnboardingStudioProps } from "./types"; import { useOnboardingSounds } from "./use-onboarding-sounds"; -import classes from "./onboarding-studio.module.css"; const AccountShell = ({ title, @@ -31,29 +30,9 @@ const AccountShell = ({ description: string; children: React.ReactNode; }) => ( -
- -
- - - - - - - - - - {title} - - {description} - - - {children} - - - -
-
+ }> + {children} + ); export const AccountSetup = ({ environment }: OnboardingStudioProps) => { diff --git a/packages/onboarding/src/index.ts b/packages/onboarding/src/index.ts index f3d372527..68389061d 100644 --- a/packages/onboarding/src/index.ts +++ b/packages/onboarding/src/index.ts @@ -1,2 +1,3 @@ +export { OnboardingAuthShell } from "./onboarding-auth-shell"; export { OnboardingStudio } from "./onboarding-studio"; export type { OnboardingEnvironment, OnboardingStudioProps } from "./types"; diff --git a/packages/onboarding/src/onboarding-auth-shell.tsx b/packages/onboarding/src/onboarding-auth-shell.tsx new file mode 100644 index 000000000..70af0c584 --- /dev/null +++ b/packages/onboarding/src/onboarding-auth-shell.tsx @@ -0,0 +1,48 @@ +import type { ReactNode } from "react"; +import { Group, Paper, Stack, Text, ThemeIcon, Title } from "@mantine/core"; + +import { OnboardingBackdrop } from "./onboarding-backdrop"; +import { OnboardingWordmark } from "./onboarding-wordmark"; +import classes from "./onboarding-studio.module.css"; + +interface OnboardingAuthShellProps { + title: ReactNode; + description: ReactNode; + icon: ReactNode; + children: ReactNode; + footer?: ReactNode; +} + +export const OnboardingAuthShell = ({ title, description, icon, children, footer }: OnboardingAuthShellProps) => ( +
+ +
+ +
+ +
+ + + + + {icon} + + + + {title} + + {description} + + + {children} + + + {footer ? ( + + {footer} + + ) : null} +
+
+
+); diff --git a/packages/onboarding/src/onboarding-studio.module.css b/packages/onboarding/src/onboarding-studio.module.css index 3143c73a3..8a9415e02 100644 --- a/packages/onboarding/src/onboarding-studio.module.css +++ b/packages/onboarding/src/onboarding-studio.module.css @@ -129,6 +129,31 @@ overflow: clip; } +.authShell { + display: grid; + min-height: 100dvh; + padding-block: clamp(1rem, 4vw, 3rem) calc(clamp(1rem, 4vw, 3rem) + env(safe-area-inset-bottom)); +} + +.authLayout { + width: min(100%, 36rem); + margin: auto; +} + +.authBrand { + position: relative; + z-index: 1; +} + +.authCard { + width: 100%; +} + +.authFooter { + max-width: 32rem; + text-wrap: balance; +} + .studioGrid { display: grid; grid-template-columns: 15rem minmax(0, 1fr);