UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: расширенный home layout и аналитические панели
This commit is contained in:
parent
d28f83fe5e
commit
7ff7d83b07
|
|
@ -15,12 +15,15 @@ import { Breadcrumbs, Header } from "@plane/ui";
|
|||
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
||||
// hooks
|
||||
import { useHome } from "@/hooks/store/use-home";
|
||||
import { useUserProfile } from "@/hooks/store/user";
|
||||
|
||||
export const WorkspaceDashboardHeader = observer(function WorkspaceDashboardHeader() {
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { toggleWidgetSettings } = useHome();
|
||||
const { data: userProfile } = useUserProfile();
|
||||
const isCompactToolbar = userProfile?.theme?.nodedcCompactToolbar === true;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -36,6 +39,7 @@ export const WorkspaceDashboardHeader = observer(function WorkspaceDashboardHead
|
|||
</Breadcrumbs>
|
||||
</div>
|
||||
</Header.LeftItem>
|
||||
{isCompactToolbar && (
|
||||
<Header.RightItem>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
|
@ -47,6 +51,7 @@ export const WorkspaceDashboardHeader = observer(function WorkspaceDashboardHead
|
|||
<div className="hidden sm:hidden md:block">{t("home.manage_widgets")}</div>
|
||||
</Button>
|
||||
</Header.RightItem>
|
||||
)}
|
||||
</Header>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@
|
|||
*/
|
||||
|
||||
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";
|
||||
|
|
@ -19,157 +17,31 @@ import {
|
|||
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 { cn, copyUrlToClipboard, joinUrlPath } from "@plane/utils";
|
||||
import { TopNavPowerK } from "@/components/navigation";
|
||||
import { joinUrlPath } from "@plane/utils";
|
||||
import { openWorkspaceNotificationsModal } from "@/components/workspace-notifications/notifications-modal.utils";
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
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 { useUser, useUserPermissions, useUserProfile } 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 { openWorkspaceNotificationsModal } from "@/components/workspace-notifications/notifications-modal.utils";
|
||||
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 { toggleCreateProjectModal } = useCommandPalette();
|
||||
|
||||
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-[rgb(var(--nodedc-on-accent-rgb))]"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<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 flex scrollbar-sm 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}
|
||||
renderInToolbarMenu
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 border-t border-white/8 px-1 pt-2">
|
||||
<Menu.Item>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-3 rounded-md px-2 py-2 text-left text-13 font-medium text-secondary transition-colors hover:bg-layer-transparent-hover hover:text-primary"
|
||||
onClick={() => toggleCreateProjectModal(true)}
|
||||
>
|
||||
<span className="grid size-8 flex-shrink-0 place-items-center">
|
||||
<PlusIcon className="size-4" />
|
||||
</span>
|
||||
<span>{t("create_project")}</span>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
import {
|
||||
DEFAULT_PROJECT_SHELL_TOOLBAR_LAYOUT,
|
||||
PROJECT_SHELL_TOOLBAR_LAYOUTS,
|
||||
type TProjectShellToolbarLayout,
|
||||
type TToolbarItem,
|
||||
} from "./top-toolbar";
|
||||
|
||||
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 { data: userProfile } = useUserProfile();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { unreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
|
||||
const { preferences: personalPreferences } = usePersonalNavigationPreferences();
|
||||
|
|
@ -186,7 +58,7 @@ export const ProjectShellTopToolbar = observer(function ProjectShellTopToolbar()
|
|||
);
|
||||
|
||||
const isMentionsEnabled = unreadNotificationsCount.mention_unread_notifications_count > 0;
|
||||
const totalNotifications = isMentionsEnabled
|
||||
const notificationsCount = isMentionsEnabled
|
||||
? unreadNotificationsCount.mention_unread_notifications_count
|
||||
: unreadNotificationsCount.total_unread_notifications_count;
|
||||
|
||||
|
|
@ -249,62 +121,27 @@ export const ProjectShellTopToolbar = observer(function ProjectShellTopToolbar()
|
|||
}).sort((a, b) => a.sort_order - b.sort_order),
|
||||
[pathname, workspacePreferences, workspaceSlug]
|
||||
);
|
||||
|
||||
const workspaceSlugValue = workspaceSlug?.toString();
|
||||
const isWorkspaceHome = pathname === `/${workspaceSlugValue}` || pathname === `/${workspaceSlugValue}/`;
|
||||
const toolbarLayout: TProjectShellToolbarLayout =
|
||||
userProfile?.theme?.nodedcCompactToolbar === true ? "compact" : DEFAULT_PROJECT_SHELL_TOOLBAR_LAYOUT;
|
||||
const ToolbarLayout = PROJECT_SHELL_TOOLBAR_LAYOUTS[toolbarLayout];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("z-20 w-full flex-shrink-0 px-4 pt-4 pb-3", {
|
||||
"nodedc-home-top-toolbar": isWorkspaceHome,
|
||||
})}
|
||||
>
|
||||
<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">
|
||||
<WorkspaceMenuRoot variant="toolbar" />
|
||||
<TopNavPowerK variant="sidebar" />
|
||||
<UserMenuRoot variant="toolbar" />
|
||||
<Tooltip tooltipContent={t("notification.label")} position="bottom">
|
||||
<button
|
||||
type="button"
|
||||
className="nodedc-toolbar-icon-button relative flex h-8 w-8 items-center justify-center"
|
||||
data-active={false}
|
||||
aria-label={t("notification.label")}
|
||||
onClick={() => openWorkspaceNotificationsModal()}
|
||||
>
|
||||
<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" />
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
<ToolbarIconButton
|
||||
label={t("app_header.add_task")}
|
||||
onClick={() => toggleCreateIssueModal(true)}
|
||||
disabled={!canCreateIssue || joinedProjectIds.length === 0}
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
</ToolbarIconButton>
|
||||
</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>
|
||||
<ToolbarLayout
|
||||
canCreateIssue={canCreateIssue}
|
||||
draftsItem={primaryItems.find((item) => item.key === "drafts")}
|
||||
homeItem={primaryItems.find((item) => item.key === "home")}
|
||||
isWorkspaceHome={isWorkspaceHome}
|
||||
joinedProjectIdsCount={joinedProjectIds.length}
|
||||
notificationsCount={notificationsCount}
|
||||
primaryItems={primaryItems}
|
||||
profileItem={primaryItems.find((item) => item.key === "your_work")}
|
||||
secondaryItems={secondaryItems}
|
||||
stickiesItem={primaryItems.find((item) => item.key === "stickies")}
|
||||
onCreateIssue={() => toggleCreateIssueModal(true)}
|
||||
onOpenNotifications={() => openWorkspaceNotificationsModal()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { useProject } from "@/hooks/store/use-project";
|
|||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web imports
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
|
||||
import { ExpandedToolbarBreadcrumbs } from "@/plane-web/components/breadcrumbs/expanded-toolbar-breadcrumbs";
|
||||
import { ProjectFeatureBreadcrumb } from "@/plane-web/components/breadcrumbs/project-feature";
|
||||
import { PageDetailsHeaderExtraActions } from "@/plane-web/components/pages";
|
||||
import { EPageStoreType, usePage, usePageStore } from "@/plane-web/hooks/store";
|
||||
|
|
@ -68,7 +69,7 @@ export const PageDetailsHeader = observer(function PageDetailsHeader() {
|
|||
<Header>
|
||||
<Header.LeftItem>
|
||||
<div>
|
||||
<Breadcrumbs isLoading={loader === "init-loader"}>
|
||||
<ExpandedToolbarBreadcrumbs isLoading={loader === "init-loader"}>
|
||||
<CommonProjectBreadcrumbs workspaceSlug={workspaceSlug?.toString()} projectId={projectId?.toString()} />
|
||||
<ProjectFeatureBreadcrumb
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
|
|
@ -94,7 +95,7 @@ export const PageDetailsHeader = observer(function PageDetailsHeader() {
|
|||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</ExpandedToolbarBreadcrumbs>
|
||||
</div>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem>
|
||||
|
|
|
|||
|
|
@ -14,13 +14,14 @@ import { useTranslation } from "@plane/i18n";
|
|||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { TPage } from "@plane/types";
|
||||
// plane ui
|
||||
import { Breadcrumbs, Header } from "@plane/ui";
|
||||
import { Header } from "@plane/ui";
|
||||
// components
|
||||
import { AppHeaderPrimaryActionButton } from "@/components/core/app-header/primary-action-button";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
// plane web imports
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
|
||||
import { ExpandedToolbarBreadcrumbs } from "@/plane-web/components/breadcrumbs/expanded-toolbar-breadcrumbs";
|
||||
import { ProjectFeatureBreadcrumb } from "@/plane-web/components/breadcrumbs/project-feature";
|
||||
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
|
||||
|
||||
|
|
@ -62,7 +63,7 @@ export const PagesListHeader = observer(function PagesListHeader() {
|
|||
return (
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
<Breadcrumbs isLoading={loader === "init-loader"}>
|
||||
<ExpandedToolbarBreadcrumbs isLoading={loader === "init-loader"}>
|
||||
<CommonProjectBreadcrumbs workspaceSlug={workspaceSlug?.toString()} projectId={projectId?.toString()} />
|
||||
<ProjectFeatureBreadcrumb
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
|
|
@ -70,7 +71,7 @@ export const PagesListHeader = observer(function PagesListHeader() {
|
|||
featureKey={EProjectFeatureKey.PAGES}
|
||||
isLast
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</ExpandedToolbarBreadcrumbs>
|
||||
</Header.LeftItem>
|
||||
{canCurrentUserCreatePage && (
|
||||
<Header.RightItem>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
"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 { useTranslation } from "@plane/i18n";
|
||||
import { PlusIcon } from "@plane/propel/icons";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { TopNavPowerK } from "@/components/navigation";
|
||||
import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
|
||||
import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root";
|
||||
import { ProjectsToolbarMenu } from "./projects-toolbar-menu";
|
||||
import { ToolbarIconButton, ToolbarIconLink, ToolbarNotificationsButton } from "./toolbar-controls";
|
||||
// types
|
||||
import type { TProjectShellToolbarLayoutProps } from "./types";
|
||||
|
||||
export const CompactProjectShellToolbarLayout = ({
|
||||
canCreateIssue,
|
||||
isWorkspaceHome,
|
||||
joinedProjectIdsCount,
|
||||
notificationsCount,
|
||||
primaryItems,
|
||||
secondaryItems,
|
||||
onCreateIssue,
|
||||
onOpenNotifications,
|
||||
}: TProjectShellToolbarLayoutProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("z-20 w-full flex-shrink-0 px-4 pt-4 pb-3", {
|
||||
"nodedc-home-top-toolbar": isWorkspaceHome,
|
||||
})}
|
||||
>
|
||||
<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">
|
||||
<WorkspaceMenuRoot variant="toolbar" />
|
||||
<TopNavPowerK variant="sidebar" />
|
||||
<UserMenuRoot variant="toolbar" />
|
||||
<ToolbarNotificationsButton
|
||||
label={t("notification.label")}
|
||||
notificationsCount={notificationsCount}
|
||||
onClick={onOpenNotifications}
|
||||
/>
|
||||
<ToolbarIconButton
|
||||
label={t("app_header.add_task")}
|
||||
onClick={onCreateIssue}
|
||||
disabled={!canCreateIssue || joinedProjectIdsCount === 0}
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
</ToolbarIconButton>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
"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 { useTranslation } from "@plane/i18n";
|
||||
import { Shapes } from "lucide-react";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { TopNavPowerK } from "@/components/navigation";
|
||||
import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
|
||||
import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root";
|
||||
import { useHome } from "@/hooks/store/use-home";
|
||||
import { ProjectsToolbarMenu } from "./projects-toolbar-menu";
|
||||
import { ExpandedToolbarLink, ExpandedToolbarToolButton, ToolbarNotificationsButton } from "./toolbar-controls";
|
||||
// types
|
||||
import type { TProjectShellToolbarLayoutProps } from "./types";
|
||||
|
||||
export const ExpandedProjectShellToolbarLayout = ({
|
||||
draftsItem,
|
||||
homeItem,
|
||||
isWorkspaceHome,
|
||||
notificationsCount,
|
||||
profileItem,
|
||||
stickiesItem,
|
||||
onOpenNotifications,
|
||||
}: TProjectShellToolbarLayoutProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { toggleWidgetSettings } = useHome();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("z-20 w-full flex-shrink-0 px-5 pt-4 pb-3", {
|
||||
"nodedc-home-top-toolbar": isWorkspaceHome,
|
||||
})}
|
||||
>
|
||||
<div className="nodedc-expanded-toolbar">
|
||||
<div className="nodedc-expanded-toolbar-top">
|
||||
<div className="nodedc-expanded-toolbar-left">
|
||||
<img src="/nodedc-logo.svg" alt="NODE DC" className="nodedc-expanded-brand-logo" />
|
||||
</div>
|
||||
|
||||
<div className="nodedc-expanded-toolbar-center">
|
||||
<WorkspaceMenuRoot variant="expanded-toolbar" />
|
||||
<div className="nodedc-expanded-nav-group">
|
||||
<ExpandedToolbarLink item={homeItem} label="Главная" />
|
||||
<ProjectsToolbarMenu variant="expanded" />
|
||||
<ExpandedToolbarLink item={stickiesItem} label="Стикеры" />
|
||||
<ExpandedToolbarLink item={draftsItem} label="Черновики" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-expanded-toolbar-right">
|
||||
<div className="nodedc-expanded-user-group">
|
||||
<ExpandedToolbarLink item={profileItem} label="Профиль" />
|
||||
<ToolbarNotificationsButton
|
||||
label={t("notification.label")}
|
||||
notificationsCount={notificationsCount}
|
||||
onClick={onOpenNotifications}
|
||||
variant="expanded"
|
||||
/>
|
||||
<UserMenuRoot variant="expanded-toolbar" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-expanded-toolbar-tools-row">
|
||||
<div className="nodedc-expanded-breadcrumbs-slot" data-nodedc-expanded-breadcrumbs-slot />
|
||||
{!isWorkspaceHome && (
|
||||
<div className="nodedc-expanded-main-tool-cluster">
|
||||
<TopNavPowerK variant="expanded-toolbar" />
|
||||
<div className="nodedc-expanded-header-filters-slot" data-nodedc-expanded-header-filters-slot />
|
||||
</div>
|
||||
)}
|
||||
<div className="nodedc-expanded-action-tool-cluster">
|
||||
<div className="nodedc-expanded-tool-slot" data-nodedc-voice-task-toolbar-slot />
|
||||
{isWorkspaceHome && (
|
||||
<ExpandedToolbarToolButton label={t("home.manage_widgets")} onClick={() => toggleWidgetSettings(true)}>
|
||||
<Shapes className="size-4" />
|
||||
</ExpandedToolbarToolButton>
|
||||
)}
|
||||
<div className="nodedc-expanded-primary-action-slot" data-nodedc-expanded-primary-action-slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./layout-registry";
|
||||
export * from "./types";
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { ComponentType } from "react";
|
||||
// components
|
||||
import { CompactProjectShellToolbarLayout } from "./compact-layout";
|
||||
import { ExpandedProjectShellToolbarLayout } from "./expanded-layout";
|
||||
// types
|
||||
import type { TProjectShellToolbarLayout, TProjectShellToolbarLayoutProps } from "./types";
|
||||
|
||||
export const DEFAULT_PROJECT_SHELL_TOOLBAR_LAYOUT: TProjectShellToolbarLayout = "expanded";
|
||||
|
||||
export const PROJECT_SHELL_TOOLBAR_LAYOUTS: Record<
|
||||
TProjectShellToolbarLayout,
|
||||
ComponentType<TProjectShellToolbarLayoutProps>
|
||||
> = {
|
||||
compact: CompactProjectShellToolbarLayout,
|
||||
expanded: ExpandedProjectShellToolbarLayout,
|
||||
};
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
"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 { Menu } from "@headlessui/react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { PlusIcon, ProjectIcon } from "@plane/propel/icons";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { cn, copyUrlToClipboard } from "@plane/utils";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
// components
|
||||
import { SidebarProjectsListItem } from "@/components/workspace/sidebar/projects-list-item";
|
||||
|
||||
export const ProjectsToolbarMenu = observer(function ProjectsToolbarMenu({
|
||||
variant = "compact",
|
||||
}: {
|
||||
variant?: "compact" | "expanded";
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const pathname = usePathname();
|
||||
const { workspaceSlug } = useParams();
|
||||
const { joinedProjectIds } = useProject();
|
||||
const { toggleCreateProjectModal } = useCommandPalette();
|
||||
|
||||
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={cn(
|
||||
variant === "expanded"
|
||||
? "nodedc-expanded-nav-button"
|
||||
: "nodedc-toolbar-icon-button grid h-8 w-8 place-items-center"
|
||||
)}
|
||||
data-active={pathname.includes("/projects/")}
|
||||
aria-label={t("workspace_sidebar.projects.main")}
|
||||
>
|
||||
{variant === "expanded" ? null : (
|
||||
<span className="nodedc-toolbar-icon-active-dot">
|
||||
<ProjectIcon className="size-4" />
|
||||
</span>
|
||||
)}
|
||||
{variant === "expanded" && <span>Проекты</span>}
|
||||
</Menu.Button>
|
||||
|
||||
<Menu.Items
|
||||
className={cn(
|
||||
"absolute top-full z-[170] mt-2",
|
||||
variant === "expanded" ? "left-0 origin-top-left" : "-right-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 flex scrollbar-sm 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}
|
||||
renderInToolbarMenu
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 border-t border-white/8 px-1 pt-2">
|
||||
<Menu.Item>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-3 rounded-md px-2 py-2 text-left text-13 font-medium text-secondary transition-colors hover:bg-layer-transparent-hover hover:text-primary"
|
||||
onClick={() => toggleCreateProjectModal(true)}
|
||||
>
|
||||
<span className="grid size-8 flex-shrink-0 place-items-center">
|
||||
<PlusIcon className="size-4" />
|
||||
</span>
|
||||
<span>{t("create_project")}</span>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
"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 Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { InboxIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/utils";
|
||||
// types
|
||||
import type { TToolbarItem } from "./types";
|
||||
|
||||
export 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>
|
||||
);
|
||||
};
|
||||
|
||||
export const ToolbarIconButton = ({
|
||||
label,
|
||||
active = false,
|
||||
children,
|
||||
onClick,
|
||||
disabled = false,
|
||||
}: {
|
||||
label: string;
|
||||
active?: boolean;
|
||||
children: 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>
|
||||
);
|
||||
|
||||
export const ToolbarNotificationsButton = ({
|
||||
label,
|
||||
notificationsCount,
|
||||
onClick,
|
||||
variant = "compact",
|
||||
}: {
|
||||
label: string;
|
||||
notificationsCount: number;
|
||||
onClick: () => void;
|
||||
variant?: "compact" | "expanded";
|
||||
}) => (
|
||||
<Tooltip tooltipContent={label} position="bottom">
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"nodedc-toolbar-icon-button relative flex items-center justify-center",
|
||||
variant === "expanded" ? "nodedc-expanded-notification-button" : "h-8 w-8"
|
||||
)}
|
||||
data-active={false}
|
||||
aria-label={label}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="nodedc-toolbar-icon-active-dot">
|
||||
<InboxIcon className={variant === "expanded" ? "size-5" : "size-4"} />
|
||||
</span>
|
||||
{notificationsCount > 0 && (
|
||||
<span className="nodedc-toolbar-notification-dot absolute top-1.5 right-1.5 size-2 rounded-full bg-danger-primary" />
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export const ExpandedToolbarLink = ({ item, label }: { item?: TToolbarItem; label: string }) => {
|
||||
if (!item?.href) return null;
|
||||
|
||||
return (
|
||||
<Link href={item.href} className="nodedc-expanded-nav-button" data-active={item.active}>
|
||||
<span>{label}</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExpandedToolbarToolButton = ({
|
||||
label,
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
onClick?: () => void;
|
||||
}) => (
|
||||
<Tooltip tooltipContent={label} position="bottom">
|
||||
<button type="button" className="nodedc-expanded-tool-button" aria-label={label} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type TProjectShellToolbarLayout = "compact" | "expanded";
|
||||
|
||||
export type TToolbarItem = {
|
||||
key: string;
|
||||
href?: string;
|
||||
labelTranslationKey: string;
|
||||
active: boolean;
|
||||
icon: ReactNode;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export type TProjectShellToolbarLayoutProps = {
|
||||
canCreateIssue: boolean;
|
||||
draftsItem?: TToolbarItem;
|
||||
homeItem?: TToolbarItem;
|
||||
isWorkspaceHome: boolean;
|
||||
joinedProjectIdsCount: number;
|
||||
notificationsCount: number;
|
||||
primaryItems: TToolbarItem[];
|
||||
profileItem?: TToolbarItem;
|
||||
secondaryItems: TToolbarItem[];
|
||||
stickiesItem?: TToolbarItem;
|
||||
onCreateIssue: () => void;
|
||||
onOpenNotifications: () => void;
|
||||
};
|
||||
|
|
@ -6,18 +6,28 @@
|
|||
|
||||
// local components
|
||||
import { useProjectNavigationPreferences } from "@/hooks/use-navigation-preferences";
|
||||
import { useUserProfile } from "@/hooks/store/user";
|
||||
import { ProjectBreadcrumb } from "./project";
|
||||
|
||||
type TCommonProjectBreadcrumbProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
shouldTruncate?: boolean;
|
||||
};
|
||||
|
||||
export function CommonProjectBreadcrumbs(props: TCommonProjectBreadcrumbProps) {
|
||||
const { workspaceSlug, projectId } = props;
|
||||
const { workspaceSlug, projectId, shouldTruncate } = props;
|
||||
// preferences
|
||||
const { preferences: projectPreferences } = useProjectNavigationPreferences();
|
||||
const { data: userProfile } = useUserProfile();
|
||||
const shouldUseCompactToolbar = userProfile?.theme?.nodedcCompactToolbar === true;
|
||||
|
||||
if (projectPreferences.navigationMode === "TABBED") return null;
|
||||
return <ProjectBreadcrumb workspaceSlug={workspaceSlug} projectId={projectId} />;
|
||||
return (
|
||||
<ProjectBreadcrumb
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
shouldTruncate={shouldTruncate ?? shouldUseCompactToolbar}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
"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 { useEffect, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { observer } from "mobx-react";
|
||||
import { Breadcrumbs } from "@plane/ui";
|
||||
import { useUserProfile } from "@/hooks/store/user";
|
||||
|
||||
type TExpandedToolbarBreadcrumbsProps = {
|
||||
children: ReactNode;
|
||||
isLoading?: boolean;
|
||||
onBack?: () => void;
|
||||
};
|
||||
|
||||
export const ExpandedToolbarBreadcrumbs = observer(function ExpandedToolbarBreadcrumbs(
|
||||
props: TExpandedToolbarBreadcrumbsProps
|
||||
) {
|
||||
const { children, isLoading = false, onBack } = props;
|
||||
const { data: userProfile } = useUserProfile();
|
||||
const [target, setTarget] = useState<HTMLElement | null>(null);
|
||||
|
||||
const isCompactToolbar = userProfile?.theme?.nodedcCompactToolbar === true;
|
||||
|
||||
useEffect(() => {
|
||||
if (isCompactToolbar || typeof document === "undefined") {
|
||||
setTarget(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const animationFrame = window.requestAnimationFrame(() => {
|
||||
setTarget(document.querySelector<HTMLElement>("[data-nodedc-expanded-breadcrumbs-slot]"));
|
||||
});
|
||||
|
||||
return () => window.cancelAnimationFrame(animationFrame);
|
||||
}, [isCompactToolbar]);
|
||||
|
||||
const content = (
|
||||
<div className="flex min-w-0 items-center gap-2.5 overflow-visible">
|
||||
<Breadcrumbs
|
||||
onBack={onBack}
|
||||
isLoading={isLoading}
|
||||
className={isCompactToolbar ? "flex-grow-0" : "nodedc-expanded-breadcrumbs flex-grow-0"}
|
||||
>
|
||||
{children}
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!isCompactToolbar && target) return createPortal(content, target);
|
||||
|
||||
return content;
|
||||
});
|
||||
|
|
@ -19,10 +19,11 @@ import { BreadcrumbNavigationSearchDropdown } from "@plane/ui";
|
|||
type TProjectBreadcrumbProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
shouldTruncate?: boolean;
|
||||
};
|
||||
|
||||
export const ProjectBreadcrumb = observer(function ProjectBreadcrumb(props: TProjectBreadcrumbProps) {
|
||||
const { workspaceSlug, projectId } = props;
|
||||
const { workspaceSlug, projectId, shouldTruncate = true } = props;
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// store hooks
|
||||
|
|
@ -69,7 +70,7 @@ export const ProjectBreadcrumb = observer(function ProjectBreadcrumb(props: TPro
|
|||
title={currentProjectDetails?.name}
|
||||
icon={renderIcon(currentProjectDetails)}
|
||||
openOnLabelClick
|
||||
shouldTruncate
|
||||
shouldTruncate={shouldTruncate}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { useTranslation } from "@plane/i18n";
|
|||
import { NewTabIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
import { Breadcrumbs, Header } from "@plane/ui";
|
||||
import { Header } from "@plane/ui";
|
||||
import { CountChip } from "@/components/common/count-chip";
|
||||
import { AppHeaderPrimaryActionButton } from "@/components/core/app-header/primary-action-button";
|
||||
// constants
|
||||
|
|
@ -31,11 +31,12 @@ import { HeaderFilters } from "@/components/issues/filters";
|
|||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useUserPermissions, useUserProfile } from "@/hooks/store/user";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web imports
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
|
||||
import { ExpandedToolbarBreadcrumbs } from "@/plane-web/components/breadcrumbs/expanded-toolbar-breadcrumbs";
|
||||
import { ProjectFeatureBreadcrumb } from "@/plane-web/components/breadcrumbs/project-feature";
|
||||
|
||||
export const IssuesHeader = observer(function IssuesHeader() {
|
||||
|
|
@ -53,22 +54,22 @@ export const IssuesHeader = observer(function IssuesHeader() {
|
|||
|
||||
const { toggleCreateIssueModal } = useCommandPalette();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { data: userProfile } = useUserProfile();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const SPACE_APP_URL = (SPACE_BASE_URL.trim() === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH;
|
||||
const publishedURL = `${SPACE_APP_URL}/issues/${currentProjectDetails?.anchor}`;
|
||||
|
||||
const issuesCount = getGroupIssueCount(undefined, undefined, false);
|
||||
const isCompactToolbar = userProfile?.theme?.nodedcCompactToolbar === true;
|
||||
const canUserCreateIssue = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<Header.LeftItem className="nodedc-bottom-dock-left">
|
||||
<div className="flex min-w-0 items-center gap-2.5 overflow-hidden">
|
||||
<Breadcrumbs onBack={() => router.back()} isLoading={loader === "init-loader"} className="flex-grow-0">
|
||||
const breadcrumbsContent = (
|
||||
<>
|
||||
<ExpandedToolbarBreadcrumbs onBack={() => router.back()} isLoading={loader === "init-loader"}>
|
||||
<CommonProjectBreadcrumbs workspaceSlug={workspaceSlug?.toString()} projectId={projectId?.toString()} />
|
||||
<ProjectFeatureBreadcrumb
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
|
|
@ -76,8 +77,8 @@ export const IssuesHeader = observer(function IssuesHeader() {
|
|||
featureKey={EProjectFeatureKey.WORK_ITEMS}
|
||||
isLast
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
{issuesCount && issuesCount > 0 ? (
|
||||
</ExpandedToolbarBreadcrumbs>
|
||||
{isCompactToolbar && issuesCount && issuesCount > 0 ? (
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={t("issues_header.count_tooltip", { count: issuesCount })}
|
||||
|
|
@ -86,7 +87,13 @@ export const IssuesHeader = observer(function IssuesHeader() {
|
|||
<CountChip count={issuesCount} />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<Header.LeftItem className="nodedc-bottom-dock-left">
|
||||
{breadcrumbsContent}
|
||||
{currentProjectDetails?.anchor ? (
|
||||
<a
|
||||
href={publishedURL}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ import { useParams } from "next/navigation";
|
|||
import { RefreshCcw } from "lucide-react";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Breadcrumbs, Header } from "@plane/ui";
|
||||
import { Header } from "@plane/ui";
|
||||
import { AppHeaderPrimaryActionButton } from "@/components/core/app-header/primary-action-button";
|
||||
import { FiltersToggle } from "@/components/rich-filters/filters-toggle";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
|
||||
import { ExpandedToolbarBreadcrumbs } from "@/plane-web/components/breadcrumbs/expanded-toolbar-breadcrumbs";
|
||||
import { ProjectFeatureBreadcrumb } from "@/plane-web/components/breadcrumbs/project-feature";
|
||||
import { useExternalContoursFilter } from "./filters/provider";
|
||||
import { ExternalContourCreateModalRoot } from "./create-modal";
|
||||
|
|
@ -39,7 +40,7 @@ export const ProjectExternalContoursHeader = observer(function ProjectExternalCo
|
|||
<Header>
|
||||
<Header.LeftItem className="nodedc-bottom-dock-left">
|
||||
<div className="flex min-w-0 flex-grow items-center gap-4 overflow-hidden">
|
||||
<Breadcrumbs isLoading={currentProjectDetailsLoader === "init-loader"}>
|
||||
<ExpandedToolbarBreadcrumbs isLoading={currentProjectDetailsLoader === "init-loader"}>
|
||||
<CommonProjectBreadcrumbs workspaceSlug={workspaceSlug?.toString()} projectId={projectId?.toString()} />
|
||||
<ProjectFeatureBreadcrumb
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
|
|
@ -47,7 +48,7 @@ export const ProjectExternalContoursHeader = observer(function ProjectExternalCo
|
|||
featureKey="external_contours"
|
||||
isLast
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</ExpandedToolbarBreadcrumbs>
|
||||
|
||||
{(loader === "mutation-loading" || loader === "issue-loading") && (
|
||||
<div className="flex items-center gap-1.5 text-tertiary">
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { RefreshCcw } from "lucide-react";
|
|||
// ui
|
||||
import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Breadcrumbs, Header } from "@plane/ui";
|
||||
import { Header } from "@plane/ui";
|
||||
// components
|
||||
import { AppHeaderPrimaryActionButton } from "@/components/core/app-header/primary-action-button";
|
||||
import { FiltersRoot } from "@/components/inbox/inbox-filter";
|
||||
|
|
@ -22,6 +22,7 @@ import { useProjectInbox } from "@/hooks/store/use-project-inbox";
|
|||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
// plane web imports
|
||||
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
|
||||
import { ExpandedToolbarBreadcrumbs } from "@/plane-web/components/breadcrumbs/expanded-toolbar-breadcrumbs";
|
||||
import { ProjectFeatureBreadcrumb } from "@/plane-web/components/breadcrumbs/project-feature";
|
||||
|
||||
export const ProjectInboxHeader = observer(function ProjectInboxHeader() {
|
||||
|
|
@ -46,7 +47,7 @@ export const ProjectInboxHeader = observer(function ProjectInboxHeader() {
|
|||
<Header>
|
||||
<Header.LeftItem className="nodedc-bottom-dock-left">
|
||||
<div className="flex min-w-0 flex-grow items-center gap-4 overflow-hidden">
|
||||
<Breadcrumbs isLoading={currentProjectDetailsLoader === "init-loader"}>
|
||||
<ExpandedToolbarBreadcrumbs isLoading={currentProjectDetailsLoader === "init-loader"}>
|
||||
<CommonProjectBreadcrumbs workspaceSlug={workspaceSlug?.toString()} projectId={projectId?.toString()} />
|
||||
<ProjectFeatureBreadcrumb
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
|
|
@ -54,7 +55,7 @@ export const ProjectInboxHeader = observer(function ProjectInboxHeader() {
|
|||
featureKey={EProjectFeatureKey.INTAKE}
|
||||
isLast
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</ExpandedToolbarBreadcrumbs>
|
||||
|
||||
{loader === "pagination-loading" && (
|
||||
<div className="flex items-center gap-1.5 text-tertiary">
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { observer } from "mobx-react";
|
|||
import { Row } from "@plane/ui";
|
||||
// components
|
||||
import { cn } from "@plane/utils";
|
||||
import { useUserProfile } from "@/hooks/store/user";
|
||||
import { ExtendedAppHeader } from "@/plane-web/components/common/extended-app-header";
|
||||
|
||||
export interface AppHeaderProps {
|
||||
|
|
@ -24,6 +25,13 @@ export const AppHeader = observer(function AppHeader(props: AppHeaderProps) {
|
|||
const { header, mobileHeader, className, rowClassName } = props;
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [dockStyle, setDockStyle] = useState<CSSProperties | undefined>(undefined);
|
||||
const { data: userProfile } = useUserProfile();
|
||||
const isCompactToolbar = userProfile?.theme?.nodedcCompactToolbar === true;
|
||||
const effectiveDockStyle = isCompactToolbar
|
||||
? dockStyle
|
||||
: {
|
||||
left: typeof dockStyle?.left === "number" ? dockStyle.left : 0,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
|
@ -61,7 +69,15 @@ export const AppHeader = observer(function AppHeader(props: AppHeaderProps) {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={cn("fixed right-0 bottom-0 z-[18]", className)} style={dockStyle}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
"fixed bottom-0 z-[18]",
|
||||
isCompactToolbar ? "right-0 nodedc-app-header-compact" : "nodedc-app-header-expanded",
|
||||
className
|
||||
)}
|
||||
style={effectiveDockStyle}
|
||||
>
|
||||
<Row
|
||||
className={cn(
|
||||
"nodedc-bottom-dock flex h-[var(--nodedc-bottom-dock-height)] w-full items-center gap-2",
|
||||
|
|
|
|||
|
|
@ -4,22 +4,65 @@
|
|||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import type { ComponentProps } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { PlusIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/utils";
|
||||
import { useUserProfile } from "@/hooks/store/user";
|
||||
|
||||
type TPrimaryActionButtonProps = ComponentProps<typeof Button>;
|
||||
|
||||
export const AppHeaderPrimaryActionButton = (props: TPrimaryActionButtonProps) => {
|
||||
const { children, className, ...buttonProps } = props;
|
||||
const { children, className, disabled, onClick, ...buttonProps } = props;
|
||||
const { t } = useTranslation();
|
||||
const { data: userProfile } = useUserProfile();
|
||||
const [expandedToolbarTarget, setExpandedToolbarTarget] = useState<HTMLElement | null>(null);
|
||||
const isCompactToolbar = userProfile?.theme?.nodedcCompactToolbar === true;
|
||||
|
||||
useEffect(() => {
|
||||
if (isCompactToolbar || typeof document === "undefined") {
|
||||
setExpandedToolbarTarget(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const animationFrame = window.requestAnimationFrame(() => {
|
||||
setExpandedToolbarTarget(document.querySelector<HTMLElement>("[data-nodedc-expanded-primary-action-slot]"));
|
||||
});
|
||||
|
||||
return () => window.cancelAnimationFrame(animationFrame);
|
||||
}, [isCompactToolbar]);
|
||||
|
||||
if (!isCompactToolbar) {
|
||||
if (!expandedToolbarTarget) return null;
|
||||
|
||||
return createPortal(
|
||||
<Tooltip tooltipContent={typeof children === "string" ? children : t("app_header.add_task")} position="bottom">
|
||||
<button
|
||||
type="button"
|
||||
className="nodedc-expanded-tool-button"
|
||||
aria-label={typeof children === "string" ? children : t("app_header.add_task")}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
data-ph-element={(buttonProps as { "data-ph-element"?: string })["data-ph-element"]}
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
</button>
|
||||
</Tooltip>,
|
||||
expandedToolbarTarget
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className={cn("nodedc-toolbar-primary nodedc-toolbar-primary-wide", className)}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
{...buttonProps}
|
||||
>
|
||||
{children ?? t("app_header.add_task")}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { WorkspaceService } from "@/services/workspace.service";
|
|||
import { HomeCardShell } from "./home-card-shell";
|
||||
import { HomeGanttPreview } from "./home-gantt-preview";
|
||||
import { HomeRecentIssueDecks } from "./home-recent-issue-decks";
|
||||
import { HomeActivityTrendCard, HomeOperationsCard, HomeRhythmRecentOverview } from "./home-project-insights";
|
||||
import { HomeAnalyticsBottomRow, HomeAnalyticsRail, HomeIndividualAnalyticsPanel } from "./home-project-insights";
|
||||
import { HomeProjectStack } from "./home-project-stack";
|
||||
import { aggregateProjectAnalytics, type THomeProjectData } from "./home.utils";
|
||||
import { StickiesWidget } from "../stickies/widget";
|
||||
|
|
@ -184,7 +184,7 @@ export const DashboardWidgets = observer(function DashboardWidgets(props: Dashbo
|
|||
/>
|
||||
) : null;
|
||||
|
||||
const sideWidgetCards = [
|
||||
const bottomWidgetCards = [
|
||||
isQuickLinksEnabled ? (
|
||||
<HomeCardShell key="quick_links" className="overflow-hidden" contentClassName="p-5">
|
||||
<DashboardQuickLinks workspaceSlug={workspaceSlugValue} />
|
||||
|
|
@ -213,7 +213,7 @@ export const DashboardWidgets = observer(function DashboardWidgets(props: Dashbo
|
|||
workspaceName={currentWorkspace?.name}
|
||||
/>
|
||||
|
||||
<div className="nodedc-home-dashboard-grid grid xl:grid-cols-[minmax(320px,360px)_minmax(0,1fr)] xl:items-stretch">
|
||||
<div className="nodedc-home-dashboard-grid grid xl:grid-cols-[minmax(320px,360px)_minmax(0,1fr)_minmax(320px,360px)] xl:items-stretch">
|
||||
<div className="flex min-w-0">
|
||||
<HomeProjectStack
|
||||
className="h-full"
|
||||
|
|
@ -231,33 +231,18 @@ export const DashboardWidgets = observer(function DashboardWidgets(props: Dashbo
|
|||
analytics={selectedProjectAnalytics}
|
||||
workspaceSlug={workspaceSlugValue}
|
||||
/>
|
||||
<HomeRhythmRecentOverview
|
||||
<HomeIndividualAnalyticsPanel project={selectedProject} locale={currentLocale} />
|
||||
</div>
|
||||
<HomeAnalyticsRail
|
||||
project={selectedProject}
|
||||
analytics={selectedProjectAnalytics}
|
||||
analyticsCollection={analyticsCollection}
|
||||
recents={workspaceRecents}
|
||||
recentActivitySlot={recentActivityCard}
|
||||
locale={currentLocale}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-home-lower-grid grid xl:grid-cols-[minmax(320px,360px)_minmax(0,1fr)]">
|
||||
<HomeOperationsCard
|
||||
project={selectedProject}
|
||||
analytics={selectedProjectAnalytics}
|
||||
analyticsCollection={analyticsCollection}
|
||||
recents={workspaceRecents}
|
||||
locale={currentLocale}
|
||||
/>
|
||||
<HomeActivityTrendCard
|
||||
project={selectedProject}
|
||||
analytics={selectedProjectAnalytics}
|
||||
analyticsCollection={analyticsCollection}
|
||||
recents={workspaceRecents}
|
||||
locale={currentLocale}
|
||||
/>
|
||||
</div>
|
||||
<HomeAnalyticsBottomRow recentActivitySlot={recentActivityCard} />
|
||||
|
||||
{isProjectLatestIssuesEnabled && (
|
||||
<HomeRecentIssueDecks project={selectedProject} workspaceSlug={workspaceSlugValue} />
|
||||
|
|
@ -268,11 +253,12 @@ export const DashboardWidgets = observer(function DashboardWidgets(props: Dashbo
|
|||
|
||||
{hasSecondaryWidgets && (
|
||||
<div
|
||||
className={cn("grid gap-5", {
|
||||
"md:grid-cols-2": sideWidgetCards.length > 1,
|
||||
className={cn("nodedc-home-bottom-widgets grid gap-5", {
|
||||
"md:grid-cols-2": bottomWidgetCards.length === 2,
|
||||
"xl:grid-cols-3": bottomWidgetCards.length >= 3,
|
||||
})}
|
||||
>
|
||||
{sideWidgetCards}
|
||||
{bottomWidgetCards}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,14 @@
|
|||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { type ReactNode, useId, useMemo } from "react";
|
||||
import { type ReactNode, useEffect, useId, useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Activity, CheckCircle2, Layers3, UsersRound } from "lucide-react";
|
||||
import type { TActivityEntityData, TProjectAnalyticsCount } from "@plane/types";
|
||||
import { type TActivityEntityData, type TProjectAnalyticsCount } from "@plane/types";
|
||||
import CreatedVsResolved from "@/components/analytics/work-items/created-vs-resolved";
|
||||
import CustomizedInsights from "@/components/analytics/work-items/customized-insights";
|
||||
import WorkItemsInsightTable from "@/components/analytics/work-items/workitems-insight-table";
|
||||
import { useAnalytics } from "@/hooks/store/use-analytics";
|
||||
import {
|
||||
aggregateProjectAnalytics,
|
||||
getActivityProjectId,
|
||||
|
|
@ -294,14 +299,8 @@ export function HomeActivityTrendCard(props: HomeProjectInsightsProps) {
|
|||
}
|
||||
|
||||
export function HomeRhythmCard(props: HomeProjectInsightsProps) {
|
||||
const {
|
||||
completedIssues,
|
||||
metricCards,
|
||||
openIssues,
|
||||
project,
|
||||
recentTouchpoints,
|
||||
totalIssues,
|
||||
} = useHomeProjectInsightData(props);
|
||||
const { completedIssues, metricCards, openIssues, project, recentTouchpoints, totalIssues } =
|
||||
useHomeProjectInsightData(props);
|
||||
|
||||
return (
|
||||
<section className="nodedc-home-subpanel nodedc-home-rhythm-card space-y-4 p-5">
|
||||
|
|
@ -368,9 +367,7 @@ export function HomeRhythmCard(props: HomeProjectInsightsProps) {
|
|||
}
|
||||
|
||||
export function HomeOperationsCard(props: HomeProjectInsightsProps) {
|
||||
const {
|
||||
progressRows,
|
||||
} = useHomeProjectInsightData(props);
|
||||
const { progressRows } = useHomeProjectInsightData(props);
|
||||
|
||||
return (
|
||||
<section className="nodedc-home-subpanel nodedc-home-operations-card space-y-4 p-5">
|
||||
|
|
@ -437,6 +434,196 @@ export function HomeRhythmRecentOverview(props: HomeProjectInsightsProps) {
|
|||
);
|
||||
}
|
||||
|
||||
function HomeActivityMiniCard(props: HomeProjectInsightsProps) {
|
||||
const { activitySeries, chart, chartId, project, recentTouchpoints } = useHomeProjectInsightData(props);
|
||||
|
||||
return (
|
||||
<section className="nodedc-home-subpanel nodedc-home-activity-mini p-5">
|
||||
<div className="mb-4 flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[11px] font-semibold tracking-[0.2em] text-placeholder uppercase">
|
||||
{project?.identifier ?? "Workspace"}
|
||||
</div>
|
||||
<div className="text-15 mt-2 font-semibold text-primary">Активность</div>
|
||||
<div className="mt-1 text-12 text-secondary">Касания за последние 7 дней.</div>
|
||||
</div>
|
||||
<div className="nodedc-home-focus-chip">{recentTouchpoints}</div>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-home-activity-mini-chart">
|
||||
<svg viewBox={`0 0 ${chart.width} ${chart.height}`} className="h-full w-full" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<linearGradient id={`${chartId}-mini-fill`} x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stopColor="rgba(var(--nodedc-accent-rgb),0.3)" />
|
||||
<stop offset="100%" stopColor="rgba(var(--nodedc-accent-rgb),0.02)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{[0.25, 0.5, 0.75].map((position) => {
|
||||
const y = chart.height - chart.paddingY - position * (chart.height - chart.paddingY * 2);
|
||||
|
||||
return <line key={position} x1={12} x2={chart.width - 12} y1={y} y2={y} stroke="rgba(255,255,255,0.07)" />;
|
||||
})}
|
||||
<path d={chart.areaPath} fill={`url(#${chartId}-mini-fill)`} />
|
||||
<path
|
||||
d={chart.linePath}
|
||||
fill="none"
|
||||
stroke="rgb(var(--nodedc-accent-rgb))"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="5"
|
||||
/>
|
||||
{activitySeries.map((activityPoint, index) => {
|
||||
const point = chart.points[index];
|
||||
if (!point) return null;
|
||||
|
||||
return (
|
||||
<circle
|
||||
key={activityPoint.key}
|
||||
cx={point.x}
|
||||
cy={point.y}
|
||||
fill="rgb(var(--nodedc-accent-rgb))"
|
||||
r={activityPoint.value > 0 ? 4 : 2.2}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export const HomeAnalyticsRail = observer(function HomeAnalyticsRail(props: HomeProjectInsightsProps) {
|
||||
const { project } = props;
|
||||
const { completionRate, completedIssues, openIssues, recentTouchpoints, totalIssues } =
|
||||
useHomeProjectInsightData(props);
|
||||
const { updateIsEpic, updateIsPeekView, updateSelectedCycle, updateSelectedModule, updateSelectedProjects } =
|
||||
useAnalytics();
|
||||
|
||||
useEffect(() => {
|
||||
updateIsPeekView(true);
|
||||
updateIsEpic(false);
|
||||
updateSelectedCycle("");
|
||||
updateSelectedModule("");
|
||||
updateSelectedProjects(project?.id ? [project.id] : []);
|
||||
|
||||
return () => {
|
||||
updateSelectedProjects([]);
|
||||
updateSelectedCycle("");
|
||||
updateSelectedModule("");
|
||||
updateIsPeekView(false);
|
||||
updateIsEpic(false);
|
||||
};
|
||||
}, [project?.id, updateIsEpic, updateIsPeekView, updateSelectedCycle, updateSelectedModule, updateSelectedProjects]);
|
||||
|
||||
return (
|
||||
<aside className="nodedc-home-analytics-rail" aria-label="Аналитика проекта">
|
||||
<section className="nodedc-home-subpanel nodedc-home-analytics-intro p-5">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[11px] font-semibold tracking-[0.22em] text-placeholder uppercase">
|
||||
{project?.identifier ?? "Workspace"}
|
||||
</div>
|
||||
<div className="mt-2 text-16 font-semibold text-primary">Аналитика проекта</div>
|
||||
</div>
|
||||
<div className="nodedc-home-focus-chip">{completionRate}%</div>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-home-analytics-stat-grid mt-4">
|
||||
<div className="nodedc-home-analytics-stat">
|
||||
<span>Всего</span>
|
||||
<strong>{totalIssues}</strong>
|
||||
</div>
|
||||
<div className="nodedc-home-analytics-stat">
|
||||
<span>Открыто</span>
|
||||
<strong>{openIssues}</strong>
|
||||
</div>
|
||||
<div className="nodedc-home-analytics-stat">
|
||||
<span>Закрыто</span>
|
||||
<strong>{completedIssues}</strong>
|
||||
</div>
|
||||
<div className="nodedc-home-analytics-stat">
|
||||
<span>Касания</span>
|
||||
<strong>{recentTouchpoints}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<HomeActivityMiniCard {...props} />
|
||||
<HomeOperationsCard {...props} />
|
||||
<CreatedVsResolved />
|
||||
</aside>
|
||||
);
|
||||
});
|
||||
|
||||
export const HomeIndividualAnalyticsPanel = observer(function HomeIndividualAnalyticsPanel(
|
||||
props: Pick<HomeProjectInsightsProps, "project" | "locale">
|
||||
) {
|
||||
const { updateIsEpic, updateIsPeekView, updateSelectedCycle, updateSelectedModule, updateSelectedProjects } =
|
||||
useAnalytics();
|
||||
|
||||
useEffect(() => {
|
||||
updateIsPeekView(true);
|
||||
updateIsEpic(false);
|
||||
updateSelectedCycle("");
|
||||
updateSelectedModule("");
|
||||
updateSelectedProjects(props.project?.id ? [props.project.id] : []);
|
||||
|
||||
return () => {
|
||||
updateSelectedProjects([]);
|
||||
updateSelectedCycle("");
|
||||
updateSelectedModule("");
|
||||
updateIsPeekView(false);
|
||||
updateIsEpic(false);
|
||||
};
|
||||
}, [
|
||||
props.project?.id,
|
||||
updateIsEpic,
|
||||
updateIsPeekView,
|
||||
updateSelectedCycle,
|
||||
updateSelectedModule,
|
||||
updateSelectedProjects,
|
||||
]);
|
||||
|
||||
return (
|
||||
<section className="nodedc-home-individual-analytics" aria-label="Индивидуальные аналитические данные">
|
||||
<CustomizedInsights peekView />
|
||||
</section>
|
||||
);
|
||||
});
|
||||
|
||||
export const HomeAnalyticsBottomRow = observer(function HomeAnalyticsBottomRow({
|
||||
recentActivitySlot,
|
||||
}: {
|
||||
recentActivitySlot?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="nodedc-home-analytics-bottom-row" aria-label="Назначения и последние действия">
|
||||
<div className="nodedc-home-assignee-analytics">
|
||||
<WorkItemsInsightTable />
|
||||
</div>
|
||||
<div className="nodedc-home-subpanel nodedc-home-analytics-recents p-5">
|
||||
{recentActivitySlot ? (
|
||||
<div className="h-full min-h-[22rem]">{recentActivitySlot}</div>
|
||||
) : (
|
||||
<div className="flex h-full min-h-[22rem] flex-col justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="grid size-11 place-items-center rounded-full bg-black text-[rgb(var(--nodedc-card-active-rgb))]">
|
||||
<Layers3 className="size-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-15 font-semibold text-primary">Последние действия</div>
|
||||
<div className="text-12 text-secondary">Виджет recent activity отключен в настройках.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
});
|
||||
|
||||
export function HomeOperationsOverview(props: HomeProjectInsightsProps) {
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
|
|
|
|||
|
|
@ -4,21 +4,21 @@
|
|||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChartNoAxesColumn, SlidersHorizontal } from "lucide-react";
|
||||
import { SlidersHorizontal } from "lucide-react";
|
||||
// plane imports
|
||||
import { EIssueFilterType, ISSUE_STORE_TO_FILTERS_MAP } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
||||
import { EIssueLayoutTypes, EIssuesStoreType } from "@plane/types";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useUserProfile } from "@/hooks/store/user";
|
||||
// plane web imports
|
||||
import type { TProject } from "@/plane-web/types";
|
||||
// local imports
|
||||
import { WorkItemsModal } from "../analytics/work-items/modal";
|
||||
import { WorkItemFiltersToggle } from "../work-item-filters/filters-toggle";
|
||||
import {
|
||||
DisplayFiltersSelection,
|
||||
|
|
@ -47,20 +47,38 @@ export const HeaderFilters = observer(function HeaderFilters(props: Props) {
|
|||
currentProjectDetails,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
canUserCreateIssue,
|
||||
storeType = EIssuesStoreType.PROJECT,
|
||||
} = props;
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// states
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
const [expandedToolbarTarget, setExpandedToolbarTarget] = useState<HTMLElement | null>(null);
|
||||
// store hooks
|
||||
const { data: userProfile } = useUserProfile();
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(storeType);
|
||||
// derived values
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
const layoutDisplayFiltersOptions = ISSUE_STORE_TO_FILTERS_MAP[storeType]?.layoutOptions[activeLayout];
|
||||
const isCompactToolbar = userProfile?.theme?.nodedcCompactToolbar === true;
|
||||
|
||||
useEffect(() => {
|
||||
if (isCompactToolbar || typeof document === "undefined") {
|
||||
setExpandedToolbarTarget(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let animationFrame = 0;
|
||||
|
||||
const resolveTarget = () => {
|
||||
setExpandedToolbarTarget(document.querySelector<HTMLElement>("[data-nodedc-expanded-header-filters-slot]"));
|
||||
};
|
||||
|
||||
animationFrame = window.requestAnimationFrame(resolveTarget);
|
||||
|
||||
return () => window.cancelAnimationFrame(animationFrame);
|
||||
}, [isCompactToolbar]);
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
|
|
@ -86,29 +104,59 @@ export const HeaderFilters = observer(function HeaderFilters(props: Props) {
|
|||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
return (
|
||||
const layoutSelection = (
|
||||
<>
|
||||
<WorkItemsModal
|
||||
isOpen={analyticsModal}
|
||||
onClose={() => setAnalyticsModal(false)}
|
||||
projectDetails={currentProjectDetails ?? undefined}
|
||||
isEpic={storeType === EIssuesStoreType.EPIC}
|
||||
/>
|
||||
<div className="pointer-events-none absolute top-1/2 left-1/2 z-[1] flex -translate-x-1/2 -translate-y-1/2 items-center">
|
||||
<div className="pointer-events-auto hidden @4xl:flex">
|
||||
<LayoutSelection
|
||||
layouts={LAYOUTS}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<LayoutSelection layouts={LAYOUTS} onChange={(layout) => handleLayoutChange(layout)} selectedLayout={activeLayout} />
|
||||
</div>
|
||||
<div className="pointer-events-auto flex @4xl:hidden">
|
||||
<MobileLayoutSelection
|
||||
layouts={LAYOUTS}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
activeLayout={activeLayout}
|
||||
/>
|
||||
<MobileLayoutSelection layouts={LAYOUTS} onChange={(layout) => handleLayoutChange(layout)} activeLayout={activeLayout} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const headerTools = (
|
||||
<>
|
||||
<WorkItemFiltersToggle entityType={storeType} entityId={projectId} />
|
||||
<FiltersDropdown
|
||||
menuButton={<SlidersHorizontal className="size-4" />}
|
||||
menuButtonWrapperClassName="nodedc-expanded-tool-button"
|
||||
miniIcon={<SlidersHorizontal className="size-3.5" />}
|
||||
title={t("common.display")}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={layoutDisplayFiltersOptions}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
isEpic={storeType === EIssuesStoreType.EPIC}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</>
|
||||
);
|
||||
|
||||
const expandedToolbarControls =
|
||||
!isCompactToolbar && expandedToolbarTarget
|
||||
? createPortal(
|
||||
<div className="nodedc-expanded-header-filters">
|
||||
{layoutSelection}
|
||||
{headerTools}
|
||||
</div>,
|
||||
expandedToolbarTarget
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{expandedToolbarControls}
|
||||
{!isCompactToolbar && expandedToolbarTarget ? null : (
|
||||
<>
|
||||
<div className="pointer-events-none absolute top-1/2 left-1/2 z-[1] flex -translate-x-1/2 -translate-y-1/2 items-center">
|
||||
{layoutSelection}
|
||||
</div>
|
||||
<div className="nodedc-top-toolbar-cluster flex items-center gap-2">
|
||||
<WorkItemFiltersToggle entityType={storeType} entityId={projectId} />
|
||||
|
|
@ -128,22 +176,9 @@ export const HeaderFilters = observer(function HeaderFilters(props: Props) {
|
|||
isEpic={storeType === EIssuesStoreType.EPIC}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
{canUserCreateIssue ? (
|
||||
<Button
|
||||
className="nodedc-toolbar-pill nodedc-toolbar-pill-wide hidden md:inline-flex"
|
||||
onClick={() => setAnalyticsModal(true)}
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
>
|
||||
<div className="hidden @4xl:flex">{t("common.analytics")}</div>
|
||||
<div className="flex @4xl:hidden">
|
||||
<ChartNoAxesColumn className="size-3.5" />
|
||||
</div>
|
||||
</Button>
|
||||
) : (
|
||||
<></>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -296,10 +296,10 @@ export const BaseKanBanRoot = observer(function BaseKanBanRoot(props: IBaseKanBa
|
|||
</div>
|
||||
<IssueLayoutHOC layout={EIssueLayoutTypes.KANBAN}>
|
||||
<div
|
||||
className={`horizontal-scrollbar relative flex scrollbar-lg h-full w-full bg-surface-2 ${sub_group_by ? "vertical-scrollbar overflow-y-auto" : "overflow-x-auto overflow-y-hidden"}`}
|
||||
className={`nodedc-kanban-scroll-container horizontal-scrollbar relative flex scrollbar-lg h-full w-full bg-transparent ${sub_group_by ? "vertical-scrollbar overflow-y-auto" : "overflow-x-auto overflow-y-hidden"}`}
|
||||
ref={scrollableContainerRef}
|
||||
>
|
||||
<div className="relative h-full w-max min-w-full bg-surface-2">
|
||||
<div className="relative h-full w-max min-w-full bg-transparent">
|
||||
<div className="h-full w-max">
|
||||
<KanBanView
|
||||
issuesMap={issueMap}
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ export const KanBan = observer(function KanBan(props: IKanBan) {
|
|||
} `}
|
||||
>
|
||||
{sub_group_by === null && (
|
||||
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-surface-2 py-1">
|
||||
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-transparent py-1">
|
||||
<HeaderGroupByCard
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@ export const KanbanGroup = observer(function KanbanGroup(props: IKanbanGroup) {
|
|||
</div>
|
||||
|
||||
{shouldShowQuickAdd && (
|
||||
<div className="nodedc-bottom-dock-sticky-offset sticky z-[2] w-full bg-surface-2 py-0.5">
|
||||
<div className="nodedc-bottom-dock-sticky-offset sticky z-[2] w-full bg-transparent py-0.5">
|
||||
<QuickAddIssueRoot
|
||||
layout={EIssueLayoutTypes.KANBAN}
|
||||
QuickAddButton={KanbanQuickAddIssueButton}
|
||||
|
|
|
|||
|
|
@ -323,7 +323,7 @@ export const KanBanSwimLanes = observer(function KanBanSwimLanes(props: IKanBanS
|
|||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Row className="sticky top-0 z-[4] h-[50px] bg-surface-2">
|
||||
<Row className="sticky top-0 z-[4] h-[50px] bg-transparent">
|
||||
<SubGroupSwimlaneHeader
|
||||
getGroupIssueCount={getGroupIssueCount}
|
||||
group_by={group_by}
|
||||
|
|
|
|||
|
|
@ -24,12 +24,14 @@ import { useAppRouter } from "@/hooks/use-app-router";
|
|||
import { useExpandableSearch } from "@/hooks/use-expandable-search";
|
||||
|
||||
type TTopNavPowerKProps = {
|
||||
variant?: "top-navigation" | "sidebar";
|
||||
variant?: "top-navigation" | "sidebar" | "expanded-toolbar";
|
||||
};
|
||||
|
||||
export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
||||
const { variant = "top-navigation" } = props;
|
||||
const { t } = useTranslation();
|
||||
const isWideSearch = variant === "top-navigation" || variant === "expanded-toolbar";
|
||||
const isExpandedToolbar = variant === "expanded-toolbar";
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const params = useParams();
|
||||
|
|
@ -287,7 +289,59 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
|||
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
{variant === "top-navigation" ? (
|
||||
{isWideSearch ? (
|
||||
isExpandedToolbar ? (
|
||||
<div className="nodedc-expanded-search-control" data-open={isOpen}>
|
||||
<div
|
||||
className="nodedc-expanded-search-line-panel"
|
||||
onClick={() => {
|
||||
openPanel();
|
||||
requestAnimationFrame(() => inputRef.current?.focus());
|
||||
}}
|
||||
role="button"
|
||||
>
|
||||
<div className="nodedc-expanded-search-input-wrap">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
if (!isOpen) openPanel();
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onFocus={handleFocus}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder=""
|
||||
tabIndex={isOpen ? 0 : -1}
|
||||
className="nodedc-expanded-search-input placeholder-text-placeholder min-w-0 flex-1 bg-transparent outline-none"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button type="button" onClick={handleClear} className="nodedc-expanded-search-clear">
|
||||
<CloseIcon className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="nodedc-expanded-tool-button nodedc-expanded-search-trigger"
|
||||
data-active={isOpen}
|
||||
aria-label="Поиск"
|
||||
aria-pressed={isOpen}
|
||||
onClick={() => {
|
||||
if (isOpen) {
|
||||
closePanel();
|
||||
return;
|
||||
}
|
||||
openPanel();
|
||||
requestAnimationFrame(() => inputRef.current?.focus());
|
||||
}}
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={cn("relative z-30 flex w-[364px] items-center transition-all duration-300 ease-in-out", {
|
||||
"w-[554px]": isOpen,
|
||||
|
|
@ -303,7 +357,9 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
|||
onClick={() => inputRef.current?.focus()}
|
||||
role="button"
|
||||
>
|
||||
<SearchIcon className="mr-2 size-3.5 shrink-0 text-placeholder" />
|
||||
<span className="mr-2">
|
||||
<SearchIcon className="size-3.5 shrink-0 text-placeholder" />
|
||||
</span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
|
|
@ -325,6 +381,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="relative z-30 h-8 w-8">
|
||||
<button
|
||||
|
|
@ -347,15 +404,28 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
{variant === "top-navigation" && (
|
||||
{isWideSearch && isExpandedToolbar && (
|
||||
<div
|
||||
className={cn(
|
||||
"nodedc-expanded-search-results nodedc-glass-modal nodedc-glass-popup-surface absolute z-20 flex flex-col overflow-hidden px-0 pt-3 transition-all duration-300 ease-in-out",
|
||||
{
|
||||
"max-h-[80vh] opacity-100": isOpen,
|
||||
"h-0 w-0 opacity-0": !isOpen,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{isOpen && searchCommandContent}
|
||||
</div>
|
||||
)}
|
||||
{isWideSearch && !isExpandedToolbar && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute z-20 flex flex-col overflow-hidden px-0 transition-all duration-300 ease-in-out",
|
||||
{
|
||||
"max-h-[80vh] w-[574px] opacity-100": isOpen,
|
||||
"max-h-[80vh] opacity-100": isOpen,
|
||||
"w-[574px]": isOpen,
|
||||
"h-0 w-0 opacity-0": !isOpen,
|
||||
"-top-[6px] left-1/2 -translate-x-1/2 rounded-md border border-subtle bg-surface-1 shadow-lg pt-10":
|
||||
true,
|
||||
"-top-[6px] left-1/2 -translate-x-1/2 rounded-md border border-subtle bg-surface-1 shadow-lg pt-10": true,
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { observer } from "mobx-react";
|
|||
import { ThemeSwitcher } from "@/plane-web/components/preferences/theme-switcher";
|
||||
// local imports
|
||||
import { ProfileSettingsAccentColor } from "./accent-color";
|
||||
import { ProfileSettingsToolbarLayout } from "./toolbar-layout";
|
||||
|
||||
export const ProfileSettingsDefaultPreferencesList = observer(function ProfileSettingsDefaultPreferencesList() {
|
||||
return (
|
||||
|
|
@ -21,6 +22,7 @@ export const ProfileSettingsDefaultPreferencesList = observer(function ProfileSe
|
|||
}}
|
||||
/>
|
||||
<ProfileSettingsAccentColor />
|
||||
<ProfileSettingsToolbarLayout />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { SettingsControlItem } from "@/components/settings/control-item";
|
||||
// hooks
|
||||
import { useUserProfile } from "@/hooks/store/user";
|
||||
|
||||
export const ProfileSettingsToolbarLayout = observer(function ProfileSettingsToolbarLayout() {
|
||||
const { data: userProfile, updateUserTheme } = useUserProfile();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const isCompactToolbar = userProfile?.theme?.nodedcCompactToolbar === true;
|
||||
|
||||
const handleToggle = async () => {
|
||||
const nextValue = !isCompactToolbar;
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
await updateUserTheme({ nodedcCompactToolbar: nextValue });
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Сохранено",
|
||||
message: nextValue ? "Компактная панель инструментов включена." : "Расширенная панель инструментов включена.",
|
||||
});
|
||||
} catch (_error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Ошибка",
|
||||
message: "Не удалось обновить режим панели инструментов.",
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsControlItem
|
||||
title="Панель инструментов"
|
||||
description="Локальная настройка пользователя. Компактный режим оставляет текущую короткую верхнюю панель, расширенный режим показывает основные разделы текстовыми кнопками."
|
||||
control={
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-4 rounded-[1.25rem] bg-white/5 px-4 py-3 text-left transition sm:w-[28rem]",
|
||||
"hover:bg-white/8 focus-visible:bg-white/8",
|
||||
isSaving && "cursor-wait opacity-70"
|
||||
)}
|
||||
onClick={handleToggle}
|
||||
disabled={isSaving}
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-3">
|
||||
<span
|
||||
className={cn(
|
||||
"grid size-4 flex-shrink-0 place-items-center rounded-full transition",
|
||||
isCompactToolbar ? "bg-[rgb(var(--nodedc-accent-rgb))]" : "bg-white/10"
|
||||
)}
|
||||
>
|
||||
{isCompactToolbar && <span className="size-1.5 rounded-full bg-[rgb(var(--nodedc-on-accent-rgb))]" />}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-13 font-semibold text-primary">Компактный режим</span>
|
||||
<span className="mt-0.5 block text-12 leading-5 text-tertiary">
|
||||
{isCompactToolbar
|
||||
? "Все основные действия собраны в короткие иконки."
|
||||
: "Основные разделы вынесены в расширенную верхнюю навигацию."}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span className="hidden flex-shrink-0 rounded-full bg-white/6 px-3 py-1 text-11 font-semibold text-secondary sm:block">
|
||||
{isCompactToolbar ? "Компактно" : "Расширенно"}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
@ -866,7 +866,10 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
if (typeof document === "undefined") return;
|
||||
|
||||
const updateDockSlot = () => {
|
||||
setDockSlot(document.querySelector("[data-nodedc-voice-task-dock-slot]"));
|
||||
setDockSlot(
|
||||
document.querySelector("[data-nodedc-voice-task-toolbar-slot]") ??
|
||||
document.querySelector("[data-nodedc-voice-task-dock-slot]")
|
||||
);
|
||||
};
|
||||
|
||||
updateDockSlot();
|
||||
|
|
|
|||
|
|
@ -24,13 +24,13 @@ export const WorkspaceLogo = observer(function WorkspaceLogo(props: Props) {
|
|||
className={cn(
|
||||
`relative grid h-6 w-6 flex-shrink-0 place-items-center uppercase ${
|
||||
!props.logo && "rounded-md bg-accent-primary text-on-color"
|
||||
} ${props.classNames ? props.classNames : ""}`
|
||||
} ${props.logo && "rounded-md"} ${props.classNames ? props.classNames : ""}`
|
||||
)}
|
||||
>
|
||||
{props.logo && props.logo !== "" ? (
|
||||
<img
|
||||
src={getFileURL(props.logo)}
|
||||
className="absolute top-0 left-0 h-full w-full rounded-md object-cover"
|
||||
className="absolute top-0 left-0 h-full w-full rounded-[inherit] object-cover"
|
||||
alt={t("aria_labels.projects_sidebar.workspace_logo")}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@
|
|||
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Settings, UserPlus } from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import type { MouseEvent } from "react";
|
||||
import { Archive, BarChart3, Layers3, Settings, UserPlus } from "lucide-react";
|
||||
import { Menu } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { EUserPermissions } from "@plane/constants";
|
||||
|
|
@ -31,8 +32,18 @@ const SidebarDropdownItem = observer(function SidebarDropdownItem(props: TProps)
|
|||
const { workspace, activeWorkspace, handleItemClick, handleWorkspaceNavigation, handleClose } = props;
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
const router = useRouter();
|
||||
// hooks
|
||||
const { t } = useTranslation();
|
||||
const canOpenWorkspaceSettings = [EUserPermissions.ADMIN, EUserPermissions.MEMBER].includes(workspace?.role);
|
||||
const canInviteMembers = [EUserPermissions.ADMIN].includes(workspace?.role);
|
||||
|
||||
const handleWorkspaceAction = (e: MouseEvent<HTMLButtonElement>, action: () => void) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
action();
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
|
@ -92,36 +103,56 @@ const SidebarDropdownItem = observer(function SidebarDropdownItem(props: TProps)
|
|||
</div>
|
||||
{workspace.id === activeWorkspace?.id && (
|
||||
<>
|
||||
<div className="mt-2 mb-1 grid grid-cols-2 gap-3">
|
||||
{[EUserPermissions.ADMIN, EUserPermissions.MEMBER].includes(workspace?.role) && (
|
||||
<div className="mt-2 mb-1 flex flex-col gap-1.5">
|
||||
{canOpenWorkspaceSettings && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openWorkspaceSettingsModal("general");
|
||||
handleClose();
|
||||
}}
|
||||
className="flex min-w-0 flex-1 items-center justify-center gap-1.5 rounded-[1.25rem] border-0 bg-white/[0.05] px-5 py-2.5 text-secondary shadow-none outline-none transition-colors hover:bg-white/[0.09] hover:text-primary"
|
||||
onClick={(e) => handleWorkspaceAction(e, () => openWorkspaceSettingsModal("general"))}
|
||||
className="flex min-w-0 flex-1 items-center gap-2.5 rounded-[1.1rem] border-0 bg-white/[0.05] px-3.5 py-2.5 text-secondary shadow-none outline-none transition-colors hover:bg-white/[0.09] hover:text-primary"
|
||||
>
|
||||
<Settings className="my-auto h-4 w-4 flex-shrink-0" />
|
||||
<span className="my-auto text-13 font-medium whitespace-nowrap">{t("settings")}</span>
|
||||
</button>
|
||||
)}
|
||||
{[EUserPermissions.ADMIN].includes(workspace?.role) && (
|
||||
<Link
|
||||
href={`/${workspace.slug}/settings/members`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
}}
|
||||
className="flex min-w-0 flex-1 items-center justify-center gap-1.5 rounded-[1.25rem] border-0 bg-white/[0.05] px-5 py-2.5 text-secondary shadow-none outline-none transition-colors hover:bg-white/[0.09] hover:text-primary"
|
||||
{canInviteMembers && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleWorkspaceAction(e, () => openWorkspaceSettingsModal("members"))}
|
||||
className="flex min-w-0 flex-1 items-center gap-2.5 rounded-[1.1rem] border-0 bg-white/[0.05] px-3.5 py-2.5 text-secondary shadow-none outline-none transition-colors hover:bg-white/[0.09] hover:text-primary"
|
||||
>
|
||||
<UserPlus className="my-auto h-4 w-4 flex-shrink-0" />
|
||||
<span className="my-auto text-13 font-medium whitespace-nowrap">
|
||||
{t("project_settings.members.invite_members.title")}
|
||||
</span>
|
||||
</Link>
|
||||
</button>
|
||||
)}
|
||||
{canOpenWorkspaceSettings && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleWorkspaceAction(e, () => router.push(`/${workspace.slug}/analytics/`))}
|
||||
className="flex min-w-0 flex-1 items-center gap-2.5 rounded-[1.1rem] border-0 bg-white/[0.05] px-3.5 py-2.5 text-secondary shadow-none outline-none transition-colors hover:bg-white/[0.09] hover:text-primary"
|
||||
>
|
||||
<BarChart3 className="my-auto h-4 w-4 flex-shrink-0" />
|
||||
<span className="my-auto text-13 font-medium whitespace-nowrap">Analytics</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleWorkspaceAction(e, () => router.push(`/${workspace.slug}/workspace-views/all-issues/`))}
|
||||
className="flex min-w-0 flex-1 items-center gap-2.5 rounded-[1.1rem] border-0 bg-white/[0.05] px-3.5 py-2.5 text-secondary shadow-none outline-none transition-colors hover:bg-white/[0.09] hover:text-primary"
|
||||
>
|
||||
<Layers3 className="my-auto h-4 w-4 flex-shrink-0" />
|
||||
<span className="my-auto text-13 font-medium whitespace-nowrap">Представления</span>
|
||||
</button>
|
||||
{canOpenWorkspaceSettings && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleWorkspaceAction(e, () => router.push(`/${workspace.slug}/projects/archives`))}
|
||||
className="flex min-w-0 flex-1 items-center gap-2.5 rounded-[1.1rem] border-0 bg-white/[0.05] px-3.5 py-2.5 text-secondary shadow-none outline-none transition-colors hover:bg-white/[0.09] hover:text-primary"
|
||||
>
|
||||
<Archive className="my-auto h-4 w-4 flex-shrink-0" />
|
||||
<span className="my-auto text-13 font-medium whitespace-nowrap">{t("archives")}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
|||
import { useUser } from "@/hooks/store/user";
|
||||
|
||||
type TUserMenuRootProps = {
|
||||
variant?: "default" | "sidebar-utility" | "toolbar";
|
||||
variant?: "default" | "sidebar-utility" | "toolbar" | "expanded-toolbar";
|
||||
};
|
||||
|
||||
export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootProps) {
|
||||
|
|
@ -43,6 +43,7 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootP
|
|||
|
||||
const isSidebarUtilityVariant = variant === "sidebar-utility";
|
||||
const isToolbarVariant = variant === "toolbar";
|
||||
const isExpandedToolbarVariant = variant === "expanded-toolbar";
|
||||
|
||||
const handleSignOut = () => {
|
||||
signOut().catch(() =>
|
||||
|
|
@ -137,16 +138,18 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: TUserMenuRootP
|
|||
className="flex items-center"
|
||||
buttonAsChild
|
||||
button={
|
||||
isToolbarVariant ? (
|
||||
isToolbarVariant || isExpandedToolbarVariant ? (
|
||||
<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]"
|
||||
className={`flex items-center justify-center overflow-hidden rounded-full border-0 bg-white/[0.04] backdrop-blur-[18px] transition-all hover:bg-white/[0.07] ${
|
||||
isExpandedToolbarVariant ? "nodedc-expanded-user-avatar-button size-12" : "size-8"
|
||||
}`}
|
||||
>
|
||||
<Avatar
|
||||
name={currentUser?.display_name}
|
||||
src={getFileURL(currentUser?.avatar_url ?? "")}
|
||||
size={18}
|
||||
size={isExpandedToolbarVariant ? 48 : 18}
|
||||
shape="circle"
|
||||
/>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import { WorkspaceLogo } from "../logo";
|
|||
import SidebarDropdownItem from "./dropdown-item";
|
||||
|
||||
type WorkspaceMenuRootProps = {
|
||||
variant: "sidebar" | "top-navigation" | "sidebar-panel" | "toolbar";
|
||||
variant: "sidebar" | "top-navigation" | "sidebar-panel" | "toolbar" | "expanded-toolbar";
|
||||
};
|
||||
|
||||
type WorkspaceMenuStateSyncProps = {
|
||||
|
|
@ -46,7 +46,12 @@ function WorkspaceMenuStateSync(props: WorkspaceMenuStateSyncProps) {
|
|||
const { open, variant, sidebarPanelButtonRef, onSidebarDropdownToggle, onSidebarPanelPositionChange } = props;
|
||||
|
||||
const updateSidebarPanelMenuPosition = useCallback(() => {
|
||||
if (!["sidebar-panel", "toolbar"].includes(variant) || !sidebarPanelButtonRef.current || typeof window === "undefined") return;
|
||||
if (
|
||||
!["sidebar-panel", "toolbar", "expanded-toolbar"].includes(variant) ||
|
||||
!sidebarPanelButtonRef.current ||
|
||||
typeof window === "undefined"
|
||||
)
|
||||
return;
|
||||
|
||||
const rect = sidebarPanelButtonRef.current.getBoundingClientRect();
|
||||
const width = 480;
|
||||
|
|
@ -64,7 +69,7 @@ function WorkspaceMenuStateSync(props: WorkspaceMenuStateSyncProps) {
|
|||
}, [onSidebarDropdownToggle, open]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!open || !["sidebar-panel", "toolbar"].includes(variant)) {
|
||||
if (!open || !["sidebar-panel", "toolbar", "expanded-toolbar"].includes(variant)) {
|
||||
onSidebarPanelPositionChange(null);
|
||||
return;
|
||||
}
|
||||
|
|
@ -133,7 +138,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",
|
||||
"w-fit max-w-none justify-center text-center": ["toolbar", "expanded-toolbar"].includes(variant),
|
||||
})}
|
||||
>
|
||||
{({ open, close }: { open: boolean; close: () => void }) => {
|
||||
|
|
@ -221,11 +226,12 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
|
|||
/>
|
||||
</Menu.Button>
|
||||
)}
|
||||
{variant === "toolbar" && (
|
||||
{["toolbar", "expanded-toolbar"].includes(variant) && (
|
||||
<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",
|
||||
"flex items-center justify-center rounded-full bg-white/[0.04] backdrop-blur-[18px] transition-all hover:bg-white/[0.07] focus:outline-none",
|
||||
variant === "expanded-toolbar" ? "size-12" : "size-8",
|
||||
{
|
||||
"bg-white/[0.08]": open,
|
||||
}
|
||||
|
|
@ -235,7 +241,7 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
|
|||
<WorkspaceLogo
|
||||
logo={activeWorkspace?.logo_url}
|
||||
name={activeWorkspace?.name}
|
||||
classNames="size-8 rounded-[0.9rem]"
|
||||
classNames={variant === "expanded-toolbar" ? "size-12 rounded-full" : "size-8 rounded-[0.9rem]"}
|
||||
/>
|
||||
</Menu.Button>
|
||||
)}
|
||||
|
|
@ -247,15 +253,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":
|
||||
!["sidebar-panel", "toolbar"].includes(variant),
|
||||
!["sidebar-panel", "toolbar", "expanded-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":
|
||||
["sidebar-panel", "toolbar"].includes(variant),
|
||||
["sidebar-panel", "toolbar", "expanded-toolbar"].includes(variant),
|
||||
}
|
||||
)}
|
||||
style={
|
||||
["sidebar-panel", "toolbar"].includes(variant) && sidebarPanelMenuPosition
|
||||
["sidebar-panel", "toolbar", "expanded-toolbar"].includes(variant) && sidebarPanelMenuPosition
|
||||
? {
|
||||
position: "fixed",
|
||||
left: `${sidebarPanelMenuPosition.left}px`,
|
||||
|
|
@ -270,8 +276,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": !["sidebar-panel", "toolbar"].includes(variant),
|
||||
"bg-transparent": ["sidebar-panel", "toolbar"].includes(variant),
|
||||
"rounded-md bg-surface-1": !["sidebar-panel", "toolbar", "expanded-toolbar"].includes(variant),
|
||||
"bg-transparent": ["sidebar-panel", "toolbar", "expanded-toolbar"].includes(variant),
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
|
@ -343,7 +349,7 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
|
|||
</Menu.Items>
|
||||
);
|
||||
|
||||
if (["sidebar-panel", "toolbar"].includes(variant)) {
|
||||
if (["sidebar-panel", "toolbar", "expanded-toolbar"].includes(variant)) {
|
||||
if (!open || !sidebarPanelMenuPosition || typeof document === "undefined") return null;
|
||||
return createPortal(menuItems, document.body);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export class ProfileStore implements IUserProfileStore {
|
|||
background: undefined,
|
||||
darkPalette: false,
|
||||
nodedcAccent: undefined,
|
||||
nodedcCompactToolbar: undefined,
|
||||
},
|
||||
onboarding_step: {
|
||||
workspace_join: false,
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
<svg id="nodedc-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220.82 54.55"><defs><style>.cls-1{fill:#e2e1e1;}.cls-2{fill:#dbdbdb;stroke:#dbdbdb;stroke-miterlimit:10;stroke-width:0.75px;}</style></defs><path class="cls-1" d="M52.8,23.61,46.92,33.76,41.05,23.61H52.8m18-10.39H23.06L46.92,54.55Z"/><polygon class="cls-1" points="31.28 33.13 18.11 10.34 75.73 10.34 62.59 33.13 74.28 33.13 93.22 0 0 0 19.61 33.13 31.28 33.13"/><path class="cls-2" d="M116.35,18.49V1h1.27l10.34,15V1h1.33V18.49H128l-10.34-15v15Z"/><path class="cls-2" d="M140.43,18.64c-4.79,0-8.16-3.72-8.16-8.89S135.64.86,140.43.86s8.17,3.72,8.17,8.89S145.25,18.64,140.43,18.64Zm0-1.25c4,0,6.79-3.17,6.79-7.64s-2.77-7.64-6.79-7.64-6.77,3.17-6.77,7.64S136.44,17.39,140.43,17.39Z"/><path class="cls-2" d="M151.6,18.49V1h5.1c5.54,0,8.79,3.42,8.79,8.74s-3.25,8.74-8.79,8.74ZM153,17.24h3.75c4.77,0,7.42-2.92,7.42-7.49s-2.65-7.49-7.42-7.49H153Z"/><path class="cls-2" d="M168.49,1h10.77V2.26h-9.42V8.93h7.89v1.25h-7.89v7.06h9.74v1.25H168.49Z"/><path class="cls-2" d="M188.88,18.49V1H194c5.54,0,8.79,3.42,8.79,8.74s-3.25,8.74-8.79,8.74Zm1.35-1.25H194c4.77,0,7.41-2.92,7.41-7.49S198.75,2.26,194,2.26h-3.75Z"/><path class="cls-2" d="M205.15,9.75c0-5.24,3.19-8.89,8.11-8.89a6.8,6.8,0,0,1,7.1,5.52h-1.43a5.54,5.54,0,0,0-5.74-4.27c-4.05,0-6.64,3.17-6.64,7.64s2.54,7.64,6.59,7.64a5.46,5.46,0,0,0,5.74-4.29h1.43c-.75,3.52-3.4,5.54-7.15,5.54C208.27,18.64,205.15,15.05,205.15,9.75Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -450,6 +450,29 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nodedc-app-header-expanded {
|
||||
right: auto !important;
|
||||
bottom: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nodedc-app-header-expanded .nodedc-bottom-dock {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.nodedc-app-header-expanded .nodedc-bottom-dock > [class~="w-full"] {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.nodedc-app-header-expanded .nodedc-bottom-dock-left {
|
||||
max-width: min(31rem, 48vw) !important;
|
||||
}
|
||||
|
||||
.nodedc-app-header-expanded .nodedc-bottom-dock-voice-slot {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.nodedc-glass-modal [data-slot="button"],
|
||||
.nodedc-glass-modal [data-slot="icon-button"] {
|
||||
border: none !important;
|
||||
|
|
@ -754,6 +777,516 @@
|
|||
color: rgb(var(--nodedc-on-accent-rgb));
|
||||
}
|
||||
|
||||
.nodedc-expanded-toolbar {
|
||||
display: flex;
|
||||
min-height: 4.25rem;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.nodedc-expanded-toolbar-top {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
min-height: 3rem;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nodedc-expanded-toolbar-left,
|
||||
.nodedc-expanded-toolbar-center,
|
||||
.nodedc-expanded-toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nodedc-expanded-toolbar-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.nodedc-expanded-toolbar-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nodedc-expanded-toolbar-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.nodedc-expanded-brand-logo {
|
||||
display: block;
|
||||
width: 7.25rem;
|
||||
height: auto;
|
||||
max-height: 2.2rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.nodedc-expanded-user-group {
|
||||
display: inline-flex;
|
||||
height: 3.45rem;
|
||||
min-height: 3.45rem;
|
||||
align-items: center;
|
||||
gap: 0.22rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(64, 64, 64, 0.48);
|
||||
padding: 0.32rem;
|
||||
}
|
||||
|
||||
.nodedc-expanded-user-group .nodedc-expanded-nav-button {
|
||||
min-height: 2.78rem;
|
||||
padding-inline: 1.2rem;
|
||||
}
|
||||
|
||||
.nodedc-expanded-user-group .nodedc-expanded-nav-button:not([data-active="true"]) {
|
||||
color: rgba(255, 255, 255, 0.68);
|
||||
}
|
||||
|
||||
.nodedc-expanded-nav-group {
|
||||
display: inline-flex;
|
||||
min-height: 3.45rem;
|
||||
align-items: center;
|
||||
gap: 0.18rem;
|
||||
border: 0 !important;
|
||||
border-radius: 999px;
|
||||
background: rgba(64, 64, 64, 0.48);
|
||||
padding: 0.32rem;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-toolbar-tools-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.45rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nodedc-expanded-main-tool-cluster,
|
||||
.nodedc-expanded-action-tool-cluster {
|
||||
position: fixed;
|
||||
z-index: 80;
|
||||
display: inline-flex;
|
||||
min-height: 3rem;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
min-width: 0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.nodedc-expanded-main-tool-cluster {
|
||||
left: 50%;
|
||||
bottom: 1.1rem;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.nodedc-expanded-action-tool-cluster {
|
||||
right: 2.75rem;
|
||||
bottom: 1.1rem;
|
||||
}
|
||||
|
||||
.nodedc-expanded-main-tool-cluster:empty,
|
||||
.nodedc-expanded-action-tool-cluster:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nodedc-expanded-nav-button {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
min-height: 2.78rem;
|
||||
border: 0 !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 999px !important;
|
||||
background: transparent !important;
|
||||
color: rgba(255, 255, 255, 0.68);
|
||||
padding: 0.2rem 1.22rem;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
background-color 160ms ease,
|
||||
color 160ms ease,
|
||||
opacity 160ms ease;
|
||||
}
|
||||
|
||||
.nodedc-expanded-nav-button:hover {
|
||||
background: rgba(255, 255, 255, 0.07) !important;
|
||||
color: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.nodedc-expanded-nav-button[data-active="true"] {
|
||||
background: rgba(255, 255, 255, 0.92) !important;
|
||||
color: rgba(8, 8, 10, 0.96);
|
||||
}
|
||||
|
||||
.nodedc-expanded-nav-icon {
|
||||
display: grid;
|
||||
width: 2.1rem;
|
||||
height: 2.1rem;
|
||||
flex-shrink: 0;
|
||||
place-items: center;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.nodedc-expanded-nav-button[data-active="true"] .nodedc-expanded-nav-icon {
|
||||
background: rgb(var(--nodedc-accent-rgb));
|
||||
color: rgb(var(--nodedc-on-accent-rgb));
|
||||
}
|
||||
|
||||
.nodedc-expanded-tool-button,
|
||||
.nodedc-expanded-tool-slot .nodedc-bottom-dock-voice-button {
|
||||
display: grid !important;
|
||||
height: 3rem !important;
|
||||
width: 3rem !important;
|
||||
min-width: 3rem;
|
||||
place-items: center;
|
||||
border: 0 !important;
|
||||
border-radius: 999px !important;
|
||||
background: rgba(7, 7, 10, 0.94) !important;
|
||||
color: rgba(255, 255, 255, 0.68) !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
transition:
|
||||
background-color 180ms ease,
|
||||
color 180ms ease,
|
||||
transform 180ms ease;
|
||||
}
|
||||
|
||||
.nodedc-expanded-tool-button:hover,
|
||||
.nodedc-expanded-tool-slot .nodedc-bottom-dock-voice-button:hover {
|
||||
color: rgba(255, 255, 255, 0.92) !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-tool-button[data-active="true"] {
|
||||
background: rgba(255, 255, 255, 0.94) !important;
|
||||
color: rgba(8, 8, 10, 0.94) !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-tool-slot {
|
||||
display: grid;
|
||||
min-height: 3rem;
|
||||
min-width: 3rem;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.nodedc-expanded-header-filters-slot,
|
||||
.nodedc-expanded-primary-action-slot {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.nodedc-expanded-header-filters-slot:empty,
|
||||
.nodedc-expanded-primary-action-slot:empty,
|
||||
.nodedc-expanded-breadcrumbs-slot:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs-slot {
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
justify-content: flex-start;
|
||||
position: fixed;
|
||||
z-index: 80;
|
||||
left: 1.85rem;
|
||||
bottom: 1.1rem;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
main:has(.nodedc-expanded-toolbar) .nodedc-kanban-scroll-container {
|
||||
height: calc(100% - 4.5rem) !important;
|
||||
max-height: calc(100% - 4.5rem);
|
||||
scrollbar-color: transparent transparent;
|
||||
}
|
||||
|
||||
main:has(.nodedc-expanded-toolbar) .nodedc-kanban-scroll-container::-webkit-scrollbar,
|
||||
main:has(.nodedc-expanded-toolbar) .nodedc-kanban-scroll-container::-webkit-scrollbar-track,
|
||||
main:has(.nodedc-expanded-toolbar) .nodedc-kanban-scroll-container::-webkit-scrollbar-thumb {
|
||||
background: transparent !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs {
|
||||
display: inline-flex !important;
|
||||
flex: 0 1 auto !important;
|
||||
max-width: min(52rem, 48vw);
|
||||
height: 3rem;
|
||||
align-items: center;
|
||||
gap: 0.45rem !important;
|
||||
overflow: visible !important;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs > div {
|
||||
height: 3rem !important;
|
||||
min-width: max-content;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs [class~="rounded-sm"] {
|
||||
border-radius: 999px !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs [class~="outline-none"],
|
||||
.nodedc-expanded-breadcrumbs > div > .group {
|
||||
min-height: 3rem;
|
||||
border: 0 !important;
|
||||
border-radius: 999px !important;
|
||||
background: rgba(64, 64, 64, 0.52) !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs > div:first-child [class~="outline-none"],
|
||||
.nodedc-expanded-breadcrumbs > div:first-child > .group {
|
||||
background: rgba(7, 7, 10, 0.96) !important;
|
||||
color: rgba(255, 255, 255, 0.86) !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs > div:first-child [class*="text-"],
|
||||
.nodedc-expanded-breadcrumbs > div:first-child svg {
|
||||
color: rgba(255, 255, 255, 0.78) !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs [class~="outline-none"] > div,
|
||||
.nodedc-expanded-breadcrumbs [class~="outline-none"] [class~="bg-layer-1"],
|
||||
.nodedc-expanded-breadcrumbs [class~="outline-none"] [class~="hover:bg-layer-1"],
|
||||
.nodedc-expanded-breadcrumbs [class~="outline-none"] [class~="hover:bg-surface-2"],
|
||||
.nodedc-expanded-breadcrumbs [class~="outline-none"] [class~="rounded-r-sm"],
|
||||
.nodedc-expanded-breadcrumbs [class~="outline-none"] [class~="rounded-r-none"] {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs [class~="outline-none"] [class~="bg-surface-1"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs [class~="truncate"] {
|
||||
max-width: 18rem !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs [class~="text-13"] {
|
||||
font-size: 0.75rem !important;
|
||||
font-weight: 700 !important;
|
||||
letter-spacing: 0 !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs [class~="px-1.5"] {
|
||||
padding-left: 0.95rem !important;
|
||||
padding-right: 0.95rem !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs [class~="rounded-r-none"] {
|
||||
border-top-right-radius: 999px !important;
|
||||
border-bottom-right-radius: 999px !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs [class~="rounded-r-sm"] {
|
||||
margin-left: -0.65rem;
|
||||
border-top-left-radius: 999px !important;
|
||||
border-bottom-left-radius: 999px !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs [class~="rounded-r-sm"] span[class*="bg-surface"],
|
||||
.nodedc-expanded-breadcrumbs [class~="rounded-r-sm"] span[class*="bg-layer"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs [class~="text-tertiary"],
|
||||
.nodedc-expanded-breadcrumbs [class~="text-placeholder"] {
|
||||
color: rgba(255, 255, 255, 0.66) !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs [class~="hover:bg-surface-2"]:hover,
|
||||
.nodedc-expanded-breadcrumbs [class~="hover:bg-layer-1"]:hover {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs > div:last-child [class~="outline-none"],
|
||||
.nodedc-expanded-breadcrumbs > div:last-child > .group {
|
||||
background: rgba(255, 255, 255, 0.94) !important;
|
||||
color: rgba(8, 8, 10, 0.96) !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs > div:last-child > div [class~="text-tertiary"],
|
||||
.nodedc-expanded-breadcrumbs > div:last-child > div [class~="text-placeholder"],
|
||||
.nodedc-expanded-breadcrumbs > div:last-child > div [class~="text-primary"],
|
||||
.nodedc-expanded-breadcrumbs > div:last-child [class*="text-"] {
|
||||
color: rgba(8, 8, 10, 0.96) !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-breadcrumbs > div:last-child svg {
|
||||
color: rgba(8, 8, 10, 0.86) !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-header-filters {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.nodedc-expanded-header-filters .nodedc-toolbar-group {
|
||||
min-height: 3rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.nodedc-expanded-header-filters .nodedc-toolbar-filter-toggle {
|
||||
display: grid !important;
|
||||
height: 3rem !important;
|
||||
width: 3rem !important;
|
||||
min-width: 3rem !important;
|
||||
place-items: center;
|
||||
border: 0 !important;
|
||||
border-radius: 999px !important;
|
||||
background: rgba(7, 7, 10, 0.94) !important;
|
||||
color: rgba(255, 255, 255, 0.68) !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-header-filters .nodedc-toolbar-filter-toggle[data-active="true"] {
|
||||
background: rgba(255, 255, 255, 0.94) !important;
|
||||
color: rgba(8, 8, 10, 0.94) !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-search-control {
|
||||
position: relative;
|
||||
z-index: 30;
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
flex-shrink: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.nodedc-expanded-search-line-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 0;
|
||||
height: 3rem;
|
||||
width: min(36rem, 42vw);
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
|
||||
.nodedc-expanded-search-trigger {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.nodedc-expanded-search-line-panel::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 3.3rem;
|
||||
bottom: 0.95rem;
|
||||
left: 0;
|
||||
height: 1px;
|
||||
transform: scaleX(0);
|
||||
transform-origin: right center;
|
||||
background: rgba(255, 255, 255, 0.34);
|
||||
transition: transform 230ms ease;
|
||||
}
|
||||
|
||||
.nodedc-expanded-search-control[data-open="true"] .nodedc-expanded-search-line-panel {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.nodedc-expanded-search-control[data-open="true"] .nodedc-expanded-search-line-panel::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.nodedc-expanded-search-input-wrap {
|
||||
display: flex;
|
||||
height: 3rem;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
padding: 0 3.3rem 0 0;
|
||||
opacity: 0;
|
||||
transition: opacity 170ms ease 100ms;
|
||||
}
|
||||
|
||||
.nodedc-expanded-search-control[data-open="true"] .nodedc-expanded-search-input-wrap {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nodedc-expanded-search-input {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.08rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.nodedc-expanded-search-clear {
|
||||
display: grid;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
flex-shrink: 0;
|
||||
place-items: center;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.nodedc-expanded-search-results {
|
||||
top: calc(100% + 0.85rem);
|
||||
right: 0;
|
||||
width: min(36rem, 42vw);
|
||||
border-radius: 1.45rem;
|
||||
}
|
||||
|
||||
.nodedc-expanded-main-tool-cluster .nodedc-expanded-search-results {
|
||||
top: auto;
|
||||
bottom: calc(100% + 0.85rem);
|
||||
}
|
||||
|
||||
.nodedc-expanded-notification-button {
|
||||
height: 2.78rem;
|
||||
width: 2.78rem;
|
||||
background: transparent !important;
|
||||
color: rgba(255, 255, 255, 0.68) !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-notification-button .nodedc-toolbar-icon-active-dot {
|
||||
height: auto;
|
||||
width: auto;
|
||||
color: rgba(255, 255, 255, 0.68) !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-notification-button:hover,
|
||||
.nodedc-expanded-notification-button:hover .nodedc-toolbar-icon-active-dot {
|
||||
color: rgba(255, 255, 255, 0.94) !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-notification-button .nodedc-toolbar-notification-dot {
|
||||
top: 0.58rem !important;
|
||||
right: 0.58rem !important;
|
||||
background: rgb(var(--nodedc-accent-rgb)) !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-user-avatar-button {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.nodedc-expanded-user-avatar-button:hover {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.nodedc-toolbar-pill {
|
||||
position: relative;
|
||||
display: inline-flex !important;
|
||||
|
|
@ -1289,8 +1822,7 @@
|
|||
border: 0 !important;
|
||||
outline: none !important;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.075) 0%, rgba(255, 255, 255, 0.026) 100%),
|
||||
rgba(8, 9, 12, 0.78) !important;
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.075) 0%, rgba(255, 255, 255, 0.026) 100%), rgba(8, 9, 12, 0.78) !important;
|
||||
-webkit-backdrop-filter: blur(28px);
|
||||
backdrop-filter: blur(28px);
|
||||
}
|
||||
|
|
@ -1720,8 +2252,7 @@
|
|||
linear-gradient(180deg, rgba(255, 255, 255, 0.024) 0%, rgba(255, 255, 255, 0.008) 100%),
|
||||
rgb(var(--nodedc-card-active-rgb)) !important;
|
||||
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
|
||||
box-shadow:
|
||||
0 12px 32px rgba(0, 0, 0, 0.16) !important;
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.16) !important;
|
||||
}
|
||||
|
||||
.nodedc-external-card[data-active="true"] .text-primary {
|
||||
|
|
@ -1940,6 +2471,8 @@
|
|||
|
||||
.nodedc-home-route-surface {
|
||||
min-height: 100vh;
|
||||
padding-right: 1.25rem !important;
|
||||
padding-left: 1.25rem !important;
|
||||
background: var(--background-color-surface-1) !important;
|
||||
}
|
||||
|
||||
|
|
@ -1948,7 +2481,8 @@
|
|||
}
|
||||
|
||||
.nodedc-home-page-shell {
|
||||
max-width: min(1840px, calc(100vw - 5rem));
|
||||
max-width: none;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.nodedc-home-dashboard-shell {
|
||||
|
|
@ -1972,11 +2506,267 @@
|
|||
}
|
||||
|
||||
.nodedc-home-main-column {
|
||||
--nodedc-home-gantt-height: 35.75rem;
|
||||
--nodedc-home-individual-chart-height: 11.25rem;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-rail {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
height: auto;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
overflow: hidden;
|
||||
padding-right: 0;
|
||||
scrollbar-color: rgba(var(--nodedc-card-active-rgb), 0.58) rgba(255, 255, 255, 0.035);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-rail::-webkit-scrollbar {
|
||||
width: 0.45rem;
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-rail::-webkit-scrollbar-track {
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-rail::-webkit-scrollbar-thumb {
|
||||
border-radius: 999px;
|
||||
background: rgba(var(--nodedc-card-active-rgb), 0.58);
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-stat {
|
||||
display: flex;
|
||||
min-height: 4.2rem;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
border-radius: 1.15rem;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-stat span {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-stat strong {
|
||||
color: var(--text-color-primary);
|
||||
font-size: 1.35rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-rail .nodedc-external-section,
|
||||
.nodedc-home-analytics-table-wrap,
|
||||
.nodedc-home-individual-analytics,
|
||||
.nodedc-home-assignee-analytics {
|
||||
border: 0 !important;
|
||||
border-radius: 1.5rem !important;
|
||||
background: rgba(10, 10, 12, 0.7) !important;
|
||||
padding: 1rem !important;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.014) !important;
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
backdrop-filter: blur(24px);
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-rail .nodedc-external-section,
|
||||
.nodedc-home-assignee-analytics,
|
||||
.nodedc-home-analytics-recents {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.022) 0%, rgba(255, 255, 255, 0.006) 100%), rgba(10, 10, 12, 0.7) !important;
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-rail > .nodedc-external-section,
|
||||
.nodedc-home-analytics-rail > .nodedc-home-subpanel {
|
||||
flex: 0 0 auto;
|
||||
min-height: 15.75rem;
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-rail .nodedc-home-subpanel {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.022) 0%, rgba(255, 255, 255, 0.006) 100%), rgba(10, 10, 12, 0.7) !important;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.014) !important;
|
||||
}
|
||||
|
||||
.nodedc-home-individual-analytics {
|
||||
flex: 0 0 auto;
|
||||
background: rgba(10, 10, 12, 0.72) !important;
|
||||
}
|
||||
|
||||
.nodedc-home-individual-analytics > .nodedc-external-section,
|
||||
.nodedc-home-assignee-analytics > .nodedc-external-section {
|
||||
height: 100%;
|
||||
border: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.nodedc-home-individual-analytics .nodedc-external-section > .mb-5,
|
||||
.nodedc-home-analytics-rail .nodedc-external-section > .mb-5 {
|
||||
margin-bottom: 0.85rem !important;
|
||||
}
|
||||
|
||||
.nodedc-home-individual-analytics .nodedc-external-section h1,
|
||||
.nodedc-home-analytics-rail .nodedc-external-section h1 {
|
||||
font-size: 0.92rem !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-rail .nodedc-analytics-summary-card {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 1.25rem !important;
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-rail .nodedc-analytics-summary-item {
|
||||
min-height: 4.35rem !important;
|
||||
padding: 0.8rem !important;
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-rail .nodedc-analytics-summary-item::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nodedc-home-individual-analytics .h-\[350px\],
|
||||
.nodedc-home-analytics-rail .h-\[350px\] {
|
||||
height: 13.5rem !important;
|
||||
}
|
||||
|
||||
.nodedc-home-individual-analytics .h-\[350px\] {
|
||||
height: calc(var(--nodedc-home-individual-chart-height) + 0.75rem) !important;
|
||||
}
|
||||
|
||||
.nodedc-home-individual-analytics .nodedc-analytics-chart-inner,
|
||||
.nodedc-home-analytics-rail .nodedc-analytics-chart-inner {
|
||||
min-width: 24rem;
|
||||
height: 14rem !important;
|
||||
}
|
||||
|
||||
.nodedc-home-priority-cell .nodedc-analytics-chart-inner {
|
||||
width: 100% !important;
|
||||
min-width: 18rem;
|
||||
height: 12rem !important;
|
||||
}
|
||||
|
||||
.nodedc-home-individual-analytics .nodedc-analytics-chart-stack,
|
||||
.nodedc-home-analytics-rail .nodedc-analytics-chart-stack {
|
||||
gap: 0.9rem !important;
|
||||
}
|
||||
|
||||
.nodedc-home-individual-analytics .nodedc-analytics-table-toolbar,
|
||||
.nodedc-home-analytics-rail .nodedc-analytics-table-toolbar {
|
||||
align-items: flex-start;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.nodedc-home-individual-analytics .nodedc-analytics-table-surface,
|
||||
.nodedc-home-analytics-rail .nodedc-analytics-table-surface {
|
||||
max-height: 13rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.nodedc-home-individual-analytics .nodedc-analytics-chart-stack {
|
||||
display: grid !important;
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(19rem, 0.9fr);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.nodedc-home-individual-analytics .nodedc-analytics-chart-viewport,
|
||||
.nodedc-home-individual-analytics .nodedc-analytics-table-surface {
|
||||
height: var(--nodedc-home-individual-chart-height);
|
||||
min-height: var(--nodedc-home-individual-chart-height);
|
||||
border-radius: 1.25rem !important;
|
||||
background: rgba(0, 0, 0, 0.18) !important;
|
||||
}
|
||||
|
||||
.nodedc-home-individual-analytics .nodedc-analytics-chart-inner {
|
||||
width: 100% !important;
|
||||
min-width: 22rem;
|
||||
height: calc(var(--nodedc-home-individual-chart-height) - 0.75rem) !important;
|
||||
}
|
||||
|
||||
.nodedc-home-individual-analytics .nodedc-analytics-table-surface table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-bottom-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
align-items: stretch;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.nodedc-home-assignee-analytics,
|
||||
.nodedc-home-analytics-recents {
|
||||
min-width: 0;
|
||||
min-height: 22rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nodedc-home-assignee-analytics .nodedc-analytics-table-surface {
|
||||
max-height: 17.5rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-recents {
|
||||
max-height: 24rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nodedc-home-analytics-intro {
|
||||
min-height: 10rem;
|
||||
}
|
||||
|
||||
.nodedc-home-activity-mini {
|
||||
flex: 0 0 auto;
|
||||
min-height: 15rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nodedc-home-activity-mini-chart {
|
||||
position: relative;
|
||||
height: 10.5rem;
|
||||
overflow: hidden;
|
||||
border-radius: 1.25rem;
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
.nodedc-home-bottom-widgets {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.nodedc-home-bottom-widgets .nodedc-home-card {
|
||||
min-height: 16rem;
|
||||
}
|
||||
|
||||
.nodedc-home-bottom-widgets .nodedc-home-card > div {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 1279px) {
|
||||
.nodedc-home-individual-analytics .nodedc-analytics-chart-stack,
|
||||
.nodedc-home-analytics-bottom-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.nodedc-home-hero {
|
||||
--nodedc-home-column-gap: 0.75rem;
|
||||
--nodedc-home-title-width: 13.25rem;
|
||||
|
|
@ -2177,9 +2967,12 @@
|
|||
|
||||
.nodedc-home-gantt-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
min-height: 30rem;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
border-radius: 2rem !important;
|
||||
background: #050506 !important;
|
||||
box-shadow: none !important;
|
||||
|
|
@ -2441,7 +3234,10 @@
|
|||
|
||||
.nodedc-home-gantt-surface {
|
||||
position: relative;
|
||||
min-height: 23.5rem;
|
||||
flex: 1 1 var(--nodedc-home-gantt-height);
|
||||
height: auto;
|
||||
min-height: var(--nodedc-home-gantt-height);
|
||||
max-height: none;
|
||||
margin: 0 1.25rem 1.25rem;
|
||||
overflow: hidden;
|
||||
border-radius: 1.75rem;
|
||||
|
|
@ -2449,15 +3245,18 @@
|
|||
}
|
||||
|
||||
.nodedc-home-gantt-scroll {
|
||||
min-height: 23.5rem;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
max-height: none;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-color: rgba(var(--nodedc-card-active-rgb), 0.65) rgba(255, 255, 255, 0.04);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-scroll::-webkit-scrollbar {
|
||||
height: 0.55rem;
|
||||
width: 0.45rem;
|
||||
height: 0.45rem;
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-scroll::-webkit-scrollbar-track {
|
||||
|
|
@ -2472,7 +3271,7 @@
|
|||
|
||||
.nodedc-home-gantt-canvas {
|
||||
position: relative;
|
||||
min-height: 23.5rem;
|
||||
min-height: 100%;
|
||||
padding: 1rem;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px) 0 0 / 100% 4.2rem,
|
||||
|
|
@ -3579,8 +4378,7 @@
|
|||
|
||||
.nodedc-home-operations-card {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.022) 0%, rgba(255, 255, 255, 0.006) 100%),
|
||||
rgba(10, 10, 12, 0.68) !important;
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.022) 0%, rgba(255, 255, 255, 0.006) 100%), rgba(10, 10, 12, 0.68) !important;
|
||||
-webkit-backdrop-filter: blur(28px);
|
||||
backdrop-filter: blur(28px);
|
||||
}
|
||||
|
|
@ -4257,5 +5055,4 @@
|
|||
offset-distance: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export type TUserProfile = {
|
|||
theme: {
|
||||
theme: string | undefined;
|
||||
nodedcAccent?: string | undefined;
|
||||
nodedcCompactToolbar?: boolean | undefined;
|
||||
};
|
||||
|
||||
onboarding_step: {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export type TUserProfile = {
|
|||
background: string | undefined;
|
||||
darkPalette: boolean | undefined;
|
||||
nodedcAccent?: string | undefined;
|
||||
nodedcCompactToolbar?: boolean | undefined;
|
||||
};
|
||||
onboarding_step: TOnboardingSteps;
|
||||
is_onboarded: boolean;
|
||||
|
|
@ -109,6 +110,7 @@ export interface IUserTheme {
|
|||
background?: string | undefined;
|
||||
darkPalette?: boolean | undefined;
|
||||
nodedcAccent?: string | undefined;
|
||||
nodedcCompactToolbar?: boolean | undefined;
|
||||
}
|
||||
|
||||
export interface IUserMemberLite extends IUserLite {
|
||||
|
|
|
|||
Loading…
Reference in New Issue