-
+ {/* 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 (
-
+