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

This commit is contained in:
DCCONSTRUCTIONS 2026-05-01 12:13:47 +03:00
parent a7ab8ee123
commit d7260bdfce
10 changed files with 700 additions and 96 deletions

View File

@ -33,7 +33,7 @@ export const ExpandedProjectShellToolbarLayout = ({
return (
<div
className={cn("z-20 w-full flex-shrink-0 px-5 pt-4 pb-3", {
className={cn("nodedc-expanded-toolbar-shell w-full flex-shrink-0 px-5 pt-4 pb-3", {
"nodedc-home-top-toolbar": isWorkspaceHome,
})}
>

View File

@ -5,12 +5,11 @@
*/
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { createRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
startTransition(() => {
hydrateRoot(
document,
createRoot(document).render(
<StrictMode>
<HydratedRouter />
</StrictMode>

View File

@ -8,7 +8,7 @@ import type { CSSProperties, ReactNode } from "react";
import Script from "next/script";
import { Links, Meta, Outlet, Scripts } from "react-router";
import type { LinksFunction } from "react-router";
import { ThemeProvider, useTheme } from "next-themes";
import { ThemeProvider } from "next-themes";
// plane imports
import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants";
import { cn } from "@plane/utils";
@ -24,7 +24,6 @@ import globalStyles from "@/styles/globals.css?url";
import type { Route } from "./+types/root";
import designConfig from "../design.config.json";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
// local
import { CustomErrorComponent } from "./error";
import { AppProvider } from "./provider";
@ -192,16 +191,7 @@ export default function Root() {
}
export function HydrateFallback() {
const { resolvedTheme } = useTheme();
// if we are on the server or the theme is not resolved, return an empty div
if (typeof window === "undefined" || resolvedTheme === undefined) return <div />;
return (
<div className="relative flex h-screen w-full items-center justify-center bg-canvas">
<LogoSpinner />
</div>
);
return null;
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {

View File

@ -7,6 +7,7 @@
import { useEffect, useRef, useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
// plane imports
import { Row } from "@plane/ui";
// components
@ -25,8 +26,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 pathname = usePathname();
const { workspaceSlug } = useParams();
const { data: userProfile } = useUserProfile();
const isCompactToolbar = userProfile?.theme?.nodedcCompactToolbar === true;
const workspaceSlugValue = workspaceSlug?.toString();
const isWorkspaceHome =
!!workspaceSlugValue && (pathname === `/${workspaceSlugValue}` || pathname === `/${workspaceSlugValue}/`);
const effectiveDockStyle = isCompactToolbar
? dockStyle
: {
@ -49,10 +55,7 @@ export const AppHeader = observer(function AppHeader(props: AppHeaderProps) {
width,
});
document.documentElement.style.setProperty(
"--nodedc-bottom-dock-offset",
`${Math.max(height, 0)}px`
);
document.documentElement.style.setProperty("--nodedc-bottom-dock-offset", `${Math.max(height, 0)}px`);
};
updateDockBounds();
@ -73,9 +76,10 @@ export const AppHeader = observer(function AppHeader(props: AppHeaderProps) {
ref={containerRef}
className={cn(
"fixed bottom-0 z-[18]",
isCompactToolbar ? "right-0 nodedc-app-header-compact" : "nodedc-app-header-expanded",
isCompactToolbar ? "nodedc-app-header-compact right-0" : "nodedc-app-header-expanded",
className
)}
data-nodedc-footer-scrim={isWorkspaceHome ? "false" : "true"}
style={effectiveDockStyle}
>
<Row
@ -87,7 +91,7 @@ export const AppHeader = observer(function AppHeader(props: AppHeaderProps) {
<ExtendedAppHeader header={header} />
<div className="nodedc-bottom-dock-voice-slot" data-nodedc-voice-task-dock-slot />
</Row>
{mobileHeader && mobileHeader}
{mobileHeader ?? null}
</div>
);
});

View File

@ -35,6 +35,12 @@ import { IssueStats } from "@/plane-web/components/issues/issue-layouts/issue-st
// types
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
import { calculateIdentifierWidth } from "../utils";
import {
applyNodedcListPropertiesWidth,
NODEDC_LIST_PROPERTIES_WIDTH_CSS_VAR,
NODEDC_LIST_PROPERTIES_WIDTH_DEFAULT,
persistNodedcListPropertiesWidth,
} from "./list-properties-width";
import type { TRenderQuickActions } from "./list-view-types";
interface IssueBlockProps {
@ -157,6 +163,44 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
}
};
const handlePropertiesResizePointerDown = (event: React.PointerEvent<HTMLSpanElement>) => {
if (event.button !== 0 || typeof window === "undefined") return;
event.preventDefault();
event.stopPropagation();
const root = document.documentElement;
const computedWidth = Number.parseFloat(
getComputedStyle(root).getPropertyValue(NODEDC_LIST_PROPERTIES_WIDTH_CSS_VAR)
);
const initialWidth = Number.isFinite(computedWidth) ? computedWidth : NODEDC_LIST_PROPERTIES_WIDTH_DEFAULT;
const initialClientX = event.clientX;
let latestWidth = initialWidth;
const previousCursor = document.body.style.cursor;
const previousUserSelect = document.body.style.userSelect;
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
const handlePointerMove = (moveEvent: PointerEvent) => {
moveEvent.preventDefault();
latestWidth = applyNodedcListPropertiesWidth(initialWidth - (moveEvent.clientX - initialClientX));
};
const handlePointerUp = () => {
persistNodedcListPropertiesWidth(latestWidth);
document.body.style.cursor = previousCursor;
document.body.style.userSelect = previousUserSelect;
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
window.removeEventListener("pointercancel", handlePointerUp);
};
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
window.addEventListener("pointercancel", handlePointerUp);
};
// Calculate width for: projectIdentifier + "-" + dynamic sequence number digits
// Use next_work_item_sequence from backend (static value from project endpoint)
const maxSequenceId = currentProjectNextSequenceId ?? 1;
@ -184,7 +228,7 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
<Row
ref={issueRef}
className={cn(
"group/list-block relative flex min-h-11 flex-col gap-3 bg-layer-transparent py-3 text-13 transition-colors hover:bg-layer-transparent-hover",
"nodedc-list-work-item-row group/list-block relative flex min-h-11 flex-col gap-3 bg-layer-transparent py-3 text-13 transition-colors hover:bg-layer-transparent-hover",
{
"border-accent-strong": getIsIssuePeeked(issue.id) && peekIssue?.nestingLevel === nestingLevel,
"border-strong-1": isIssueActive,
@ -207,8 +251,19 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
}
}}
>
<div className="flex w-full gap-2 truncate">
<div className="flex flex-grow items-center gap-0.5 truncate">
<span
className="nodedc-list-properties-resize-handle"
role="separator"
aria-orientation="vertical"
aria-label="Resize list properties panel"
onPointerDown={handlePropertiesResizePointerDown}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
}}
/>
<div className="nodedc-list-work-item-main flex w-full gap-2 truncate">
<div className="flex min-w-0 flex-grow items-center gap-0.5 truncate">
<div className="flex items-center gap-1" style={isSubIssue ? { marginLeft } : {}}>
{/* select checkbox */}
{projectId && canSelectIssues && !isEpic && (
@ -282,7 +337,9 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
disabled={isCurrentBlockDragging}
renderByDefault={false}
>
<p className="cursor-pointer truncate text-body-xs-medium text-primary">{issue.name}</p>
<p className="nodedc-list-work-item-title cursor-pointer truncate text-body-xs-medium text-primary">
{issue.name}
</p>
</Tooltip>
{isEpic && displayProperties && (
<WithDisplayPropertiesHOC
@ -308,11 +365,11 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
</div>
)}
</div>
<div className="flex flex-shrink-0 items-center gap-2">
<div className="nodedc-list-work-item-side flex flex-shrink-0 items-center gap-2">
{!issue?.tempId ? (
<>
<IssueProperties
className={`relative flex flex-wrap ${isSidebarCollapsed ? "md:flex-shrink-0 md:flex-grow" : "lg:flex-shrink-0 lg:flex-grow"} items-center gap-2 whitespace-nowrap`}
className={`nodedc-list-work-item-properties relative flex flex-wrap ${isSidebarCollapsed ? "md:flex-shrink-0 md:flex-grow" : "lg:flex-shrink-0 lg:flex-grow"} items-center gap-2 whitespace-nowrap`}
issue={issue}
isReadOnly={!canEditIssueProperties}
updateIssue={updateIssue}

View File

@ -34,6 +34,7 @@ import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-sta
import type { GroupDropLocation } from "../utils";
import { getGroupByColumns, isWorkspaceLevel, isSubGrouped } from "../utils";
import { ListGroup } from "./list-group";
import { applyStoredNodedcListPropertiesWidth } from "./list-properties-width";
import type { TRenderQuickActions } from "./list-view-types";
export interface IList {
@ -107,6 +108,17 @@ export const List = observer(function List(props: IList) {
);
}, [containerRef]);
useEffect(() => {
if (typeof window === "undefined") return;
const syncPropertiesWidth = () => applyStoredNodedcListPropertiesWidth();
syncPropertiesWidth();
window.addEventListener("resize", syncPropertiesWidth);
return () => window.removeEventListener("resize", syncPropertiesWidth);
}, []);
if (!groups) return null;
const getGroupIndex = (groupId: string | undefined) => groups.findIndex(({ id }) => id === groupId);

View File

@ -252,6 +252,12 @@ export const ListGroup = observer(function ListGroup(props: Props) {
const isGroupByCreatedBy = group_by === "created_by";
const shouldExpand = (!!groupIssueCount && isExpanded) || !group_by;
const shouldShowQuickAdd =
enableIssueQuickAdd &&
!disableIssueCreation &&
!isGroupByCreatedBy &&
!isCompletedCycle &&
!isWorkflowIssueCreationDisabled;
return validateEmptyIssueGroups(groupIssueCount) ? (
<div
@ -295,51 +301,49 @@ export const ListGroup = observer(function ListGroup(props: Props) {
isDraggingOverColumn={isDraggingOverColumn}
isEpic={isEpic}
/>
{groupIssueIds && (
<IssueBlocksList
issueIds={groupIssueIds}
groupId={group.id}
issuesMap={issuesMap}
updateIssue={updateIssue}
quickActions={quickActions}
displayProperties={displayProperties}
canEditProperties={canEditProperties}
containerRef={containerRef}
isDragAllowed={isDragAllowed}
canDropOverIssue={!canOverlayBeVisible}
selectionHelpers={selectionHelpers}
isEpic={isEpic}
/>
)}
{shouldLoadMore &&
(group_by ? (
<>{loadMore}</>
) : (
<>
{Array.from({ length: 2 }).map((_, index) => (
<ListLoaderItemRow key={index} />
))}
<ListLoaderItemRow ref={setIntersectionElement} />
</>
))}
{enableIssueQuickAdd &&
!disableIssueCreation &&
!isGroupByCreatedBy &&
!isCompletedCycle &&
!isWorkflowIssueCreationDisabled && (
<div className="nodedc-bottom-dock-sticky-offset sticky z-[1] w-full flex-shrink-0">
<QuickAddIssueRoot
layout={EIssueLayoutTypes.LIST}
QuickAddButton={ListQuickAddIssueButton}
prePopulatedData={prePopulateQuickAddData(group_by, group.id)}
containerClassName="border-b border-t border-subtle bg-surface-1 "
quickAddCallback={quickAddCallback}
isEpic={isEpic}
/>
</div>
<div className={cn({ "nodedc-bottom-dock-aware-padding": shouldShowQuickAdd })}>
{groupIssueIds && (
<IssueBlocksList
issueIds={groupIssueIds}
groupId={group.id}
issuesMap={issuesMap}
updateIssue={updateIssue}
quickActions={quickActions}
displayProperties={displayProperties}
canEditProperties={canEditProperties}
containerRef={containerRef}
isDragAllowed={isDragAllowed}
canDropOverIssue={!canOverlayBeVisible}
selectionHelpers={selectionHelpers}
isEpic={isEpic}
/>
)}
{shouldLoadMore &&
(group_by ? (
<>{loadMore}</>
) : (
<>
{Array.from({ length: 2 }).map((_, index) => (
<ListLoaderItemRow key={index} />
))}
<ListLoaderItemRow ref={setIntersectionElement} />
</>
))}
</div>
{shouldShowQuickAdd && (
<div className="nodedc-list-quick-add-sticky nodedc-bottom-dock-sticky-offset sticky z-[2] w-full flex-shrink-0 bg-transparent py-0.5">
<QuickAddIssueRoot
layout={EIssueLayoutTypes.LIST}
QuickAddButton={ListQuickAddIssueButton}
prePopulatedData={prePopulateQuickAddData(group_by, group.id)}
containerClassName="border-b border-t border-subtle bg-surface-1 "
quickAddCallback={quickAddCallback}
isEpic={isEpic}
/>
</div>
)}
</div>
)}
</div>

View File

@ -0,0 +1,46 @@
export const NODEDC_LIST_PROPERTIES_WIDTH_STORAGE_KEY = "nodedc_project_list_properties_width";
export const NODEDC_LIST_PROPERTIES_WIDTH_CSS_VAR = "--nodedc-list-properties-panel-width";
export const NODEDC_LIST_PROPERTIES_WIDTH_DEFAULT = 1024;
export const NODEDC_LIST_PROPERTIES_WIDTH_MIN = 760;
export const NODEDC_LIST_PROPERTIES_WIDTH_MAX = 1280;
const getRuntimeMaxWidth = () => {
if (typeof window === "undefined") return NODEDC_LIST_PROPERTIES_WIDTH_MAX;
return Math.max(
NODEDC_LIST_PROPERTIES_WIDTH_MIN,
Math.min(NODEDC_LIST_PROPERTIES_WIDTH_MAX, window.innerWidth - 420)
);
};
export const clampNodedcListPropertiesWidth = (width: number) => {
const maxWidth = getRuntimeMaxWidth();
const normalizedWidth = Number.isFinite(width) ? width : NODEDC_LIST_PROPERTIES_WIDTH_DEFAULT;
return Math.min(Math.max(normalizedWidth, NODEDC_LIST_PROPERTIES_WIDTH_MIN), maxWidth);
};
export const applyNodedcListPropertiesWidth = (width: number) => {
if (typeof document === "undefined") return width;
const clampedWidth = clampNodedcListPropertiesWidth(width);
document.documentElement.style.setProperty(NODEDC_LIST_PROPERTIES_WIDTH_CSS_VAR, `${clampedWidth}px`);
return clampedWidth;
};
export const applyStoredNodedcListPropertiesWidth = () => {
if (typeof window === "undefined") return NODEDC_LIST_PROPERTIES_WIDTH_DEFAULT;
const storedWidth = Number(window.localStorage.getItem(NODEDC_LIST_PROPERTIES_WIDTH_STORAGE_KEY));
return applyNodedcListPropertiesWidth(
Number.isFinite(storedWidth) && storedWidth > 0 ? storedWidth : NODEDC_LIST_PROPERTIES_WIDTH_DEFAULT
);
};
export const persistNodedcListPropertiesWidth = (width: number) => {
if (typeof window === "undefined") return;
window.localStorage.setItem(NODEDC_LIST_PROPERTIES_WIDTH_STORAGE_KEY, String(clampNodedcListPropertiesWidth(width)));
};

View File

@ -59,7 +59,7 @@ export interface IIssueProperties {
}
export const IssueProperties = observer(function IssueProperties(props: IIssueProperties) {
const { issue, updateIssue, displayProperties, isReadOnly, className, isEpic = false } = props;
const { issue, updateIssue, displayProperties, isReadOnly, className, activeLayout, isEpic = false } = props;
// i18n
const { t } = useTranslation();
// store hooks
@ -175,6 +175,15 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
const redirectToIssueDetail = () => router.push(`${workItemLink}#sub-issues`);
if (!displayProperties || !issue.project_id) return null;
const isListLayout = activeLayout === "List";
const propertySlotClassName = (slot: string, ...classNames: (string | false | null | undefined)[]) =>
cn(
"h-5",
isListLayout && "nodedc-list-property-slot",
isListLayout && `nodedc-list-property-${slot}`,
...classNames
);
const listIconControlClassName = isListLayout ? "nodedc-list-icon-control" : undefined;
// date range is enabled only when both dates are available and both dates are enabled
const isDateRangeEnabled: boolean = Boolean(
@ -196,7 +205,11 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
{/* basic properties */}
{/* state */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="state">
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<div
className={propertySlotClassName("state")}
onFocus={handleEventPropagation}
onClick={handleEventPropagation}
>
<StateDropdown
buttonContainerClassName="truncate max-w-40"
value={issue.state_id}
@ -212,12 +225,17 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
{/* priority */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="priority">
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<div
className={propertySlotClassName("priority", isListLayout && "nodedc-list-property-icon-only")}
onFocus={handleEventPropagation}
onClick={handleEventPropagation}
>
<PriorityDropdown
value={issue?.priority}
onChange={handlePriority}
disabled={isReadOnly}
buttonVariant="border-without-text"
buttonContainerClassName={listIconControlClassName}
renderByDefault={isMobile}
showTooltip
/>
@ -230,7 +248,11 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
displayPropertyKey={["start_date", "due_date"]}
shouldRenderProperty={() => isDateRangeEnabled}
>
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<div
className={propertySlotClassName("date-range")}
onFocus={handleEventPropagation}
onClick={handleEventPropagation}
>
<DateRangeDropdown
value={{
from: getDate(issue.start_date) || undefined,
@ -255,6 +277,7 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
showTooltip
renderPlaceholder={false}
customTooltipHeading="Date Range"
buttonContainerClassName={isListLayout ? "nodedc-list-valued-date-control" : undefined}
/>
</div>
</WithDisplayPropertiesHOC>
@ -265,7 +288,14 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
displayPropertyKey="start_date"
shouldRenderProperty={() => !isDateRangeEnabled}
>
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<div
className={propertySlotClassName(
"start-date",
isListLayout && (issue.start_date ? "nodedc-list-property-valued-date" : "nodedc-list-property-icon-only")
)}
onFocus={handleEventPropagation}
onClick={handleEventPropagation}
>
<DateDropdown
value={issue.start_date ?? null}
rangePreview={{
@ -277,6 +307,7 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
placeholder={t("common.order_by.start_date")}
icon={<StartDatePropertyIcon className="h-3 w-3 flex-shrink-0" />}
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
buttonContainerClassName={issue.start_date ? undefined : listIconControlClassName}
optionsClassName="z-10"
disabled={isReadOnly}
renderByDefault={isMobile}
@ -292,7 +323,14 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
displayPropertyKey="due_date"
shouldRenderProperty={() => !isDateRangeEnabled}
>
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<div
className={propertySlotClassName(
"due-date",
isListLayout && (issue.target_date ? "nodedc-list-property-valued-date" : "nodedc-list-property-icon-only")
)}
onFocus={handleEventPropagation}
onClick={handleEventPropagation}
>
<DateDropdown
value={issue?.target_date ?? null}
rangePreview={{
@ -304,6 +342,7 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
placeholder={t("common.order_by.due_date")}
icon={<DueDatePropertyIcon className="h-3 w-3 shrink-0" />}
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
buttonContainerClassName={issue.target_date ? undefined : listIconControlClassName}
buttonClassName={
shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group) ? "text-danger-primary" : ""
}
@ -319,7 +358,11 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
{/* assignee */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="assignee">
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<div
className={propertySlotClassName("assignee")}
onFocus={handleEventPropagation}
onClick={handleEventPropagation}
>
<MemberDropdown
projectId={issue?.project_id}
value={issue?.assignee_ids}
@ -343,7 +386,11 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
{/* modules */}
{projectDetails?.module_view && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="modules">
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<div
className={propertySlotClassName("module")}
onFocus={handleEventPropagation}
onClick={handleEventPropagation}
>
<ModuleDropdown
buttonContainerClassName="truncate max-w-40"
projectId={issue?.project_id}
@ -363,7 +410,11 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
{/* cycles */}
{projectDetails?.cycle_view && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="cycle">
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<div
className={propertySlotClassName("cycle")}
onFocus={handleEventPropagation}
onClick={handleEventPropagation}
>
<CycleDropdown
buttonContainerClassName="truncate max-w-40"
projectId={issue?.project_id}
@ -384,7 +435,11 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
{/* estimates */}
{projectId && areEstimateEnabledByProjectId(projectId?.toString()) && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="estimate">
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<div
className={propertySlotClassName("estimate")}
onFocus={handleEventPropagation}
onClick={handleEventPropagation}
>
<EstimateDropdown
value={issue.estimate_point ?? undefined}
onChange={handleEstimate}
@ -421,6 +476,7 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
}}
className={cn(
"flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded-sm border-[0.5px] border-strong px-2.5 py-1",
isListLayout && "nodedc-list-property-slot nodedc-list-property-sub-issues",
{
"cursor-pointer hover:bg-layer-1": subIssueCount,
}
@ -446,7 +502,10 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
renderByDefault={false}
>
<div
className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded-sm border-[0.5px] border-strong px-2.5 py-1"
className={cn(
"flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded-sm border-[0.5px] border-strong px-2.5 py-1",
isListLayout && "nodedc-list-property-slot nodedc-list-property-attachments"
)}
onFocus={handleEventPropagation}
onClick={handleEventPropagation}
>
@ -469,7 +528,10 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
renderByDefault={false}
>
<div
className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded-sm border-[0.5px] border-strong px-2.5 py-1"
className={cn(
"flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded-sm border-[0.5px] border-strong px-2.5 py-1",
isListLayout && "nodedc-list-property-slot nodedc-list-property-links"
)}
onFocus={handleEventPropagation}
onClick={handleEventPropagation}
>
@ -484,16 +546,22 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr
{/* label */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="labels">
<IssuePropertyLabels
projectId={issue?.project_id || null}
value={issue?.label_ids || []}
defaultOptions={defaultLabelOptions}
onChange={handleLabel}
disabled={isReadOnly}
renderByDefault={isMobile}
hideDropdownArrow
maxRender={3}
/>
<div
className={propertySlotClassName("labels")}
onFocus={handleEventPropagation}
onClick={handleEventPropagation}
>
<IssuePropertyLabels
projectId={issue?.project_id || null}
value={issue?.label_ids || []}
defaultOptions={defaultLabelOptions}
onChange={handleLabel}
disabled={isReadOnly}
renderByDefault={isMobile}
hideDropdownArrow
maxRender={3}
/>
</div>
</WithDisplayPropertiesHOC>
</div>
);

View File

@ -49,6 +49,13 @@
--nodedc-bottom-dock-height: 2.75rem;
--nodedc-bottom-dock-offset: 2.75rem;
--nodedc-bottom-dock-visual-overlap: 0.625rem;
--nodedc-expanded-footer-height: 4.95rem;
--nodedc-active-footer-offset: var(--nodedc-bottom-dock-offset, var(--nodedc-bottom-dock-height, 2.75rem));
--nodedc-list-property-chip-height: 2.12rem;
--nodedc-list-property-avatar-size: 1.9rem;
--nodedc-list-property-icon-size: var(--nodedc-list-property-chip-height);
--nodedc-list-properties-panel-width: 64rem;
--nodedc-list-quick-actions-width: 3.15rem;
--nodedc-quick-add-reserve: 2.5rem;
--brand-default: rgb(var(--nodedc-accent-rgb));
--brand-300: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 65%, white);
@ -425,6 +432,30 @@
padding-bottom: calc(var(--nodedc-quick-add-reserve, 2.5rem) + 0.5rem);
}
body:has(.nodedc-expanded-toolbar):not(:has(.nodedc-home-top-toolbar)) {
--nodedc-active-footer-offset: var(--nodedc-expanded-footer-height);
}
body:has(.nodedc-expanded-toolbar):not(:has(.nodedc-home-top-toolbar))::after {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 40;
height: var(--nodedc-expanded-footer-height);
background: var(--bg-surface-1);
border-top: 0;
box-shadow: none;
-webkit-backdrop-filter: none;
backdrop-filter: none;
content: "";
pointer-events: none;
}
main:has(.nodedc-expanded-toolbar) .nodedc-bottom-dock-aware-padding {
padding-bottom: calc(var(--nodedc-expanded-footer-height) + var(--nodedc-quick-add-reserve, 2.5rem));
}
.nodedc-bottom-dock-sticky-offset {
bottom: max(
calc(
@ -435,6 +466,24 @@
);
}
.nodedc-list-quick-add-sticky {
bottom: var(--nodedc-active-footer-offset);
}
.nodedc-app-header-compact {
isolation: isolate;
}
.nodedc-app-header-compact[data-nodedc-footer-scrim="true"]::before {
display: none;
content: none;
}
.nodedc-app-header-compact > * {
position: relative;
z-index: 1;
}
.nodedc-bottom-dock [class~="bg-surface-1"] {
background: transparent !important;
}
@ -446,6 +495,376 @@
border-color: transparent !important;
}
.nodedc-list-work-item-row {
border-color: rgba(255, 255, 255, 0.055) !important;
padding-inline: 1.45rem 1.7rem !important;
padding-block: 0.28rem !important;
}
.nodedc-list-work-item-main,
.nodedc-list-work-item-side {
min-width: 0;
}
.nodedc-list-work-item-title {
line-height: 1.45;
}
.nodedc-list-properties-resize-handle {
display: none;
}
.nodedc-list-work-item-properties {
display: grid !important;
grid-template-columns:
7.9rem
var(--nodedc-list-property-icon-size)
var(--nodedc-list-property-icon-size)
9rem
4rem
minmax(17rem, max-content)
5.2rem
5.2rem
var(--nodedc-list-property-icon-size)
var(--nodedc-list-property-icon-size)
var(--nodedc-list-property-icon-size)
var(--nodedc-list-property-icon-size);
grid-auto-flow: row;
justify-content: start;
align-items: center;
column-gap: 0.28rem !important;
row-gap: 0.35rem !important;
width: var(--nodedc-list-properties-panel-width);
max-width: var(--nodedc-list-properties-panel-width);
justify-self: end;
min-width: 0;
}
.nodedc-list-property-slot {
display: inline-flex;
height: var(--nodedc-list-property-chip-height) !important;
width: 100%;
min-width: 0;
align-items: center;
justify-content: center;
}
.nodedc-list-property-state {
grid-column: 1;
}
.nodedc-list-property-priority {
grid-column: 2;
}
.nodedc-list-property-start-date {
grid-column: 3;
}
.nodedc-list-property-due-date {
grid-column: 4;
}
.nodedc-list-property-date-range {
grid-column: 3 / 5;
width: max-content;
justify-self: center;
}
.nodedc-list-property-assignee {
grid-column: 5;
justify-content: center;
}
.nodedc-list-property-labels {
grid-column: 6;
width: max-content;
gap: 0.35rem;
overflow: visible;
justify-content: flex-start;
justify-self: start;
}
.nodedc-list-property-labels > * {
flex: 0 0 auto;
min-width: max-content;
max-width: none;
}
.nodedc-list-property-labels [class~="line-clamp-1"][class~="truncate"] {
max-width: none !important;
overflow: visible !important;
text-overflow: clip !important;
white-space: nowrap !important;
}
.nodedc-list-property-module {
grid-column: 7;
}
.nodedc-list-property-cycle {
grid-column: 8;
}
.nodedc-list-property-estimate {
grid-column: 9;
}
.nodedc-list-property-icon-only {
width: var(--nodedc-list-property-icon-size);
min-width: var(--nodedc-list-property-icon-size);
justify-content: center;
justify-self: center;
}
.nodedc-list-property-valued-date {
width: max-content;
min-width: var(--nodedc-list-property-icon-size);
justify-content: center;
justify-self: center;
}
.nodedc-list-property-start-date.nodedc-list-property-valued-date {
grid-column: 3 / 5;
}
.nodedc-list-property-sub-issues {
grid-column: 10;
justify-content: center;
min-width: var(--nodedc-list-property-icon-size);
width: max-content;
}
.nodedc-list-property-attachments {
grid-column: 11;
justify-content: center;
min-width: var(--nodedc-list-property-icon-size);
width: max-content;
}
.nodedc-list-property-links {
grid-column: 12;
justify-content: center;
min-width: var(--nodedc-list-property-icon-size);
width: max-content;
}
.nodedc-list-work-item-properties button,
.nodedc-list-work-item-properties [role="button"],
.nodedc-list-work-item-properties .nodedc-list-property-slot,
.nodedc-list-work-item-properties [class~="border-strong"],
.nodedc-list-work-item-properties [class~="border-[0.5px]"] {
border-width: 0 !important;
border-color: transparent !important;
outline: none !important;
box-shadow: none !important;
}
.nodedc-list-property-slot > button,
.nodedc-list-property-slot > div,
.nodedc-list-property-slot [role="button"] {
min-height: var(--nodedc-list-property-chip-height) !important;
min-width: var(--nodedc-list-property-icon-size) !important;
border-radius: 999px !important;
}
.nodedc-list-property-icon-only button,
.nodedc-list-property-icon-only > div,
.nodedc-list-property-icon-only [role="button"],
.nodedc-list-icon-control {
width: var(--nodedc-list-property-icon-size) !important;
min-width: var(--nodedc-list-property-icon-size) !important;
height: var(--nodedc-list-property-icon-size) !important;
min-height: var(--nodedc-list-property-icon-size) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
border-radius: 999px !important;
padding-inline: 0 !important;
}
.nodedc-list-property-icon-only > div > button,
.nodedc-list-property-icon-only > div > [role="button"],
.nodedc-list-icon-control > button,
.nodedc-list-icon-control > div,
.nodedc-list-icon-control > [role="button"] {
width: var(--nodedc-list-property-icon-size) !important;
min-width: var(--nodedc-list-property-icon-size) !important;
height: var(--nodedc-list-property-icon-size) !important;
min-height: var(--nodedc-list-property-icon-size) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
border-radius: 999px !important;
padding-inline: 0 !important;
}
.nodedc-list-property-priority svg {
transform: none !important;
}
.nodedc-list-valued-date-control {
min-width: var(--nodedc-list-property-icon-size) !important;
}
.nodedc-list-work-item-properties [class~="border-strong"],
.nodedc-list-work-item-properties [class~="border-[0.5px]"],
.nodedc-list-work-item-properties [class~="bg-layer-3"],
.nodedc-list-work-item-properties [class~="bg-layer-transparent-active"] {
border-radius: 999px !important;
background: rgba(255, 255, 255, 0.07) !important;
}
.nodedc-list-work-item-properties button:hover [class~="border-strong"],
.nodedc-list-work-item-properties button:hover [class~="border-[0.5px]"],
.nodedc-list-work-item-properties button:hover [class~="bg-layer-3"] {
background: rgba(255, 255, 255, 0.1) !important;
}
.nodedc-list-work-item-properties .nodedc-list-property-slot [class~="h-5"][class~="w-5"],
.nodedc-list-work-item-properties .nodedc-list-property-slot [class~="h-4"][class~="w-4"] {
width: var(--nodedc-list-property-avatar-size) !important;
height: var(--nodedc-list-property-avatar-size) !important;
}
.nodedc-list-work-item-properties .nodedc-list-property-slot [class~="border-subtle-1"] {
border-width: 0 !important;
}
.nodedc-list-work-item-properties svg[class~="h-3"][class~="w-3"] {
width: 1rem !important;
height: 1rem !important;
}
.nodedc-list-work-item-properties svg[class~="h-3.5"][class~="w-3.5"] {
width: 1.05rem !important;
height: 1.05rem !important;
}
.nodedc-list-work-item-properties button:focus,
.nodedc-list-work-item-properties button:focus-visible,
.nodedc-list-work-item-properties [role="button"]:focus,
.nodedc-list-work-item-properties [role="button"]:focus-visible {
outline: none !important;
box-shadow:
inset 0 0 0 1px rgba(var(--nodedc-accent-rgb), 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.025) !important;
}
@media (min-width: 1024px) {
.nodedc-list-work-item-row {
display: grid !important;
grid-template-columns:
minmax(18rem, 1fr)
minmax(
calc(var(--nodedc-list-properties-panel-width) + var(--nodedc-list-quick-actions-width)),
calc(var(--nodedc-list-properties-panel-width) + var(--nodedc-list-quick-actions-width))
);
align-items: center;
column-gap: 1.5rem !important;
}
.nodedc-list-properties-resize-handle {
position: absolute;
top: 0.35rem;
bottom: 0.35rem;
right: calc(var(--nodedc-list-properties-panel-width) + var(--nodedc-list-quick-actions-width) + 2.45rem);
z-index: 5;
display: block;
width: 0.95rem;
cursor: col-resize;
touch-action: none;
}
.nodedc-list-properties-resize-handle::before {
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: 3px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.38);
box-shadow: none;
content: "";
transform: translateX(-50%);
transition:
background-color 120ms ease,
box-shadow 120ms ease;
}
.nodedc-list-work-item-row:hover .nodedc-list-properties-resize-handle::before,
.nodedc-list-properties-resize-handle:hover::before {
background: rgba(0, 0, 0, 0.55);
box-shadow: none;
}
.nodedc-list-work-item-main {
width: auto !important;
}
.nodedc-list-work-item-side {
display: grid !important;
grid-template-columns: minmax(0, var(--nodedc-list-properties-panel-width)) max-content;
justify-self: stretch;
width: 100%;
}
}
@media (max-width: 1280px) {
.nodedc-list-work-item-row {
grid-template-columns:
minmax(16rem, 1fr)
minmax(
calc(var(--nodedc-list-properties-panel-width) + var(--nodedc-list-quick-actions-width)),
calc(var(--nodedc-list-properties-panel-width) + var(--nodedc-list-quick-actions-width))
);
}
.nodedc-list-work-item-properties {
grid-template-columns:
7.2rem
var(--nodedc-list-property-icon-size)
var(--nodedc-list-property-icon-size)
8.1rem
3.6rem
minmax(13rem, max-content)
4.6rem
4.6rem
var(--nodedc-list-property-icon-size)
var(--nodedc-list-property-icon-size)
var(--nodedc-list-property-icon-size)
var(--nodedc-list-property-icon-size);
column-gap: 0.22rem !important;
}
}
@media (max-width: 1023px) {
.nodedc-list-work-item-properties {
display: flex !important;
min-width: 0;
width: auto;
max-width: 100%;
justify-content: flex-start;
}
.nodedc-list-property-state,
.nodedc-list-property-priority,
.nodedc-list-property-start-date,
.nodedc-list-property-due-date,
.nodedc-list-property-date-range,
.nodedc-list-property-assignee,
.nodedc-list-property-module,
.nodedc-list-property-cycle,
.nodedc-list-property-labels,
.nodedc-list-property-estimate,
.nodedc-list-property-sub-issues,
.nodedc-list-property-attachments,
.nodedc-list-property-links {
grid-column: auto;
width: auto;
}
}
.nodedc-bottom-dock-left {
max-width: min(22rem, 26vw) !important;
flex-grow: 0 !important;
@ -788,6 +1207,11 @@
gap: 0;
}
.nodedc-expanded-toolbar-shell {
position: relative;
z-index: 80;
}
.nodedc-expanded-toolbar-top {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
@ -872,7 +1296,7 @@
.nodedc-expanded-main-tool-cluster,
.nodedc-expanded-action-tool-cluster {
position: fixed;
z-index: 80;
z-index: 120;
display: inline-flex;
min-height: 3rem;
align-items: center;
@ -1000,7 +1424,7 @@
min-width: 0;
justify-content: flex-start;
position: fixed;
z-index: 80;
z-index: 120;
left: 1.85rem;
bottom: 1.1rem;
pointer-events: auto;