UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: локальная привязка окна профиля в верхнем toolbar
This commit is contained in:
parent
f1f29185bf
commit
6337b6e4ac
|
|
@ -7,9 +7,7 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
import { ProjectsAppPowerKProvider } from "@/components/power-k/projects-app-provider";
|
import { ProjectsAppPowerKProvider } from "@/components/power-k/projects-app-provider";
|
||||||
// plane web components
|
import { ProjectShellTopToolbar } from "./project-shell-top-toolbar";
|
||||||
import { ProjectAppSidebar } from "./_sidebar";
|
|
||||||
import { ExtendedProjectSidebar } from "./extended-project-sidebar";
|
|
||||||
|
|
||||||
function WorkspaceLayout() {
|
function WorkspaceLayout() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -18,10 +16,11 @@ function WorkspaceLayout() {
|
||||||
<div className="relative flex h-full w-full flex-col overflow-hidden rounded-lg border border-subtle">
|
<div className="relative flex h-full w-full flex-col overflow-hidden rounded-lg border border-subtle">
|
||||||
<div id="full-screen-portal" className="absolute inset-0 w-full" />
|
<div id="full-screen-portal" className="absolute inset-0 w-full" />
|
||||||
<div className="relative flex size-full overflow-hidden">
|
<div className="relative flex size-full overflow-hidden">
|
||||||
<ProjectAppSidebar />
|
<main className="relative flex h-full w-full min-w-0 flex-col overflow-hidden bg-surface-1">
|
||||||
<ExtendedProjectSidebar />
|
<ProjectShellTopToolbar />
|
||||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-surface-1">
|
<div className="relative flex min-h-0 w-full flex-1 overflow-hidden">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,284 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Menu } from "@headlessui/react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { useParams, usePathname } from "next/navigation";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import {
|
||||||
|
EUserPermissions,
|
||||||
|
EUserPermissionsLevel,
|
||||||
|
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS,
|
||||||
|
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS,
|
||||||
|
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS,
|
||||||
|
} from "@plane/constants";
|
||||||
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { InboxIcon, PlusIcon, ProjectIcon } from "@plane/propel/icons";
|
||||||
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
|
import { Tooltip } from "@plane/propel/tooltip";
|
||||||
|
import { copyUrlToClipboard, joinUrlPath } from "@plane/utils";
|
||||||
|
import { TopNavPowerK } from "@/components/navigation";
|
||||||
|
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||||
|
import { useProject } from "@/hooks/store/use-project";
|
||||||
|
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||||
|
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
||||||
|
import {
|
||||||
|
usePersonalNavigationPreferences,
|
||||||
|
useWorkspaceNavigationPreferences,
|
||||||
|
} from "@/hooks/use-navigation-preferences";
|
||||||
|
import { SidebarProjectsListItem } from "@/components/workspace/sidebar/projects-list-item";
|
||||||
|
import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
|
||||||
|
import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root";
|
||||||
|
import { getSidebarNavigationItemIcon } from "@/plane-web/components/workspace/sidebar/helper";
|
||||||
|
|
||||||
|
type TToolbarItem = {
|
||||||
|
key: string;
|
||||||
|
href?: string;
|
||||||
|
labelTranslationKey: string;
|
||||||
|
active: boolean;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolbarIconLink = ({ item }: { item: TToolbarItem }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip tooltipContent={t(item.labelTranslationKey)} position="bottom">
|
||||||
|
<Link
|
||||||
|
href={item.href ?? "#"}
|
||||||
|
className="nodedc-toolbar-icon-button flex h-8 w-8 items-center justify-center"
|
||||||
|
data-active={item.active}
|
||||||
|
aria-label={t(item.labelTranslationKey)}
|
||||||
|
>
|
||||||
|
<span className="nodedc-toolbar-icon-active-dot">{item.icon}</span>
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolbarIconButton = ({
|
||||||
|
label,
|
||||||
|
active = false,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
disabled = false,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
active?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}) => (
|
||||||
|
<Tooltip tooltipContent={label} position="bottom">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="nodedc-toolbar-icon-button flex h-8 w-8 items-center justify-center"
|
||||||
|
data-active={active}
|
||||||
|
aria-label={label}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<span className="nodedc-toolbar-icon-active-dot">{children}</span>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ProjectsToolbarMenu = observer(function ProjectsToolbarMenu() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const { workspaceSlug } = useParams();
|
||||||
|
const { joinedProjectIds } = useProject();
|
||||||
|
|
||||||
|
const handleCopyText = (projectId: string) =>
|
||||||
|
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => {
|
||||||
|
setToast({
|
||||||
|
type: TOAST_TYPE.SUCCESS,
|
||||||
|
title: t("link_copied"),
|
||||||
|
message: t("project_link_copied_to_clipboard"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu as="div" className="relative">
|
||||||
|
<Menu.Button
|
||||||
|
type="button"
|
||||||
|
title={t("workspace_sidebar.projects.main")}
|
||||||
|
className="nodedc-toolbar-icon-button grid h-8 w-8 place-items-center"
|
||||||
|
aria-label={t("workspace_sidebar.projects.main")}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`nodedc-toolbar-icon-active-dot ${
|
||||||
|
pathname.includes("/projects/") ? "bg-[rgb(var(--nodedc-accent-rgb))] text-[#0b1117]" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ProjectIcon className="size-4" />
|
||||||
|
</span>
|
||||||
|
</Menu.Button>
|
||||||
|
|
||||||
|
<Menu.Items className="absolute top-full -right-2 z-[170] mt-2 origin-top-right">
|
||||||
|
<div className="nodedc-glass-modal nodedc-glass-popup-surface flex max-h-[70vh] min-w-[26rem] flex-col overflow-hidden rounded-[1.5rem] border-0 p-2 shadow-none outline-none">
|
||||||
|
<div className="vertical-scrollbar scrollbar-sm flex max-h-[70vh] flex-col gap-0.5 overflow-y-auto pr-1">
|
||||||
|
{joinedProjectIds.map((projectId, index) => (
|
||||||
|
<SidebarProjectsListItem
|
||||||
|
key={projectId}
|
||||||
|
projectId={projectId}
|
||||||
|
handleCopyText={() => handleCopyText(projectId)}
|
||||||
|
projectListType="JOINED"
|
||||||
|
disableDrag
|
||||||
|
disableDrop
|
||||||
|
isLastChild={index === joinedProjectIds.length - 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ProjectShellTopToolbar = observer(function ProjectShellTopToolbar() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const { workspaceSlug } = useParams();
|
||||||
|
const { toggleCreateIssueModal } = useCommandPalette();
|
||||||
|
const { joinedProjectIds } = useProject();
|
||||||
|
const { data: currentUser } = useUser();
|
||||||
|
const { allowPermissions } = useUserPermissions();
|
||||||
|
const { unreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
|
||||||
|
const { preferences: personalPreferences } = usePersonalNavigationPreferences();
|
||||||
|
const { preferences: workspacePreferences } = useWorkspaceNavigationPreferences();
|
||||||
|
|
||||||
|
const canCreateIssue = allowPermissions(
|
||||||
|
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||||
|
EUserPermissionsLevel.WORKSPACE
|
||||||
|
);
|
||||||
|
|
||||||
|
useSWR(
|
||||||
|
workspaceSlug ? "WORKSPACE_UNREAD_NOTIFICATION_COUNT" : null,
|
||||||
|
workspaceSlug ? () => getUnreadNotificationsCount(workspaceSlug.toString()) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
const isMentionsEnabled = unreadNotificationsCount.mention_unread_notifications_count > 0;
|
||||||
|
const totalNotifications = isMentionsEnabled
|
||||||
|
? unreadNotificationsCount.mention_unread_notifications_count
|
||||||
|
: unreadNotificationsCount.total_unread_notifications_count;
|
||||||
|
|
||||||
|
const primaryItems = useMemo(() => {
|
||||||
|
const items = [...WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS];
|
||||||
|
const personalItems: Array<(typeof items)[0] & { sort_order: number }> = [];
|
||||||
|
|
||||||
|
const stickiesItem = WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["stickies"];
|
||||||
|
if (personalPreferences.items.stickies?.enabled && stickiesItem) {
|
||||||
|
personalItems.push({
|
||||||
|
...stickiesItem,
|
||||||
|
sort_order: personalPreferences.items.stickies.sort_order,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (personalPreferences.items.your_work?.enabled && WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["your-work"]) {
|
||||||
|
personalItems.push({
|
||||||
|
...WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["your-work"],
|
||||||
|
sort_order: personalPreferences.items.your_work.sort_order,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (personalPreferences.items.drafts?.enabled && WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["drafts"]) {
|
||||||
|
personalItems.push({
|
||||||
|
...WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["drafts"],
|
||||||
|
sort_order: personalPreferences.items.drafts.sort_order,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
personalItems.sort((a, b) => a.sort_order - b.sort_order);
|
||||||
|
|
||||||
|
return [...items, ...personalItems].map((item) => {
|
||||||
|
const href =
|
||||||
|
item.key === "your_work" && currentUser?.id
|
||||||
|
? joinUrlPath(workspaceSlug?.toString() ?? "", item.href, currentUser.id)
|
||||||
|
: joinUrlPath(workspaceSlug?.toString() ?? "", item.href);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: item.key,
|
||||||
|
href,
|
||||||
|
labelTranslationKey: item.labelTranslationKey,
|
||||||
|
active: item.highlight(pathname, href),
|
||||||
|
icon: getSidebarNavigationItemIcon(item.key),
|
||||||
|
} satisfies TToolbarItem;
|
||||||
|
});
|
||||||
|
}, [currentUser?.id, pathname, personalPreferences, workspaceSlug]);
|
||||||
|
|
||||||
|
const secondaryItems = useMemo(
|
||||||
|
() =>
|
||||||
|
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS.map((item) => {
|
||||||
|
const href = joinUrlPath(workspaceSlug?.toString() ?? "", item.href);
|
||||||
|
const preference = workspacePreferences.items[item.key];
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: item.key,
|
||||||
|
href,
|
||||||
|
labelTranslationKey: item.labelTranslationKey,
|
||||||
|
active: item.highlight(pathname, href),
|
||||||
|
icon: getSidebarNavigationItemIcon(item.key),
|
||||||
|
sort_order: preference ? preference.sort_order : 0,
|
||||||
|
};
|
||||||
|
}).sort((a, b) => a.sort_order - b.sort_order),
|
||||||
|
[pathname, workspacePreferences, workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="z-20 w-full flex-shrink-0 px-4 pt-4 pb-3">
|
||||||
|
<div className="nodedc-glass-modal flex w-full flex-wrap items-center justify-between gap-4 rounded-[1.6rem] px-4 py-3">
|
||||||
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
|
<div className="nodedc-toolbar-group relative flex items-center gap-1 overflow-visible">
|
||||||
|
<ToolbarIconButton
|
||||||
|
label={t("app_header.add_task")}
|
||||||
|
onClick={() => toggleCreateIssueModal(true)}
|
||||||
|
disabled={!canCreateIssue || joinedProjectIds.length === 0}
|
||||||
|
>
|
||||||
|
<PlusIcon className="size-4" />
|
||||||
|
</ToolbarIconButton>
|
||||||
|
<TopNavPowerK variant="sidebar" />
|
||||||
|
<Tooltip tooltipContent={t("notification.label")} position="bottom">
|
||||||
|
<Link
|
||||||
|
href={`/${workspaceSlug?.toString()}/notifications/`}
|
||||||
|
className="nodedc-toolbar-icon-button relative flex h-8 w-8 items-center justify-center"
|
||||||
|
data-active={pathname.includes("/notifications/")}
|
||||||
|
aria-label={t("notification.label")}
|
||||||
|
>
|
||||||
|
<span className="nodedc-toolbar-icon-active-dot">
|
||||||
|
<InboxIcon className="size-4" />
|
||||||
|
</span>
|
||||||
|
{totalNotifications > 0 && (
|
||||||
|
<span className="absolute top-1.5 right-1.5 size-2 rounded-full bg-danger-primary" />
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
<UserMenuRoot variant="toolbar" />
|
||||||
|
<WorkspaceMenuRoot variant="toolbar" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex min-w-0 items-center justify-end gap-3">
|
||||||
|
<div className="nodedc-toolbar-group flex items-center gap-1">
|
||||||
|
{primaryItems.map((item) => (
|
||||||
|
<ToolbarIconLink key={item.key} item={item} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="nodedc-toolbar-group relative flex items-center gap-1 overflow-visible">
|
||||||
|
<ProjectsToolbarMenu />
|
||||||
|
{secondaryItems.map((item) => (
|
||||||
|
<ToolbarIconLink key={item.key} item={item} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -132,7 +132,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
||||||
const width = 320;
|
const width = 320;
|
||||||
const viewportPadding = 16;
|
const viewportPadding = 16;
|
||||||
const left = Math.min(rect.left, window.innerWidth - width - viewportPadding);
|
const left = Math.min(rect.left, window.innerWidth - width - viewportPadding);
|
||||||
const top = rect.top + rect.height / 2;
|
const top = rect.bottom + 10;
|
||||||
|
|
||||||
setSidebarSearchPosition({
|
setSidebarSearchPosition({
|
||||||
left,
|
left,
|
||||||
|
|
@ -331,7 +331,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
||||||
ref={sidebarSearchButtonRef}
|
ref={sidebarSearchButtonRef}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
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={() => {
|
onClick={() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
|
|
@ -374,7 +374,6 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
||||||
left: `${sidebarSearchPosition.left}px`,
|
left: `${sidebarSearchPosition.left}px`,
|
||||||
top: `${sidebarSearchPosition.top}px`,
|
top: `${sidebarSearchPosition.top}px`,
|
||||||
width: `${sidebarSearchPosition.width}px`,
|
width: `${sidebarSearchPosition.width}px`,
|
||||||
transform: "translateY(-50%)",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -400,7 +399,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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="px-4 pb-2">
|
||||||
<div className="text-[13px] font-medium text-secondary">
|
<div className="text-[13px] font-medium text-secondary">
|
||||||
{t("power_k.search_menu.quick_access_title")}
|
{t("power_k.search_menu.quick_access_title")}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { Menu } from "@headlessui/react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { LogOut, Settings, Settings2 } from "lucide-react";
|
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";
|
import { useUser } from "@/hooks/store/user";
|
||||||
|
|
||||||
type TUserMenuRootProps = {
|
type TUserMenuRootProps = {
|
||||||
variant?: "default" | "sidebar-utility";
|
variant?: "default" | "sidebar-utility" | "toolbar";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootProps) {
|
export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootProps) {
|
||||||
|
|
@ -42,6 +43,9 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootP
|
||||||
// translation
|
// translation
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const isSidebarUtilityVariant = variant === "sidebar-utility";
|
||||||
|
const isToolbarVariant = variant === "toolbar";
|
||||||
|
|
||||||
const handleSignOut = () => {
|
const handleSignOut = () => {
|
||||||
signOut().catch(() =>
|
signOut().catch(() =>
|
||||||
setToast({
|
setToast({
|
||||||
|
|
@ -58,46 +62,8 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootP
|
||||||
else toggleAnySidebarDropdown(false);
|
else toggleAnySidebarDropdown(false);
|
||||||
}, [isUserMenuOpen, toggleAnySidebarDropdown]);
|
}, [isUserMenuOpen, toggleAnySidebarDropdown]);
|
||||||
|
|
||||||
return (
|
const menuContent = (
|
||||||
<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
|
|
||||||
>
|
|
||||||
<div className="relative h-29 w-full rounded-lg">
|
<div className="relative h-29 w-full rounded-lg">
|
||||||
<CoverImage
|
<CoverImage
|
||||||
src={currentUser?.cover_image_url ?? undefined}
|
src={currentUser?.cover_image_url ?? undefined}
|
||||||
|
|
@ -164,6 +130,77 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootP
|
||||||
{t("enter_god_mode")}
|
{t("enter_god_mode")}
|
||||||
</CustomMenu.MenuItem>
|
</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>
|
</CustomMenu>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import { WorkspaceLogo } from "../logo";
|
||||||
import SidebarDropdownItem from "./dropdown-item";
|
import SidebarDropdownItem from "./dropdown-item";
|
||||||
|
|
||||||
type WorkspaceMenuRootProps = {
|
type WorkspaceMenuRootProps = {
|
||||||
variant: "sidebar" | "top-navigation" | "sidebar-panel";
|
variant: "sidebar" | "top-navigation" | "sidebar-panel" | "toolbar";
|
||||||
};
|
};
|
||||||
|
|
||||||
type WorkspaceMenuStateSyncProps = {
|
type WorkspaceMenuStateSyncProps = {
|
||||||
|
|
@ -46,7 +46,7 @@ function WorkspaceMenuStateSync(props: WorkspaceMenuStateSyncProps) {
|
||||||
const { open, variant, sidebarPanelButtonRef, onSidebarDropdownToggle, onSidebarPanelPositionChange } = props;
|
const { open, variant, sidebarPanelButtonRef, onSidebarDropdownToggle, onSidebarPanelPositionChange } = props;
|
||||||
|
|
||||||
const updateSidebarPanelMenuPosition = useCallback(() => {
|
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 rect = sidebarPanelButtonRef.current.getBoundingClientRect();
|
||||||
const width = 480;
|
const width = 480;
|
||||||
|
|
@ -64,7 +64,7 @@ function WorkspaceMenuStateSync(props: WorkspaceMenuStateSyncProps) {
|
||||||
}, [onSidebarDropdownToggle, open]);
|
}, [onSidebarDropdownToggle, open]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!open || variant !== "sidebar-panel") {
|
if (!open || !["sidebar-panel", "toolbar"].includes(variant)) {
|
||||||
onSidebarPanelPositionChange(null);
|
onSidebarPanelPositionChange(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +133,7 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
|
||||||
"w-full justify-center text-center": variant === "sidebar",
|
"w-full justify-center text-center": variant === "sidebar",
|
||||||
"flex-grow justify-stretch text-left": variant === "top-navigation",
|
"flex-grow justify-stretch text-left": variant === "top-navigation",
|
||||||
"w-full max-w-none justify-stretch text-left": variant === "sidebar-panel",
|
"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 }) => {
|
{({ open, close }: { open: boolean; close: () => void }) => {
|
||||||
|
|
@ -220,6 +221,24 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
|
||||||
/>
|
/>
|
||||||
</Menu.Button>
|
</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 = (
|
const menuItems = (
|
||||||
<Menu.Items
|
<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",
|
"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":
|
"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-11 left-14": variant === "sidebar",
|
||||||
"top-10 left-4": variant === "top-navigation",
|
"top-10 left-4": variant === "top-navigation",
|
||||||
"nodedc-glass-modal nodedc-glass-popup-surface rounded-[1.5rem] divide-white/10":
|
"nodedc-glass-modal nodedc-glass-popup-surface rounded-[1.5rem] divide-white/10":
|
||||||
variant === "sidebar-panel",
|
["sidebar-panel", "toolbar"].includes(variant),
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
style={
|
style={
|
||||||
variant === "sidebar-panel" && sidebarPanelMenuPosition
|
["sidebar-panel", "toolbar"].includes(variant) && sidebarPanelMenuPosition
|
||||||
? {
|
? {
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
left: `${sidebarPanelMenuPosition.left}px`,
|
left: `${sidebarPanelMenuPosition.left}px`,
|
||||||
|
|
@ -251,8 +270,8 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
|
||||||
className={cn(
|
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",
|
"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",
|
"rounded-md bg-surface-1": !["sidebar-panel", "toolbar"].includes(variant),
|
||||||
"bg-transparent": variant === "sidebar-panel",
|
"bg-transparent": ["sidebar-panel", "toolbar"].includes(variant),
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -324,7 +343,7 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (variant === "sidebar-panel") {
|
if (["sidebar-panel", "toolbar"].includes(variant)) {
|
||||||
if (!open || !sidebarPanelMenuPosition || typeof document === "undefined") return null;
|
if (!open || !sidebarPanelMenuPosition || typeof document === "undefined") return null;
|
||||||
return createPortal(menuItems, document.body);
|
return createPortal(menuItems, document.body);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue