feat: Adds user profile cards on hover (#13230)

* wip

* Add option to suppress hover card
This commit is contained in:
Tom Moor
2026-08-01 18:12:09 -04:00
committed by GitHub
parent 8b35f7bd17
commit 93638564cf
12 changed files with 651 additions and 70 deletions
+39 -19
View File
@@ -28,9 +28,9 @@ export interface IAvatar {
id?: string;
}
type Props = {
export type AvatarProps = {
/** The size of the avatar */
size: AvatarSize;
size?: AvatarSize;
/** The variant of the avatar */
variant?: AvatarVariant;
/** The source of the avatar image, if not passing a model. */
@@ -40,58 +40,78 @@ type Props = {
/** The alt text for the image */
alt?: string;
/** Optional click handler */
onClick?: React.MouseEventHandler<HTMLImageElement>;
onClick?: React.MouseEventHandler<HTMLElement>;
/** Optional class name */
className?: string;
/** Optional style */
style?: React.CSSProperties;
/** Whether to show a tooltip */
showTooltip?: boolean;
};
/** Whether to show a profile card on hover for users, defaults to true */
showHoverCard?: boolean;
} & Omit<
React.ComponentPropsWithoutRef<"div">,
"onClick" | "className" | "style"
>;
function Avatar(props: Props) {
const Avatar = React.forwardRef(function Avatar_(
props: AvatarProps,
ref: React.Ref<HTMLDivElement>
) {
const {
model,
style,
variant = AvatarVariant.Round,
size = AvatarSize.Medium,
className,
showTooltip,
showHoverCard: _showHoverCard,
src: srcProp,
alt,
onClick,
...rest
} = props;
const src = props.src || model?.avatarUrl;
const src = srcProp || model?.avatarUrl;
const [error, handleError] = useBoolean(false);
const initial =
model?.initial || (model?.name ? model.name[0] : "").toUpperCase();
const content = (
<Relative
ref={ref}
style={style}
$variant={variant}
$size={props.size}
$size={size}
className={className}
{...rest}
>
{src && !error ? (
<Image onError={handleError} src={src} {...rest} />
) : model ? (
<Initials color={model.color} {...rest}>
{initial}
</Initials>
<Image
onError={handleError}
onClick={onClick}
src={src}
alt={alt}
size={size}
/>
) : (
<Initials {...rest} />
<Initials
color={model?.color}
onClick={onClick}
aria-label={alt}
size={size}
>
{model ? initial : null}
</Initials>
)}
</Relative>
);
return showTooltip ? (
<Tooltip content={props.alt || model?.name || ""}>{content}</Tooltip>
<Tooltip content={alt || model?.name || ""}>{content}</Tooltip>
) : (
content
);
}
Avatar.defaultProps = {
size: AvatarSize.Medium,
};
});
const Relative = styled.div<{ $variant: AvatarVariant; $size: AvatarSize }>`
position: relative;
@@ -0,0 +1,29 @@
import { observer } from "mobx-react";
import * as React from "react";
import { UserHoverCard } from "~/components/UserHoverCard";
import User from "~/models/User";
import type { AvatarProps } from "./Avatar";
import Avatar from "./Avatar";
/**
* An avatar that additionally displays a profile card on hover when the model
* it represents is a user.
*/
const AvatarWithHoverCard = React.forwardRef(function AvatarWithHoverCard_(
{ showTooltip, showHoverCard = true, ...props }: AvatarProps,
ref: React.Ref<HTMLDivElement>
) {
const { model } = props;
if (!showHoverCard || !(model instanceof User)) {
return <Avatar ref={ref} showTooltip={showTooltip} {...props} />;
}
return (
<UserHoverCard user={model}>
<Avatar ref={ref} {...props} />
</UserHoverCard>
);
});
export default observer(AvatarWithHoverCard);
+45 -30
View File
@@ -1,10 +1,12 @@
import { observer } from "mobx-react";
import { EditIcon, EyeIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import type User from "~/models/User";
import User from "~/models/User";
import Tooltip from "~/components/Tooltip";
import { UserHoverCard } from "~/components/UserHoverCard";
import Avatar, { AvatarSize } from "./Avatar";
/**
@@ -41,7 +43,7 @@ type Props = {
* - Observing users have a colored border matching their user color
* - Hovering shows a colored border
*
* A tooltip displays the user's name and current status.
* A profile card on hover displays the user's name and current status.
*
* @param props - Component properties
* @returns React component
@@ -58,44 +60,57 @@ function AvatarWithPresence({
alt,
}: Props) {
const { t } = useTranslation();
const { name } = user;
const status = isPresent
? isEditing
? t("currently editing")
: t("currently viewing")
: t("previously edited");
return (
<>
<Tooltip
content={
<Centered>
<strong>{user.name}</strong> {isCurrentUser && `(${t("You")})`}
{status && (
<>
<br />
{status}
</>
)}
</Centered>
}
placement="bottom"
>
<AvatarPresence
$isPresent={isPresent}
$isObserving={isObserving}
$color={user.color}
style={style}
>
<Avatar model={user} onClick={onClick} size={size} alt={alt} />
</AvatarPresence>
const avatar = (
<AvatarPresence
$isPresent={isPresent}
$isObserving={isObserving}
$color={user.color}
style={style}
>
<Avatar model={user} onClick={onClick} size={size} alt={alt} />
</AvatarPresence>
);
// The facepile overflow badge is rendered with a placeholder in place of a
// real user, there's no profile to show for it.
if (!(user instanceof User)) {
return (
<Tooltip content={<Centered>{name}</Centered>} placement="bottom">
{avatar}
</Tooltip>
</>
);
}
return (
<UserHoverCard
user={user}
side="bottom"
info={
<>
{isPresent && !isEditing ? (
<EyeIcon size={18} />
) : (
<EditIcon size={18} />
)}
<span>
{status}
{isCurrentUser && ` (${t("You")})`}
</span>
</>
}
>
{avatar}
</UserHoverCard>
);
}
/**
* Centered container for tooltip content
*/
const Centered = styled.div`
text-align: center;
`;
+9 -3
View File
@@ -1,8 +1,14 @@
import type { IAvatar } from "./Avatar";
import Avatar, { AvatarSize, AvatarVariant } from "./Avatar";
import type { AvatarProps, IAvatar } from "./Avatar";
import { AvatarSize, AvatarVariant } from "./Avatar";
import AvatarWithHoverCard from "./AvatarWithHoverCard";
import AvatarWithPresence from "./AvatarWithPresence";
import { GroupAvatar } from "./GroupAvatar";
/**
* The default avatar, which shows a profile card on hover for users.
*/
const Avatar = AvatarWithHoverCard;
export { Avatar, GroupAvatar, AvatarSize, AvatarVariant, AvatarWithPresence };
export type { IAvatar };
export type { AvatarProps, IAvatar };
+15 -9
View File
@@ -1,22 +1,28 @@
import { transparentize } from "polished";
import { darken, transparentize } from "polished";
import styled from "styled-components";
const Badge = styled.span<{ yellow?: boolean; primary?: boolean }>`
padding: 1.5px 5.5px;
margin: 0 2px;
background-color: ${({ yellow, primary, theme }) =>
yellow ? theme.yellow : primary ? theme.accent : "transparent"};
background: ${({ yellow, primary, theme }) =>
yellow
? transparentize(0.85, theme.yellow)
: primary
? transparentize(0.85, theme.accent)
: "transparent"};
color: ${({ primary, yellow, theme }) =>
primary
? theme.accentText
? theme.accent
: yellow
? theme.almostBlack
? darken(0.1, theme.yellow)
: theme.textTertiary};
border: 1px solid
${({ primary, yellow, theme }) =>
primary || yellow
? "transparent"
: transparentize(0.4, theme.textTertiary)};
${({ yellow, primary, theme }) =>
yellow
? transparentize(0.25, theme.yellow)
: primary
? transparentize(0.25, theme.accent)
: theme.textTertiary};
border-radius: 8px;
font-size: 12px;
font-weight: 500;
+2 -5
View File
@@ -3,6 +3,7 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import type User from "~/models/User";
import type { AvatarProps } from "~/components/Avatar";
import { Avatar, AvatarSize } from "~/components/Avatar";
import Flex from "~/components/Flex";
import { s } from "@shared/styles";
@@ -18,11 +19,7 @@ type Props = {
/** The maximum number of users to display, defaults to 8 */
limit?: number;
/** A component to render the avatar, defaults to Avatar. */
renderAvatar?: React.ComponentType<
React.ComponentProps<typeof Avatar> & {
model: User;
}
>;
renderAvatar?: React.ComponentType<AvatarProps>;
/** Whether to show tooltips on hover, defaults to true */
showTooltip?: boolean;
};
+1
View File
@@ -290,6 +290,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function Sidebar_(
alt={t("Avatar of {{ name }}", { name: user.name })}
model={user}
size={24}
showHoverCard={false}
/>
}
>
+179
View File
@@ -0,0 +1,179 @@
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { observer } from "mobx-react";
import { ClockIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { depths, s } from "@shared/styles";
import Avatar, { AvatarSize } from "~/components/Avatar/Avatar";
import Badge from "~/components/Badge";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import Time from "~/components/Time";
import { usePortalContext } from "~/components/Portal";
import useMobile from "~/hooks/useMobile";
import type User from "~/models/User";
import { fadeAndScaleIn } from "~/styles/animations";
type Props = {
/** The user to display a profile card for. */
user: User;
/** Additional information to display alongside the user's activity. */
info?: React.ReactNode;
/** The preferred side of the trigger to render against when open. */
side?: "top" | "right" | "bottom" | "left";
/** The element that triggers the card on hover. */
children: React.ReactNode;
};
/**
* Displays a card with a summary of the given user's profile when the wrapped
* element is hovered.
*/
export const UserHoverCard = ({ user, info, side, children }: Props) => {
const container = usePortalContext();
const isMobile = useMobile();
if (isMobile) {
return <>{children}</>;
}
return (
<HoverCardPrimitive.Root openDelay={500} closeDelay={150}>
<HoverCardPrimitive.Trigger asChild>
{children}
</HoverCardPrimitive.Trigger>
<HoverCardPrimitive.Portal container={container}>
<Content side={side} sideOffset={8} collisionPadding={8}>
<Profile user={user} info={info} />
</Content>
</HoverCardPrimitive.Portal>
</HoverCardPrimitive.Root>
);
};
/**
* The contents of the hover card, rendered only while the card is open so that
* the per-minute clock updates do not affect avatars on the page.
*/
const Profile = observer(function Profile_({
user,
info,
}: Pick<Props, "user" | "info">) {
const { t } = useTranslation();
const localTime = user.localTime;
return (
<>
<Flex gap={12} align="center">
<Avatar model={user} size={AvatarSize.XLarge} />
<Flex column justify="center">
<NameRow>
<Name>{user.name}</Name>
{user.isAdmin ? (
<RoleBadge primary>{t("Admin")}</RoleBadge>
) : user.isGuest ? (
<RoleBadge>{t("Guest")}</RoleBadge>
) : null}
{user.isSuspended && <RoleBadge>{t("Suspended")}</RoleBadge>}
</NameRow>
<Info>
{user.isInvited ? (
t("Invited")
) : user.isRecentlyActive ? (
t("Online")
) : (
<>
{t("Last seen")} <Time dateTime={user.lastActiveAt} addSuffix />
</>
)}
</Info>
</Flex>
</Flex>
{(localTime || info) && (
<>
<Divider />
<Flex column gap={8}>
{localTime && (
<Row>
<ClockIcon size={18} />
{t("{{ time }} local time", { time: localTime })}
</Row>
)}
{info && <Row>{info}</Row>}
</Flex>
</>
)}
</>
);
});
const NameRow = styled(Flex).attrs({ align: "center", gap: 4 })`
min-width: 0;
`;
const Name = styled(Text).attrs({
as: "h3",
weight: "bold",
ellipsis: true,
})`
margin: 0;
min-width: 0;
font-size: 15px;
`;
// Scaled down from the default badge to sit comfortably beside the name.
const RoleBadge = styled(Badge)`
flex-shrink: 0;
margin: 0;
padding: 1px 4.5px;
border-radius: 6px;
font-size: 10px;
`;
const Info = styled(Text).attrs({
as: "p",
type: "tertiary",
size: "xsmall",
})`
margin: 0;
white-space: nowrap;
`;
const Divider = styled.div`
height: 1px;
margin: 12px 0;
background: ${s("divider")};
`;
const Row = styled(Flex).attrs({ align: "center", gap: 8 })`
color: ${s("textSecondary")};
font-size: 13px;
white-space: nowrap;
svg {
color: ${s("textTertiary")};
flex-shrink: 0;
}
`;
const Content = styled(HoverCardPrimitive.Content)`
z-index: ${depths.tooltip};
transform-origin: var(--radix-hover-card-content-transform-origin);
background: ${s("menuBackground")};
box-shadow: ${s("menuShadow")};
border-radius: 6px;
padding: 12px;
min-width: 180px;
max-width: 320px;
outline: none;
user-select: none;
&[data-state="open"] {
animation: ${fadeAndScaleIn} 150ms cubic-bezier(0.08, 0.82, 0.17, 1);
}
@media print {
display: none;
}
`;
+25
View File
@@ -12,6 +12,7 @@ import {
} from "@shared/types";
import type { NotificationSettings } from "@shared/types";
import type { locales } from "@shared/utils/date";
import { unicodeCLDRtoBCP47 } from "@shared/utils/date";
import { client } from "~/utils/ApiClient";
import type Document from "./Document";
import type Group from "./Group";
@@ -152,6 +153,30 @@ class User extends ParanoidModel implements Searchable {
return new Date(this.lastActiveAt) > subMinutes(now(10000), 5);
}
/**
* The current time where the user is located, formatted for the locale of the
* signed-in user.
*
* @returns the formatted time, or undefined if the user's timezone is unknown
*/
@computed
get localTime(): string | undefined {
if (!this.timezone) {
return undefined;
}
const language = this.store.rootStore.auth?.user?.language;
try {
return new Date(now(60000)).toLocaleTimeString(
language ? unicodeCLDRtoBCP47(language) : undefined,
{ hour: "numeric", minute: "numeric", timeZone: this.timezone }
);
} catch {
return undefined;
}
}
/**
* Returns whether this user is using a separate editing mode behind an "Edit"
* button rather than seamless always-editing.
+1
View File
@@ -93,6 +93,7 @@
"@radix-ui/react-dialog": "^1.1.19",
"@radix-ui/react-direction": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.20",
"@radix-ui/react-hover-card": "^1.1.23",
"@radix-ui/react-one-time-password-field": "^0.1.12",
"@radix-ui/react-popover": "^1.1.19",
"@radix-ui/react-select": "^2.3.3",
+5 -2
View File
@@ -596,6 +596,11 @@
"A confirmation email will be sent to the new address before it is changed.": "A confirmation email will be sent to the new address before it is changed.",
"New email": "New email",
"Email can't be empty": "Email can't be empty",
"Admin": "Admin",
"Guest": "Guest",
"Online": "Online",
"Last seen": "Last seen",
"{{ time }} local time": "{{ time }} local time",
"Your import completed": "Your import completed",
"View comment thread": "View comment thread",
"Previous match": "Previous match",
@@ -938,7 +943,6 @@
"Sorry, you can only send {{MAX_INVITES}} invites at a time": "Sorry, you can only send {{MAX_INVITES}} invites at a time",
"Invited {{roleName}} will receive access to": "Invited {{roleName}} will receive access to",
"{{collectionCount}} collections": "{{collectionCount}} collections",
"Admin": "Admin",
"Can manage all workspace settings": "Can manage all workspace settings",
"Can create, edit, and delete documents": "Can create, edit, and delete documents",
"Can view and comment": "Can view and comment",
@@ -1251,7 +1255,6 @@
"You can import a zip file that was previously exported from an Outline installation collections, documents, and images will be imported. In Outline, open <em>Export</em> in the Settings sidebar and click on <em>Export Data</em>.": "You can import a zip file that was previously exported from an Outline installation collections, documents, and images will be imported. In Outline, open <em>Export</em> in the Settings sidebar and click on <em>Export Data</em>.",
"Drag and drop the zip file from the Markdown export option in {{appName}}, or click to upload": "Drag and drop the zip file from the Markdown export option in {{appName}}, or click to upload",
"Configure": "Configure",
"Guest": "Guest",
"Never used": "Never used",
"Are you sure you want to revoke access?": "Are you sure you want to revoke access?",
"Are you sure you want to delete the {{ appName }} application? This cannot be undone.": "Are you sure you want to delete the {{ appName }} application? This cannot be undone.",
+301 -2
View File
@@ -4355,6 +4355,13 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/primitive@npm:1.1.7":
version: 1.1.7
resolution: "@radix-ui/primitive@npm:1.1.7"
checksum: 10c0/6b1f8b964a57ece3490582d60df79a1c80fdba4c4331b806c4e04b11f42ac46d40a927bfc692dbd881136ec38c3f060c525dbacd6182ba6f3f0bd4284db1028e
languageName: node
linkType: hard
"@radix-ui/react-arrow@npm:1.1.11":
version: 1.1.11
resolution: "@radix-ui/react-arrow@npm:1.1.11"
@@ -4374,6 +4381,25 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-arrow@npm:1.1.15":
version: 1.1.15
resolution: "@radix-ui/react-arrow@npm:1.1.15"
dependencies:
"@radix-ui/react-primitive": "npm:2.1.10"
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
checksum: 10c0/06fa0a29ae332617d7912e4f25e952d110905264362e7266d2aa423e81aff492d0f1eaa7dd834b3271e6f5d45f56c8c54585d10d771356fb789e50c959f555ca
languageName: node
linkType: hard
"@radix-ui/react-collapsible@npm:^1.1.16":
version: 1.1.16
resolution: "@radix-ui/react-collapsible@npm:1.1.16"
@@ -4435,6 +4461,19 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-compose-refs@npm:1.1.5":
version: 1.1.5
resolution: "@radix-ui/react-compose-refs@npm:1.1.5"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10c0/ab5cc8cc2f5be285981cffb64aa956d3a28427c649a953d125e900e1befb8b9a55f2d7a31bc01059d29df43ff4ae14ebc6cca7e4c2084d638cef5e5aef7cc263
languageName: node
linkType: hard
"@radix-ui/react-context-menu@npm:^2.3.3":
version: 2.3.3
resolution: "@radix-ui/react-context-menu@npm:2.3.3"
@@ -4471,6 +4510,19 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-context@npm:1.2.2":
version: 1.2.2
resolution: "@radix-ui/react-context@npm:1.2.2"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10c0/f5c15339558d64901c99f243a2538be9b28eb20e0dae1e44c8872ed12580ec27f5f309fd491f85f1b0dcb214c90fdae7eede7fe2b0d26569c4dbeffbeff451c0
languageName: node
linkType: hard
"@radix-ui/react-dialog@npm:^1.1.1, @radix-ui/react-dialog@npm:^1.1.19":
version: 1.1.19
resolution: "@radix-ui/react-dialog@npm:1.1.19"
@@ -4539,6 +4591,29 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-dismissable-layer@npm:1.1.19":
version: 1.1.19
resolution: "@radix-ui/react-dismissable-layer@npm:1.1.19"
dependencies:
"@radix-ui/primitive": "npm:1.1.7"
"@radix-ui/react-compose-refs": "npm:1.1.5"
"@radix-ui/react-primitive": "npm:2.1.10"
"@radix-ui/react-use-callback-ref": "npm:1.1.4"
"@radix-ui/react-use-effect-event": "npm:0.0.5"
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
checksum: 10c0/205f2fded4a9e303fcc0d67b78274db84d37d0d35177bb99602c72202a6dcadc39f4cbf936a09e1303b4b42a2b494a279ead9e626cb1a41015152a91ad069fc5
languageName: node
linkType: hard
"@radix-ui/react-dropdown-menu@npm:^2.1.20":
version: 2.1.20
resolution: "@radix-ui/react-dropdown-menu@npm:2.1.20"
@@ -4598,6 +4673,33 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-hover-card@npm:^1.1.23":
version: 1.1.23
resolution: "@radix-ui/react-hover-card@npm:1.1.23"
dependencies:
"@radix-ui/primitive": "npm:1.1.7"
"@radix-ui/react-compose-refs": "npm:1.1.5"
"@radix-ui/react-context": "npm:1.2.2"
"@radix-ui/react-dismissable-layer": "npm:1.1.19"
"@radix-ui/react-popper": "npm:1.3.7"
"@radix-ui/react-portal": "npm:1.1.17"
"@radix-ui/react-presence": "npm:1.1.10"
"@radix-ui/react-primitive": "npm:2.1.10"
"@radix-ui/react-use-controllable-state": "npm:1.2.6"
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
checksum: 10c0/08b62baccf478f9cb5e2fa6da27a85d9b9499735c7a29c4e5986404ec9ee2c393464baec7739fa34a18681f1eb65d784271e991c7eeb6b91573046eaba232621
languageName: node
linkType: hard
"@radix-ui/react-id@npm:1.1.2":
version: 1.1.2
resolution: "@radix-ui/react-id@npm:1.1.2"
@@ -4740,7 +4842,35 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-portal@npm:1.1.13, @radix-ui/react-portal@npm:^1.0.1":
"@radix-ui/react-popper@npm:1.3.7":
version: 1.3.7
resolution: "@radix-ui/react-popper@npm:1.3.7"
dependencies:
"@floating-ui/react-dom": "npm:^2.0.0"
"@radix-ui/react-arrow": "npm:1.1.15"
"@radix-ui/react-compose-refs": "npm:1.1.5"
"@radix-ui/react-context": "npm:1.2.2"
"@radix-ui/react-primitive": "npm:2.1.10"
"@radix-ui/react-use-callback-ref": "npm:1.1.4"
"@radix-ui/react-use-layout-effect": "npm:1.1.4"
"@radix-ui/react-use-rect": "npm:1.1.4"
"@radix-ui/react-use-size": "npm:1.1.4"
"@radix-ui/rect": "npm:1.1.3"
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
checksum: 10c0/f32e85f70d2161d724df4adae4103d5c1f61ff344d25ad51c4f3d0a02b6251890b864dde3313093d748efe824dc9cceb40a068ac68b10bd1163d4b678e01d6e3
languageName: node
linkType: hard
"@radix-ui/react-portal@npm:1.1.13":
version: 1.1.13
resolution: "@radix-ui/react-portal@npm:1.1.13"
dependencies:
@@ -4760,6 +4890,45 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-portal@npm:1.1.17, @radix-ui/react-portal@npm:^1.0.1":
version: 1.1.17
resolution: "@radix-ui/react-portal@npm:1.1.17"
dependencies:
"@radix-ui/react-primitive": "npm:2.1.10"
"@radix-ui/react-use-layout-effect": "npm:1.1.4"
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
checksum: 10c0/f3358ef906582ee8dd8ae62db1f1c1e9810e0554ff8e1a887c731fb35d3b673791e9d338cab884f3eddd74e24afd8e21679b1cc24fd035c68ad71dd09ed71bae
languageName: node
linkType: hard
"@radix-ui/react-presence@npm:1.1.10":
version: 1.1.10
resolution: "@radix-ui/react-presence@npm:1.1.10"
dependencies:
"@radix-ui/react-use-layout-effect": "npm:1.1.4"
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
checksum: 10c0/5ef8048ea02ecfd11284e229efa33f75074189788b68edb9fe38140cbc56cc40ee77f017f34e3c3d3a5dbc1fb12b5a18d5a9ef2a84ef966a1d90502be5a84617
languageName: node
linkType: hard
"@radix-ui/react-presence@npm:1.1.7":
version: 1.1.7
resolution: "@radix-ui/react-presence@npm:1.1.7"
@@ -4779,6 +4948,25 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-primitive@npm:2.1.10":
version: 2.1.10
resolution: "@radix-ui/react-primitive@npm:2.1.10"
dependencies:
"@radix-ui/react-slot": "npm:1.3.3"
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
checksum: 10c0/3be05e842d20a5eb08cebbc1d4d08f24a4c09af8cb864e1d1dddf27f362b739ce0edc1c9fc188a7c734cecd8f5df21e379c15774230f4ab11ce2a8229d3598da
languageName: node
linkType: hard
"@radix-ui/react-primitive@npm:2.1.7":
version: 2.1.7
resolution: "@radix-ui/react-primitive@npm:2.1.7"
@@ -4886,7 +5074,7 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-slot@npm:1.3.0, @radix-ui/react-slot@npm:^1.3.0":
"@radix-ui/react-slot@npm:1.3.0":
version: 1.3.0
resolution: "@radix-ui/react-slot@npm:1.3.0"
dependencies:
@@ -4901,6 +5089,21 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-slot@npm:1.3.3, @radix-ui/react-slot@npm:^1.3.0":
version: 1.3.3
resolution: "@radix-ui/react-slot@npm:1.3.3"
dependencies:
"@radix-ui/react-compose-refs": "npm:1.1.5"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10c0/7ed356819bbb40415ccd16bc8a8da3162966e827ea8fa404c2ae547518777aa193b0aad07f139f3c9d3280f51433e1cc4fa4cfd8dc7e8e6215e7b0d7a25e4b39
languageName: node
linkType: hard
"@radix-ui/react-switch@npm:^1.3.3":
version: 1.3.3
resolution: "@radix-ui/react-switch@npm:1.3.3"
@@ -5066,6 +5269,19 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-use-callback-ref@npm:1.1.4":
version: 1.1.4
resolution: "@radix-ui/react-use-callback-ref@npm:1.1.4"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10c0/d5cc1248ab6a58b12eefd3115c98fb438b74b2e6dec463d2e097d27df8e9f40b9d8693ee10a422da4f7a688fbe52c62cc2fe3aa5972020b9425b86a2a28f5f01
languageName: node
linkType: hard
"@radix-ui/react-use-controllable-state@npm:1.2.3":
version: 1.2.3
resolution: "@radix-ui/react-use-controllable-state@npm:1.2.3"
@@ -5082,6 +5298,23 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-use-controllable-state@npm:1.2.6":
version: 1.2.6
resolution: "@radix-ui/react-use-controllable-state@npm:1.2.6"
dependencies:
"@radix-ui/primitive": "npm:1.1.7"
"@radix-ui/react-use-effect-event": "npm:0.0.5"
"@radix-ui/react-use-layout-effect": "npm:1.1.4"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10c0/1672c3fa5c1f2dbc354db4d403523eef15bcbe24448b911e67e3e95f351b72ffc1dc1021a225d98116cd78d1c96e6280f1645ac6d6b9930df46b180b8a6f11a5
languageName: node
linkType: hard
"@radix-ui/react-use-effect-event@npm:0.0.3":
version: 0.0.3
resolution: "@radix-ui/react-use-effect-event@npm:0.0.3"
@@ -5097,6 +5330,21 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-use-effect-event@npm:0.0.5":
version: 0.0.5
resolution: "@radix-ui/react-use-effect-event@npm:0.0.5"
dependencies:
"@radix-ui/react-use-layout-effect": "npm:1.1.4"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10c0/36eb3523d3c30e88615e0492aed5c644e61eb4fec98097de0fb2ee1cf33d7684b76e8a34bedb0aa7c25b412e0ef56c83b16772e9716d03d61901bc1b98cb1993
languageName: node
linkType: hard
"@radix-ui/react-use-is-hydrated@npm:0.1.1":
version: 0.1.1
resolution: "@radix-ui/react-use-is-hydrated@npm:0.1.1"
@@ -5123,6 +5371,19 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-use-layout-effect@npm:1.1.4":
version: 1.1.4
resolution: "@radix-ui/react-use-layout-effect@npm:1.1.4"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10c0/427df10bb3c55de36afd6b9a749fb4b72b0f78840ae4abb5dda24aa09d653ede2d264c96a8030f58db353498f42b2bce929510c777004e250f7845b06c782bcf
languageName: node
linkType: hard
"@radix-ui/react-use-previous@npm:1.1.2":
version: 1.1.2
resolution: "@radix-ui/react-use-previous@npm:1.1.2"
@@ -5151,6 +5412,21 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-use-rect@npm:1.1.4":
version: 1.1.4
resolution: "@radix-ui/react-use-rect@npm:1.1.4"
dependencies:
"@radix-ui/rect": "npm:1.1.3"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10c0/3acfe9265f9fbdcdead05ec4540a95d8932fb53e9eabd73c080650122424d3d5247260d349ea2d91b2712af9670a27ac2110400671b98d061b2347390fe29490
languageName: node
linkType: hard
"@radix-ui/react-use-size@npm:1.1.2":
version: 1.1.2
resolution: "@radix-ui/react-use-size@npm:1.1.2"
@@ -5166,6 +5442,21 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-use-size@npm:1.1.4":
version: 1.1.4
resolution: "@radix-ui/react-use-size@npm:1.1.4"
dependencies:
"@radix-ui/react-use-layout-effect": "npm:1.1.4"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10c0/ebb4a0c6e71768ae231635cc721d7f01de0a31637e73cccea095ad63921bfece9635eb9500cbe32504585380910f578216fb3eebdfc21b511f6dd6df81659f75
languageName: node
linkType: hard
"@radix-ui/react-visually-hidden@npm:1.2.7, @radix-ui/react-visually-hidden@npm:^1.2.7":
version: 1.2.7
resolution: "@radix-ui/react-visually-hidden@npm:1.2.7"
@@ -5192,6 +5483,13 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/rect@npm:1.1.3":
version: 1.1.3
resolution: "@radix-ui/rect@npm:1.1.3"
checksum: 10c0/9396d6c4dbd8da51c0bef7481e1e46e7f371e27cc6a55c1079e854f3aa9f7dd89e02d98cc36e861e8d43fbb775eb76b06bdf5879fa12f25116bea676af2926da
languageName: node
linkType: hard
"@reach/observe-rect@npm:^1.1.0":
version: 1.2.0
resolution: "@reach/observe-rect@npm:1.2.0"
@@ -15456,6 +15754,7 @@ __metadata:
"@radix-ui/react-dialog": "npm:^1.1.19"
"@radix-ui/react-direction": "npm:^1.1.2"
"@radix-ui/react-dropdown-menu": "npm:^2.1.20"
"@radix-ui/react-hover-card": "npm:^1.1.23"
"@radix-ui/react-one-time-password-field": "npm:^0.1.12"
"@radix-ui/react-popover": "npm:^1.1.19"
"@radix-ui/react-select": "npm:^2.3.3"