diff --git a/login/apps/login/locales/de.json b/login/apps/login/locales/de.json index 75897a628e..7d3173bb94 100644 --- a/login/apps/login/locales/de.json +++ b/login/apps/login/locales/de.json @@ -6,7 +6,9 @@ "title": "Konten", "description": "Wählen Sie das Konto aus, das Sie verwenden möchten.", "addAnother": "Ein weiteres Konto hinzufügen", - "noResults": "Keine Konten gefunden" + "noResults": "Keine Konten gefunden", + "verified": "verifiziert", + "expired": "abgelaufen" }, "logout": { "title": "Logout", diff --git a/login/apps/login/locales/en.json b/login/apps/login/locales/en.json index 9f95403063..eeecdd023f 100644 --- a/login/apps/login/locales/en.json +++ b/login/apps/login/locales/en.json @@ -6,7 +6,9 @@ "title": "Accounts", "description": "Select the account you want to use.", "addAnother": "Add another account", - "noResults": "No accounts found" + "noResults": "No accounts found", + "verified": "verified", + "expired": "expired" }, "logout": { "title": "Logout", diff --git a/login/apps/login/locales/es.json b/login/apps/login/locales/es.json index fe88bb94c6..558dbd03b5 100644 --- a/login/apps/login/locales/es.json +++ b/login/apps/login/locales/es.json @@ -4,9 +4,11 @@ }, "accounts": { "title": "Cuentas", - "description": "Selecciona la cuenta que deseas usar.", + "description": "Seleccione la cuenta que desea utilizar.", "addAnother": "Agregar otra cuenta", - "noResults": "No se encontraron cuentas" + "noResults": "No se encontraron cuentas", + "verified": "verificado", + "expired": "expirado" }, "logout": { "title": "Cerrar sesión", diff --git a/login/apps/login/locales/it.json b/login/apps/login/locales/it.json index 1229a1a4c0..cc5a0b8c4f 100644 --- a/login/apps/login/locales/it.json +++ b/login/apps/login/locales/it.json @@ -4,9 +4,11 @@ }, "accounts": { "title": "Account", - "description": "Seleziona l'account che desideri utilizzare.", + "description": "Seleziona l'account che vuoi utilizzare.", "addAnother": "Aggiungi un altro account", - "noResults": "Nessun account trovato" + "noResults": "Nessun account trovato", + "verified": "verificato", + "expired": "scaduto" }, "logout": { "title": "Esci", diff --git a/login/apps/login/locales/pl.json b/login/apps/login/locales/pl.json index 9fea6a19fa..cd418d5c8c 100644 --- a/login/apps/login/locales/pl.json +++ b/login/apps/login/locales/pl.json @@ -6,7 +6,9 @@ "title": "Konta", "description": "Wybierz konto, którego chcesz użyć.", "addAnother": "Dodaj kolejne konto", - "noResults": "Nie znaleziono kont" + "noResults": "Nie znaleziono kont", + "verified": "zweryfikowany", + "expired": "wygasł" }, "logout": { "title": "Wyloguj się", diff --git a/login/apps/login/locales/ru.json b/login/apps/login/locales/ru.json index e745f1ae59..e5f735db94 100644 --- a/login/apps/login/locales/ru.json +++ b/login/apps/login/locales/ru.json @@ -6,7 +6,9 @@ "title": "Аккаунты", "description": "Выберите аккаунт, который хотите использовать.", "addAnother": "Добавить другой аккаунт", - "noResults": "Аккаунты не найдены" + "noResults": "Аккаунты не найдены", + "verified": "проверенный", + "expired": "истёк" }, "logout": { "title": "Выход", diff --git a/login/apps/login/locales/zh.json b/login/apps/login/locales/zh.json index 5a9cb3a4eb..73af30ab97 100644 --- a/login/apps/login/locales/zh.json +++ b/login/apps/login/locales/zh.json @@ -4,9 +4,11 @@ }, "accounts": { "title": "账户", - "description": "选择您想使用的账户。", + "description": "选择您要使用的账户。", "addAnother": "添加另一个账户", - "noResults": "未找到账户" + "noResults": "未找到账户", + "verified": "已验证", + "expired": "已过期" }, "logout": { "title": "注销", diff --git a/login/apps/login/package.json b/login/apps/login/package.json index 655aeb8f28..e45dbae7f9 100644 --- a/login/apps/login/package.json +++ b/login/apps/login/package.json @@ -25,6 +25,7 @@ "dependencies": { "@headlessui/react": "^2.1.9", "@heroicons/react": "2.1.3", + "@radix-ui/react-tooltip": "^1.2.7", "@tailwindcss/forms": "0.5.7", "@vercel/analytics": "^1.2.2", "@zitadel/client": "latest", diff --git a/login/apps/login/src/app/(login)/accounts/page.tsx b/login/apps/login/src/app/(login)/accounts/page.tsx index cebcd014bb..e4e6b387dc 100644 --- a/login/apps/login/src/app/(login)/accounts/page.tsx +++ b/login/apps/login/src/app/(login)/accounts/page.tsx @@ -15,12 +15,12 @@ import { headers } from "next/headers"; import Link from "next/link"; async function loadSessions({ serviceUrl }: { serviceUrl: string }) { - const ids: (string | undefined)[] = await getAllSessionCookieIds(); + const cookieIds = await getAllSessionCookieIds(); - if (ids && ids.length) { + if (cookieIds && cookieIds.length) { const response = await listSessions({ serviceUrl, - ids: ids.filter((id) => !!id) as string[], + ids: cookieIds.filter((id) => !!id) as string[], }); return response?.sessions ?? []; } else { diff --git a/login/apps/login/src/app/(login)/layout.tsx b/login/apps/login/src/app/(login)/layout.tsx index 0a75a920f1..dbce9804c9 100644 --- a/login/apps/login/src/app/(login)/layout.tsx +++ b/login/apps/login/src/app/(login)/layout.tsx @@ -5,6 +5,7 @@ import { LanguageSwitcher } from "@/components/language-switcher"; import { Skeleton } from "@/components/skeleton"; import { Theme } from "@/components/theme"; import { ThemeProvider } from "@/components/theme-provider"; +import * as Tooltip from "@radix-ui/react-tooltip"; import { Analytics } from "@vercel/analytics/react"; import { Lato } from "next/font/google"; import { ReactNode, Suspense } from "react"; @@ -24,36 +25,38 @@ export default async function RootLayout({ - -
- -
-
-
- + + +
+ +
+
+
+ +
-
- } - > - -
-
- {children} -
- - + } + > + +
+
+ {children} +
+ + +
-
- - + + + diff --git a/login/apps/login/src/app/(login)/logout/page.tsx b/login/apps/login/src/app/(login)/logout/page.tsx index 853461d7d6..76e4dc3800 100644 --- a/login/apps/login/src/app/(login)/logout/page.tsx +++ b/login/apps/login/src/app/(login)/logout/page.tsx @@ -12,12 +12,12 @@ import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { headers } from "next/headers"; async function loadSessions({ serviceUrl }: { serviceUrl: string }) { - const ids: (string | undefined)[] = await getAllSessionCookieIds(); + const cookieIds = await getAllSessionCookieIds(); - if (ids && ids.length) { + if (cookieIds && cookieIds.length) { const response = await listSessions({ serviceUrl, - ids: ids.filter((id) => !!id) as string[], + ids: cookieIds.filter((id) => !!id) as string[], }); return response?.sessions ?? []; } else { diff --git a/login/apps/login/src/app/(login)/otp/[method]/page.tsx b/login/apps/login/src/app/(login)/otp/[method]/page.tsx index af4159d62e..94164c757f 100644 --- a/login/apps/login/src/app/(login)/otp/[method]/page.tsx +++ b/login/apps/login/src/app/(login)/otp/[method]/page.tsx @@ -41,17 +41,13 @@ export default async function Page(props: { const { method } = params; const session = sessionId - ? await loadSessionById(serviceUrl, sessionId, organization) + ? await loadSessionById(sessionId, organization) : await loadMostRecentSession({ serviceUrl, sessionParams: { loginName, organization }, }); - async function loadSessionById( - host: string, - sessionId: string, - organization?: string, - ) { + async function loadSessionById(sessionId: string, organization?: string) { const recent = await getSessionCookieById({ sessionId, organization }); return getSession({ serviceUrl, diff --git a/login/apps/login/src/app/(login)/otp/[method]/set/page.tsx b/login/apps/login/src/app/(login)/otp/[method]/set/page.tsx index f74093ce8e..b43bb973f5 100644 --- a/login/apps/login/src/app/(login)/otp/[method]/set/page.tsx +++ b/login/apps/login/src/app/(login)/otp/[method]/set/page.tsx @@ -66,7 +66,6 @@ export default async function Page(props: { error = err; }); } else if (method === "sms") { - // does not work await addOTPSMS({ serviceUrl, userId: session.factors.user.id, @@ -74,7 +73,6 @@ export default async function Page(props: { error = new Error("Could not add OTP via SMS"); }); } else if (method === "email") { - // works await addOTPEmail({ serviceUrl, userId: session.factors.user.id, @@ -106,6 +104,7 @@ export default async function Page(props: { paramsToContinue.append("requestId", requestId); } urlToContinue = `/otp/${method}?` + paramsToContinue; + // immediately check the OTP on the next page if sms or email was set up if (["email", "sms"].includes(method)) { return redirect(urlToContinue); diff --git a/login/apps/login/src/components/session-item.tsx b/login/apps/login/src/components/session-item.tsx index f189d05828..7faeade9b0 100644 --- a/login/apps/login/src/components/session-item.tsx +++ b/login/apps/login/src/components/session-item.tsx @@ -3,6 +3,7 @@ import { sendLoginname } from "@/lib/server/loginname"; import { clearSession, continueWithSession } from "@/lib/server/session"; import { XCircleIcon } from "@heroicons/react/24/outline"; +import * as Tooltip from "@radix-ui/react-tooltip"; import { Timestamp, timestampDate } from "@zitadel/client"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import moment from "moment"; @@ -10,6 +11,7 @@ import { useLocale } from "next-intl"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { Avatar } from "./avatar"; +import { Translated } from "./translated"; export function isSessionValid(session: Partial): { valid: boolean; @@ -66,91 +68,107 @@ export function SessionItem({ const router = useRouter(); return ( - + className="group flex flex-row items-center rounded-md border border-divider-light bg-background-light-400 px-4 py-2 transition-all hover:shadow-lg dark:bg-background-dark-400 dark:hover:bg-white/10" + > +
+ +
+ +
+ {session.factors?.user?.displayName} + + {session.factors?.user?.loginName} + + {valid ? ( + + {" "} + {verifiedAt && moment(timestampDate(verifiedAt)).fromNow()} + + ) : ( + verifiedAt && ( + + {" "} + {session.expirationDate && + moment(timestampDate(session.expirationDate)).fromNow()} + + ) + )} +
+ + +
+ {valid ? ( +
+ ) : ( +
+ )} + + { + event.preventDefault(); + event.stopPropagation(); + clearSessionId(session.id).then(() => { + reload(); + }); + }} + /> +
+ + + {valid && session.expirationDate && ( + + + Expires {moment(timestampDate(session.expirationDate)).fromNow()} + + + + )} + ); } diff --git a/login/apps/login/src/lib/cookies.ts b/login/apps/login/src/lib/cookies.ts index 45463d7a4d..71008de24e 100644 --- a/login/apps/login/src/lib/cookies.ts +++ b/login/apps/login/src/lib/cookies.ts @@ -242,7 +242,7 @@ export async function getSessionCookieByLoginName({ */ export async function getAllSessionCookieIds( cleanup: boolean = false, -): Promise { +): Promise { const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); diff --git a/login/apps/login/src/lib/server/cookie.ts b/login/apps/login/src/lib/server/cookie.ts index 841fc06b3a..7f87f49731 100644 --- a/login/apps/login/src/lib/server/cookie.ts +++ b/login/apps/login/src/lib/server/cookie.ts @@ -55,10 +55,21 @@ export async function createSessionAndUpdateCookie(command: { const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); + let sessionLifetime = command.lifetime; + + if (!sessionLifetime) { + console.warn("No session lifetime provided, using default of 24 hours."); + + sessionLifetime = { + seconds: BigInt(24 * 60 * 60), // 24 hours + nanos: 0, + } as Duration; // for usecases where the lifetime is not specified (user discovery) + } + const createdSession = await createSessionFromChecks({ serviceUrl, checks: command.checks, - lifetime: command.lifetime, + lifetime: sessionLifetime, }); if (createdSession) { @@ -126,11 +137,24 @@ export async function createSessionForIdpAndUpdateCookie({ const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); + let sessionLifetime = lifetime; + + if (!sessionLifetime) { + console.warn( + "No IDP session lifetime provided, using default of 24 hours.", + ); + + sessionLifetime = { + seconds: BigInt(24 * 60 * 60), // 24 hours + nanos: 0, + } as Duration; + } + const createdSession = await createSessionForUserIdAndIdpIntent({ serviceUrl, userId, idpIntent, - lifetime, + lifetime: sessionLifetime, }).catch((error: ErrorDetail | CredentialsCheckError) => { console.error("Could not set session", error); if ("failedAttempts" in error && error.failedAttempts) { @@ -190,41 +214,41 @@ export type SessionWithChallenges = Session & { challenges: Challenges | undefined; }; -export async function setSessionAndUpdateCookie( - recentCookie: CustomCookieData, - checks?: Checks, - challenges?: RequestChallenges, - requestId?: string, - lifetime?: Duration, -) { +export async function setSessionAndUpdateCookie(command: { + recentCookie: CustomCookieData; + checks?: Checks; + challenges?: RequestChallenges; + requestId?: string; + lifetime: Duration; +}) { const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); return setSession({ serviceUrl, - sessionId: recentCookie.id, - sessionToken: recentCookie.token, - challenges, - checks, - lifetime, + sessionId: command.recentCookie.id, + sessionToken: command.recentCookie.token, + challenges: command.challenges, + checks: command.checks, + lifetime: command.lifetime, }) .then((updatedSession) => { if (updatedSession) { const sessionCookie: CustomCookieData = { - id: recentCookie.id, + id: command.recentCookie.id, token: updatedSession.sessionToken, - creationTs: recentCookie.creationTs, - expirationTs: recentCookie.expirationTs, + creationTs: command.recentCookie.creationTs, + expirationTs: command.recentCookie.expirationTs, // just overwrite the changeDate with the new one changeTs: updatedSession.details?.changeDate ? `${timestampMs(updatedSession.details.changeDate)}` : "", - loginName: recentCookie.loginName, - organization: recentCookie.organization, + loginName: command.recentCookie.loginName, + organization: command.recentCookie.organization, }; - if (requestId) { - sessionCookie.requestId = requestId; + if (command.requestId) { + sessionCookie.requestId = command.requestId; } return getSession({ diff --git a/login/apps/login/src/lib/server/otp.ts b/login/apps/login/src/lib/server/otp.ts deleted file mode 100644 index f3d4a1536a..0000000000 --- a/login/apps/login/src/lib/server/otp.ts +++ /dev/null @@ -1,83 +0,0 @@ -"use server"; - -import { setSessionAndUpdateCookie } from "@/lib/server/cookie"; -import { create } from "@zitadel/client"; -import { - CheckOTPSchema, - ChecksSchema, - CheckTOTPSchema, -} from "@zitadel/proto/zitadel/session/v2/session_service_pb"; -import { headers } from "next/headers"; -import { - getMostRecentSessionCookie, - getSessionCookieById, - getSessionCookieByLoginName, -} from "../cookies"; -import { getServiceUrlFromHeaders } from "../service-url"; -import { getLoginSettings } from "../zitadel"; - -export type SetOTPCommand = { - loginName?: string; - sessionId?: string; - organization?: string; - requestId?: string; - code: string; - method: string; -}; - -export async function setOTP(command: SetOTPCommand) { - const _headers = await headers(); - const { serviceUrl } = getServiceUrlFromHeaders(_headers); - - const recentSession = command.sessionId - ? await getSessionCookieById({ sessionId: command.sessionId }).catch( - (error) => { - return Promise.reject(error); - }, - ) - : command.loginName - ? await getSessionCookieByLoginName({ - loginName: command.loginName, - organization: command.organization, - }).catch((error) => { - return Promise.reject(error); - }) - : await getMostRecentSessionCookie().catch((error) => { - return Promise.reject(error); - }); - - const checks = create(ChecksSchema, {}); - - if (command.method === "time-based") { - checks.totp = create(CheckTOTPSchema, { - code: command.code, - }); - } else if (command.method === "sms") { - checks.otpSms = create(CheckOTPSchema, { - code: command.code, - }); - } else if (command.method === "email") { - checks.otpEmail = create(CheckOTPSchema, { - code: command.code, - }); - } - - const loginSettings = await getLoginSettings({ - serviceUrl, - organization: command.organization, - }); - - return setSessionAndUpdateCookie( - recentSession, - checks, - undefined, - command.requestId, - loginSettings?.secondFactorCheckLifetime, - ).then((session) => { - return { - sessionId: session.id, - factors: session.factors, - challenges: session.challenges, - }; - }); -} diff --git a/login/apps/login/src/lib/server/passkeys.ts b/login/apps/login/src/lib/server/passkeys.ts index 60aa2c92b4..ca603471ab 100644 --- a/login/apps/login/src/lib/server/passkeys.ts +++ b/login/apps/login/src/lib/server/passkeys.ts @@ -211,19 +211,27 @@ export async function sendPasskey(command: SendPasskeyCommand) { organization, }); - const lifetime = checks?.webAuthN + let lifetime = checks?.webAuthN ? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey : checks?.otpEmail || checks?.otpSms ? loginSettings?.secondFactorCheckLifetime : undefined; - const session = await setSessionAndUpdateCookie( - recentSession, + if (!lifetime) { + console.warn("No passkey lifetime provided, defaulting to 24 hours"); + + lifetime = { + seconds: BigInt(60 * 60 * 24), // default to 24 hours + nanos: 0, + } as Duration; + } + + const session = await setSessionAndUpdateCookie({ + recentCookie: recentSession, checks, - undefined, requestId, lifetime, - ); + }); if (!session || !session?.factors?.user?.id) { return { error: "Could not update session" }; diff --git a/login/apps/login/src/lib/server/password.ts b/login/apps/login/src/lib/server/password.ts index 5c6fb03aa5..013255d06f 100644 --- a/login/apps/login/src/lib/server/password.ts +++ b/login/apps/login/src/lib/server/password.ts @@ -16,7 +16,7 @@ import { setPassword, setUserPassword, } from "@/lib/zitadel"; -import { ConnectError, create } from "@zitadel/client"; +import { ConnectError, create, Duration } from "@zitadel/client"; import { createUserServiceClient } from "@zitadel/client/v2"; import { Checks, @@ -152,14 +152,32 @@ export async function sendPassword(command: UpdateSessionCommand) { // this is a fake error message to hide that the user does not even exist return { error: "Could not verify password" }; } else { + loginSettings = await getLoginSettings({ + serviceUrl, + organization: sessionCookie.organization, + }); + + if (!loginSettings) { + return { error: "Could not load login settings" }; + } + + let lifetime = loginSettings.passwordCheckLifetime; + + if (!lifetime) { + console.warn("No password lifetime provided, defaulting to 24 hours"); + lifetime = { + seconds: BigInt(60 * 60 * 24), // default to 24 hours + nanos: 0, + } as Duration; + } + try { - session = await setSessionAndUpdateCookie( - sessionCookie, - command.checks, - undefined, - command.requestId, - loginSettings?.passwordCheckLifetime, - ); + session = await setSessionAndUpdateCookie({ + recentCookie: sessionCookie, + checks: command.checks, + requestId: command.requestId, + lifetime, + }); } catch (error: any) { if ("failedAttempts" in error && error.failedAttempts) { const lockoutSettings = await getLockoutSettings({ diff --git a/login/apps/login/src/lib/server/session.ts b/login/apps/login/src/lib/server/session.ts index d04d1d25b4..957c89ad81 100644 --- a/login/apps/login/src/lib/server/session.ts +++ b/login/apps/login/src/lib/server/session.ts @@ -154,19 +154,27 @@ export async function updateSession(options: UpdateSessionCommand) { organization, }); - const lifetime = checks?.webAuthN + let lifetime = checks?.webAuthN ? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey : checks?.otpEmail || checks?.otpSms ? loginSettings?.secondFactorCheckLifetime : undefined; - const session = await setSessionAndUpdateCookie( - recentSession, + if (!lifetime) { + console.warn("No lifetime provided for session, defaulting to 24 hours"); + lifetime = { + seconds: BigInt(60 * 60 * 24), // default to 24 hours + nanos: 0, + } as Duration; + } + + const session = await setSessionAndUpdateCookie({ + recentCookie: recentSession, checks, challenges, requestId, lifetime, - ); + }); if (!session) { return { error: "Could not update session" }; diff --git a/login/apps/login/src/lib/zitadel.ts b/login/apps/login/src/lib/zitadel.ts index bad773092c..5e4583f8af 100644 --- a/login/apps/login/src/lib/zitadel.ts +++ b/login/apps/login/src/lib/zitadel.ts @@ -298,7 +298,7 @@ export async function createSessionFromChecks({ }: { serviceUrl: string; checks: Checks; - lifetime?: Duration; + lifetime: Duration; }) { const sessionService: Client = await createServiceForHost(SessionService, serviceUrl); @@ -320,7 +320,7 @@ export async function createSessionForUserIdAndIdpIntent({ idpIntentId?: string | undefined; idpIntentToken?: string | undefined; }; - lifetime?: Duration; + lifetime: Duration; }) { const sessionService: Client = await createServiceForHost(SessionService, serviceUrl); @@ -355,7 +355,7 @@ export async function setSession({ sessionToken: string; challenges: RequestChallenges | undefined; checks?: Checks; - lifetime?: Duration; + lifetime: Duration; }) { const sessionService: Client = await createServiceForHost(SessionService, serviceUrl); diff --git a/login/apps/login/turbo.json b/login/apps/login/turbo.json index f05cd41d01..db0e93d5ed 100644 --- a/login/apps/login/turbo.json +++ b/login/apps/login/turbo.json @@ -8,6 +8,9 @@ "build:login:standalone": { "outputs": ["dist/**", ".next/**", "!.next/cache/**"] }, + "dev": { + "dependsOn": ["@zitadel/client#build", "@zitadel/proto#generate"] + }, "test": { "dependsOn": ["@zitadel/client#build"] }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8a22a41ce..901b332f2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -421,6 +421,9 @@ importers: '@heroicons/react': specifier: 2.1.3 version: 2.1.3(react@19.1.0) + '@radix-ui/react-tooltip': + specifier: ^1.2.7 + version: 1.2.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tailwindcss/forms': specifier: 0.5.7 version: 0.5.7(tailwindcss@3.4.14) @@ -4259,6 +4262,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-tooltip@1.2.7': + resolution: {integrity: sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-use-callback-ref@1.1.0': resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} peerDependencies: @@ -4398,6 +4414,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} @@ -19496,6 +19525,15 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-avatar@1.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-context': 1.1.1(@types/react@19.1.2)(react@18.3.1) @@ -19548,6 +19586,12 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.2)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-context@1.1.1(@types/react@19.1.2)(react@18.3.1)': dependencies: react: 18.3.1 @@ -19560,6 +19604,12 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-context@1.1.2(@types/react@19.1.2)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-direction@1.1.0(@types/react@19.1.2)(react@18.3.1)': dependencies: react: 18.3.1 @@ -19585,6 +19635,19 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -19676,6 +19739,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-id@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-popover@1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -19753,6 +19823,24 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-popper@1.2.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/react-dom': 2.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/rect': 1.1.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-portal@1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -19783,6 +19871,16 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-presence@1.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@18.3.1) @@ -19803,6 +19901,16 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-presence@1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-primitive@2.0.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-slot': 1.1.1(@types/react@19.1.2)(react@18.3.1) @@ -19830,6 +19938,15 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -19885,6 +20002,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-slot@1.2.3(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-tabs@1.1.12(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -19921,6 +20045,26 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-tooltip@1.2.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.1.2)(react@18.3.1)': dependencies: react: 18.3.1 @@ -19933,6 +20077,12 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.1.2)(react@18.3.1)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.2)(react@18.3.1) @@ -19948,6 +20098,14 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.2)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@18.3.1) @@ -19955,6 +20113,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.1.2)(react@18.3.1)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.2)(react@18.3.1) @@ -19969,6 +20134,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.1.2)(react@18.3.1)': dependencies: react: 18.3.1 @@ -19981,6 +20153,12 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-use-previous@1.1.0(@types/react@19.1.2)(react@18.3.1)': dependencies: react: 18.3.1 @@ -20001,6 +20179,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-use-rect@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-use-size@1.1.0(@types/react@19.1.2)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.2)(react@18.3.1) @@ -20015,6 +20200,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@radix-ui/react-use-size@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + '@radix-ui/react-visually-hidden@1.1.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -20024,6 +20216,15 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/rect@1.1.0': {} '@radix-ui/rect@1.1.1': {}