UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: фильтры и настройки вида домашнего Ганта
This commit is contained in:
parent
ad1d9c34ea
commit
eff71d7258
|
|
@ -4,8 +4,8 @@
|
|||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { CalendarDays, Filter, SlidersHorizontal } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { CalendarDays, Check, Filter, SlidersHorizontal } from "lucide-react";
|
||||
import type { ChartDataType, IGanttBlock } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
import { getItemPositionWidth } from "@/components/gantt-chart/views/helpers";
|
||||
|
|
@ -19,11 +19,17 @@ const GANTT_CANVAS_PADDING = 32;
|
|||
export type TGanttPreviewRange = (typeof GANTT_RANGES)[number];
|
||||
|
||||
export type TGanttTimelinePreviewItem = {
|
||||
assignee_ids?: string[] | null;
|
||||
completed_at?: string | null;
|
||||
created_at?: string | null;
|
||||
created_by?: string | null;
|
||||
id: string;
|
||||
name: string;
|
||||
identifier: string;
|
||||
priority?: string | null;
|
||||
sort_order?: number | null;
|
||||
start_date?: string | null;
|
||||
state_group?: TGanttPreviewStateGroup | null;
|
||||
target_date?: string | null;
|
||||
};
|
||||
|
||||
|
|
@ -50,6 +56,11 @@ type TGanttPreviewBlock = TGanttTimelinePreviewItem & {
|
|||
width: number;
|
||||
};
|
||||
|
||||
type TGanttPreviewStateGroup = "backlog" | "unstarted" | "started" | "completed" | "cancelled";
|
||||
type TGanttPreviewStatusFilter = "open" | "backlog" | "unstarted" | "started" | "completed" | "cancelled";
|
||||
type TGanttPreviewDateFilter = "overdue" | "complete-range";
|
||||
type TGanttPreviewSortMode = "target_date_asc" | "target_date_desc" | "start_date_asc" | "created_at_desc";
|
||||
|
||||
const RANGE_SETTINGS: Record<TGanttPreviewRange, TRangeSettings> = {
|
||||
Live: {
|
||||
dayWidth: 76,
|
||||
|
|
@ -81,6 +92,31 @@ const RANGE_SETTINGS: Record<TGanttPreviewRange, TRangeSettings> = {
|
|||
},
|
||||
};
|
||||
|
||||
const GANTT_STATUS_FILTER_OPTIONS: {
|
||||
groups: TGanttPreviewStateGroup[];
|
||||
key: TGanttPreviewStatusFilter;
|
||||
label: string;
|
||||
}[] = [
|
||||
{ groups: ["backlog", "unstarted", "started"], key: "open", label: "Открытые" },
|
||||
{ groups: ["backlog"], key: "backlog", label: "Бэклог" },
|
||||
{ groups: ["unstarted"], key: "unstarted", label: "Todo" },
|
||||
{ groups: ["started"], key: "started", label: "В процессе" },
|
||||
{ groups: ["completed"], key: "completed", label: "Закрытые" },
|
||||
{ groups: ["cancelled"], key: "cancelled", label: "Отмененные" },
|
||||
];
|
||||
|
||||
const GANTT_DATE_FILTER_OPTIONS: { key: TGanttPreviewDateFilter; label: string }[] = [
|
||||
{ key: "overdue", label: "Просроченные" },
|
||||
{ key: "complete-range", label: "С началом и сроком" },
|
||||
];
|
||||
|
||||
const GANTT_SORT_OPTIONS: { key: TGanttPreviewSortMode; label: string }[] = [
|
||||
{ key: "target_date_asc", label: "Ближайший срок" },
|
||||
{ key: "target_date_desc", label: "Дальний срок" },
|
||||
{ key: "start_date_asc", label: "Раннее начало" },
|
||||
{ key: "created_at_desc", label: "Новые сверху" },
|
||||
];
|
||||
|
||||
const startOfDay = (date: Date) => {
|
||||
const nextDate = new Date(date);
|
||||
nextDate.setHours(0, 0, 0, 0);
|
||||
|
|
@ -156,15 +192,74 @@ const getShortDateLabel = (date: Date, locale: string) =>
|
|||
.format(date)
|
||||
.toUpperCase();
|
||||
|
||||
const getDateSortValue = (value?: string | null, emptyValue = Number.POSITIVE_INFINITY) =>
|
||||
getDateFromValue(value)?.getTime() ?? emptyValue;
|
||||
|
||||
const isOverdueItem = (item: TGanttTimelinePreviewItem, today: Date) => {
|
||||
const targetDate = getDateFromValue(item.target_date);
|
||||
if (!targetDate) return false;
|
||||
|
||||
return targetDate < today && item.state_group !== "completed" && item.state_group !== "cancelled";
|
||||
};
|
||||
|
||||
const matchesStatusFilters = (item: TGanttTimelinePreviewItem, activeStatusFilters: TGanttPreviewStatusFilter[]) => {
|
||||
if (activeStatusFilters.length === 0) return true;
|
||||
if (!item.state_group) return false;
|
||||
|
||||
return activeStatusFilters.some((filterKey) =>
|
||||
GANTT_STATUS_FILTER_OPTIONS.find((option) => option.key === filterKey)?.groups.includes(item.state_group)
|
||||
);
|
||||
};
|
||||
|
||||
const sortPreviewItems = (items: TGanttTimelinePreviewItem[], sortMode: TGanttPreviewSortMode) =>
|
||||
[...items].sort((firstItem, secondItem) => {
|
||||
if (sortMode === "target_date_desc") {
|
||||
return (
|
||||
getDateSortValue(secondItem.target_date, Number.NEGATIVE_INFINITY) -
|
||||
getDateSortValue(firstItem.target_date, Number.NEGATIVE_INFINITY)
|
||||
);
|
||||
}
|
||||
|
||||
if (sortMode === "start_date_asc") {
|
||||
return getDateSortValue(firstItem.start_date) - getDateSortValue(secondItem.start_date);
|
||||
}
|
||||
|
||||
if (sortMode === "created_at_desc") {
|
||||
return (
|
||||
getDateSortValue(secondItem.created_at, Number.NEGATIVE_INFINITY) -
|
||||
getDateSortValue(firstItem.created_at, Number.NEGATIVE_INFINITY)
|
||||
);
|
||||
}
|
||||
|
||||
return getDateSortValue(firstItem.target_date) - getDateSortValue(secondItem.target_date);
|
||||
});
|
||||
|
||||
export function GanttTimelinePreview(props: TGanttTimelinePreviewProps) {
|
||||
const { emptyMessage, isLoading = false, items, locale, subtitle, title } = props;
|
||||
const [activeRange, setActiveRange] = useState<TGanttPreviewRange>("Live");
|
||||
const [isCompactMode, setIsCompactMode] = useState(false);
|
||||
const [showCompleteBlocksOnly, setShowCompleteBlocksOnly] = useState(false);
|
||||
const [activePanel, setActivePanel] = useState<"filters" | "view" | null>(null);
|
||||
const [activeDateFilters, setActiveDateFilters] = useState<TGanttPreviewDateFilter[]>([]);
|
||||
const [activeStatusFilters, setActiveStatusFilters] = useState<TGanttPreviewStatusFilter[]>([]);
|
||||
const [showFullTaskName, setShowFullTaskName] = useState(false);
|
||||
const [sortMode, setSortMode] = useState<TGanttPreviewSortMode>("target_date_asc");
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const visibleItems = useMemo(() => {
|
||||
const today = startOfDay(new Date());
|
||||
const filteredItems = items.filter((item) => {
|
||||
if (!matchesStatusFilters(item, activeStatusFilters)) return false;
|
||||
if (activeDateFilters.includes("overdue") && !isOverdueItem(item, today)) return false;
|
||||
if (activeDateFilters.includes("complete-range") && (!item.start_date || !item.target_date)) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return sortPreviewItems(filteredItems, sortMode);
|
||||
}, [activeDateFilters, activeStatusFilters, items, sortMode]);
|
||||
|
||||
const timeline = useMemo(() => {
|
||||
const settings = RANGE_SETTINGS[activeRange];
|
||||
const { endDate, startDate, today } = getTimelineBounds(items, settings);
|
||||
const { endDate, startDate, today } = getTimelineBounds(visibleItems, settings);
|
||||
const totalDays = getDaysBetween(startDate, endDate) + 1;
|
||||
const timelineWidth = Math.max(totalDays * settings.dayWidth, 620);
|
||||
const canvasWidth = GANTT_LABEL_COLUMN_WIDTH + GANTT_LABEL_GAP + timelineWidth + GANTT_CANVAS_PADDING;
|
||||
|
|
@ -180,10 +275,8 @@ export function GanttTimelinePreview(props: TGanttTimelinePreviewProps) {
|
|||
},
|
||||
};
|
||||
|
||||
const blocks = items
|
||||
const blocks = visibleItems
|
||||
.map((item, index) => {
|
||||
if (showCompleteBlocksOnly && (!item.start_date || !item.target_date)) return undefined;
|
||||
|
||||
const block: IGanttBlock = {
|
||||
data: item,
|
||||
id: item.id,
|
||||
|
|
@ -216,9 +309,44 @@ export function GanttTimelinePreview(props: TGanttTimelinePreviewProps) {
|
|||
timelineWidth,
|
||||
todayLeft: getDaysBetween(startDate, today) * settings.dayWidth,
|
||||
};
|
||||
}, [activeRange, items, locale, showCompleteBlocksOnly]);
|
||||
}, [activeRange, locale, visibleItems]);
|
||||
|
||||
const hiddenItemsCount = Math.max(items.length - timeline.blocks.length, 0);
|
||||
const activeFilterCount =
|
||||
activeDateFilters.length + activeStatusFilters.length + (sortMode === "target_date_asc" ? 0 : 1);
|
||||
|
||||
const toggleStatusFilter = (filterKey: TGanttPreviewStatusFilter) =>
|
||||
setActiveStatusFilters((currentFilters) =>
|
||||
currentFilters.includes(filterKey)
|
||||
? currentFilters.filter((currentFilter) => currentFilter !== filterKey)
|
||||
: [...currentFilters, filterKey]
|
||||
);
|
||||
|
||||
const toggleDateFilter = (filterKey: TGanttPreviewDateFilter) =>
|
||||
setActiveDateFilters((currentFilters) =>
|
||||
currentFilters.includes(filterKey)
|
||||
? currentFilters.filter((currentFilter) => currentFilter !== filterKey)
|
||||
: [...currentFilters, filterKey]
|
||||
);
|
||||
|
||||
const resetFilters = () => {
|
||||
setActiveDateFilters([]);
|
||||
setActiveStatusFilters([]);
|
||||
setSortMode("target_date_asc");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const scrollElement = scrollContainerRef.current;
|
||||
if (!scrollElement || isLoading) return;
|
||||
|
||||
const todayCanvasLeft = GANTT_LABEL_COLUMN_WIDTH + GANTT_LABEL_GAP + timeline.todayLeft;
|
||||
const nextScrollLeft = Math.max(todayCanvasLeft - scrollElement.clientWidth * 0.45, 0);
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
scrollElement.scrollTo({ behavior: "auto", left: nextScrollLeft });
|
||||
});
|
||||
|
||||
return () => window.cancelAnimationFrame(frame);
|
||||
}, [activeRange, isLoading, items.length, timeline.timelineWidth, timeline.todayLeft]);
|
||||
|
||||
return (
|
||||
<section className="nodedc-home-gantt-card">
|
||||
|
|
@ -245,33 +373,148 @@ export function GanttTimelinePreview(props: TGanttTimelinePreviewProps) {
|
|||
{item}
|
||||
</button>
|
||||
))}
|
||||
<div className="nodedc-home-gantt-action-group">
|
||||
<button
|
||||
type="button"
|
||||
className={cn("nodedc-home-gantt-round-button", {
|
||||
"nodedc-home-gantt-round-button-active": isCompactMode,
|
||||
"nodedc-home-gantt-round-button-active": showFullTaskName || activePanel === "view",
|
||||
})}
|
||||
aria-pressed={isCompactMode}
|
||||
aria-label="Плотный режим Ганта"
|
||||
onClick={() => setIsCompactMode((value) => !value)}
|
||||
aria-expanded={activePanel === "view"}
|
||||
aria-label="Настройки вида Ганта"
|
||||
aria-pressed={showFullTaskName}
|
||||
onClick={() => setActivePanel((currentPanel) => (currentPanel === "view" ? null : "view"))}
|
||||
>
|
||||
<SlidersHorizontal className="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={cn("nodedc-home-gantt-round-button", {
|
||||
"nodedc-home-gantt-round-button-active": showCompleteBlocksOnly,
|
||||
"nodedc-home-gantt-round-button-active": activeFilterCount > 0 || activePanel === "filters",
|
||||
})}
|
||||
aria-pressed={showCompleteBlocksOnly}
|
||||
aria-label="Показывать только задачи с началом и сроком"
|
||||
onClick={() => setShowCompleteBlocksOnly((value) => !value)}
|
||||
aria-expanded={activePanel === "filters"}
|
||||
aria-label="Фильтры задач Ганта"
|
||||
aria-pressed={activeFilterCount > 0}
|
||||
onClick={() => setActivePanel((currentPanel) => (currentPanel === "filters" ? null : "filters"))}
|
||||
>
|
||||
<Filter className="size-4" />
|
||||
</button>
|
||||
|
||||
{activePanel === "view" && (
|
||||
<div className="nodedc-home-gantt-popover">
|
||||
<div className="nodedc-home-gantt-popover-section">
|
||||
<button
|
||||
type="button"
|
||||
className={cn("nodedc-home-gantt-popover-option", {
|
||||
"nodedc-home-gantt-popover-option-active": showFullTaskName,
|
||||
})}
|
||||
onClick={() => setShowFullTaskName((value) => !value)}
|
||||
>
|
||||
<span className="nodedc-home-gantt-popover-option-left">
|
||||
<span className="nodedc-home-gantt-popover-check">
|
||||
{showFullTaskName && <Check className="size-3" />}
|
||||
</span>
|
||||
<span>Показать полное название</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activePanel === "filters" && (
|
||||
<div className="nodedc-home-gantt-popover nodedc-home-gantt-popover-wide">
|
||||
<div className="nodedc-home-gantt-popover-section">
|
||||
<div className="nodedc-home-gantt-popover-title">Статус</div>
|
||||
{GANTT_STATUS_FILTER_OPTIONS.map((option) => {
|
||||
const isActive = activeStatusFilters.includes(option.key);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={option.key}
|
||||
type="button"
|
||||
className={cn("nodedc-home-gantt-popover-option", {
|
||||
"nodedc-home-gantt-popover-option-active": isActive,
|
||||
})}
|
||||
onClick={() => toggleStatusFilter(option.key)}
|
||||
>
|
||||
<span className="nodedc-home-gantt-popover-option-left">
|
||||
<span className="nodedc-home-gantt-popover-check">
|
||||
{isActive && <Check className="size-3" />}
|
||||
</span>
|
||||
<span>{option.label}</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="nodedc-home-gantt-popover-section">
|
||||
<div className="nodedc-home-gantt-popover-title">Даты</div>
|
||||
{GANTT_DATE_FILTER_OPTIONS.map((option) => {
|
||||
const isActive = activeDateFilters.includes(option.key);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={option.key}
|
||||
type="button"
|
||||
className={cn("nodedc-home-gantt-popover-option", {
|
||||
"nodedc-home-gantt-popover-option-active": isActive,
|
||||
})}
|
||||
onClick={() => toggleDateFilter(option.key)}
|
||||
>
|
||||
<span className="nodedc-home-gantt-popover-option-left">
|
||||
<span className="nodedc-home-gantt-popover-check">
|
||||
{isActive && <Check className="size-3" />}
|
||||
</span>
|
||||
<span>{option.label}</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="nodedc-home-gantt-popover-section">
|
||||
<div className="nodedc-home-gantt-popover-title">Сортировка</div>
|
||||
{GANTT_SORT_OPTIONS.map((option) => {
|
||||
const isActive = sortMode === option.key;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={option.key}
|
||||
type="button"
|
||||
className={cn("nodedc-home-gantt-popover-option", {
|
||||
"nodedc-home-gantt-popover-option-active": isActive,
|
||||
})}
|
||||
onClick={() => setSortMode(option.key)}
|
||||
>
|
||||
<span className="nodedc-home-gantt-popover-option-left">
|
||||
<span className="nodedc-home-gantt-popover-check">
|
||||
{isActive && <Check className="size-3" />}
|
||||
</span>
|
||||
<span>{option.label}</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{activeFilterCount > 0 && (
|
||||
<button type="button" className="nodedc-home-gantt-popover-reset" onClick={resetFilters}>
|
||||
Сбросить фильтры
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-home-gantt-surface">
|
||||
<div className="nodedc-home-gantt-scroll" tabIndex={0} aria-label="Горизонтальная прокрутка окна Ганта">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="nodedc-home-gantt-scroll"
|
||||
tabIndex={0}
|
||||
aria-label="Горизонтальная прокрутка окна Ганта"
|
||||
>
|
||||
<div className="nodedc-home-gantt-canvas" style={{ width: `${timeline.canvasWidth}px` }}>
|
||||
<div className="nodedc-home-gantt-grid" style={{ width: `${timeline.timelineWidth}px` }} aria-hidden="true">
|
||||
{timeline.ticks.map((tick) => (
|
||||
|
|
@ -284,7 +527,7 @@ export function GanttTimelinePreview(props: TGanttTimelinePreviewProps) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cn("relative z-[1] space-y-3 pt-12", { "space-y-2": isCompactMode })}>
|
||||
<div className="relative z-[1] space-y-3 pt-12">
|
||||
{isLoading ? (
|
||||
Array.from({ length: 4 }, (_, index) => (
|
||||
<div key={index} className="h-12 animate-pulse rounded-[1.25rem] bg-white/5" />
|
||||
|
|
@ -293,13 +536,20 @@ export function GanttTimelinePreview(props: TGanttTimelinePreviewProps) {
|
|||
timeline.blocks.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn("nodedc-home-gantt-row", { "nodedc-home-gantt-row-compact": isCompactMode })}
|
||||
className="nodedc-home-gantt-row"
|
||||
style={{
|
||||
gridTemplateColumns: `${GANTT_LABEL_COLUMN_WIDTH}px ${timeline.timelineWidth}px`,
|
||||
}}
|
||||
>
|
||||
<div className="nodedc-home-gantt-row-label min-w-0">
|
||||
<div className="truncate text-12 font-semibold text-primary">{item.name}</div>
|
||||
<div
|
||||
className={cn("text-12 font-semibold text-primary", {
|
||||
"nodedc-home-gantt-row-name-full": showFullTaskName,
|
||||
truncate: !showFullTaskName,
|
||||
})}
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
<div className="mt-0.5 truncate text-11 text-placeholder">{item.identifier}</div>
|
||||
</div>
|
||||
<div className="nodedc-home-gantt-track" style={{ width: `${timeline.timelineWidth}px` }}>
|
||||
|
|
@ -326,7 +576,7 @@ export function GanttTimelinePreview(props: TGanttTimelinePreviewProps) {
|
|||
|
||||
{!isLoading && hiddenItemsCount > 0 && (
|
||||
<div className="nodedc-home-gantt-footnote">
|
||||
{hiddenItemsCount} задач без подходящего диапазона скрыто из календарного окна.
|
||||
{hiddenItemsCount} задач скрыто фильтрами или без подходящего диапазона.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -34,15 +34,26 @@ const getIssueResults = (response: unknown): TIssue[] => {
|
|||
return [];
|
||||
};
|
||||
|
||||
const getIssueStateGroup = (issue: TIssue): TGanttTimelinePreviewItem["state_group"] =>
|
||||
issue.state__group ??
|
||||
(issue as TIssue & { state_detail?: { group?: TGanttTimelinePreviewItem["state_group"] } }).state_detail?.group ??
|
||||
null;
|
||||
|
||||
const buildPreviewItems = (issues: TIssue[], project: THomeProjectData | undefined): TGanttTimelinePreviewItem[] =>
|
||||
issues.map((issue, index) => ({
|
||||
assignee_ids: issue.assignee_ids,
|
||||
completed_at: issue.completed_at,
|
||||
created_at: issue.created_at,
|
||||
created_by: issue.created_by,
|
||||
id: issue.id,
|
||||
identifier: project
|
||||
? `${project.identifier}-${issue.sequence_id ?? index + 1}`
|
||||
: `#${issue.sequence_id ?? index + 1}`,
|
||||
name: issue.name,
|
||||
priority: issue.priority,
|
||||
sort_order: issue.sort_order,
|
||||
start_date: issue.start_date,
|
||||
state_group: getIssueStateGroup(issue),
|
||||
target_date: issue.target_date,
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -1721,6 +1721,118 @@
|
|||
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-action-group {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-popover {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.55rem);
|
||||
right: 0;
|
||||
z-index: 12;
|
||||
min-width: 13.5rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 1.25rem;
|
||||
background: rgba(9, 9, 11, 0.96);
|
||||
padding: 0.38rem;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.04),
|
||||
0 18px 38px rgba(0, 0, 0, 0.28);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
backdrop-filter: blur(24px);
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-popover-wide {
|
||||
min-width: 18rem;
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-popover-section {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-popover-section + .nodedc-home-gantt-popover-section {
|
||||
margin-top: 0.45rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.055);
|
||||
padding-top: 0.45rem;
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-popover-title {
|
||||
padding: 0.35rem 0.7rem 0.25rem;
|
||||
color: var(--text-color-placeholder);
|
||||
font-size: 0.65rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-popover-option {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 2.35rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
border: 0 !important;
|
||||
outline: none !important;
|
||||
border-radius: 0.95rem !important;
|
||||
background: transparent !important;
|
||||
padding: 0.55rem 0.7rem;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 750;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-popover-option-left {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.58rem;
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-popover-option:hover,
|
||||
.nodedc-home-gantt-popover-option-active {
|
||||
background: rgba(var(--nodedc-card-active-rgb), 0.16) !important;
|
||||
color: var(--text-color-primary);
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-popover-check {
|
||||
display: grid;
|
||||
width: 1.1rem;
|
||||
min-width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
border-radius: 999px;
|
||||
color: rgb(var(--nodedc-card-active-rgb));
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-popover-option-active .nodedc-home-gantt-popover-check {
|
||||
border-color: rgb(var(--nodedc-card-active-rgb));
|
||||
background: rgba(var(--nodedc-card-active-rgb), 0.16);
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-popover-reset {
|
||||
width: 100%;
|
||||
min-height: 2.35rem;
|
||||
margin-top: 0.45rem;
|
||||
border: 0 !important;
|
||||
outline: none !important;
|
||||
border-radius: 0.95rem !important;
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
color: var(--text-color-primary);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-popover-reset:hover {
|
||||
background: rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-surface {
|
||||
position: relative;
|
||||
min-height: 23.5rem;
|
||||
|
|
@ -1868,6 +1980,13 @@
|
|||
min-height: 2.8rem;
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-row-name-full {
|
||||
overflow: visible;
|
||||
line-height: 1.2;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.nodedc-home-gantt-row-label {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue