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
@@ -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>
</>
)}
</>
@@ -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}
@@ -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")}
@@ -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)}
/>
);