UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: локальная привязка окна профиля в верхнем toolbar

This commit is contained in:
DCCONSTRUCTIONS
2026-04-21 19:35:45 +03:00
parent f1f29185bf
commit 6337b6e4ac
5 changed files with 399 additions and 61 deletions
@@ -132,7 +132,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
const width = 320;
const viewportPadding = 16;
const left = Math.min(rect.left, window.innerWidth - width - viewportPadding);
const top = rect.top + rect.height / 2;
const top = rect.bottom + 10;
setSidebarSearchPosition({
left,
@@ -331,7 +331,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
ref={sidebarSearchButtonRef}
type="button"
className={cn(
"absolute left-0 top-0 z-[161] flex size-8 items-center justify-center rounded-full border border-white/8 bg-white/[0.04] text-placeholder backdrop-blur-[18px] outline-none transition-all hover:bg-white/[0.07]"
"absolute left-0 top-0 z-[161] flex size-8 items-center justify-center rounded-full border-0 bg-white/[0.04] text-placeholder backdrop-blur-[18px] outline-none transition-all hover:bg-white/[0.07]"
)}
onClick={() => {
if (isOpen) {
@@ -374,7 +374,6 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
left: `${sidebarSearchPosition.left}px`,
top: `${sidebarSearchPosition.top}px`,
width: `${sidebarSearchPosition.width}px`,
transform: "translateY(-50%)",
}}
>
<div className="relative">
@@ -400,7 +399,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
</button>
)}
</div>
<div className="nodedc-glass-modal nodedc-glass-popup-surface absolute bottom-full left-0 mb-3 flex max-h-[70vh] w-full flex-col overflow-hidden rounded-[1.5rem] pt-3">
<div className="nodedc-glass-modal nodedc-glass-popup-surface absolute top-full left-0 mt-3 flex max-h-[70vh] w-full flex-col overflow-hidden rounded-[1.5rem] pt-3">
<div className="px-4 pb-2">
<div className="text-[13px] font-medium text-secondary">
{t("power_k.search_menu.quick_access_title")}
@@ -5,6 +5,7 @@
*/
import { useState, useEffect } from "react";
import { Menu } from "@headlessui/react";
import { observer } from "mobx-react";
import { useRouter } from "next/navigation";
import { LogOut, Settings, Settings2 } from "lucide-react";
@@ -23,7 +24,7 @@ import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useUser } from "@/hooks/store/user";
type TUserMenuRootProps = {
variant?: "default" | "sidebar-utility";
variant?: "default" | "sidebar-utility" | "toolbar";
};
export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootProps) {
@@ -42,6 +43,9 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootP
// translation
const { t } = useTranslation();
const isSidebarUtilityVariant = variant === "sidebar-utility";
const isToolbarVariant = variant === "toolbar";
const handleSignOut = () => {
signOut().catch(() =>
setToast({
@@ -58,46 +62,8 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootP
else toggleAnySidebarDropdown(false);
}, [isUserMenuOpen, toggleAnySidebarDropdown]);
return (
<CustomMenu
className="flex items-center"
customButton={
variant === "sidebar-utility" ? (
<button
type="button"
className="flex size-8 items-center justify-center rounded-full border border-white/8 bg-white/[0.04] backdrop-blur-[18px] transition-all hover:bg-white/[0.07]"
>
<Avatar
name={currentUser?.display_name}
src={getFileURL(currentUser?.avatar_url ?? "")}
size={18}
shape="circle"
/>
</button>
) : (
<AppSidebarItem
variant="button"
item={{
icon: (
<Avatar
name={currentUser?.display_name}
src={getFileURL(currentUser?.avatar_url ?? "")}
size={20}
shape="circle"
/>
),
isActive: isUserMenuOpen,
}}
/>
)
}
menuButtonOnClick={() => !isUserMenuOpen && setIsUserMenuOpen(true)}
onMenuClose={() => setIsUserMenuOpen(false)}
placement="bottom-end"
maxHeight="2xl"
optionsClassName="w-72 p-3 flex flex-col gap-y-3"
closeOnSelect
>
const menuContent = (
<>
<div className="relative h-29 w-full rounded-lg">
<CoverImage
src={currentUser?.cover_image_url ?? undefined}
@@ -164,6 +130,77 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootP
{t("enter_god_mode")}
</CustomMenu.MenuItem>
)}
</>
);
if (isToolbarVariant) {
return (
<Menu as="div" className="relative">
<Menu.Button
type="button"
aria-label={t("profile")}
className="flex size-8 items-center justify-center rounded-full border-0 bg-white/[0.04] backdrop-blur-[18px] transition-all hover:bg-white/[0.07]"
>
<Avatar
name={currentUser?.display_name}
src={getFileURL(currentUser?.avatar_url ?? "")}
size={18}
shape="circle"
/>
</Menu.Button>
<Menu.Items className="absolute top-full left-0 z-[170] mt-2 origin-top-left">
<div className="nodedc-glass-modal nodedc-glass-popup-surface flex w-72 flex-col gap-y-3 rounded-[1.25rem] border-0 p-3 shadow-none outline-none">
{menuContent}
</div>
</Menu.Items>
</Menu>
);
}
return (
<CustomMenu
className="flex items-center"
customButtonClassName={
isSidebarUtilityVariant
? "flex size-8 items-center justify-center rounded-full border-0 bg-white/[0.04] backdrop-blur-[18px] transition-all hover:bg-white/[0.07]"
: ""
}
customButton={
isSidebarUtilityVariant ? (
<span className="pointer-events-none flex size-8 items-center justify-center">
<Avatar
name={currentUser?.display_name}
src={getFileURL(currentUser?.avatar_url ?? "")}
size={18}
shape="circle"
/>
</span>
) : (
<AppSidebarItem
variant="button"
item={{
icon: (
<Avatar
name={currentUser?.display_name}
src={getFileURL(currentUser?.avatar_url ?? "")}
size={20}
shape="circle"
/>
),
isActive: isUserMenuOpen,
}}
/>
)
}
menuButtonOnClick={() => !isUserMenuOpen && setIsUserMenuOpen(true)}
onMenuClose={() => setIsUserMenuOpen(false)}
placement="bottom-end"
maxHeight="2xl"
optionsClassName="w-72 p-3 flex flex-col gap-y-3"
closeOnSelect
>
{menuContent}
</CustomMenu>
);
});
@@ -31,7 +31,7 @@ import { WorkspaceLogo } from "../logo";
import SidebarDropdownItem from "./dropdown-item";
type WorkspaceMenuRootProps = {
variant: "sidebar" | "top-navigation" | "sidebar-panel";
variant: "sidebar" | "top-navigation" | "sidebar-panel" | "toolbar";
};
type WorkspaceMenuStateSyncProps = {
@@ -46,7 +46,7 @@ function WorkspaceMenuStateSync(props: WorkspaceMenuStateSyncProps) {
const { open, variant, sidebarPanelButtonRef, onSidebarDropdownToggle, onSidebarPanelPositionChange } = props;
const updateSidebarPanelMenuPosition = useCallback(() => {
if (variant !== "sidebar-panel" || !sidebarPanelButtonRef.current || typeof window === "undefined") return;
if (!["sidebar-panel", "toolbar"].includes(variant) || !sidebarPanelButtonRef.current || typeof window === "undefined") return;
const rect = sidebarPanelButtonRef.current.getBoundingClientRect();
const width = 480;
@@ -64,7 +64,7 @@ function WorkspaceMenuStateSync(props: WorkspaceMenuStateSyncProps) {
}, [onSidebarDropdownToggle, open]);
useLayoutEffect(() => {
if (!open || variant !== "sidebar-panel") {
if (!open || !["sidebar-panel", "toolbar"].includes(variant)) {
onSidebarPanelPositionChange(null);
return;
}
@@ -133,6 +133,7 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
"w-full justify-center text-center": variant === "sidebar",
"flex-grow justify-stretch text-left": variant === "top-navigation",
"w-full max-w-none justify-stretch text-left": variant === "sidebar-panel",
"w-fit max-w-none justify-center text-center": variant === "toolbar",
})}
>
{({ open, close }: { open: boolean; close: () => void }) => {
@@ -220,6 +221,24 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
/>
</Menu.Button>
)}
{variant === "toolbar" && (
<Menu.Button
ref={sidebarPanelButtonRef}
className={cn(
"flex size-8 items-center justify-center rounded-full bg-white/[0.04] backdrop-blur-[18px] transition-all hover:bg-white/[0.07] focus:outline-none",
{
"bg-white/[0.08]": open,
}
)}
aria-label={t("aria_labels.projects_sidebar.open_workspace_switcher")}
>
<WorkspaceLogo
logo={activeWorkspace?.logo_url}
name={activeWorkspace?.name}
classNames="size-8 rounded-[0.9rem]"
/>
</Menu.Button>
)}
{(() => {
const menuItems = (
<Menu.Items
@@ -228,15 +247,15 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
"z-21 mt-1 flex min-w-[30rem] origin-top-left flex-col divide-y overflow-hidden outline-none",
{
"fixed divide-subtle rounded-md border-[0.5px] border-strong bg-surface-1 shadow-raised-200":
variant !== "sidebar-panel",
!["sidebar-panel", "toolbar"].includes(variant),
"top-11 left-14": variant === "sidebar",
"top-10 left-4": variant === "top-navigation",
"nodedc-glass-modal nodedc-glass-popup-surface rounded-[1.5rem] divide-white/10":
variant === "sidebar-panel",
["sidebar-panel", "toolbar"].includes(variant),
}
)}
style={
variant === "sidebar-panel" && sidebarPanelMenuPosition
["sidebar-panel", "toolbar"].includes(variant) && sidebarPanelMenuPosition
? {
position: "fixed",
left: `${sidebarPanelMenuPosition.left}px`,
@@ -251,8 +270,8 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
className={cn(
"sticky top-0 z-21 h-full w-full flex-shrink-0 truncate px-4 pt-3 pb-1 text-left text-13 font-medium text-placeholder",
{
"rounded-md bg-surface-1": variant !== "sidebar-panel",
"bg-transparent": variant === "sidebar-panel",
"rounded-md bg-surface-1": !["sidebar-panel", "toolbar"].includes(variant),
"bg-transparent": ["sidebar-panel", "toolbar"].includes(variant),
}
)}
>
@@ -324,7 +343,7 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
</Menu.Items>
);
if (variant === "sidebar-panel") {
if (["sidebar-panel", "toolbar"].includes(variant)) {
if (!open || !sidebarPanelMenuPosition || typeof document === "undefined") return null;
return createPortal(menuItems, document.body);
}