[WEB-4546] chore: header enhancements + quickstart guide ui update + breadcrumb #7451

This commit is contained in:
Akshita Goyal
2025-07-22 16:47:14 +05:30
committed by GitHub
parent 5c22a6cecc
commit 763a28ab60
22 changed files with 307 additions and 148 deletions

View File

@@ -3,24 +3,25 @@
import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme } from "next-themes";
import { Home } from "lucide-react";
import { Home, Shapes } from "lucide-react";
// images
import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png";
// ui
import { GITHUB_REDIRECTED_TRACKER_EVENT, HEADER_GITHUB_ICON } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs, Header } from "@plane/ui";
import { Breadcrumbs, Button, Header } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
// constants
// hooks
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
import { useHome } from "@/hooks/store/use-home";
export const WorkspaceDashboardHeader = observer(() => {
// hooks
const { resolvedTheme } = useTheme();
const { toggleWidgetSettings } = useHome();
const { t } = useTranslation();
return (
@@ -38,6 +39,15 @@ export const WorkspaceDashboardHeader = observer(() => {
</div>
</Header.LeftItem>
<Header.RightItem>
<Button
variant="neutral-primary"
size="sm"
onClick={() => toggleWidgetSettings(true)}
className="my-auto mb-0"
>
<Shapes size={16} />
<div className="text-xs font-medium">{t("home.manage_widgets")}</div>
</Button>
<a
onClick={() =>
captureElementAndEvent({

View File

@@ -2,6 +2,7 @@
import { CommandPalette } from "@/components/command-palette";
import { AuthenticationWrapper } from "@/lib/wrappers";
// plane web components
import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper";
import { ProjectAppSidebar } from "./_sidebar";

View File

@@ -4,7 +4,7 @@ import { useCallback, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// icons
import { PanelRight } from "lucide-react";
import { ChartNoAxesColumn, ListFilter, PanelRight, SlidersHorizontal } from "lucide-react";
// plane imports
import {
EIssueFilterType,
@@ -30,7 +30,13 @@ import { cn, isIssueFilterActive } from "@plane/utils";
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
import { SwitcherLabel } from "@/components/common";
import { CycleQuickActions } from "@/components/cycles";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
import {
DisplayFiltersSelection,
FiltersDropdown,
FilterSelection,
LayoutSelection,
MobileLayoutSelection,
} from "@/components/issues";
// hooks
import {
useCommandPalette,
@@ -207,21 +213,31 @@ export const CycleIssuesHeader: React.FC = observer(() => {
</Header.LeftItem>
<Header.RightItem className="items-center">
<div className="hidden items-center gap-2 md:flex ">
<LayoutSelection
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<div className="hidden @4xl:flex">
<LayoutSelection
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
</div>
<div className="flex @4xl:hidden">
<MobileLayoutSelection
layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN, EIssueLayoutTypes.CALENDAR]}
onChange={(layout) => handleLayoutChange(layout)}
activeLayout={activeLayout}
/>
</div>
<FiltersDropdown
title={t("common.filters")}
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
miniIcon={<ListFilter className="size-3.5" />}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
@@ -238,7 +254,11 @@ export const CycleIssuesHeader: React.FC = observer(() => {
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
<FiltersDropdown title={t("common.display")} placement="bottom-end">
<FiltersDropdown
title={t("common.display")}
placement="bottom-end"
miniIcon={<SlidersHorizontal className="size-3.5" />}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
@@ -256,7 +276,10 @@ export const CycleIssuesHeader: React.FC = observer(() => {
{canUserCreateIssue && (
<>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
{t("common.analytics")}
<div className="hidden @4xl:flex">Analytics</div>
<div className="flex @4xl:hidden">
<ChartNoAxesColumn className="size-3.5" />
</div>
</Button>
{!isCompletedCycle && (
<Button

View File

@@ -24,6 +24,7 @@ import {
FilterSelection,
FiltersDropdown,
IssueLayoutIcon,
MobileLayoutSelection,
} from "@/components/issues/issue-layouts";
// helpers
// hooks
@@ -108,32 +109,10 @@ export const ProjectIssuesMobileHeader = observer(() => {
projectDetails={currentProjectDetails ?? undefined}
/>
<div className="md:hidden flex justify-evenly border-b border-custom-border-200 py-2 z-[13] bg-custom-background-100">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-sm text-custom-text-200"
placement="bottom-start"
customButton={
<div className="flex flex-start text-sm text-custom-text-200">
{t("common.layout")}
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
</div>
}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
>
{layouts.map((layout, index) => (
<CustomMenu.MenuItem
key={index}
onClick={() => {
handleLayoutChange(ISSUE_LAYOUTS[index].key);
}}
className="flex items-center gap-2"
>
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
<div className="text-custom-text-300">{t(layout.titleTranslationKey)}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<MobileLayoutSelection
layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN, EIssueLayoutTypes.CALENDAR]}
onChange={handleLayoutChange}
/>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown
title={t("common.filters")}

View File

@@ -4,7 +4,7 @@ import { useCallback, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// icons
import { PanelRight } from "lucide-react";
import { ChartNoAxesColumn, ListFilter, PanelRight, SlidersHorizontal } from "lucide-react";
// plane imports
import {
EIssueFilterType,
@@ -27,7 +27,13 @@ import { cn, isIssueFilterActive } from "@plane/utils";
// components
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
import { SwitcherLabel } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
import {
DisplayFiltersSelection,
FiltersDropdown,
FilterSelection,
LayoutSelection,
MobileLayoutSelection,
} from "@/components/issues";
// helpers
import { ModuleQuickActions } from "@/components/modules";
// hooks
@@ -198,21 +204,31 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
</Header.LeftItem>
<Header.RightItem className="items-center">
<div className="hidden gap-2 md:flex">
<LayoutSelection
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<div className="hidden @4xl:flex">
<LayoutSelection
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
</div>
<div className="flex @4xl:hidden">
<MobileLayoutSelection
layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN, EIssueLayoutTypes.CALENDAR]}
onChange={(layout) => handleLayoutChange(layout)}
activeLayout={activeLayout}
/>
</div>
<FiltersDropdown
title="Filters"
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
miniIcon={<ListFilter className="size-3.5" />}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
@@ -229,7 +245,11 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<FiltersDropdown
title="Display"
placement="bottom-end"
miniIcon={<SlidersHorizontal className="size-3.5" />}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
@@ -253,7 +273,10 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
variant="neutral-primary"
size="sm"
>
Analytics
<div className="hidden @4xl:flex">Analytics</div>
<div className="flex @4xl:hidden">
<ChartNoAxesColumn className="size-3.5" />
</div>
</Button>
<Button
className="hidden sm:flex"

View File

@@ -65,6 +65,7 @@ export const ProjectBreadcrumb = observer((props: TProjectBreadcrumbProps) => {
if (handleOnClick) handleOnClick();
else router.push(`/${workspaceSlug}/projects/${currentProjectDetails.id}/issues/`);
}}
shouldTruncate
/>
}
showSeparator={false}

View File

@@ -0,0 +1,16 @@
import { ReactNode } from "react";
import { AppSidebarToggleButton } from "@/components/sidebar";
import { useAppTheme } from "@/hooks/store/use-app-theme";
export const ExtendedAppHeader = (props: { header: ReactNode }) => {
const { header } = props;
// store hooks
const { sidebarCollapsed } = useAppTheme();
return (
<>
{sidebarCollapsed && <AppSidebarToggleButton />}
<div className="w-full">{header}</div>
</>
);
};

View File

@@ -1 +1,2 @@
export * from "./subscription";
export * from "./extended-app-header";

View File

@@ -5,9 +5,7 @@ import { observer } from "mobx-react";
// plane imports
import { Row } from "@plane/ui";
// components
import { AppSidebarToggleButton } from "@/components/sidebar";
// hooks
import { useAppTheme } from "@/hooks/store";
import { ExtendedAppHeader } from "@/plane-web/components/common";
export interface AppHeaderProps {
header: ReactNode;
@@ -16,14 +14,11 @@ export interface AppHeaderProps {
export const AppHeader = observer((props: AppHeaderProps) => {
const { header, mobileHeader } = props;
// store hooks
const { sidebarCollapsed } = useAppTheme();
return (
<div className="z-[18]">
<Row className="h-header flex gap-2 w-full items-center border-b border-custom-border-200 bg-custom-sidebar-background-100">
{sidebarCollapsed && <AppSidebarToggleButton />}
<div className="w-full">{header}</div>
<ExtendedAppHeader header={header} />
</Row>
{mobileHeader && mobileHeader}
</div>

View File

@@ -12,7 +12,6 @@ import { captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
import { useUserProfile, useUser } from "@/hooks/store";
import { useHome } from "@/hooks/store/use-home";
import useSize from "@/hooks/use-window-size";
// plane web components
import { HomePeekOverviewsRoot } from "@/plane-web/components/home";
// local imports
@@ -24,8 +23,7 @@ export const WorkspaceHomeView = observer(() => {
const { workspaceSlug } = useParams();
const { data: currentUser } = useUser();
const { data: currentUserProfile, updateTourCompleted } = useUserProfile();
const { toggleWidgetSettings, fetchWidgets } = useHome();
const [windowWidth] = useSize();
const { fetchWidgets } = useHome();
useSWR(
workspaceSlug ? `HOME_DASHBOARD_WIDGETS_${workspaceSlug}` : null,
@@ -62,12 +60,8 @@ export const WorkspaceHomeView = observer(() => {
)}
<>
<HomePeekOverviewsRoot />
<ContentWrapper
className={cn("gap-6 bg-custom-background-90/20", {
"vertical-scrollbar scrollbar-lg": windowWidth >= 768,
})}
>
{currentUser && <UserGreetingsView user={currentUser} handleWidgetModal={() => toggleWidgetSettings(true)} />}
<ContentWrapper className={cn("gap-6 bg-custom-background-100 max-w-[750px] mx-auto scrollbar-hide")}>
{currentUser && <UserGreetingsView user={currentUser} />}
<DashboardWidgets />
</ContentWrapper>
</>

View File

@@ -1,20 +1,17 @@
import { FC } from "react";
import { Shapes } from "lucide-react";
// plane types
import { useTranslation } from "@plane/i18n";
import { IUser } from "@plane/types";
// plane ui
import { Button } from "@plane/ui";
// hooks
import { useCurrentTime } from "@/hooks/use-current-time";
export interface IUserGreetingsView {
user: IUser;
handleWidgetModal: () => void;
}
export const UserGreetingsView: FC<IUserGreetingsView> = (props) => {
const { user, handleWidgetModal } = props;
const { user } = props;
// current time hook
const { currentTime } = useCurrentTime();
// store hooks
@@ -44,22 +41,16 @@ export const UserGreetingsView: FC<IUserGreetingsView> = (props) => {
const greeting = parseInt(hour, 10) < 12 ? "morning" : parseInt(hour, 10) < 18 ? "afternoon" : "evening";
return (
<div className="flex justify-between">
<div>
<h3 className="text-xl font-semibold text-center">
{t("good")} {t(greeting)}, {user?.first_name} {user?.last_name}
</h3>
<h6 className="flex items-center gap-2 font-medium text-custom-text-400">
<div>{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}</div>
<div>
{weekDay}, {date} {timeString}
</div>
</h6>
</div>
<Button variant="neutral-primary" size="sm" onClick={handleWidgetModal} className="my-auto mb-0">
<Shapes size={16} />
<div className="text-xs font-medium">{t("home.manage_widgets")}</div>
</Button>
<div className="flex flex-col items-center my-6">
<h3 className="text-xl font-semibold text-center">
{t("good")} {t(greeting)}, {user?.first_name} {user?.last_name}
</h3>
<h6 className="flex items-center gap-2 font-medium text-custom-text-400">
<div>{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}</div>
<div>
{weekDay}, {date} {timeString}
</div>
</h6>
</div>
);
};

View File

@@ -44,7 +44,7 @@ export const NoProjectsEmptyState = observer(() => {
id: "create-project",
title: "home.empty.create_project.title",
description: "home.empty.create_project.description",
icon: <Briefcase className="size-10" />,
icon: <Briefcase className="size-4" />,
flag: "projects",
cta: {
text: "home.empty.create_project.cta",
@@ -62,7 +62,7 @@ export const NoProjectsEmptyState = observer(() => {
id: "invite-team",
title: "home.empty.invite_team.title",
description: "home.empty.invite_team.description",
icon: <Users className="size-10" />,
icon: <Users className="size-4" />,
flag: "visited_members",
cta: {
text: "home.empty.invite_team.cta",
@@ -74,7 +74,7 @@ export const NoProjectsEmptyState = observer(() => {
id: "configure-workspace",
title: "home.empty.configure_workspace.title",
description: "home.empty.configure_workspace.description",
icon: <Hotel className="size-10" />,
icon: <Hotel className="size-4" />,
flag: "visited_workspace",
cta: {
text: "home.empty.configure_workspace.cta",
@@ -89,7 +89,7 @@ export const NoProjectsEmptyState = observer(() => {
icon:
currentUser?.avatar_url && currentUser?.avatar_url.trim() !== "" ? (
<Link href={`/${workspaceSlug}/profile/${currentUser?.id}`}>
<span className="relative flex h-6 w-6 items-center justify-center rounded-full p-4 capitalize text-white">
<span className="relative flex size-4 items-center justify-center rounded-full p-4 capitalize text-white">
<img
src={getFileURL(currentUser?.avatar_url)}
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
@@ -99,7 +99,7 @@ export const NoProjectsEmptyState = observer(() => {
</Link>
) : (
<Link href={`/${workspaceSlug}/profile/${currentUser?.id}`}>
<span className="relative flex h-6 w-6 items-center justify-center rounded-full bg-gray-700 p-4 capitalize text-white text-sm">
<span className="relative flex size-4 items-center justify-center rounded-full bg-gray-700 p-4 capitalize text-white text-sm">
{(currentUser?.email ?? currentUser?.display_name ?? "?")[0]}
</span>
</Link>
@@ -142,17 +142,17 @@ export const NoProjectsEmptyState = observer(() => {
{t("home.empty.not_right_now")}
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{EMPTY_STATE_DATA.map((item) => {
const isStateComplete = isComplete(item.flag);
return (
<div
key={item.id}
className="flex flex-col items-center justify-center p-6 bg-custom-background-100 rounded-lg text-center border border-custom-border-200/40"
className="flex flex-col p-4 bg-custom-background-100 rounded-xl border border-custom-border-200/40"
>
<div
className={cn(
"grid place-items-center bg-custom-background-90 rounded-full size-20 mb-3 text-custom-text-400",
"grid place-items-center bg-custom-background-90 rounded-full size-9 mb-3 text-custom-text-400",
{
"text-custom-primary-100 bg-custom-primary-100/10": !isStateComplete,
}
@@ -160,10 +160,10 @@ export const NoProjectsEmptyState = observer(() => {
>
<span className="text-3xl my-auto">{item.icon}</span>
</div>
<h3 className="text-base font-medium text-custom-text-100 mb-2">{t(item.title)}</h3>
<p className="text-sm text-custom-text-300 mb-2">{t(item.description)}</p>
<h3 className="text-sm font-medium text-custom-text-100 mb-2">{t(item.title)}</h3>
<p className="text-[11px] text-custom-text-300 mb-2">{t(item.description)}</p>
{isStateComplete ? (
<div className="flex items-center gap-2 bg-[#17a34a] rounded-full p-1">
<div className="flex items-center gap-2 bg-[#17a34a] rounded-full p-1 w-fit">
<Check className="size-3 text-custom-primary-100 text-white" />
</div>
) : (

View File

@@ -17,13 +17,21 @@ import {
import { Button } from "@plane/ui";
// components
import { isIssueFilterActive } from "@plane/utils";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
import {
DisplayFiltersSelection,
FiltersDropdown,
FilterSelection,
IssueLayoutIcon,
LayoutSelection,
MobileLayoutSelection,
} from "@/components/issues";
// helpers
// hooks
import { useLabel, useProjectState, useMember, useIssues } from "@/hooks/store";
// plane web types
import { TProject } from "@/plane-web/types";
import { WorkItemsModal } from "../analytics/work-items/modal";
import { ChartNoAxesColumn, ChevronDown, ListFilter, SlidersHorizontal } from "lucide-react";
type Props = {
currentProjectDetails: TProject | undefined;
@@ -32,6 +40,13 @@ type Props = {
canUserCreateIssue: boolean | undefined;
storeType?: EIssuesStoreType.PROJECT | EIssuesStoreType.EPIC;
};
const LAYOUTS = [
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
];
const HeaderFilters = observer((props: Props) => {
const {
currentProjectDetails,
@@ -109,21 +124,25 @@ const HeaderFilters = observer((props: Props) => {
projectDetails={currentProjectDetails ?? undefined}
isEpic={storeType === EIssuesStoreType.EPIC}
/>
<LayoutSelection
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<div className="hidden @4xl:flex">
<LayoutSelection
layouts={LAYOUTS}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
</div>
<div className="flex @4xl:hidden">
<MobileLayoutSelection
layouts={LAYOUTS}
onChange={(layout) => handleLayoutChange(layout)}
activeLayout={activeLayout}
/>
</div>
<FiltersDropdown
title={t("common.filters")}
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
miniIcon={<ListFilter className="size-3.5" />}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
@@ -140,7 +159,11 @@ const HeaderFilters = observer((props: Props) => {
isEpic={storeType === EIssuesStoreType.EPIC}
/>
</FiltersDropdown>
<FiltersDropdown title={t("common.display")} placement="bottom-end">
<FiltersDropdown
miniIcon={<SlidersHorizontal className="size-3.5" />}
title={t("common.display")}
placement="bottom-end"
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={layoutDisplayFiltersOptions}
displayFilters={issueFilters?.displayFilters ?? {}}
@@ -153,8 +176,16 @@ const HeaderFilters = observer((props: Props) => {
/>
</FiltersDropdown>
{canUserCreateIssue ? (
<Button className="hidden md:block" onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
{t("common.analytics")}
<Button
className="hidden md:block px-2"
onClick={() => setAnalyticsModal(true)}
variant="neutral-primary"
size="sm"
>
<div className="hidden @4xl:flex">{t("common.analytics")}</div>
<div className="flex @4xl:hidden">
<ChartNoAxesColumn className="size-3.5" />
</div>
</Button>
) : (
<></>

View File

@@ -13,6 +13,7 @@ import { Button } from "@plane/ui";
type Props = {
children: React.ReactNode;
icon?: React.ReactNode;
miniIcon?: React.ReactNode;
title?: string;
placement?: Placement;
disabled?: boolean;
@@ -24,6 +25,7 @@ type Props = {
export const FiltersDropdown: React.FC<Props> = (props) => {
const {
children,
miniIcon,
icon,
title = "Dropdown",
placement,
@@ -33,7 +35,7 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
isFiltersApplied = false,
} = props;
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | HTMLDivElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
@@ -53,27 +55,42 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
{menuButton}
</button>
) : (
<Button
disabled={disabled}
ref={setReferenceElement}
variant="neutral-primary"
size="sm"
prependIcon={icon}
appendIcon={
<ChevronUp className={`transition-all ${open ? "" : "rotate-180"}`} size={14} strokeWidth={2} />
}
tabIndex={tabIndex}
className="relative"
>
<>
<div className={`${open ? "text-custom-text-100" : "text-custom-text-200"}`}>
<span>{title}</span>
</div>
{isFiltersApplied && (
<span className="absolute h-2 w-2 -right-0.5 -top-0.5 bg-custom-primary-100 rounded-full" />
)}
</>
</Button>
<div ref={setReferenceElement}>
<div className="hidden @4xl:flex">
<Button
disabled={disabled}
variant="neutral-primary"
size="sm"
prependIcon={icon}
appendIcon={
<ChevronUp className={`transition-all ${open ? "" : "rotate-180"}`} size={14} strokeWidth={2} />
}
tabIndex={tabIndex}
className="relative"
>
<>
<div className={`${open ? "text-custom-text-100" : "text-custom-text-200"}`}>
<span>{title}</span>
</div>
{isFiltersApplied && (
<span className="absolute h-2 w-2 -right-0.5 -top-0.5 bg-custom-primary-100 rounded-full" />
)}
</>
</Button>
</div>
<div className="flex @4xl:hidden">
<Button
disabled={disabled}
ref={setReferenceElement}
variant="neutral-primary"
size="sm"
tabIndex={tabIndex}
className="relative px-2"
>
{miniIcon || title}
</Button>
</div>
</div>
)}
</Popover.Button>
<Transition

View File

@@ -2,3 +2,4 @@ export * from "./display-filters";
export * from "./filters";
export * from "./helpers";
export * from "./layout-selection";
export * from "./mobile-layout-selection";

View File

@@ -0,0 +1,54 @@
import { ChevronDown } from "lucide-react";
import { ISSUE_LAYOUTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EIssueLayoutTypes } from "@plane/types";
import { Button, CustomMenu } from "@plane/ui";
import { IssueLayoutIcon } from "../../layout-icon";
export const MobileLayoutSelection = ({
layouts,
onChange,
activeLayout,
}: {
layouts: EIssueLayoutTypes[];
onChange: (layout: EIssueLayoutTypes) => void;
activeLayout?: EIssueLayoutTypes;
isMobile?: boolean;
}) => {
const { t } = useTranslation();
return (
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-sm text-custom-text-200"
placement="bottom-start"
customButton={
activeLayout ? (
<Button variant="neutral-primary" size="sm" className="relative px-2">
<IssueLayoutIcon layout={activeLayout} size={14} strokeWidth={2} className={`h-3.5 w-3.5`} />
<ChevronDown className="size-3 text-custom-text-200 my-auto" strokeWidth={2} />
</Button>
) : (
<div className="flex flex-start text-sm text-custom-text-200">
{t("common.layout")}
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
</div>
)
}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
>
{ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout, index) => (
<CustomMenu.MenuItem
key={index}
onClick={() => {
onChange(layout.key);
}}
className="flex items-center gap-2"
>
<IssueLayoutIcon layout={layout.key} className="h-3 w-3" />
<div className="text-custom-text-300">{t(layout.i18n_title)}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
);
};

View File

@@ -0,0 +1 @@
export * from "./extended-app-header";

View File

@@ -0,0 +1 @@
export * from "./extended-app-header";

View File

@@ -487,6 +487,14 @@ module.exports = {
paddingRight: "1.35rem",
},
},
// Hide scrollbar but keep functionality
".scrollbar-hide": {
"-ms-overflow-style": "none" /* IE and Edge */,
"scrollbar-width": "none" /* Firefox */,
"&::-webkit-scrollbar": {
display: "none" /* Chrome, Safari and Opera */,
},
},
};
addUtilities(newUtilities, ["responsive"]);

View File

@@ -45,8 +45,11 @@ export const BreadcrumbNavigationDropdown = (props: TBreadcrumbNavigationDropdow
}
)}
>
{selectedItemIcon && <Breadcrumbs.Icon>{selectedItemIcon}</Breadcrumbs.Icon>}
<Breadcrumbs.Label>{selectedItem.title}</Breadcrumbs.Label>
<div className="flex @4xl:hidden text-custom-text-300">...</div>
<div className="hidden @4xl:flex gap-2">
{selectedItemIcon && <Breadcrumbs.Icon>{selectedItemIcon}</Breadcrumbs.Icon>}
<Breadcrumbs.Label>{selectedItem.title}</Breadcrumbs.Label>
</div>
</button>
</Tooltip>
);

View File

@@ -16,6 +16,7 @@ type TBreadcrumbNavigationSearchDropdownProps = {
isLast?: boolean;
handleOnClick?: () => void;
disableRootHover?: boolean;
shouldTruncate?: boolean;
};
export const BreadcrumbNavigationSearchDropdown: React.FC<TBreadcrumbNavigationSearchDropdownProps> = (props) => {
@@ -28,6 +29,7 @@ export const BreadcrumbNavigationSearchDropdown: React.FC<TBreadcrumbNavigationS
navigationDisabled = false,
isLast = false,
handleOnClick,
shouldTruncate = false,
} = props;
// state
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
@@ -65,8 +67,15 @@ export const BreadcrumbNavigationSearchDropdown: React.FC<TBreadcrumbNavigationS
}
)}
>
{icon && <Breadcrumbs.Icon>{icon}</Breadcrumbs.Icon>}
<Breadcrumbs.Label>{title}</Breadcrumbs.Label>
{shouldTruncate && <div className="flex @4xl:hidden text-custom-text-300">...</div>}
<div
className={cn("flex gap-2", {
"hidden @4xl:flex gap-2": shouldTruncate,
})}
>
{icon && <Breadcrumbs.Icon>{icon}</Breadcrumbs.Icon>}
<Breadcrumbs.Label>{title}</Breadcrumbs.Label>
</div>
</button>
</Tooltip>
<Breadcrumbs.Separator

View File

@@ -24,5 +24,5 @@ export const minHeights: IHeaderProperties = {
export const getHeaderStyle = (variant: THeaderVariant, setMinHeight: boolean, showOnMobile: boolean) => {
const height = setMinHeight ? minHeights[variant] : "";
const display = variant === EHeaderVariant.SECONDARY ? (showOnMobile ? "flex" : "hidden md:flex") : "";
return " " + headerStyle[variant] + " " + height + " " + display;
return " @container " + headerStyle[variant] + " " + height + " " + display;
};