diff --git a/apps/web/app/(all)/[workspaceSlug]/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/layout.tsx index 65ad48f7f38..0e489644d4a 100644 --- a/apps/web/app/(all)/[workspaceSlug]/layout.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/layout.tsx @@ -1,15 +1,18 @@ import { Outlet } from "react-router"; import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; import { WorkspaceContentWrapper } from "@/plane-web/components/workspace/content-wrapper"; +import { AppRailVisibilityProvider } from "@/plane-web/hooks/app-rail"; import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper"; export default function WorkspaceLayout() { return ( - - - + + + + + ); diff --git a/apps/web/ce/components/app-rail/app-rail-hoc.tsx b/apps/web/ce/components/app-rail/app-rail-hoc.tsx index 562695dcd8e..89b9e7778fc 100644 --- a/apps/web/ce/components/app-rail/app-rail-hoc.tsx +++ b/apps/web/ce/components/app-rail/app-rail-hoc.tsx @@ -20,7 +20,7 @@ export function withDockItems

(WrappedComponent: Re const dockItems: (AppSidebarItemData & { shouldRender: boolean })[] = [ { label: "Projects", - icon: , + icon: , href: `/${workspaceSlug}/`, isActive: isProjectsPath && !isNotificationsPath, shouldRender: true, diff --git a/apps/web/ce/components/workspace/content-wrapper.tsx b/apps/web/ce/components/workspace/content-wrapper.tsx index 462fcd584a1..0ebe7a88627 100644 --- a/apps/web/ce/components/workspace/content-wrapper.tsx +++ b/apps/web/ce/components/workspace/content-wrapper.tsx @@ -3,7 +3,8 @@ import { observer } from "mobx-react"; // plane imports import { cn } from "@plane/utils"; import { AppRailRoot } from "@/components/navigation"; -// plane web imports +import { useAppRailVisibility } from "@/lib/app-rail"; +// local imports import { TopNavigationRoot } from "../navigations"; export const WorkspaceContentWrapper = observer(function WorkspaceContentWrapper({ @@ -11,14 +12,21 @@ export const WorkspaceContentWrapper = observer(function WorkspaceContentWrapper }: { children: React.ReactNode; }) { + // Use the context to determine if app rail should render + const { shouldRenderAppRail } = useAppRailVisibility(); + return (

- + {/* Conditionally render AppRailRoot based on context */} + {shouldRenderAppRail && }
{children} diff --git a/apps/web/ce/hooks/app-rail/index.ts b/apps/web/ce/hooks/app-rail/index.ts new file mode 100644 index 00000000000..1a8f850f5f8 --- /dev/null +++ b/apps/web/ce/hooks/app-rail/index.ts @@ -0,0 +1 @@ +export * from "./provider"; diff --git a/apps/web/ce/hooks/app-rail/provider.tsx b/apps/web/ce/hooks/app-rail/provider.tsx new file mode 100644 index 00000000000..f53d7d2eb53 --- /dev/null +++ b/apps/web/ce/hooks/app-rail/provider.tsx @@ -0,0 +1,17 @@ +"use client"; + +import React from "react"; +import { observer } from "mobx-react"; +import { AppRailVisibilityProvider as CoreProvider } from "@/lib/app-rail"; + +interface AppRailVisibilityProviderProps { + children: React.ReactNode; +} + +/** + * CE AppRailVisibilityProvider + * Wraps core provider with isEnabled hardcoded to false + */ +export const AppRailVisibilityProvider = observer(({ children }: AppRailVisibilityProviderProps) => ( + {children} +)); diff --git a/apps/web/core/components/navigation/app-rail-root.tsx b/apps/web/core/components/navigation/app-rail-root.tsx index 38b8804843b..864a81a00fb 100644 --- a/apps/web/core/components/navigation/app-rail-root.tsx +++ b/apps/web/core/components/navigation/app-rail-root.tsx @@ -8,6 +8,7 @@ import { cn } from "@plane/utils"; import { AppSidebarItem } from "@/components/sidebar/sidebar-item"; // hooks import { useAppRailPreferences } from "@/hooks/use-navigation-preferences"; +import { useAppRailVisibility } from "@/lib/app-rail/context"; // plane web imports import { DesktopSidebarWorkspaceMenu } from "@/plane-web/components/desktop"; // local imports @@ -19,6 +20,7 @@ export const AppRailRoot = observer(() => { const pathname = usePathname(); // preferences const { preferences, updateDisplayMode } = useAppRailPreferences(); + const { isCollapsed, toggleAppRail } = useAppRailVisibility(); const isSettingsPath = pathname.includes(`/${workspaceSlug}/settings`); const showLabel = preferences.displayMode === "icon_with_label"; @@ -70,6 +72,10 @@ export const AppRailRoot = observer(() => { {preferences.displayMode === "icon_with_label" && }
+ + + {isCollapsed ? "Dock App Rail" : "Undock App Rail"} + diff --git a/apps/web/core/components/workspace/sidebar/help-section/root.tsx b/apps/web/core/components/workspace/sidebar/help-section/root.tsx index 0fb0b31783b..c746b084e70 100644 --- a/apps/web/core/components/workspace/sidebar/help-section/root.tsx +++ b/apps/web/core/components/workspace/sidebar/help-section/root.tsx @@ -32,7 +32,7 @@ export const HelpMenuRoot = observer(function HelpMenuRoot() { , + icon: , isActive: isNeedHelpOpen, }} /> diff --git a/apps/web/core/components/workspace/sidebar/user-menu-root.tsx b/apps/web/core/components/workspace/sidebar/user-menu-root.tsx index e930396dd7a..6cb7e8813b7 100644 --- a/apps/web/core/components/workspace/sidebar/user-menu-root.tsx +++ b/apps/web/core/components/workspace/sidebar/user-menu-root.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import Link from "next/link"; import { useParams } from "next/navigation"; // icons -import { LogOut, Settings } from "lucide-react"; +import { LogOut, Settings, Settings2 } from "lucide-react"; // plane imports import { GOD_MODE_URL } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; @@ -74,7 +74,7 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: Props) { maxHeight="lg" closeOnSelect > -
+
{currentUser?.email} @@ -84,6 +84,14 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: Props) {
+ + +
+ + Preferences +
+
+
diff --git a/apps/web/core/components/workspace/sidebar/workspace-menu-root.tsx b/apps/web/core/components/workspace/sidebar/workspace-menu-root.tsx index 51071d52e1c..29630324164 100644 --- a/apps/web/core/components/workspace/sidebar/workspace-menu-root.tsx +++ b/apps/web/core/components/workspace/sidebar/workspace-menu-root.tsx @@ -118,7 +118,7 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work

{activeWorkspace?.name ?? t("loading")} diff --git a/apps/web/core/lib/app-rail/context.tsx b/apps/web/core/lib/app-rail/context.tsx new file mode 100644 index 00000000000..b1625af3639 --- /dev/null +++ b/apps/web/core/lib/app-rail/context.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { createContext, useContext } from "react"; +import type { IAppRailVisibilityContext } from "./types"; + +/** + * Context for app-rail visibility control + * Provides access to app rail enabled state, collapse state, and toggle function + */ +export const AppRailVisibilityContext = createContext(undefined); + +/** + * Hook to consume the AppRailVisibilityContext + * Must be used within an AppRailVisibilityProvider + * + * @returns The app rail visibility context + * @throws Error if used outside of AppRailVisibilityProvider + */ +export const useAppRailVisibility = (): IAppRailVisibilityContext => { + const context = useContext(AppRailVisibilityContext); + if (context === undefined) { + throw new Error("useAppRailVisibility must be used within AppRailVisibilityProvider"); + } + return context; +}; diff --git a/apps/web/core/lib/app-rail/index.ts b/apps/web/core/lib/app-rail/index.ts new file mode 100644 index 00000000000..053a94f0c6b --- /dev/null +++ b/apps/web/core/lib/app-rail/index.ts @@ -0,0 +1,3 @@ +export * from "./context"; +export * from "./provider"; +export * from "./types"; diff --git a/apps/web/core/lib/app-rail/provider.tsx b/apps/web/core/lib/app-rail/provider.tsx new file mode 100644 index 00000000000..1785a07c57b --- /dev/null +++ b/apps/web/core/lib/app-rail/provider.tsx @@ -0,0 +1,46 @@ +"use client"; + +import React, { useCallback, useMemo } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import useLocalStorage from "@/hooks/use-local-storage"; +import { AppRailVisibilityContext } from "./context"; +import type { IAppRailVisibilityContext } from "./types"; + +interface AppRailVisibilityProviderProps { + children: React.ReactNode; + isEnabled?: boolean; // Allow override, default false +} + +/** + * AppRailVisibilityProvider - manages app rail visibility state + * Base provider that accepts isEnabled as a prop + */ +export const AppRailVisibilityProvider = observer(({ children, isEnabled = false }: AppRailVisibilityProviderProps) => { + const { workspaceSlug } = useParams(); + + // User preference from localStorage + const { storedValue: isCollapsed, setValue: setIsCollapsed } = useLocalStorage( + `APP_RAIL_${workspaceSlug}`, + false // Default: not collapsed (app rail visible) + ); + + const toggleAppRail = useCallback(() => { + setIsCollapsed(!isCollapsed); + }, [isCollapsed, setIsCollapsed]); + + // Compute final visibility: enabled and not collapsed + const shouldRenderAppRail = isEnabled && !isCollapsed; + + const value: IAppRailVisibilityContext = useMemo( + () => ({ + isEnabled, + isCollapsed: isCollapsed ?? false, + shouldRenderAppRail, + toggleAppRail, + }), + [isEnabled, isCollapsed, shouldRenderAppRail, toggleAppRail] + ); + + return {children}; +}); diff --git a/apps/web/core/lib/app-rail/types.ts b/apps/web/core/lib/app-rail/types.ts new file mode 100644 index 00000000000..36dbcb129e4 --- /dev/null +++ b/apps/web/core/lib/app-rail/types.ts @@ -0,0 +1,26 @@ +/** + * Type definitions for app-rail visibility context + */ + +export interface IAppRailVisibilityContext { + /** + * Whether the app rail is enabled + */ + isEnabled: boolean; + + /** + * Whether the app rail is collapsed (user preference from localStorage) + */ + isCollapsed: boolean; + + /** + * Computed property: whether the app rail should actually render + * True only if isEnabled && !isCollapsed + */ + shouldRenderAppRail: boolean; + + /** + * Toggle the collapse state of the app rail + */ + toggleAppRail: () => void; +} diff --git a/apps/web/ee/hooks/app-rail/index.ts b/apps/web/ee/hooks/app-rail/index.ts new file mode 100644 index 00000000000..0bfcd74e046 --- /dev/null +++ b/apps/web/ee/hooks/app-rail/index.ts @@ -0,0 +1 @@ +export * from "ce/hooks/app-rail"; diff --git a/packages/i18n/src/locales/en/translations.ts b/packages/i18n/src/locales/en/translations.ts index b5b186a4331..a6a991f72f9 100644 --- a/packages/i18n/src/locales/en/translations.ts +++ b/packages/i18n/src/locales/en/translations.ts @@ -2699,8 +2699,8 @@ export default { // Navigation customization customize_navigation: "Customize navigation", personal: "Personal", - accordion_navigation_control: "Accordion navigation control", - horizontal_navigation_bar: "Horizontal navigation bar", + accordion_navigation_control: "Accordion sidebar navigation", + horizontal_navigation_bar: "Tabbed Navigation", show_limited_projects_on_sidebar: "Show limited projects on sidebar", enter_number_of_projects: "Enter number of projects", pin: "Pin", diff --git a/packages/propel/src/icons/sub-brand/plane-icon.tsx b/packages/propel/src/icons/sub-brand/plane-icon.tsx index 01d8fe4e420..978cc1e726d 100644 --- a/packages/propel/src/icons/sub-brand/plane-icon.tsx +++ b/packages/propel/src/icons/sub-brand/plane-icon.tsx @@ -4,16 +4,14 @@ import { IconWrapper } from "../icon-wrapper"; import type { ISvgIcons } from "../type"; export function PlaneNewIcon({ color = "currentColor", ...rest }: ISvgIcons) { - const clipPathId = React.useId(); - return ( - + diff --git a/packages/propel/src/icons/sub-brand/wiki-icon.tsx b/packages/propel/src/icons/sub-brand/wiki-icon.tsx index c6da52c03a5..d062f448bc8 100644 --- a/packages/propel/src/icons/sub-brand/wiki-icon.tsx +++ b/packages/propel/src/icons/sub-brand/wiki-icon.tsx @@ -4,12 +4,10 @@ import { IconWrapper } from "../icon-wrapper"; import type { ISvgIcons } from "../type"; export function WikiIcon({ color = "currentColor", ...rest }: ISvgIcons) { - const clipPathId = React.useId(); - return ( - +