refactor(tasks): stabilize grouped board headers
This commit is contained in:
parent
31a0ec922f
commit
63b7da46d1
@ -899,6 +899,19 @@ html[data-bs-theme="dark"] .shadow-xl {
|
||||
box-shadow: var(--shadow-card) !important;
|
||||
}
|
||||
|
||||
html[data-bs-theme="dark"] .tasks-board-column-header,
|
||||
html[data-bs-theme="dark"] .tasks-board-subcolumn-header {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
html[data-bs-theme="dark"] .tasks-board-column-header-sticky {
|
||||
background: var(--surface-elevated);
|
||||
}
|
||||
|
||||
html[data-bs-theme="dark"] .tasks-board-group-header-sticky {
|
||||
background: var(--surface-elevated);
|
||||
}
|
||||
|
||||
|
||||
/* Скрытые полосы прокрутки везде */
|
||||
* {
|
||||
@ -1001,35 +1014,66 @@ html[data-bs-theme="dark"] .shadow-xl {
|
||||
--tasks-board-stack-gap: 0.5rem;
|
||||
--tasks-board-column-header-height: 4rem;
|
||||
--tasks-board-subcolumn-header-height: 4rem;
|
||||
--tasks-board-group-header-stack-height: calc(
|
||||
var(--tasks-board-column-header-height, 4rem)
|
||||
+ var(--tasks-board-stack-gap, 0.5rem)
|
||||
+ var(--tasks-board-subcolumn-header-height, 4rem)
|
||||
+ 1rem
|
||||
+ 1px
|
||||
);
|
||||
}
|
||||
|
||||
.tasks-board-column-header {
|
||||
.tasks-board-column-header-sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 30;
|
||||
padding-bottom: var(--tasks-board-stack-gap, 0.5rem);
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.tasks-board-column-header-sticky-standalone {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.tasks-board-column-header {
|
||||
min-height: var(--tasks-board-column-header-height, 4rem);
|
||||
background: var(--surface-elevated);
|
||||
}
|
||||
|
||||
.tasks-board-subcolumn-header-row {
|
||||
.tasks-board-group-header-sticky {
|
||||
position: sticky;
|
||||
top: calc(
|
||||
var(--tasks-board-column-header-height, 4rem)
|
||||
+ var(--tasks-board-stack-gap, 0.5rem)
|
||||
);
|
||||
z-index: 20;
|
||||
top: 0;
|
||||
z-index: 30;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.tasks-board-subcolumn-header-row {
|
||||
min-height: var(--tasks-board-subcolumn-header-height, 4rem);
|
||||
}
|
||||
|
||||
.tasks-board-subcolumn-header-surface::after {
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
bottom: -0.75rem;
|
||||
left: -1px;
|
||||
height: 0.75rem;
|
||||
border: 1px solid transparent;
|
||||
border-top: 0;
|
||||
border-radius: 0 0 0.75rem 0.75rem;
|
||||
pointer-events: none;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.tasks-board-column-group[data-content-bottom-reached="true"] .tasks-board-subcolumn-header-surface::after {
|
||||
border-color: var(--border-subtle);
|
||||
border-top-color: transparent;
|
||||
background: var(--surface-secondary);
|
||||
}
|
||||
|
||||
.tasks-board-lane-header {
|
||||
position: sticky;
|
||||
top: calc(
|
||||
var(--tasks-board-column-header-height, 4rem)
|
||||
+ var(--tasks-board-subcolumn-header-height, 4rem)
|
||||
+ var(--tasks-board-stack-gap, 0.5rem)
|
||||
+ var(--tasks-board-stack-gap, 0.5rem)
|
||||
);
|
||||
top: var(--tasks-board-group-header-stack-height, calc(9.5rem + 1px));
|
||||
z-index: 15;
|
||||
background: var(--surface-secondary);
|
||||
}
|
||||
@ -1043,4 +1087,5 @@ html[data-bs-theme="dark"] .shadow-xl {
|
||||
height: 3.9375rem;
|
||||
content: "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1009,11 +1009,12 @@ function BoardColumn({
|
||||
isDragging ? "opacity-70" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`tasks-board-column-header app-surface flex min-h-16 items-center justify-between gap-3 rounded-xl px-3 py-3 transition ${columnBorderClass} ${
|
||||
isDragging ? "shadow-xl" : ""
|
||||
}`}
|
||||
>
|
||||
<div className={`tasks-board-column-header-sticky ${contentVisible ? "" : "tasks-board-column-header-sticky-standalone"}`}>
|
||||
<div
|
||||
className={`tasks-board-column-header app-surface flex min-h-16 items-center justify-between gap-3 rounded-xl px-3 py-3 transition ${columnBorderClass} ${
|
||||
isDragging ? "shadow-xl" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
ref={setActivatorNodeRef}
|
||||
className="min-w-0 flex-1 cursor-grab touch-none overflow-hidden active:cursor-grabbing"
|
||||
@ -1126,9 +1127,10 @@ function BoardColumn({
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{contentVisible ? <div
|
||||
className={`tasks-column-scroll mt-2 min-h-0 flex-1 space-y-2 overflow-y-auto rounded-xl border bg-[var(--surface-primary)] p-3 transition xl:flex-none xl:overflow-visible ${columnBorderClass}`}
|
||||
className={`tasks-column-scroll mt-2 min-h-0 flex-1 space-y-2 overflow-y-auto rounded-xl border bg-[var(--surface-primary)] p-3 transition xl:mt-0 xl:flex-none xl:overflow-visible ${columnBorderClass}`}
|
||||
>
|
||||
<TaskColumnCards
|
||||
tasks={tasks}
|
||||
@ -1209,14 +1211,16 @@ function BoardSubcolumnHeader({
|
||||
<section
|
||||
ref={setHeaderNodeRef}
|
||||
style={{
|
||||
transform: sortable ? CSS.Transform.toString(transform) : undefined,
|
||||
transition: sortable ? transition : undefined,
|
||||
transform: sortable && synchronizedColumnDrag && transform
|
||||
? CSS.Transform.toString({ ...transform, y: 0 })
|
||||
: undefined,
|
||||
transition: sortable && synchronizedColumnDrag ? transition : undefined,
|
||||
zIndex: isDragging ? 20 : undefined,
|
||||
}}
|
||||
className={`flex w-full min-w-0 flex-col transition ${isDragging ? "opacity-70" : ""}`}
|
||||
>
|
||||
<div
|
||||
className={`flex min-h-16 items-center justify-between gap-3 rounded-xl border bg-[var(--surface-primary)] px-3 py-3 shadow-sm transition ${columnBorderClass} ${
|
||||
className={`tasks-board-subcolumn-header flex min-h-16 items-center justify-between gap-3 rounded-xl border bg-[var(--surface-primary)] px-3 py-3 shadow-sm transition ${columnBorderClass} ${
|
||||
isDragging ? "shadow-xl" : ""
|
||||
}`}
|
||||
>
|
||||
@ -1363,6 +1367,8 @@ function BoardColumnGroup({
|
||||
onColumnMount,
|
||||
contentVisible,
|
||||
onToggleContentVisibility,
|
||||
subcolumnHeaders,
|
||||
subcolumnGridTemplate,
|
||||
hideOnMobile = false,
|
||||
}: {
|
||||
column: TaskColumn;
|
||||
@ -1379,6 +1385,8 @@ function BoardColumnGroup({
|
||||
onColumnMount: (columnId: number, node: HTMLElement | null) => void;
|
||||
contentVisible: boolean;
|
||||
onToggleContentVisibility: () => void;
|
||||
subcolumnHeaders: ReactNode;
|
||||
subcolumnGridTemplate: string;
|
||||
hideOnMobile?: boolean;
|
||||
}) {
|
||||
const {
|
||||
@ -1408,14 +1416,15 @@ function BoardColumnGroup({
|
||||
}}
|
||||
className={`${hideOnMobile ? "hidden xl:flex" : "flex"} tasks-board-column-group min-w-0 flex-col ${isDragging ? "opacity-70" : ""}`}
|
||||
>
|
||||
<div className="tasks-board-column-header app-surface flex min-h-16 items-center justify-between gap-3 rounded-xl px-3 py-3">
|
||||
<div
|
||||
ref={setActivatorNodeRef}
|
||||
className="min-w-0 flex-1 cursor-grab touch-none active:cursor-grabbing"
|
||||
title="Перетащите, чтобы изменить порядок колонок"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<div className="tasks-board-group-header-sticky">
|
||||
<div className="tasks-board-column-header app-surface flex min-h-16 items-center justify-between gap-3 rounded-xl px-3 py-3">
|
||||
<div
|
||||
ref={setActivatorNodeRef}
|
||||
className="min-w-0 flex-1 cursor-grab touch-none active:cursor-grabbing"
|
||||
title="Перетащите, чтобы изменить порядок колонок"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
||||
@ -1514,10 +1523,21 @@ function BoardColumnGroup({
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{contentVisible ? (
|
||||
<div className="tasks-board-subcolumn-header-surface relative mt-2 rounded-t-xl border border-b-0 border-[var(--border-subtle)] bg-[var(--surface-secondary)] p-2">
|
||||
<div
|
||||
className="tasks-board-subcolumn-header-row flex min-w-0 flex-col gap-2 xl:grid xl:w-max"
|
||||
style={{ gridTemplateColumns: subcolumnGridTemplate }}
|
||||
>
|
||||
{subcolumnHeaders}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{contentVisible ? (
|
||||
<div className="mt-2 flex w-full min-w-0 flex-col gap-2 rounded-xl border border-[var(--border-subtle)] bg-[var(--surface-secondary)] p-2 xl:w-auto xl:min-w-max">
|
||||
<div className="tasks-board-column-content flex w-full min-w-0 flex-col gap-2 rounded-b-xl border border-t-0 border-[var(--border-subtle)] bg-[var(--surface-secondary)] px-2 pb-2 xl:w-auto xl:min-w-max">
|
||||
{children}
|
||||
</div>
|
||||
) : null}
|
||||
@ -1550,7 +1570,7 @@ function BoardColumnRowView({
|
||||
<section className={`tasks-board-content-section min-w-0 ${
|
||||
hasFollowingSection ? "tasks-board-content-section-continuous" : ""
|
||||
}`}>
|
||||
<div className={`tasks-board-lane-header flex items-center justify-between gap-3 px-1 ${expanded ? "mb-2" : ""}`}>
|
||||
<div className={`tasks-board-lane-header flex items-center justify-between gap-3 px-1 ${expanded ? "mb-2 xl:mb-0 xl:pb-2" : ""}`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
@ -1647,7 +1667,7 @@ function BoardBaseColumnsView({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
className={`tasks-board-lane-header flex w-full items-center gap-2 rounded-lg px-1 py-1 text-left ${headers ? "mt-2" : ""} ${expanded ? "mb-2" : ""}`}
|
||||
className={`tasks-board-lane-header flex w-full items-center gap-2 rounded-lg px-1 py-1 text-left ${headers ? "mt-2" : ""} ${expanded ? "mb-2 xl:mb-0 xl:pb-2" : ""}`}
|
||||
aria-expanded={expanded}
|
||||
>
|
||||
<ChevronRight
|
||||
@ -3405,14 +3425,26 @@ function TasksPageContent() {
|
||||
const container = boardScrollRef.current;
|
||||
if (!container || loading) return undefined;
|
||||
|
||||
const updateViewportHeight = () => {
|
||||
const updateBoardViewport = () => {
|
||||
setBoardViewportHeight(container.clientHeight);
|
||||
container.querySelectorAll<HTMLElement>(".tasks-board-column-group").forEach((group) => {
|
||||
const content = group.querySelector<HTMLElement>(".tasks-board-column-content");
|
||||
const stickyHeader = group.querySelector<HTMLElement>(".tasks-board-group-header-sticky");
|
||||
const contentBottomReached = content && stickyHeader
|
||||
? content.getBoundingClientRect().bottom <= stickyHeader.getBoundingClientRect().bottom + 1
|
||||
: false;
|
||||
group.dataset.contentBottomReached = contentBottomReached ? "true" : "false";
|
||||
});
|
||||
};
|
||||
updateViewportHeight();
|
||||
updateBoardViewport();
|
||||
|
||||
const observer = new ResizeObserver(updateViewportHeight);
|
||||
container.addEventListener("scroll", updateBoardViewport, { passive: true });
|
||||
const observer = new ResizeObserver(updateBoardViewport);
|
||||
observer.observe(container);
|
||||
return () => observer.disconnect();
|
||||
return () => {
|
||||
container.removeEventListener("scroll", updateBoardViewport);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [board?.id, desktopWideMode, loading]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -6954,29 +6986,7 @@ function TasksPageContent() {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BoardColumnGroup
|
||||
key={column.id}
|
||||
column={column}
|
||||
tasksCount={columnCounts.get(column.id) || 0}
|
||||
onCreateTask={() => {
|
||||
if (subcolumns[0]) startQuickTask(subcolumns[0].id);
|
||||
else startQuickTask(column.id);
|
||||
}}
|
||||
onCreateSubcolumn={() => openCreateColumn(column.id)}
|
||||
onCreateRow={() => openCreateColumnRow(column.id)}
|
||||
onEditColumn={openEditColumn}
|
||||
onDeleteColumn={deleteColumn}
|
||||
columnMenuOpen={columnMenuId === column.id}
|
||||
columnMenuRef={columnMenuRef}
|
||||
onToggleColumnMenu={() => toggleColumnMenu(column.id)}
|
||||
onColumnMount={registerColumnNode}
|
||||
contentVisible={!boardViewState.hidden_column_ids.includes(column.id)}
|
||||
onToggleContentVisibility={() => toggleBoardViewState("column", column.id)}
|
||||
hideOnMobile={hideOnMobile}
|
||||
>
|
||||
{(() => {
|
||||
const cellColumns = subcolumns.length ? subcolumns : [column];
|
||||
const cellColumns = subcolumns.length ? subcolumns : [column];
|
||||
const activePreviewIndex = cellColumns.findIndex(
|
||||
(item) => item.id === activeColumnDragId,
|
||||
);
|
||||
@ -7068,41 +7078,55 @@ function TasksPageContent() {
|
||||
) : renderSubcolumnHeader(column, false);
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col gap-2 xl:w-max">
|
||||
<div
|
||||
className="tasks-board-subcolumn-header-row flex min-w-0 flex-col gap-2 xl:grid xl:w-max"
|
||||
style={{ gridTemplateColumns }}
|
||||
>
|
||||
{subcolumnHeaders}
|
||||
</div>
|
||||
<BoardColumnGroup
|
||||
key={column.id}
|
||||
column={column}
|
||||
tasksCount={columnCounts.get(column.id) || 0}
|
||||
onCreateTask={() => {
|
||||
if (subcolumns[0]) startQuickTask(subcolumns[0].id);
|
||||
else startQuickTask(column.id);
|
||||
}}
|
||||
onCreateSubcolumn={() => openCreateColumn(column.id)}
|
||||
onCreateRow={() => openCreateColumnRow(column.id)}
|
||||
onEditColumn={openEditColumn}
|
||||
onDeleteColumn={deleteColumn}
|
||||
columnMenuOpen={columnMenuId === column.id}
|
||||
columnMenuRef={columnMenuRef}
|
||||
onToggleColumnMenu={() => toggleColumnMenu(column.id)}
|
||||
onColumnMount={registerColumnNode}
|
||||
contentVisible={!boardViewState.hidden_column_ids.includes(column.id)}
|
||||
onToggleContentVisibility={() => toggleBoardViewState("column", column.id)}
|
||||
subcolumnHeaders={subcolumnHeaders}
|
||||
subcolumnGridTemplate={gridTemplateColumns}
|
||||
hideOnMobile={hideOnMobile}
|
||||
>
|
||||
{baseColumns}
|
||||
<div className="flex min-w-0 flex-col gap-2">
|
||||
{columnRows.map((row, rowIndex) => {
|
||||
const rowTasksCount = filteredTasks.filter((task) => task.row === row.id).length;
|
||||
return (
|
||||
<BoardColumnRowView
|
||||
key={row.id}
|
||||
row={{ ...row, tasks_count: rowTasksCount }}
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
onCreateTask={() => {
|
||||
if (cellColumns[0]) startQuickTask(cellColumns[0].id, row.id);
|
||||
}}
|
||||
onEdit={() => openEditColumnRow(row)}
|
||||
onDelete={() => void deleteColumnRow(row)}
|
||||
expanded={!boardViewState.collapsed_row_ids.includes(row.id)}
|
||||
onToggle={() => toggleBoardViewState("row", row.id)}
|
||||
hasFollowingSection={rowIndex < columnRows.length - 1}
|
||||
>
|
||||
{previewCellColumns.map((item) => renderTaskCell(item, row.id))}
|
||||
</BoardColumnRowView>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{columnRows.length > 0 ? (
|
||||
<div className="flex min-w-0 flex-col gap-2">
|
||||
{columnRows.map((row, rowIndex) => {
|
||||
const rowTasksCount = filteredTasks.filter((task) => task.row === row.id).length;
|
||||
return (
|
||||
<BoardColumnRowView
|
||||
key={row.id}
|
||||
row={{ ...row, tasks_count: rowTasksCount }}
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
onCreateTask={() => {
|
||||
if (cellColumns[0]) startQuickTask(cellColumns[0].id, row.id);
|
||||
}}
|
||||
onEdit={() => openEditColumnRow(row)}
|
||||
onDelete={() => void deleteColumnRow(row)}
|
||||
expanded={!boardViewState.collapsed_row_ids.includes(row.id)}
|
||||
onToggle={() => toggleBoardViewState("row", row.id)}
|
||||
hasFollowingSection={rowIndex < columnRows.length - 1}
|
||||
>
|
||||
{previewCellColumns.map((item) => renderTaskCell(item, row.id))}
|
||||
</BoardColumnRowView>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</BoardColumnGroup>
|
||||
);
|
||||
})()}
|
||||
</BoardColumnGroup>
|
||||
);
|
||||
})}
|
||||
<AddColumnCard
|
||||
onClick={() => openCreateColumn()}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user