UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: перенос режимов проекта в нижнюю панель

This commit is contained in:
DCCONSTRUCTIONS 2026-05-01 14:16:28 +03:00
parent ae262487ac
commit 119d503d96
11 changed files with 176 additions and 134 deletions

View File

@ -13,6 +13,7 @@ import { WorkItemsIcon } from "@plane/propel/icons";
import { Breadcrumbs, Header } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { HeaderFilters } from "@/components/issues/filters";
import { IssueDetailQuickActions } from "@/components/issues/issue-detail/issue-detail-quick-actions";
// hooks
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
@ -62,6 +63,14 @@ export const WorkItemDetailsHeader = observer(function WorkItemDetailsHeader() {
</Breadcrumbs>
</Header.LeftItem>
<Header.RightItem>
<div className="hidden items-center gap-2 md:flex">
<HeaderFilters
projectId={projectId.toString()}
currentProjectDetails={projectDetails}
workspaceSlug={workspaceSlug.toString()}
canUserCreateIssue={undefined}
/>
</div>
{projectId && issueId && (
<IssueDetailQuickActions
workspaceSlug={workspaceSlug?.toString()}

View File

@ -36,7 +36,6 @@ import {
DisplayFiltersSelection,
FiltersDropdown,
LayoutSelection,
MobileLayoutSelection,
} from "@/components/issues/issue-layouts/filters";
import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle";
// hooks
@ -185,32 +184,17 @@ export const CycleIssuesHeader = observer(function CycleIssuesHeader() {
</Header.LeftItem>
<Header.RightItem className="items-center">
<div className="hidden items-center gap-2 md:flex">
<div className="hidden @4xl:flex">
<LayoutSelection
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
</div>
<div className="flex @4xl:hidden">
<MobileLayoutSelection
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)}
activeLayout={activeLayout}
/>
</div>
<LayoutSelection
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<WorkItemFiltersToggle entityType={EIssuesStoreType.CYCLE} entityId={cycleId} />
<FiltersDropdown
title={t("common.display")}

View File

@ -18,7 +18,7 @@ import { WorkItemsModal } from "@/components/analytics/work-items/modal";
import {
DisplayFiltersSelection,
FiltersDropdown,
MobileLayoutSelection,
LayoutSelection,
} from "@/components/issues/issue-layouts/filters";
// hooks
import { useCycle } from "@/hooks/store/use-cycle";
@ -93,9 +93,15 @@ export const CycleIssuesMobileHeader = observer(function CycleIssuesMobileHeader
cycleDetails={cycleDetails ?? undefined}
/>
<div className="flex justify-evenly border-b border-subtle bg-surface-1 py-2 md:hidden">
<MobileLayoutSelection
activeLayout={activeLayout}
layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN, EIssueLayoutTypes.CALENDAR]}
<LayoutSelection
selectedLayout={activeLayout}
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={handleLayoutChange}
/>
<div className="flex flex-grow items-center justify-center border-l border-subtle text-13 text-secondary">

View File

@ -18,7 +18,7 @@ import { WorkItemsModal } from "@/components/analytics/work-items/modal";
import {
DisplayFiltersSelection,
FiltersDropdown,
MobileLayoutSelection,
LayoutSelection,
} from "@/components/issues/issue-layouts/filters";
// hooks
import { useIssues } from "@/hooks/store/use-issues";
@ -69,9 +69,16 @@ export const ProjectIssuesMobileHeader = observer(function ProjectIssuesMobileHe
projectDetails={currentProjectDetails ?? undefined}
/>
<div className="z-[13] flex justify-evenly border-b border-subtle bg-surface-1 py-2 md:hidden">
<MobileLayoutSelection
layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN, EIssueLayoutTypes.CALENDAR]}
<LayoutSelection
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={handleLayoutChange}
selectedLayout={activeLayout}
/>
<div className="flex flex-grow items-center justify-center border-l border-subtle text-13 text-secondary">
<FiltersDropdown

View File

@ -32,7 +32,6 @@ import {
DisplayFiltersSelection,
FiltersDropdown,
LayoutSelection,
MobileLayoutSelection,
} from "@/components/issues/issue-layouts/filters";
import { ModuleQuickActions } from "@/components/modules";
import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle";
@ -179,32 +178,17 @@ export const ModuleIssuesHeader = observer(function ModuleIssuesHeader() {
</Header.LeftItem>
<Header.RightItem className="items-center">
<div className="hidden gap-2 md:flex">
<div className="hidden @4xl:flex">
<LayoutSelection
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
</div>
<div className="flex @4xl:hidden">
<MobileLayoutSelection
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)}
activeLayout={activeLayout}
/>
</div>
<LayoutSelection
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
{moduleId && <WorkItemFiltersToggle entityType={EIssuesStoreType.MODULE} entityId={moduleId} />}
<FiltersDropdown
title="Display"

View File

@ -18,7 +18,7 @@ import { WorkItemsModal } from "@/components/analytics/work-items/modal";
import {
DisplayFiltersSelection,
FiltersDropdown,
MobileLayoutSelection,
LayoutSelection,
} from "@/components/issues/issue-layouts/filters";
// hooks
import { useIssues } from "@/hooks/store/use-issues";
@ -75,9 +75,15 @@ export const ModuleIssuesMobileHeader = observer(function ModuleIssuesMobileHead
projectDetails={currentProjectDetails}
/>
<div className="flex justify-evenly border-b border-subtle bg-surface-1 py-2">
<MobileLayoutSelection
activeLayout={activeLayout}
layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN, EIssueLayoutTypes.CALENDAR]}
<LayoutSelection
selectedLayout={activeLayout}
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={handleLayoutChange}
/>
<div className="flex flex-grow items-center justify-center border-l border-subtle text-13 text-secondary">

View File

@ -24,7 +24,6 @@ import {
DisplayFiltersSelection,
FiltersDropdown,
LayoutSelection,
MobileLayoutSelection,
} from "./issue-layouts/filters";
type Props = {
@ -104,15 +103,10 @@ export const HeaderFilters = observer(function HeaderFilters(props: Props) {
[workspaceSlug, projectId, updateFilters]
);
const layoutSelection = (
<>
<div className="pointer-events-auto hidden @4xl:flex">
<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} />
</div>
</>
const dockLayoutSelection = (
<div className="nodedc-project-layout-controls pointer-events-auto flex">
<LayoutSelection layouts={LAYOUTS} onChange={(layout) => handleLayoutChange(layout)} selectedLayout={activeLayout} />
</div>
);
const headerTools = (
@ -143,7 +137,7 @@ export const HeaderFilters = observer(function HeaderFilters(props: Props) {
!isCompactToolbar && expandedToolbarTarget
? createPortal(
<div className="nodedc-expanded-header-filters">
{layoutSelection}
{dockLayoutSelection}
{headerTools}
</div>,
expandedToolbarTarget
@ -155,28 +149,28 @@ export const HeaderFilters = observer(function HeaderFilters(props: Props) {
{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} />
<FiltersDropdown
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>
</div>
<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">
{dockLayoutSelection}
</div>
<div className="nodedc-top-toolbar-cluster flex items-center gap-2">
<WorkItemFiltersToggle entityType={storeType} entityId={projectId} />
<FiltersDropdown
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>
</div>
</>
)}
</>

View File

@ -122,6 +122,8 @@ export function FiltersDropdown(props: Props) {
ref={setReferenceElement}
className={menuButtonWrapperClassName}
disabled={disabled}
data-active={isOpen}
aria-pressed={isOpen}
tabIndex={tabIndex}
onClick={toggleDropdown}
>
@ -162,6 +164,7 @@ export function FiltersDropdown(props: Props) {
variant="secondary"
tabIndex={-1}
className="nodedc-toolbar-pill nodedc-toolbar-pill-wide"
data-active={isOpen}
size="lg"
>
{miniIcon || title}

View File

@ -46,6 +46,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
left: number;
top: number;
width: number;
listMaxHeight: number;
} | null>(null);
const sidebarSearchPortalRef = useRef<HTMLDivElement>(null);
@ -133,13 +134,21 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
const rect = sidebarSearchButtonRef.current.getBoundingClientRect();
const width = 320;
const viewportPadding = 16;
const left = Math.min(rect.left, window.innerWidth - width - viewportPadding);
const top = rect.bottom + 10;
const topSafetyOffset = 88;
const inputHeight = 32;
const inputToListGap = 12;
const dockGap = 18;
const availableAboveDock = Math.max(260, rect.top - topSafetyOffset - inputHeight - inputToListGap - dockGap);
const listMaxHeight = Math.min(window.innerHeight * 0.7, availableAboveDock);
const panelHeight = inputHeight + inputToListGap + listMaxHeight;
const left = Math.max(viewportPadding, Math.min(rect.left, window.innerWidth - width - viewportPadding));
const top = Math.max(topSafetyOffset, rect.top - panelHeight - dockGap);
setSidebarSearchPosition({
left,
top,
width,
listMaxHeight,
});
}, [variant]);
@ -292,37 +301,6 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
{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"
@ -414,7 +392,34 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
}
)}
>
{isOpen && searchCommandContent}
{isOpen && (
<>
<div className="nodedc-expanded-search-floating-input mx-3 mb-3 flex h-11 items-center rounded-full px-4">
<SearchIcon className="mr-2 size-3.5 shrink-0 text-placeholder" />
<input
ref={inputRef}
type="text"
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
if (!isOpen) openPanel();
}}
onMouseDown={handleMouseDown}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
placeholder={t("power_k.search_menu.quick_command_placeholder")}
className="placeholder-text-placeholder min-w-0 flex-1 bg-transparent text-13 text-primary outline-none"
autoFocus
/>
{searchTerm && (
<button type="button" onClick={handleClear} className="ml-2 shrink-0 text-placeholder hover:text-primary">
<CloseIcon className="size-3.5" />
</button>
)}
</div>
{searchCommandContent}
</>
)}
</div>
)}
{isWideSearch && !isExpandedToolbar && (
@ -469,7 +474,10 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
</button>
)}
</div>
<div className="nodedc-glass-modal nodedc-glass-popup-surface absolute top-full left-0 mt-3 flex max-h-[70vh] w-full flex-col overflow-hidden rounded-[1.5rem] pt-3">
<div
className="nodedc-glass-modal nodedc-glass-popup-surface absolute top-full left-0 mt-3 flex w-full flex-col overflow-hidden rounded-[1.5rem] pt-3"
style={{ maxHeight: `${sidebarSearchPosition.listMaxHeight}px` }}
>
<div className="px-4 pb-2">
<div className="text-[13px] font-medium text-secondary">
{t("power_k.search_menu.quick_access_title")}

