fix(login): default lifetime, show expiration on accounts page (#10297)

This PR fixes an issue where the password lifetime was not applied
correctly in certain scenarios.
It also improves the sessions page by providing more information about
expiration and verification timestamps and a mobile layout for clearing
sessions.
<img width="506" height="760" alt="Screenshot 2025-07-22 at 08 56 14"
src="https://github.com/user-attachments/assets/1e621ca2-206c-4931-b27d-9592eebc646e"
/>

Closes https://github.com/zitadel/typescript/issues/481
This commit is contained in:
Max Peintner
2025-07-22 11:18:15 +02:00
committed by GitHub
parent 9b0e5bf714
commit a3e1d6a3ff
23 changed files with 467 additions and 257 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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ę",

View File

@@ -6,7 +6,9 @@
"title": "Аккаунты",
"description": "Выберите аккаунт, который хотите использовать.",
"addAnother": "Добавить другой аккаунт",
"noResults": "Аккаунты не найдены"
"noResults": "Аккаунты не найдены",
"verified": "проверенный",
"expired": "истёк"
},
"logout": {
"title": "Выход",

View File

@@ -4,9 +4,11 @@
},
"accounts": {
"title": "账户",
"description": "选择您使用的账户。",
"description": "选择您使用的账户。",
"addAnother": "添加另一个账户",
"noResults": "未找到账户"
"noResults": "未找到账户",
"verified": "已验证",
"expired": "已过期"
},
"logout": {
"title": "注销",

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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,6 +25,7 @@ export default async function RootLayout({
<head />
<body>
<ThemeProvider>
<Tooltip.Provider>
<Suspense
fallback={
<div
@@ -54,6 +56,7 @@ export default async function RootLayout({
</div>
</LanguageProvider>
</Suspense>
</Tooltip.Provider>
</ThemeProvider>
<Analytics />
</body>

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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);

View File

@@ -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<Session>): {
valid: boolean;
@@ -66,6 +68,8 @@ export function SessionItem({
const router = useRouter();
return (
<Tooltip.Root delayDuration={300}>
<Tooltip.Trigger asChild>
<button
onClick={async () => {
if (valid && session?.factors?.user) {
@@ -119,12 +123,13 @@ export function SessionItem({
</span>
{valid ? (
<span className="text-ellipsis text-xs opacity-80">
<Translated i18nKey="verified" namespace="accounts" />{" "}
{verifiedAt && moment(timestampDate(verifiedAt)).fromNow()}
</span>
) : (
verifiedAt && (
<span className="text-ellipsis text-xs opacity-80">
expired{" "}
<Translated i18nKey="expired" namespace="accounts" />{" "}
{session.expirationDate &&
moment(timestampDate(session.expirationDate)).fromNow()}
</span>
@@ -135,13 +140,13 @@ export function SessionItem({
<span className="flex-grow"></span>
<div className="relative flex flex-row items-center">
{valid ? (
<div className="absolute right-0 mx-2 h-2 w-2 transform rounded-full bg-green-500 transition-all group-hover:right-6"></div>
<div className="absolute right-6 mx-2 h-2 w-2 transform rounded-full bg-green-500 transition-all group-hover:right-6 sm:right-0"></div>
) : (
<div className="absolute right-0 mx-2 h-2 w-2 transform rounded-full bg-red-500 transition-all group-hover:right-6"></div>
<div className="absolute right-6 mx-2 h-2 w-2 transform rounded-full bg-red-500 transition-all group-hover:right-6 sm:right-0"></div>
)}
<XCircleIcon
className="hidden h-5 w-5 opacity-50 transition-all hover:opacity-100 group-hover:block"
className="h-5 w-5 opacity-50 transition-all hover:opacity-100 group-hover:block sm:hidden"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
@@ -152,5 +157,18 @@ export function SessionItem({
/>
</div>
</button>
</Tooltip.Trigger>
{valid && session.expirationDate && (
<Tooltip.Portal>
<Tooltip.Content
className="z-50 select-none rounded-md border bg-background-light-500 px-3 py-2 text-xs text-black shadow-xl dark:border-white/20 dark:bg-background-dark-500 dark:text-white"
sideOffset={5}
>
Expires {moment(timestampDate(session.expirationDate)).fromNow()}
<Tooltip.Arrow className="fill-white dark:fill-white/20" />
</Tooltip.Content>
</Tooltip.Portal>
)}
</Tooltip.Root>
);
}

View File

@@ -242,7 +242,7 @@ export async function getSessionCookieByLoginName<T>({
*/
export async function getAllSessionCookieIds<T>(
cleanup: boolean = false,
): Promise<any> {
): Promise<string[]> {
const cookiesList = await cookies();
const stringifiedCookie = cookiesList.get("sessions");

View File

@@ -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({

View File

@@ -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,
};
});
}

View File

@@ -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" };

View File

@@ -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({

View File

@@ -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" };

View File

@@ -298,7 +298,7 @@ export async function createSessionFromChecks({
}: {
serviceUrl: string;
checks: Checks;
lifetime?: Duration;
lifetime: Duration;
}) {
const sessionService: Client<typeof SessionService> =
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<typeof SessionService> =
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<typeof SessionService> =
await createServiceForHost(SessionService, serviceUrl);

View File

@@ -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"]
},

201
pnpm-lock.yaml generated
View File

@@ -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': {}