From 93638564cf48f8f06b5a1e7d1bf2737ca1e47b98 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 1 Aug 2026 18:12:09 -0400 Subject: [PATCH] feat: Adds user profile cards on hover (#13230) * wip * Add option to suppress hover card --- app/components/Avatar/Avatar.tsx | 58 ++-- app/components/Avatar/AvatarWithHoverCard.tsx | 29 ++ app/components/Avatar/AvatarWithPresence.tsx | 75 +++-- app/components/Avatar/index.ts | 12 +- app/components/Badge.ts | 24 +- app/components/Facepile.tsx | 7 +- app/components/Sidebar/Sidebar.tsx | 1 + app/components/UserHoverCard.tsx | 179 +++++++++++ app/models/User.ts | 25 ++ package.json | 1 + shared/i18n/locales/en_US/translation.json | 7 +- yarn.lock | 303 +++++++++++++++++- 12 files changed, 651 insertions(+), 70 deletions(-) create mode 100644 app/components/Avatar/AvatarWithHoverCard.tsx create mode 100644 app/components/UserHoverCard.tsx diff --git a/app/components/Avatar/Avatar.tsx b/app/components/Avatar/Avatar.tsx index 126a0a12a0..18abcd4b9f 100644 --- a/app/components/Avatar/Avatar.tsx +++ b/app/components/Avatar/Avatar.tsx @@ -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; + onClick?: React.MouseEventHandler; /** 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 +) { 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 = ( {src && !error ? ( - - ) : model ? ( - - {initial} - + {alt} ) : ( - + + {model ? initial : null} + )} ); return showTooltip ? ( - {content} + {content} ) : ( content ); -} - -Avatar.defaultProps = { - size: AvatarSize.Medium, -}; +}); const Relative = styled.div<{ $variant: AvatarVariant; $size: AvatarSize }>` position: relative; diff --git a/app/components/Avatar/AvatarWithHoverCard.tsx b/app/components/Avatar/AvatarWithHoverCard.tsx new file mode 100644 index 0000000000..64f8000bfc --- /dev/null +++ b/app/components/Avatar/AvatarWithHoverCard.tsx @@ -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 +) { + const { model } = props; + + if (!showHoverCard || !(model instanceof User)) { + return ; + } + + return ( + + + + ); +}); + +export default observer(AvatarWithHoverCard); diff --git a/app/components/Avatar/AvatarWithPresence.tsx b/app/components/Avatar/AvatarWithPresence.tsx index 9da9ff20f5..1b14986836 100644 --- a/app/components/Avatar/AvatarWithPresence.tsx +++ b/app/components/Avatar/AvatarWithPresence.tsx @@ -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 ( - <> - - {user.name} {isCurrentUser && `(${t("You")})`} - {status && ( - <> -
- {status} - - )} - - } - placement="bottom" - > - - - + const avatar = ( + + + + ); + + // 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 ( + {name}} placement="bottom"> + {avatar} - + ); + } + + return ( + + {isPresent && !isEditing ? ( + + ) : ( + + )} + + {status} + {isCurrentUser && ` (${t("You")})`} + + + } + > + {avatar} + ); } -/** - * Centered container for tooltip content - */ const Centered = styled.div` text-align: center; `; diff --git a/app/components/Avatar/index.ts b/app/components/Avatar/index.ts index 464dc23c5f..7bd164ed25 100644 --- a/app/components/Avatar/index.ts +++ b/app/components/Avatar/index.ts @@ -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 }; diff --git a/app/components/Badge.ts b/app/components/Badge.ts index a070d6e810..01568a0faf 100644 --- a/app/components/Badge.ts +++ b/app/components/Badge.ts @@ -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; diff --git a/app/components/Facepile.tsx b/app/components/Facepile.tsx index c2d8f12753..215e327234 100644 --- a/app/components/Facepile.tsx +++ b/app/components/Facepile.tsx @@ -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 & { - model: User; - } - >; + renderAvatar?: React.ComponentType; /** Whether to show tooltips on hover, defaults to true */ showTooltip?: boolean; }; diff --git a/app/components/Sidebar/Sidebar.tsx b/app/components/Sidebar/Sidebar.tsx index f774747b72..a6a698ae1f 100644 --- a/app/components/Sidebar/Sidebar.tsx +++ b/app/components/Sidebar/Sidebar.tsx @@ -290,6 +290,7 @@ const Sidebar = React.forwardRef(function Sidebar_( alt={t("Avatar of {{ name }}", { name: user.name })} model={user} size={24} + showHoverCard={false} /> } > diff --git a/app/components/UserHoverCard.tsx b/app/components/UserHoverCard.tsx new file mode 100644 index 0000000000..9fe83bcd22 --- /dev/null +++ b/app/components/UserHoverCard.tsx @@ -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 ( + + + {children} + + + + + + + + ); +}; + +/** + * 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) { + const { t } = useTranslation(); + const localTime = user.localTime; + + return ( + <> + + + + + {user.name} + {user.isAdmin ? ( + {t("Admin")} + ) : user.isGuest ? ( + {t("Guest")} + ) : null} + {user.isSuspended && {t("Suspended")}} + + + {user.isInvited ? ( + t("Invited") + ) : user.isRecentlyActive ? ( + t("Online") + ) : ( + <> + {t("Last seen")} + + + {(localTime || info) && ( + <> + + + {localTime && ( + + + {t("{{ time }} local time", { time: localTime })} + + )} + {info && {info}} + + + )} + + ); +}); + +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; + } +`; diff --git a/app/models/User.ts b/app/models/User.ts index 63fbc77e3a..f046b6c8d9 100644 --- a/app/models/User.ts +++ b/app/models/User.ts @@ -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. diff --git a/package.json b/package.json index bdda25ce26..7962af3011 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index e6e07f44af..7686ea37eb 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -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 Export in the Settings sidebar and click on Export Data.": "You can import a zip file that was previously exported from an Outline installation – collections, documents, and images will be imported. In Outline, open Export in the Settings sidebar and click on Export Data.", "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.", diff --git a/yarn.lock b/yarn.lock index 124278a07b..3db3203f59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"