View File

@ -72,7 +72,7 @@ export const FiltersToggle = observer(function FiltersToggle<P extends TFilterPr
icon={showFilterRowChangesPill ? FilterAppliedIcon : FilterIcon}
onClick={handleToggleFilter}
className={buttonClassName}
data-active={showFilterRowChangesPill}
data-active={showFilterRowChangesPill || isFilterRowVisible}
iconClassName={cn("translate-y-[3px]", iconClassName)}
/>
);

View File

@ -1682,6 +1682,11 @@
bottom: calc(100% + 0.85rem);
}
.nodedc-expanded-search-floating-input {
background: rgba(255, 255, 255, 0.055);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.045);
}
.nodedc-expanded-notification-button {
height: 2.78rem;
width: 2.78rem;
@ -2324,6 +2329,42 @@
color: rgb(var(--nodedc-accent-rgb)) !important;
}
.nodedc-bottom-dock .nodedc-project-layout-controls .nodedc-toolbar-group {
min-height: 2.5rem;
}
.nodedc-bottom-dock .nodedc-toolbar-icon-button,
.nodedc-bottom-dock .nodedc-toolbar-filter-toggle {
background: rgba(255, 255, 255, 0.04) !important;
color: rgba(255, 255, 255, 0.72) !important;
}
.nodedc-bottom-dock .nodedc-toolbar-icon-button:hover,
.nodedc-bottom-dock .nodedc-toolbar-icon-button:focus-visible,
.nodedc-bottom-dock .nodedc-toolbar-filter-toggle:hover,
.nodedc-bottom-dock .nodedc-toolbar-filter-toggle:focus-visible {
background: rgba(255, 255, 255, 0.08) !important;
color: rgba(255, 255, 255, 0.94) !important;
}
.nodedc-bottom-dock .nodedc-toolbar-icon-button[data-active="true"],
.nodedc-bottom-dock .nodedc-toolbar-filter-toggle[data-active="true"],
.nodedc-bottom-dock .nodedc-toolbar-pill[data-active="true"] {
background: rgba(255, 255, 255, 0.94) !important;
color: rgba(8, 8, 10, 0.94) !important;
}
.nodedc-bottom-dock .nodedc-toolbar-icon-button[data-active="true"] .nodedc-toolbar-icon-active-dot {
background: transparent !important;
color: rgba(8, 8, 10, 0.94) !important;
}
.nodedc-bottom-dock .nodedc-toolbar-icon-button[data-active="true"] svg,
.nodedc-bottom-dock .nodedc-toolbar-filter-toggle[data-active="true"] svg,
.nodedc-bottom-dock .nodedc-toolbar-pill[data-active="true"] svg {
color: rgba(8, 8, 10, 0.94) !important;
}
.nodedc-filter-row-shell {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%), rgba(8, 8, 11, 0.84);