UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: выправка дизайна views аналитики и рабочих экранов

This commit is contained in:
DCCONSTRUCTIONS 2026-04-22 16:09:30 +03:00
parent 3f6219fc50
commit aa9c278b59
46 changed files with 455 additions and 192 deletions

View File

@ -74,20 +74,20 @@ function AnalyticsPage({ params }: Route.ComponentProps) {
{workspaceProjectIds.length > 0 || loader === "init-loader" ? (
<div className="flex h-full overflow-hidden">
<Tabs value={selectedTab} onValueChange={handleTabChange} className="h-full w-full">
<div className={"flex h-full w-full flex-col"}>
<div className="nodedc-workspace-page-shell flex h-full w-full flex-col gap-4 overflow-hidden">
<div
className={cn(
"flex w-full items-center justify-between gap-4 overflow-hidden border-b border-subtle bg-surface-1 px-6 py-2"
"nodedc-workspace-toolbar flex w-full flex-wrap items-center justify-between gap-3 overflow-hidden px-4 py-3 md:flex-nowrap md:px-5"
)}
>
<Tabs.List className={"flex h-7 w-fit overflow-x-auto"}>
<Tabs.List className="flex h-9 w-fit max-w-full overflow-x-auto rounded-full bg-white/6 p-1">
{ANALYTICS_TABS.map((tab) => (
<Tabs.Trigger
key={tab.key}
value={tab.key}
disabled={tab.isDisabled}
size="md"
className="h-6 px-3"
className="h-7 rounded-full px-3.5"
onClick={() => {
if (!tab.isDisabled) {
handleTabChange(tab.key);
@ -107,7 +107,7 @@ function AnalyticsPage({ params }: Route.ComponentProps) {
<Tabs.Content
key={tab.key}
value={tab.key}
className={"h-full overflow-hidden overflow-y-auto px-2"}
className="h-full overflow-hidden overflow-y-auto px-0"
>
<tab.content />
</Tabs.Content>

View File

@ -17,7 +17,9 @@ function WorkspaceDraftPage({ params }: Route.ComponentProps) {
<>
<PageHead title={pageTitle} />
<div className="relative h-full w-full overflow-hidden overflow-y-auto">
<WorkspaceDraftIssuesRoot workspaceSlug={workspaceSlug} />
<div className="nodedc-workspace-page-shell h-full">
<WorkspaceDraftIssuesRoot workspaceSlug={workspaceSlug} />
</div>
</div>
</>
);

View File

@ -13,7 +13,9 @@ export default function WorkspaceStickiesPage() {
<>
<PageHead title="Your stickies" />
<div className="relative h-full w-full overflow-hidden overflow-y-auto">
<StickiesInfinite />
<div className="nodedc-workspace-page-shell h-full">
<StickiesInfinite />
</div>
</div>
</>
);

View File

@ -15,13 +15,13 @@ import {
DEFAULT_GLOBAL_VIEWS_LIST,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { ViewsIcon } from "@plane/propel/icons";
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, ICustomSearchSelectOption } from "@plane/types";
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
import { Breadcrumbs, Header, BreadcrumbNavigationSearchDropdown } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { AppHeaderPrimaryActionButton } from "@/components/core/app-header/primary-action-button";
import { SwitcherLabel } from "@/components/common/switcher-label";
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
import { WorkItemFiltersToggle } from "@/components/work-item-filters/filters-toggle";
@ -170,14 +170,12 @@ export const GlobalIssuesHeader = observer(function GlobalIssuesHeader() {
/>
</FiltersDropdown>
)}
<Button
variant="primary"
size="lg"
<AppHeaderPrimaryActionButton
data-ph-element={GLOBAL_VIEW_TRACKER_ELEMENTS.RIGHT_HEADER_ADD_BUTTON}
onClick={() => setCreateViewModal(true)}
>
{t("workspace_views.add_view")}
</Button>
</AppHeaderPrimaryActionButton>
<div className="hidden md:block">
{viewDetails && <WorkspaceViewQuickActions workspaceSlug={workspaceSlug?.toString()} view={viewDetails} />}
{isDefaultView && defaultViewDetails && (

View File

@ -30,23 +30,25 @@ function WorkspaceViewsPage() {
<>
<PageHead title={pageTitle} />
<div className="flex h-full w-full flex-col overflow-hidden">
<div className="flex h-11 w-full items-center gap-2.5 overflow-hidden border-b border-subtle px-5 py-3">
<SearchIcon className="text-secondary" width={14} height={14} strokeWidth={2} />
<Input
className="w-full bg-transparent !p-0 text-11 leading-5 text-secondary placeholder:text-placeholder focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
mode="true-transparent"
/>
</div>
<div className="vertical-scrollbar flex scrollbar-lg h-full w-full flex-col">
{DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => t(v.i18n_label).toLowerCase().includes(query.toLowerCase())).map(
(option) => (
<GlobalDefaultViewListItem key={option.key} view={option} />
)
)}
<GlobalViewsList searchQuery={query} />
<div className="nodedc-workspace-page-shell flex h-full w-full flex-col gap-4 overflow-hidden">
<div className="nodedc-workspace-toolbar flex min-h-14 w-full items-center gap-3 overflow-hidden px-4 py-3 md:px-5">
<SearchIcon className="text-secondary" width={14} height={14} strokeWidth={2} />
<Input
className="w-full bg-transparent !p-0 text-13 leading-5 text-primary placeholder:text-placeholder focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search workspace views"
mode="true-transparent"
/>
</div>
<div className="vertical-scrollbar flex scrollbar-lg h-full w-full flex-col gap-3 overflow-y-auto pr-1">
{DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => t(v.i18n_label).toLowerCase().includes(query.toLowerCase())).map(
(option) => (
<GlobalDefaultViewListItem key={option.key} view={option} />
)
)}
<GlobalViewsList searchQuery={query} />
</div>
</div>
</div>
</>

View File

@ -16,14 +16,14 @@ type Props = {
};
function AnalyticsSectionWrapper(props: Props) {
const { title, children, className, actions, headerClassName } = props;
const { title, children, className, actions, headerClassName, subtitle } = props;
return (
<div className={className}>
<div className={cn("mb-6 flex items-center gap-2 text-nowrap", headerClassName)}>
<div className={cn("nodedc-external-section rounded-[1.7rem] px-5 py-5 md:px-6 md:py-6", className)}>
<div className={cn("mb-5 flex flex-wrap items-start justify-between gap-3", headerClassName)}>
{title && (
<div className="flex items-center gap-2">
<h1 className={"text-16 font-medium"}>{title}</h1>
{/* {subtitle && <p className="text-16 text-tertiary"> • {subtitle}</p>} */}
<div className="flex flex-col gap-1">
<h1 className="text-16 font-semibold text-primary">{title}</h1>
{subtitle ? <p className="text-12 text-secondary">{subtitle}</p> : null}
</div>
)}
{actions}

View File

@ -19,8 +19,13 @@ function AnalyticsWrapper(props: Props) {
const { i18nTitle, children, className } = props;
const { t } = useTranslation();
return (
<div className={cn("px-6 py-4", className)}>
<h1 className={"mb-4 text-20 font-bold md:mb-6"}>{t(i18nTitle)}</h1>
<div className={cn("flex flex-col gap-4 pb-5", className)}>
<div className="nodedc-external-panel flex items-center justify-between gap-3 px-5 py-4 md:px-6">
<div className="flex flex-col gap-1">
<span className="text-11 font-medium tracking-[0.24em] text-secondary uppercase">Analytics</span>
<h1 className="text-20 font-semibold text-primary md:text-[1.45rem]">{t(i18nTitle)}</h1>
</div>
</div>
{children}
</div>
);

View File

@ -20,11 +20,11 @@ function InsightCard(props: InsightCardProps) {
const count = data?.count ?? 0;
return (
<div className="flex flex-col gap-3">
<div className="text-13 text-tertiary">{label}</div>
<div className="nodedc-workspace-stat-card flex min-h-[7.5rem] flex-col justify-between gap-3 px-4 py-4 md:px-5">
<div className="text-12 font-medium text-secondary">{label}</div>
{!isLoading ? (
<div className="flex flex-col gap-1">
<div className="text-20 font-bold text-primary">{count}</div>
<div className="text-[1.65rem] font-semibold text-primary">{count}</div>
</div>
) : (
<Loader.Item height="50px" width="100%" />

View File

@ -23,9 +23,11 @@ type Props = {
function CompletionPercentage({ percentage }: { percentage: number }) {
const percentageColor =
percentage > 50 ? "bg-success-subtle text-success-primary" : "bg-danger-subtle text-danger-primary";
percentage > 50
? "bg-success-subtle/80 text-success-primary"
: "bg-danger-subtle/80 text-danger-primary";
return (
<div className={cn("flex items-center gap-2 rounded-sm p-1 text-11", percentageColor)}>
<div className={cn("flex items-center gap-2 rounded-full px-2.5 py-1 text-11 font-medium", percentageColor)}>
<span>{percentage}%</span>
</div>
);
@ -41,9 +43,9 @@ function ActiveProjectItem(props: Props) {
if (!projectDetails) return null;
return (
<div className="flex w-full items-center justify-between gap-2">
<div className="nodedc-workspace-list-row flex w-full items-center justify-between gap-3 px-3 py-3">
<div className="flex flex-1 items-center gap-2 overflow-hidden">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-layer-1">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[1rem] bg-white/6">
<span className="grid h-4 w-4 flex-shrink-0 place-items-center">
{projectDetails?.logo_props ? (
<Logo logo={projectDetails?.logo_props} size={16} />

View File

@ -63,7 +63,7 @@ const ProjectInsights = observer(function ProjectInsights() {
<EmptyStateCompact
assetKey="unknown"
assetClassName="size-20"
rootClassName="border border-subtle px-5 py-10 md:py-20 md:px-20"
rootClassName="nodedc-workspace-list-row px-5 py-10 md:px-20 md:py-20"
title={t("workspace_empty_state.analytics_work_items.title")}
/>
) : (
@ -95,9 +95,13 @@ const ProjectInsights = observer(function ProjectInsights() {
/>
</Suspense>
)}
<div className="w-full lg:w-2/5">
<div className="text-13 text-tertiary">{t("workspace_analytics.summary_of_projects")}</div>
<div className="mb-3 border-b border-subtle py-2">{t("workspace_analytics.all_projects")}</div>
<div className="nodedc-workspace-list-row flex w-full flex-col gap-3 px-4 py-4 lg:w-2/5">
<div className="text-12 font-medium tracking-[0.16em] text-secondary uppercase">
{t("workspace_analytics.summary_of_projects")}
</div>
<div className="border-b border-white/8 pb-3 text-13 font-medium text-primary">
{t("workspace_analytics.all_projects")}
</div>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between text-13 text-tertiary">
<div>{t("workspace_analytics.trend_on_charts")}</div>

View File

@ -13,9 +13,9 @@ import ProjectInsights from "./project-insights";
function Overview() {
return (
<AnalyticsWrapper i18nTitle="common.overview">
<div className="flex flex-col gap-14">
<div className="flex flex-col gap-5 md:gap-6">
<TotalInsights analyticsType="overview" />
<div className="grid grid-cols-1 gap-14 md:grid-cols-5">
<div className="grid grid-cols-1 gap-5 md:grid-cols-5 md:gap-6">
<ProjectInsights />
<ActiveProjects />
</div>

View File

@ -6,7 +6,6 @@
import { observer } from "mobx-react";
// plane package imports
import { getButtonStyling } from "@plane/propel/button";
import { Logo } from "@plane/propel/emoji-icon-picker";
import { ChevronDownIcon, ProjectIcon } from "@plane/propel/icons";
import { SearchSelectionDropdown } from "@plane/ui";
@ -49,8 +48,14 @@ export const ProjectSelect = observer(function ProjectSelect(props: Props) {
onChange={(val: string[]) => onChange(val)}
options={options}
className="border-none p-0"
menuButton={
<div className={cn(getButtonStyling("secondary", "lg"), "gap-2")}>
menuButton={({ open }) => (
<div
className={cn(
"nodedc-toolbar-pill nodedc-toolbar-pill-wide flex min-h-[2.5rem] items-center gap-2 px-4 py-0 text-13 font-medium",
open && "text-[rgb(var(--nodedc-accent-rgb))]"
)}
data-active={open}
>
<ProjectIcon className="h-4 w-4" />
{value && value.length > 3
? `3+ projects`
@ -62,7 +67,7 @@ export const ProjectSelect = observer(function ProjectSelect(props: Props) {
: "All projects"}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div>
}
)}
menuButtonWrapperClassName="h-auto w-auto border-none bg-transparent p-0 hover:bg-transparent"
multiple
/>

View File

@ -14,7 +14,7 @@ import WorkItemsInsightTable from "./workitems-insight-table";
function WorkItems() {
return (
<AnalyticsWrapper i18nTitle="sidebar.work_items">
<div className="flex flex-col gap-14">
<div className="flex flex-col gap-5 md:gap-6">
<TotalInsights analyticsType="work-items" />
<CreatedVsResolved />
<CustomizedInsights />

View File

@ -16,6 +16,7 @@ import { useTranslation } from "@plane/i18n";
// icon
import { CheckIcon, CycleGroupIcon, CycleIcon, SearchIcon } from "@plane/propel/icons";
import type { TCycleGroups } from "@plane/types";
import { cn } from "@plane/utils";
// ui
// store hooks
import { useCycle } from "@/hooks/store/use-cycle";
@ -126,17 +127,17 @@ export const CycleOptions = observer(function CycleOptions(props: CycleOptionsPr
return (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 text-11 shadow-raised-200 focus:outline-none"
className="nodedc-dropdown-surface z-30 my-1 w-52"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded-sm border border-subtle bg-surface-2 px-2">
<div className="nodedc-dropdown-search">
<SearchIcon className="h-3.5 w-3.5 text-placeholder" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-11 text-secondary placeholder:text-placeholder focus:outline-none"
className="w-full bg-transparent py-0 text-12 text-secondary placeholder:text-placeholder focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t("common.search.label")}
@ -144,7 +145,7 @@ export const CycleOptions = observer(function CycleOptions(props: CycleOptionsPr
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
<div className="vertical-scrollbar mt-2 max-h-48 space-y-1 overflow-y-auto pr-1">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
@ -152,9 +153,11 @@ export const CycleOptions = observer(function CycleOptions(props: CycleOptionsPr
key={option.value}
value={option.value}
className={({ active, selected }) =>
`flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 select-none ${
active ? "bg-layer-transparent-hover" : ""
} ${selected ? "text-primary" : "text-secondary"}`
cn("nodedc-dropdown-option", {
"bg-white/6": active,
"text-primary": selected,
"text-secondary": !selected,
})
}
>
{({ selected }) => (
@ -166,10 +169,10 @@ export const CycleOptions = observer(function CycleOptions(props: CycleOptionsPr
</Combobox.Option>
))
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("common.search.no_matches_found")}</p>
<p className="px-2 py-1 text-placeholder italic">{t("common.search.no_matches_found")}</p>
)
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("common.loading")}</p>
<p className="px-2 py-1 text-placeholder italic">{t("common.loading")}</p>
)}
</div>
</div>

View File

@ -234,17 +234,17 @@ export const EstimateDropdown = observer(function EstimateDropdown(props: Props)
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 text-11 shadow-raised-200 focus:outline-none"
className="nodedc-dropdown-surface z-30 my-1 w-52"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded-sm border border-subtle bg-surface-2 px-2">
<div className="nodedc-dropdown-search">
<SearchIcon className="h-3.5 w-3.5 text-placeholder" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-11 text-secondary placeholder:text-placeholder focus:outline-none"
className="w-full bg-transparent py-0 text-12 text-secondary placeholder:text-placeholder focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t("common.search.placeholder")}
@ -252,11 +252,9 @@ export const EstimateDropdown = observer(function EstimateDropdown(props: Props)
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
<div className="vertical-scrollbar mt-2 max-h-48 space-y-1 overflow-y-auto pr-1">
{currentActiveEstimateId === undefined ? (
<div
className={`flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 text-secondary select-none`}
>
<div className="nodedc-dropdown-option cursor-default">
{/* NOTE: This condition renders when estimates are not enabled for the project */}
<div className="flex flex-grow items-center gap-2">
<EstimatePropertyIcon className="h-3 w-3 flex-shrink-0" />
@ -272,9 +270,9 @@ export const EstimateDropdown = observer(function EstimateDropdown(props: Props)
{({ active, selected }) => (
<div
className={cn(
"flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 select-none",
"nodedc-dropdown-option",
{
"bg-layer-transparent-hover": active,
"bg-white/6": active,
"text-primary": selected,
"text-secondary": !selected,
}
@ -287,10 +285,10 @@ export const EstimateDropdown = observer(function EstimateDropdown(props: Props)
</Combobox.Option>
))
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("common.search.no_matching_results")}</p>
<p className="px-2 py-1 text-placeholder italic">{t("common.search.no_matching_results")}</p>
)
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("common.loading")}</p>
<p className="px-2 py-1 text-placeholder italic">{t("common.loading")}</p>
)}
</>
)}

View File

@ -210,14 +210,14 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
renderByDefault={renderByDefault}
>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<Combobox.Options className="fixed z-30" static>
<div
className="nodedc-glass-modal nodedc-glass-popup-surface my-1 w-52 rounded-[1.25rem] border-0 px-3 py-3 text-12 shadow-none outline-none"
className="nodedc-dropdown-surface z-30 my-1 w-52"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded-[0.95rem] border-0 bg-white/5 px-3 py-2 outline-none">
<div className="nodedc-dropdown-search">
<SearchIcon className="h-3.5 w-3.5 text-placeholder" strokeWidth={1.5} />
<Combobox.Input
as="input"
@ -230,7 +230,7 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-56 space-y-1 overflow-y-auto">
<div className="vertical-scrollbar mt-2 max-h-56 space-y-1 overflow-y-auto pr-1">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
@ -238,11 +238,11 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
key={option.value}
value={option.value}
className={({ active, selected }) =>
cn(
`flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded-[0.9rem] px-2 py-2 select-none outline-none ${
active ? "bg-white/6" : ""
} ${selected ? "text-primary" : "text-secondary"}`
)
cn("nodedc-dropdown-option", {
"bg-white/6": active,
"text-primary": selected,
"text-secondary": !selected,
})
}
>
{({ selected }) => (
@ -254,10 +254,10 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
</Combobox.Option>
))
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("no_matching_results")}</p>
<p className="px-2 py-1 text-placeholder italic">{t("no_matching_results")}</p>
)
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("loading")}</p>
<p className="px-2 py-1 text-placeholder italic">{t("loading")}</p>
)}
</div>
</div>

View File

@ -21,10 +21,11 @@ type TLayoutDropDown = {
onChange: (value: EIssueLayoutTypes) => void;
value: EIssueLayoutTypes;
disabledLayouts?: EIssueLayoutTypes[];
buttonContainerClassName?: string;
};
export const LayoutDropDown = observer(function LayoutDropDown(props: TLayoutDropDown) {
const { onChange, value = EIssueLayoutTypes.LIST, disabledLayouts = [] } = props;
const { onChange, value = EIssueLayoutTypes.LIST, disabledLayouts = [], buttonContainerClassName } = props;
// plane i18n
const { t } = useTranslation();
// derived values
@ -74,7 +75,7 @@ export const LayoutDropDown = observer(function LayoutDropDown(props: TLayoutDro
value={value?.toString()}
keyExtractor={keyExtractor}
options={options}
buttonContainerClassName={cn(getIconButtonStyling("secondary", "lg"), "w-auto px-2")}
buttonContainerClassName={cn(getIconButtonStyling("secondary", "lg"), "w-auto px-2", buttonContainerClassName)}
buttonContent={buttonContent}
renderItem={itemContent}
disableSearch

View File

@ -115,17 +115,17 @@ export const ModuleOptions = observer(function ModuleOptions(props: Props) {
return (
<Combobox.Options className="fixed z-10" static>
<div
className="my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 text-11 shadow-raised-200 focus:outline-none"
className="nodedc-dropdown-surface z-30 my-1 w-52"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded-sm border border-subtle bg-surface-2 px-2">
<div className="nodedc-dropdown-search">
<SearchIcon className="h-3.5 w-3.5 text-placeholder" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-11 text-secondary placeholder:text-placeholder focus:outline-none"
className="w-full bg-transparent py-0 text-12 text-secondary placeholder:text-placeholder focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t("common.search.label")}
@ -133,7 +133,7 @@ export const ModuleOptions = observer(function ModuleOptions(props: Props) {
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
<div className="vertical-scrollbar mt-2 max-h-48 space-y-1 overflow-y-auto pr-1">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
@ -142,9 +142,9 @@ export const ModuleOptions = observer(function ModuleOptions(props: Props) {
value={option.value}
className={({ active, selected }) =>
cn(
"flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 select-none",
"nodedc-dropdown-option",
{
"bg-layer-transparent-hover": active,
"bg-white/6": active,
"text-primary": selected,
"text-secondary": !selected,
}
@ -160,10 +160,10 @@ export const ModuleOptions = observer(function ModuleOptions(props: Props) {
</Combobox.Option>
))
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("common.search.no_matching_results")}</p>
<p className="px-2 py-1 text-placeholder italic">{t("common.search.no_matching_results")}</p>
)
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("common.loading")}</p>
<p className="px-2 py-1 text-placeholder italic">{t("common.loading")}</p>
)}
</div>
</div>

View File

@ -216,12 +216,12 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
createPortal(
<Combobox.Options data-prevent-outside-click className="fixed z-30" static>
<div
className="nodedc-glass-modal nodedc-glass-popup-surface my-1 w-52 rounded-[1.25rem] border-0 px-3 py-3 text-12 shadow-none outline-none"
className="nodedc-dropdown-surface z-30 my-1 w-52"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex items-center gap-1.5 rounded-[0.95rem] border-0 bg-white/5 px-3 py-2 outline-none">
<div className="nodedc-dropdown-search">
<SearchIcon className="h-3.5 w-3.5 text-placeholder" strokeWidth={1.5} />
<Combobox.Input
as="input"
@ -234,7 +234,7 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-56 space-y-1 overflow-y-auto">
<div className="vertical-scrollbar mt-2 max-h-56 space-y-1 overflow-y-auto pr-1">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
@ -242,11 +242,11 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
key={option.value}
value={option.value}
className={({ active, selected }) =>
cn(
`flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded-[0.9rem] px-2 py-2 select-none outline-none ${
active ? "bg-white/6" : ""
} ${selected ? "text-primary" : "text-secondary"}`
)
cn("nodedc-dropdown-option", {
"bg-white/6": active,
"text-primary": selected,
"text-secondary": !selected,
})
}
>
{({ selected }) => (
@ -258,10 +258,10 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
</Combobox.Option>
))
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("no_matching_results")}</p>
<p className="px-2 py-1 text-placeholder italic">{t("no_matching_results")}</p>
)
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("loading")}</p>
<p className="px-2 py-1 text-placeholder italic">{t("loading")}</p>
)}
</div>
</div>

View File

@ -8,12 +8,12 @@ import { useState } from "react";
import { omit } from "lodash-es";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Ellipsis } from "lucide-react";
import { MoreHorizontal } from "lucide-react";
// plane imports
import { ARCHIVABLE_STATE_GROUPS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import type { TIssue } from "@plane/types";
import { EIssuesStoreType } from "@plane/types";
import { ActionDropdown, ContextMenu } from "@plane/ui";
import { ActionDropdown, ContextMenu, cn } from "@plane/ui";
// hooks
import { useIssues } from "@/hooks/store/use-issues";
import { useProject } from "@/hooks/store/use-project";
@ -240,15 +240,17 @@ export const WorkItemDetailQuickActions = observer(function WorkItemDetailQuickA
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<ActionDropdown
button={
<div
className={
buttonClassName ??
"flex size-10 items-center justify-center rounded-[18px] border-transparent bg-layer-2/80 text-secondary shadow-none backdrop-blur-xl hover:bg-layer-2-active"
}
<button
type="button"
className={cn(
"inline-flex size-10 shrink-0 items-center justify-center rounded-[18px] border-transparent bg-layer-2/80 text-secondary shadow-none backdrop-blur-xl transition-colors hover:bg-layer-2-active focus-visible:outline-none",
buttonClassName
)}
>
<Ellipsis className="h-4 w-4" />
</div>
<MoreHorizontal className="pointer-events-none h-4 w-4 shrink-0" />
</button>
}
buttonAsChild
items={MENU_ITEMS}
placement={placements}
portalElement={portalElement}

View File

@ -42,7 +42,7 @@ export const SpreadsheetAssigneeColumn = observer(function SpreadsheetAssigneeCo
buttonVariant={
issue?.assignee_ids && issue.assignee_ids.length > 1 ? "transparent-without-text" : "transparent-with-text"
}
buttonClassName="text-left rounded-none group-[.selected-issue-row]:bg-accent-primary/5 group-[.selected-issue-row]:hover:bg-accent-primary/10 px-page-x"
buttonClassName="nodedc-spreadsheet-cell-button text-left"
buttonContainerClassName="w-full"
optionsClassName="z-[9]"
onClose={onClose}

View File

@ -47,8 +47,8 @@ export const SpreadsheetCycleColumn = observer(function SpreadsheetCycleColumn(p
disabled={disabled}
placeholder="Select cycle"
buttonVariant="transparent-with-text"
buttonContainerClassName="w-full relative flex items-center p-2 group-[.selected-issue-row]:bg-accent-primary/5 group-[.selected-issue-row]:hover:bg-accent-primary/10 px-page-x"
buttonClassName="relative leading-4 h-4.5 bg-transparent hover:bg-transparent px-0"
buttonContainerClassName="w-full"
buttonClassName="nodedc-spreadsheet-cell-button text-left"
onClose={onClose}
/>
</div>

View File

@ -52,7 +52,7 @@ export const SpreadsheetDueDateColumn = observer(function SpreadsheetDueDateColu
buttonVariant="transparent-with-text"
buttonContainerClassName="w-full"
buttonClassName={cn(
"rounded-none px-page-x text-left group-[.selected-issue-row]:bg-accent-primary/5 group-[.selected-issue-row]:hover:bg-accent-primary/10",
"nodedc-spreadsheet-cell-button text-left",
{
"text-danger-primary": shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group),
}

View File

@ -31,7 +31,7 @@ export const SpreadsheetEstimateColumn = observer(function SpreadsheetEstimateCo
projectId={issue.project_id ?? undefined}
disabled={disabled}
buttonVariant="transparent-with-text"
buttonClassName="text-left rounded-none group-[.selected-issue-row]:bg-accent-primary/5 group-[.selected-issue-row]:hover:bg-accent-primary/10 px-page-x"
buttonClassName="nodedc-spreadsheet-cell-button text-left"
buttonContainerClassName="w-full"
onClose={onClose}
/>

View File

@ -35,7 +35,7 @@ export const SpreadsheetLabelColumn = observer(function SpreadsheetLabelColumn(p
defaultOptions={defaultLabelOptions}
onChange={(data) => onChange(issue, { label_ids: data }, { changed_property: "labels", change_details: data })}
className="h-full w-full"
buttonClassName="px-page-x w-full h-full group-[.selected-issue-row]:bg-accent-primary/5 group-[.selected-issue-row]:hover:bg-accent-primary/10 rounded-none"
buttonClassName="nodedc-spreadsheet-cell-button text-left"
hideDropdownArrow
maxRender={1}
disabled={disabled}

View File

@ -55,8 +55,8 @@ export const SpreadsheetModuleColumn = observer(function SpreadsheetModuleColumn
disabled={disabled}
placeholder="Select modules"
buttonVariant="transparent-with-text"
buttonContainerClassName="w-full relative flex items-center p-2 group-[.selected-issue-row]:bg-accent-primary/5 group-[.selected-issue-row]:hover:bg-accent-primary/10 px-page-x"
buttonClassName="relative leading-4 h-4.5 bg-transparent hover:bg-transparent !px-0"
buttonContainerClassName="w-full"
buttonClassName="nodedc-spreadsheet-cell-button text-left"
onClose={onClose}
multiple
showCount

View File

@ -28,7 +28,7 @@ export const SpreadsheetPriorityColumn = observer(function SpreadsheetPriorityCo
onChange={(data) => onChange(issue, { priority: data }, { changed_property: "priority", change_details: data })}
disabled={disabled}
buttonVariant="transparent-with-text"
buttonClassName="text-left rounded-none group-[.selected-issue-row]:bg-accent-primary/5 group-[.selected-issue-row]:hover:bg-accent-primary/10 px-page-x"
buttonClassName="nodedc-spreadsheet-cell-button text-left"
buttonContainerClassName="w-full"
onClose={onClose}
/>

View File

@ -44,7 +44,7 @@ export const SpreadsheetStartDateColumn = observer(function SpreadsheetStartDate
placeholder="Start date"
icon={<StartDatePropertyIcon className="h-3 w-3 flex-shrink-0" />}
buttonVariant="transparent-with-text"
buttonClassName="text-left rounded-none group-[.selected-issue-row]:bg-accent-primary/5 group-[.selected-issue-row]:hover:bg-accent-primary/10 px-page-x"
buttonClassName="nodedc-spreadsheet-cell-button text-left"
buttonContainerClassName="w-full"
optionsClassName="z-[9]"
onClose={onClose}

View File

@ -29,7 +29,7 @@ export const SpreadsheetStateColumn = observer(function SpreadsheetStateColumn(p
onChange={(data) => onChange(issue, { state_id: data }, { changed_property: "state", change_details: data })}
disabled={disabled}
buttonVariant="transparent-with-text"
buttonClassName="text-left rounded-none group-[.selected-issue-row]:bg-accent-primary/5 group-[.selected-issue-row]:hover:bg-accent-primary/10 px-page-x"
buttonClassName="nodedc-spreadsheet-cell-button text-left"
buttonContainerClassName="w-full"
onClose={onClose}
showTooltip

View File

@ -126,7 +126,7 @@ export const DraftIssueBlock = observer(function DraftIssueBlock(props: Props) {
/>
<div
id={`issue-${issue.id}`}
className="relative w-full cursor-pointer border-b border-b-subtle-1"
className="relative mb-3 w-full cursor-pointer last:mb-0"
onDoubleClick={() => {
setIssueToEdit(issue);
setCreateUpdateIssueModal(true);
@ -135,7 +135,7 @@ export const DraftIssueBlock = observer(function DraftIssueBlock(props: Props) {
<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-workspace-list-row group/list-block relative flex min-h-[5.25rem] flex-col gap-3 px-4 py-4 text-13",
{
"md:flex-row md:items-center": isSidebarCollapsed,
"lg:flex-row lg:items-center": !isSidebarCollapsed,
@ -170,7 +170,7 @@ export const DraftIssueBlock = observer(function DraftIssueBlock(props: Props) {
{/* quick actions */}
<div
className={cn("block rounded-sm border border-strong", {
className={cn("block", {
"md:hidden": isSidebarCollapsed,
"lg:hidden": !isSidebarCollapsed,
})}

View File

@ -35,7 +35,7 @@ export const WorkspaceDraftEmptyState = observer(function WorkspaceDraftEmptySta
onClose={() => setIsDraftIssueModalOpen(false)}
isDraft
/>
<div className="relative h-full w-full overflow-y-auto">
<div className="nodedc-external-section relative h-full w-full overflow-y-auto rounded-[1.75rem] px-4 py-4 md:px-6 md:py-6">
<EmptyStateDetailed
title={t("workspace_empty_state.drafts.title")}
description={t("workspace_empty_state.drafts.description")}

View File

@ -87,7 +87,7 @@ export const WorkspaceDraftIssuesRoot = observer(function WorkspaceDraftIssuesRo
if (issueIds.length <= 0) return <WorkspaceDraftEmptyState />;
return (
<div className="relative">
<div className="relative flex flex-col gap-3">
<div className="relative">
{issueIds.map((issueId: string) => (
<DraftIssueBlock key={issueId} workspaceSlug={workspaceSlug} issueId={issueId} />
@ -100,8 +100,8 @@ export const WorkspaceDraftIssuesRoot = observer(function WorkspaceDraftIssuesRo
<WorkspaceDraftIssuesLoader items={1} />
) : (
<div
className={cn("h-11 border-b border-subtle bg-surface-1 p-3 pl-6 text-13 font-medium transition-all", {
"cursor-pointer text-accent-primary underline-offset-2 hover:text-accent-secondary hover:underline":
className={cn("h-11 text-13 font-medium transition-all", {
"nodedc-workspace-toolbar flex cursor-pointer items-center justify-center rounded-[1.25rem] border-0 bg-transparent px-4 py-3 text-primary underline-offset-2 hover:text-primary hover:underline":
paginationInfo?.next_page_results,
})}
onClick={handleNextIssues}

View File

@ -8,7 +8,7 @@ import { ArrowDownWideNarrow } from "lucide-react";
// plane imports
import { PROJECT_ORDER_BY_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { getButtonStyling } from "@plane/propel/button";
import { ChevronDownIcon } from "@plane/propel/icons";
import type { TProjectOrderByOptions } from "@plane/types";
import { SortingDropdown } from "@/components/common/sorting-dropdown";
@ -32,13 +32,15 @@ export function ProjectOrderByDropdown(props: Props) {
return (
<SortingDropdown
menuButton={
<div className={`${isMobile ? "flex w-full justify-center" : ""}`}>
<div className={getButtonStyling("secondary", "lg")}>
<div className={isMobile ? "flex w-full justify-center" : ""}>
<div className="nodedc-toolbar-pill flex min-h-[2.5rem] items-center gap-2 px-4 py-0 text-13 font-medium">
<ArrowDownWideNarrow className="size-3.5 shrink-0" strokeWidth={2} />
{orderByDetails && t(orderByDetails?.i18n_label)}
<span className="shrink-0">{orderByDetails ? t(orderByDetails.i18n_label) : t("common.order_by.label")}</span>
<ChevronDownIcon className="size-3 shrink-0" strokeWidth={2} />
</div>
</div>
}
menuButtonWrapperClassName="h-auto w-auto border-none bg-transparent p-0 hover:bg-transparent"
placement="bottom-end"
title={t("common.order_by.label")}
sections={[

View File

@ -44,7 +44,7 @@ export const ProjectSearch = observer(function ProjectSearch() {
<IconButton
variant="ghost"
size="lg"
className="-mr-1"
className="nodedc-toolbar-icon-button"
onClick={() => {
setIsSearchOpen(true);
inputRef.current?.focus();
@ -54,9 +54,9 @@ export const ProjectSearch = observer(function ProjectSearch() {
)}
<div
className={cn(
"ml-auto flex w-0 items-center justify-start gap-1 overflow-hidden rounded-md border border-transparent bg-surface-1 text-placeholder opacity-0 transition-[width] ease-linear",
"nodedc-toolbar-pill ml-auto flex w-0 items-center justify-start gap-2 overflow-hidden px-0 text-placeholder opacity-0 transition-[width] ease-linear",
{
"w-30 border-subtle px-2.5 py-1.5 opacity-100 md:w-64": isSearchOpen,
"w-30 px-3 opacity-100 md:w-64": isSearchOpen,
}
)}
>
@ -72,7 +72,7 @@ export const ProjectSearch = observer(function ProjectSearch() {
{isSearchOpen && (
<button
type="button"
className="grid place-items-center"
className="grid h-7 w-7 flex-shrink-0 place-items-center rounded-full text-secondary transition-colors hover:bg-white/6 hover:text-primary"
onClick={() => {
updateSearchQuery("");
setIsSearchOpen(false);

View File

@ -23,6 +23,7 @@ export type TAddFilterButtonProps<P extends TFilterProperty, E extends TExternal
variant?: TButtonVariant;
size?: TButtonSize;
className?: string;
appearance?: "toolbar" | "modal";
defaultOpen?: boolean;
iconConfig?: {
shouldShowIcon: boolean;
@ -42,6 +43,7 @@ export const AddFilterButton = observer(function AddFilterButton<P extends TFilt
variant = "secondary",
size = "base",
className,
appearance = "toolbar",
label,
iconConfig = { shouldShowIcon: true },
isDisabled = false,
@ -68,7 +70,11 @@ export const AddFilterButton = observer(function AddFilterButton<P extends TFilt
{...props}
buttonConfig={{
...buttonConfig,
className: cn(getButtonStyling(variant, size), "nodedc-toolbar-filter-toggle", className),
className: cn(
getButtonStyling(variant, size),
appearance === "modal" ? "nodedc-work-item-property-button" : "nodedc-toolbar-filter-toggle",
className
),
}}
handleFilterSelect={handleFilterSelect}
customButton={

View File

@ -51,7 +51,7 @@ export const DateRangeFilterValueInput = observer(function DateRangeFilterValueI
mergeDates
placeholder={{ from: EMPTY_FILTER_PLACEHOLDER_TEXT }}
buttonVariant="transparent-with-text"
buttonClassName={cn("rounded-none", {
buttonClassName={cn("rounded-none !items-center !py-0", {
[COMMON_FILTER_ITEM_BORDER_CLASSNAME]: !isDisabled,
"text-danger-primary": isIncomplete,
"hover:bg-surface-1": isDisabled,

View File

@ -33,11 +33,12 @@ export const SingleDateFilterValueInput = observer(function SingleDateFilterValu
const formattedDate = value ? renderFormattedPayloadDate(value) : null;
onChange(formattedDate);
}}
buttonClassName={cn("rounded-none", {
buttonClassName={cn("rounded-none !items-center !py-0", {
[COMMON_FILTER_ITEM_BORDER_CLASSNAME]: !isDisabled,
"text-placeholder": !conditionValue,
"hover:bg-surface-1": isDisabled,
})}
labelClassName="!text-13 !leading-none"
minDate={config.min}
maxDate={config.max}
icon={null}

View File

@ -10,7 +10,6 @@ import { ListFilterPlus } from "lucide-react";
import { Transition } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import type { IFilterInstance } from "@plane/shared-state";
import type { TExternalFilter, TFilterProperty } from "@plane/types";
import { cn, EHeaderVariant, Header, Loader } from "@plane/ui";
@ -56,6 +55,8 @@ export const FiltersRow = observer(function FiltersRow<K extends TFilterProperty
const modalButtonConfig: Partial<TAddFilterButtonProps<K, E>["buttonConfig"]> = {
label: !hasAnyConditions ? t("common.filters") : null,
appearance: "modal",
className: "!min-h-7 !px-3 !py-0 text-12 font-medium",
};
const handleUpdate = useCallback(async () => {
@ -92,42 +93,41 @@ export const FiltersRow = observer(function FiltersRow<K extends TFilterProperty
const rightContent = !disabledAllOperations && (
<>
<ElementTransition show={filter.canClearFilters}>
<Button
<DockActionButton
variant="secondary"
className={cn(COMMON_OPERATION_BUTTON_CLASSNAME, "nodedc-filter-clear-button")}
className={COMMON_OPERATION_BUTTON_CLASSNAME}
onClick={filter.clearFilters}
data-ph-element={trackerElements?.clearFilter}
>
{t("common.clear")}
</Button>
</DockActionButton>
</ElementTransition>
<ElementTransition show={filter.canSaveView}>
<Button
variant="secondary"
className={cn(COMMON_OPERATION_BUTTON_CLASSNAME, "nodedc-filter-clear-button")}
<DockActionButton
variant="primary"
className={cn(COMMON_OPERATION_BUTTON_CLASSNAME, "min-w-[14rem]")}
onClick={filter.saveView}
data-ph-element={trackerElements?.saveView}
>
{filter.saveViewOptions?.label ?? "Сохранить вид"}
</Button>
</DockActionButton>
</ElementTransition>
<ElementTransition show={filter.canUpdateView}>
<Button
<DockActionButton
variant="secondary"
className={cn(COMMON_OPERATION_BUTTON_CLASSNAME, "nodedc-filter-clear-button")}
className={COMMON_OPERATION_BUTTON_CLASSNAME}
onClick={handleUpdate}
loading={isUpdating}
disabled={isUpdating}
data-ph-element={trackerElements?.updateView}
>
{isUpdating ? "Подтверждение..." : (filter.updateViewOptions?.label ?? "Обновить вид")}
</Button>
</DockActionButton>
</ElementTransition>
</>
);
const mainContent = (
<div className="nodedc-filter-row-shell flex w-full items-start gap-2 px-3 py-2">
<div className="nodedc-filter-row-shell flex w-full items-center gap-2 px-3 py-2">
<div className="flex w-full flex-wrap items-center gap-2">{leftContent}</div>
<div
className={cn("flex items-center gap-2", {
@ -139,7 +139,11 @@ export const FiltersRow = observer(function FiltersRow<K extends TFilterProperty
</div>
);
const ModalVariant = <div className="flex min-h-11 w-full flex-wrap items-center gap-2 p-2">{mainContent}</div>;
const ModalVariant = (
<div className="nodedc-modal-field flex min-h-11 w-full flex-wrap items-center gap-2 rounded-[1.5rem] p-2">
{mainContent}
</div>
);
const HeaderVariant = (
<Header variant={EHeaderVariant.TERNARY} className="min-h-11 bg-transparent !px-0">
@ -160,7 +164,35 @@ export const FiltersRow = observer(function FiltersRow<K extends TFilterProperty
return <RowTransition show={filter.isVisible}>{variant === "modal" ? ModalVariant : HeaderVariant}</RowTransition>;
});
const COMMON_OPERATION_BUTTON_CLASSNAME = "min-h-9 px-4 py-1";
const COMMON_OPERATION_BUTTON_CLASSNAME = "px-6 py-0 text-[14px] font-medium";
type TDockActionButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
children: React.ReactNode;
variant: "primary" | "secondary";
};
function DockActionButton(props: TDockActionButtonProps) {
const { children, className, disabled, variant, type = "button", ...buttonProps } = props;
return (
<button
type={type}
disabled={disabled}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap leading-none transition-colors",
{
"nodedc-bottom-dock-primary-button": variant === "primary",
"nodedc-bottom-dock-secondary-button": variant === "secondary",
"cursor-not-allowed opacity-60": disabled,
},
className
)}
{...buttonProps}
>
{children}
</button>
);
}
type TElementTransitionProps = {
children: React.ReactNode;

View File

@ -43,7 +43,7 @@ export const StickiesInfinite = observer(function StickiesInfinite() {
useIntersectionObserver(containerRef, shouldObserve ? elementRef : null, handleLoadMore);
return (
<ContentWrapper ref={containerRef} className="space-y-4">
<ContentWrapper ref={containerRef} className="space-y-4 px-1">
<StickiesLayout
workspaceSlug={workspaceSlug.toString()}
intersectionElement={
@ -54,7 +54,7 @@ export const StickiesInfinite = observer(function StickiesInfinite() {
ref={setElementRef}
id="intersection-element"
>
<div className="flex min-h-[300px] w-full rounded-sm">
<div className="nodedc-workspace-list-row flex min-h-[300px] w-full rounded-[1.35rem]">
<Loader className="h-full w-full">
<Loader.Item height="100%" width="100%" />
</Loader>

View File

@ -22,7 +22,7 @@ import type {
} from "@plane/types";
import { EViewAccess, EIssuesStoreType } from "@plane/types";
import { Input, TextArea } from "@plane/ui";
import { getComputedDisplayFilters, getComputedDisplayProperties, getTabIndex } from "@plane/utils";
import { cn, getComputedDisplayFilters, getComputedDisplayProperties, getTabIndex } from "@plane/utils";
// components
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
@ -88,6 +88,7 @@ export const ProjectViewForm = observer(function ProjectViewForm(props: Props) {
kanbanFilters: undefined,
};
const { getIndex } = getTabIndex(ETabIndices.PROJECT_VIEW, isMobile);
const viewPropertyButtonClassName = "nodedc-work-item-property-button !min-h-7 !px-3 !py-0 text-12 font-medium";
const handleCreateUpdateView = async (formData: IProjectView) => {
await handleFormSubmit({
@ -106,8 +107,8 @@ export const ProjectViewForm = observer(function ProjectViewForm(props: Props) {
};
return (
<form onSubmit={handleSubmit(handleCreateUpdateView)}>
<div className="space-y-5 p-5">
<form onSubmit={handleSubmit(handleCreateUpdateView)} className="flex flex-col">
<div className="space-y-5 px-6 py-6">
<h3 className="text-18 font-medium text-secondary">{data ? t("view.update.label") : t("view.create.label")}</h3>
<div className="space-y-3">
<div className="flex w-full items-start gap-2">
@ -118,7 +119,7 @@ export const ProjectViewForm = observer(function ProjectViewForm(props: Props) {
className="flex-shrink0 flex items-center justify-center"
buttonClassName="flex items-center justify-center"
label={
<span className="grid h-9 w-9 place-items-center rounded-md bg-surface-2">
<span className="grid h-10 w-10 place-items-center rounded-[1.1rem] bg-white/6">
<>
{logoValue?.in_use ? (
<Logo logo={logoValue} size={18} type="lucide" />
@ -171,7 +172,7 @@ export const ProjectViewForm = observer(function ProjectViewForm(props: Props) {
onChange={onChange}
hasError={Boolean(errors.name)}
placeholder={t("common.title")}
className="w-full text-14"
className="nodedc-modal-input w-full !px-4 !py-3 text-13"
tabIndex={getIndex("name")}
autoFocus
/>
@ -189,7 +190,7 @@ export const ProjectViewForm = observer(function ProjectViewForm(props: Props) {
id="description"
name="description"
placeholder={t("common.description")}
className="min-h-24 w-full resize-none text-14"
className="nodedc-modal-input min-h-[9.5rem] w-full resize-none !rounded-[1.5rem] !px-4 !py-4 text-13"
hasError={Boolean(errors?.description)}
value={value}
onChange={onChange}
@ -198,7 +199,7 @@ export const ProjectViewForm = observer(function ProjectViewForm(props: Props) {
)}
/>
</div>
<div className="flex gap-2">
<div className="nodedc-work-item-properties-row">
<AccessController control={control} />
<Controller
control={control}
@ -214,13 +215,26 @@ export const ProjectViewForm = observer(function ProjectViewForm(props: Props) {
})
}
value={displayFilters.layout}
buttonContainerClassName={viewPropertyButtonClassName}
/>
{/* display filters dropdown */}
<Controller
control={control}
name="display_properties"
render={({ field: { onChange: onDisplayPropertiesChange, value: displayProperties } }) => (
<FiltersDropdown title={t("common.display")}>
<FiltersDropdown
title={t("common.display")}
menuButtonWrapperClassName="h-auto w-auto border-none bg-transparent p-0 hover:bg-transparent"
menuButton={({ open }) => (
<div
className={cn(viewPropertyButtonClassName, "flex items-center gap-2", {
"!bg-white/8": open,
})}
>
<span>{t("common.display")}</span>
</div>
)}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
ISSUE_DISPLAY_FILTERS_BY_PAGE.issues.layoutOptions[displayFilters.layout]
@ -279,11 +293,24 @@ export const ProjectViewForm = observer(function ProjectViewForm(props: Props) {
</div>
</div>
</div>
<div className="flex items-center justify-end gap-2 border-t-[0.5px] border-subtle px-5 py-4">
<Button variant="secondary" size="lg" onClick={handleClose} tabIndex={getIndex("cancel")}>
<div className="flex items-center justify-end gap-3 border-t-[0.5px] border-white/6 px-6 py-4">
<Button
variant="secondary"
size="lg"
onClick={handleClose}
tabIndex={getIndex("cancel")}
className="nodedc-modal-secondary-button min-w-[8.25rem]"
>
{t("common.cancel")}
</Button>
<Button variant="primary" size="lg" type="submit" tabIndex={getIndex("submit")} loading={isSubmitting}>
<Button
variant="primary"
size="lg"
type="submit"
tabIndex={getIndex("submit")}
loading={isSubmitting}
className="nodedc-modal-primary-button min-w-[8.25rem]"
>
{data
? isSubmitting
? t("common.updating")

View File

@ -88,7 +88,12 @@ export const CreateUpdateProjectViewModal = observer(function CreateUpdateProjec
});
return (
<ModalCore isOpen={isOpen} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<ModalCore
isOpen={isOpen}
position={EModalPosition.CENTER}
width={EModalWidth.XXXXL}
className="overflow-hidden rounded-[1.75rem] transition-[width] ease-linear"
>
<ProjectViewForm
data={data}
handleClose={handleClose}

View File

@ -20,13 +20,16 @@ export const GlobalDefaultViewListItem = observer(function GlobalDefaultViewList
const { t } = useTranslation();
return (
<div className="group border-b border-subtle hover:bg-surface-2">
<div className="nodedc-workspace-list-row group">
<Link href={`/${workspaceSlug}/workspace-views/${view.key}`}>
<div className="relative flex h-[52px] w-full items-center justify-between rounded-sm px-5 py-4">
<div className="relative flex min-h-[4.25rem] w-full items-center justify-between px-5 py-4">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex flex-col">
<p className="truncate text-13 leading-4 font-medium">{truncateText(t(view.i18n_label), 75)}</p>
<p className="truncate text-13 leading-4 font-medium text-primary">
{truncateText(t(view.i18n_label), 75)}
</p>
<p className="text-11 text-secondary">Default workspace view</p>
</div>
</div>
</div>

View File

@ -13,7 +13,7 @@ import { Button } from "@plane/propel/button";
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IWorkspaceView, IIssueFilters } from "@plane/types";
import { EViewAccess, EIssueLayoutTypes, EIssuesStoreType } from "@plane/types";
import { Input, TextArea } from "@plane/ui";
import { getComputedDisplayFilters, getComputedDisplayProperties } from "@plane/utils";
import { cn, getComputedDisplayFilters, getComputedDisplayProperties } from "@plane/utils";
// components
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
import { WorkspaceLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/workspace-level";
@ -66,6 +66,7 @@ export const WorkspaceViewForm = observer(function WorkspaceViewForm(props: Prop
displayProperties: getValues("display_properties"),
kanbanFilters: undefined,
};
const viewPropertyButtonClassName = "nodedc-work-item-property-button !min-h-7 !px-3 !py-0 text-12 font-medium";
const handleCreateUpdateView = async (formData: Partial<IWorkspaceView>) => {
await handleFormSubmit(formData);
@ -75,8 +76,8 @@ export const WorkspaceViewForm = observer(function WorkspaceViewForm(props: Prop
};
return (
<form onSubmit={handleSubmit(handleCreateUpdateView)}>
<div className="space-y-5 p-5">
<form onSubmit={handleSubmit(handleCreateUpdateView)} className="flex flex-col">
<div className="space-y-5 px-6 py-6">
<h3 className="text-18 font-medium text-secondary">{data ? t("view.update.label") : t("view.create.label")}</h3>
<div className="space-y-3">
<div className="space-y-1">
@ -100,7 +101,7 @@ export const WorkspaceViewForm = observer(function WorkspaceViewForm(props: Prop
ref={ref}
hasError={Boolean(errors.name)}
placeholder={t("common.title")}
className="w-full text-14"
className="nodedc-modal-input w-full !px-4 !py-3 text-13"
/>
)}
/>
@ -117,13 +118,13 @@ export const WorkspaceViewForm = observer(function WorkspaceViewForm(props: Prop
value={value}
placeholder={t("common.description")}
onChange={onChange}
className="min-h-24 w-full resize-none text-14"
className="nodedc-modal-input min-h-[9.5rem] w-full resize-none !rounded-[1.5rem] !px-4 !py-4 text-13"
hasError={Boolean(errors?.description)}
/>
)}
/>
</div>
<div className="flex gap-2">
<div className="nodedc-work-item-properties-row">
<AccessController control={control} />
{/* display filters dropdown */}
<Controller
@ -134,7 +135,19 @@ export const WorkspaceViewForm = observer(function WorkspaceViewForm(props: Prop
control={control}
name="display_properties"
render={({ field: { onChange: onDisplayPropertiesChange, value: displayProperties } }) => (
<FiltersDropdown title={t("common.display")}>
<FiltersDropdown
title={t("common.display")}
menuButtonWrapperClassName="h-auto w-auto border-none bg-transparent p-0 hover:bg-transparent"
menuButton={({ open }) => (
<div
className={cn(viewPropertyButtonClassName, "flex items-center gap-2", {
"!bg-white/8": open,
})}
>
<span>{t("common.display")}</span>
</div>
)}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.layoutOptions.spreadsheet}
displayFilters={displayFilters ?? {}}
@ -185,11 +198,16 @@ export const WorkspaceViewForm = observer(function WorkspaceViewForm(props: Prop
</div>
</div>
</div>
<div className="flex items-center justify-end gap-2 border-t-[0.5px] border-subtle px-5 py-4">
<Button variant="secondary" onClick={handleClose}>
<div className="flex items-center justify-end gap-3 border-t-[0.5px] border-white/6 px-6 py-4">
<Button variant="secondary" onClick={handleClose} className="nodedc-modal-secondary-button min-w-[8.25rem]">
{t("common.cancel")}
</Button>
<Button variant="primary" type="submit" loading={isSubmitting}>
<Button
variant="primary"
type="submit"
loading={isSubmitting}
className="nodedc-modal-primary-button min-w-[8.25rem]"
>
{data
? isSubmitting
? t("common.updating")

View File

@ -104,7 +104,13 @@ export const CreateUpdateWorkspaceViewModal = observer(function CreateUpdateWork
if (!workspaceSlug) return null;
return (
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<ModalCore
isOpen={isOpen}
handleClose={handleClose}
position={EModalPosition.CENTER}
width={EModalWidth.XXXXL}
className="overflow-hidden rounded-[1.75rem] transition-[width] ease-linear"
>
<WorkspaceViewForm
handleFormSubmit={handleFormSubmit}
handleClose={handleClose}

View File

@ -60,21 +60,25 @@ export const GlobalViewListItem = observer(function GlobalViewListItem(props: Pr
<>
<CreateUpdateWorkspaceViewModal data={view} isOpen={updateViewModal} onClose={() => setUpdateViewModal(false)} />
<DeleteGlobalViewModal data={view} isOpen={deleteViewModal} onClose={() => setDeleteViewModal(false)} />
<div className="group border-b border-subtle hover:bg-surface-2">
<div className="nodedc-workspace-list-row group">
<Link href={`/${workspaceSlug}/workspace-views/${view.id}`}>
<div className="relative flex h-[52px] w-full items-center justify-between rounded-sm p-4">
<div className="relative flex min-h-[4.5rem] w-full items-center justify-between px-4 py-4">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex flex-col">
<p className="truncate text-13 leading-4 font-medium">{truncateText(view.name, 75)}</p>
{view?.description && <p className="text-11 text-secondary">{view.description}</p>}
<p className="truncate text-13 leading-4 font-medium text-primary">{truncateText(view.name, 75)}</p>
{view?.description ? (
<p className="max-w-[38rem] truncate text-11 text-secondary">{view.description}</p>
) : (
<p className="text-11 text-tertiary">Custom workspace view</p>
)}
</div>
</div>
<div className="ml-2 flex flex-shrink-0">
<div className="flex items-center gap-4">
<ActionDropdown
placement="bottom-end"
buttonClassName="grid size-7 place-items-center rounded-sm text-secondary transition-colors hover:bg-layer-transparent-hover"
buttonClassName="grid size-8 place-items-center rounded-full bg-white/5 text-secondary transition-colors hover:bg-white/10"
items={menuItems}
/>
</div>

View File

@ -530,6 +530,7 @@
outline: none !important;
box-shadow: none !important;
border-radius: 999px !important;
height: 2.5rem !important;
min-height: 2.5rem;
padding-inline: 1.35rem;
background: rgba(18, 18, 22, 0.94) !important;
@ -558,6 +559,7 @@
outline: none !important;
box-shadow: none !important;
border-radius: 999px !important;
height: 2.5rem !important;
min-height: 2.5rem;
padding-inline: 1.55rem;
background: rgb(var(--nodedc-accent-rgb)) !important;
@ -573,6 +575,39 @@
background: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 82%, white) !important;
}
.nodedc-bottom-dock-secondary-button {
height: 3rem !important;
min-height: 3rem !important;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.2rem !important;
background: rgba(255, 255, 255, 0.06) !important;
color: var(--text-color-primary) !important;
padding-inline: 1.35rem !important;
}
.nodedc-bottom-dock-secondary-button:hover {
background: rgba(255, 255, 255, 0.1) !important;
}
.nodedc-bottom-dock-primary-button {
height: 3rem !important;
min-height: 3rem !important;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.2rem !important;
background: rgb(var(--nodedc-card-active-rgb)) !important;
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
padding-inline: 1.6rem !important;
}
.nodedc-bottom-dock-primary-button:hover {
background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important;
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
}
.nodedc-modal-secondary-button {
min-height: 2.75rem;
border: 0 !important;
@ -924,6 +959,36 @@
background: rgba(255, 255, 255, 0.08) !important;
}
.nodedc-spreadsheet-cell-button {
height: 100% !important;
min-height: 2.75rem !important;
width: 100% !important;
align-items: center !important;
justify-content: flex-start !important;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 0 !important;
background: transparent !important;
color: var(--text-color-primary) !important;
padding-inline: 1rem !important;
font-size: 0.8125rem !important;
line-height: 1rem !important;
transition: background 160ms ease;
}
.nodedc-spreadsheet-cell-button:hover {
background: rgba(255, 255, 255, 0.035) !important;
}
.selected-issue-row .nodedc-spreadsheet-cell-button {
background: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 8%, transparent) !important;
}
.selected-issue-row .nodedc-spreadsheet-cell-button:hover {
background: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 14%, transparent) !important;
}
.nodedc-calendar-shell {
@apply rounded-[1.1rem] border-0 bg-transparent p-1 shadow-none outline-none;
}
@ -1304,6 +1369,76 @@
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
}
.nodedc-workspace-page-shell {
width: 100%;
max-width: 104rem;
margin: 0 auto;
padding: 1rem 1rem 1.5rem;
}
@media (min-width: 768px) {
.nodedc-workspace-page-shell {
padding: 1.25rem 1.25rem 1.75rem;
}
}
.nodedc-workspace-toolbar {
border: 0 !important;
outline: none !important;
border-radius: 1.5rem !important;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.036) 0%, rgba(255, 255, 255, 0.016) 100%),
rgba(8, 8, 11, 0.76) !important;
box-shadow:
0 20px 52px rgba(0, 0, 0, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.025) !important;
-webkit-backdrop-filter: blur(28px);
backdrop-filter: blur(28px);
}
.nodedc-workspace-list-row {
border: 0 !important;
outline: none !important;
overflow: visible !important;
isolation: isolate;
border-radius: 1.35rem !important;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.032) 0%, rgba(255, 255, 255, 0.014) 100%),
rgba(255, 255, 255, 0.026) !important;
box-shadow:
0 12px 28px rgba(0, 0, 0, 0.14),
inset 0 1px 0 rgba(255, 255, 255, 0.016) !important;
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
transition:
background 160ms ease,
transform 160ms ease,
box-shadow 160ms ease;
}
.nodedc-workspace-list-row:hover {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.044) 0%, rgba(255, 255, 255, 0.018) 100%),
rgba(255, 255, 255, 0.036) !important;
box-shadow:
0 16px 34px rgba(0, 0, 0, 0.18),
inset 0 1px 0 rgba(255, 255, 255, 0.024) !important;
}
.nodedc-workspace-stat-card {
border: 0 !important;
outline: none !important;
border-radius: 1.3rem !important;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.01) 100%),
rgba(255, 255, 255, 0.022) !important;
box-shadow:
0 14px 32px rgba(0, 0, 0, 0.14),
inset 0 1px 0 rgba(255, 255, 255, 0.018) !important;
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
}
.nodedc-external-empty-state {
display: flex;
width: 100%;