From b210bf5f48875392a6e096253e52774995c6be12 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Tue, 20 Jan 2026 17:48:04 +0000 Subject: [PATCH 01/39] Initial hide/show the side menu with toggle switch --- .../navigation/EnvironmentSelector.tsx | 19 +- .../app/components/navigation/SideMenu.tsx | 308 ++++++++++++++++-- .../components/navigation/SideMenuItem.tsx | 34 +- .../components/navigation/SideMenuSection.tsx | 8 + .../route.tsx | 2 +- 5 files changed, 349 insertions(+), 22 deletions(-) diff --git a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx index def3996f85..c44265158a 100644 --- a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx +++ b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx @@ -9,7 +9,7 @@ import { useOrganization, type MatchedOrganization } from "~/hooks/useOrganizati import { useProject } from "~/hooks/useProject"; import { cn } from "~/utils/cn"; import { branchesPath, docsPath, v3BillingPath } from "~/utils/pathBuilder"; -import { EnvironmentCombo } from "../environments/EnvironmentLabel"; +import { EnvironmentCombo, EnvironmentIcon, environmentFullTitle } from "../environments/EnvironmentLabel"; import { ButtonContent } from "../primitives/Buttons"; import { Header2 } from "../primitives/Headers"; import { Paragraph } from "../primitives/Paragraph"; @@ -22,6 +22,7 @@ import { PopoverTrigger, } from "../primitives/Popover"; import { TextLink } from "../primitives/TextLink"; +import { SimpleTooltip } from "../primitives/Tooltip"; import { V4Badge } from "../V4Badge"; import { type SideMenuEnvironment, type SideMenuProject } from "./SideMenu"; import { Badge } from "../primitives/Badge"; @@ -31,11 +32,13 @@ export function EnvironmentSelector({ project, environment, className, + isCollapsed = false, }: { organization: MatchedOrganization; project: SideMenuProject; environment: SideMenuEnvironment; className?: string; + isCollapsed?: boolean; }) { const { isManagedCloud } = useFeatures(); const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -48,6 +51,20 @@ export function EnvironmentSelector({ const hasStaging = project.environments.some((env) => env.type === "STAGING"); + if (isCollapsed) { + return ( + + + + } + content={environmentFullTitle(environment)} + side="right" + /> + ); + } + return ( setIsMenuOpen(open)} open={isMenuOpen}> & { isImpersonating: boolean }; export type SideMenuProject = Pick< @@ -130,6 +129,7 @@ export function SideMenu({ }: SideMenuProps) { const borderRef = useRef(null); const [showHeaderDivider, setShowHeaderDivider] = useState(false); + const [isCollapsed, setIsCollapsed] = useState(false); const currentPlan = useCurrentPlan(); const { isConnected } = useDevPresence(); const isFreeUser = currentPlan?.v3Subscription?.isPaying === false; @@ -154,9 +154,11 @@ export function SideMenu({ return (
+ setIsCollapsed(!isCollapsed)} />
- {isAdmin && !user.isImpersonating ? ( + {!isCollapsed && isAdmin && !user.isImpersonating ? ( @@ -182,7 +185,7 @@ export function SideMenu({ - ) : isAdmin && user.isImpersonating ? ( + ) : !isCollapsed && isAdmin && user.isImpersonating ? ( ) : null}
@@ -190,16 +193,17 @@ export function SideMenu({ className="overflow-hidden overflow-y-auto pt-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600" ref={borderRef} > -
+
- + {!isCollapsed && }
- {environment.type === "DEVELOPMENT" && project.engine === "V2" && ( + {!isCollapsed && environment.type === "DEVELOPMENT" && project.engine === "V2" && ( @@ -236,12 +240,14 @@ export function SideMenu({ activeIconColor="text-tasks" to={v3EnvironmentPath(organization, project, environment)} data-action="tasks" + isCollapsed={isCollapsed} /> {(user.admin || user.isImpersonating || featureFlags.hasLogsPageAccess) && ( } + isCollapsed={isCollapsed} /> )} {(user.admin || user.isImpersonating || featureFlags.hasQueryAccess) && ( } + isCollapsed={isCollapsed} /> )}
- + } + isCollapsed={isCollapsed} /> - + } + isCollapsed={isCollapsed} /> {isManagedCloud && ( )} } + isCollapsed={isCollapsed} />
- -
+ {!isCollapsed && } +
- +
- {isFreeUser && ( + {!isCollapsed && isFreeUser && ( setOrgMenuOpen(open)} open={isOrgMenuOpen}> + + + + + + + + + {organization.title} / {project.name} + + + + +
+
+ + +
+ +
+ +
+ {organization.title} +
+ {plan && ( + + {plan} plan + + )} + {simplur`${organization.membersCount} member[|s]`} +
+
+
+
+ + + Settings + + {isManagedCloud && ( + + + Usage + + )} +
+
+
+ {organization.projects.map((p) => { + const isSelected = p.id === project.id; + return ( + + {p.name} +
+ } + isSelected={isSelected} + icon={isSelected ? FolderOpenIcon : FolderIcon} + leadingIconClassName="text-indigo-500" + /> + ); + })} + +
+
+ {organizations.length > 1 ? ( + + ) : ( + + )} +
+
+ +
+
+ +
+ + + ); + } + return ( setOrgMenuOpen(open)} open={isOrgMenuOpen}> - - + {!isCollapsed && } + {!isCollapsed && } ); } + +function AnimatedChevron({ + isHovering, + isCollapsed, +}: { + isHovering: boolean; + isCollapsed: boolean; +}) { + // When hovering and expanded: left chevron (pointing left to collapse) + // When hovering and collapsed: right chevron (pointing right to expand) + // When not hovering: straight vertical line + + const getRotation = () => { + if (!isHovering) return { top: 0, bottom: 0 }; + if (isCollapsed) { + // Right chevron + return { top: -17, bottom: 17 }; + } else { + // Left chevron + return { top: 17, bottom: -17 }; + } + }; + + const { top, bottom } = getRotation(); + + // Calculate horizontal offset to keep chevron centered when rotated + // Left chevron: translate left (-1.5px) + // Right chevron: translate right (+1.5px) + const getTranslateX = () => { + if (!isHovering) return 0; + return isCollapsed ? 1.5 : -1.5; + }; + + return ( + + {/* Top segment */} + + {/* Bottom segment */} + + + ); +} + +function CollapseToggle({ + isCollapsed, + onToggle, +}: { + isCollapsed: boolean; + onToggle: () => void; +}) { + const [isHovering, setIsHovering] = useState(false); + + return ( +
+ + + + + + + {isCollapsed ? "Expand" : "Collapse"} + + + +
+ ); +} diff --git a/apps/webapp/app/components/navigation/SideMenuItem.tsx b/apps/webapp/app/components/navigation/SideMenuItem.tsx index 1f89368829..c4cc4abdfb 100644 --- a/apps/webapp/app/components/navigation/SideMenuItem.tsx +++ b/apps/webapp/app/components/navigation/SideMenuItem.tsx @@ -1,8 +1,10 @@ import { type AnchorHTMLAttributes, type ReactNode } from "react"; +import { Link } from "@remix-run/react"; import { usePathName } from "~/hooks/usePathName"; import { cn } from "~/utils/cn"; import { LinkButton } from "../primitives/Buttons"; -import { type RenderIcon } from "../primitives/Icon"; +import { type RenderIcon, Icon } from "../primitives/Icon"; +import { SimpleTooltip } from "../primitives/Tooltip"; export function SideMenuItem({ icon, @@ -14,6 +16,7 @@ export function SideMenuItem({ to, badge, target, + isCollapsed = false, }: { icon?: RenderIcon; activeIconColor?: string; @@ -24,10 +27,39 @@ export function SideMenuItem({ to: string; badge?: ReactNode; target?: AnchorHTMLAttributes["target"]; + isCollapsed?: boolean; }) { const pathName = usePathName(); const isActive = pathName === to; + if (isCollapsed) { + return ( + + + + } + content={name} + side="right" + asChild + /> + ); + } + return ( void; children: React.ReactNode; + /** When true, hides the section header and shows only children */ + isSideMenuCollapsed?: boolean; }; /** A collapsible section for the side menu @@ -17,6 +19,7 @@ export function SideMenuSection({ initialCollapsed = false, onCollapseToggle, children, + isSideMenuCollapsed = false, }: Props) { const [isCollapsed, setIsCollapsed] = useState(initialCollapsed); @@ -26,6 +29,11 @@ export function SideMenuSection({ onCollapseToggle?.(newIsCollapsed); }, [isCollapsed, onCollapseToggle]); + // When the side menu is collapsed, just render the children without the header + if (isSideMenuCollapsed) { + return
{children}
; + } + return (
-
+
Date: Tue, 20 Jan 2026 18:01:09 +0000 Subject: [PATCH 02/39] Move Waitpoint tokens into the main list --- .../app/components/navigation/SideMenu.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index a76e4a42dd..45f52de799 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -273,6 +273,14 @@ export function SideMenu({ data-action="queues" isCollapsed={isCollapsed} /> + } + isCollapsed={isCollapsed} + /> - - } - isCollapsed={isCollapsed} - /> - - Date: Tue, 20 Jan 2026 20:10:05 +0000 Subject: [PATCH 03/39] Nicer styling for the toggle button --- .../app/components/navigation/SideMenu.tsx | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 45f52de799..35f334a023 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -856,43 +856,43 @@ function AnimatedChevron({ return ( {/* Top segment */} {/* Bottom segment */} + {/* Vertical line to mask the side menu border */} +
@@ -920,7 +922,7 @@ function CollapseToggle({ onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} className={cn( - "group flex h-10 w-6 items-center justify-center rounded-md text-text-dimmed transition-all duration-200", + "group flex h-12 w-6 items-center justify-center rounded-md text-text-dimmed transition-all duration-200", isHovering ? "border border-grid-bright bg-background-bright shadow-md hover:bg-charcoal-750 hover:text-text-bright" : "border border-transparent bg-transparent" From b4735c20d9388f0149186845c3a4c4b0213aefd5 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Tue, 20 Jan 2026 21:18:18 +0000 Subject: [PATCH 04/39] Animate collapsing the side menu --- .../navigation/EnvironmentSelector.tsx | 49 ++- .../app/components/navigation/SideMenu.tsx | 329 ++++++++---------- .../components/navigation/SideMenuHeader.tsx | 25 +- .../components/navigation/SideMenuItem.tsx | 85 ++--- .../components/navigation/SideMenuSection.tsx | 22 +- 5 files changed, 258 insertions(+), 252 deletions(-) diff --git a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx index c44265158a..0678c50e94 100644 --- a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx +++ b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx @@ -1,5 +1,6 @@ import { ChevronRightIcon, Cog8ToothIcon } from "@heroicons/react/20/solid"; import { useNavigation } from "@remix-run/react"; +import { motion } from "framer-motion"; import { useEffect, useRef, useState } from "react"; import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons"; import { useEnvironment } from "~/hooks/useEnvironment"; @@ -9,7 +10,7 @@ import { useOrganization, type MatchedOrganization } from "~/hooks/useOrganizati import { useProject } from "~/hooks/useProject"; import { cn } from "~/utils/cn"; import { branchesPath, docsPath, v3BillingPath } from "~/utils/pathBuilder"; -import { EnvironmentCombo, EnvironmentIcon, environmentFullTitle } from "../environments/EnvironmentLabel"; +import { EnvironmentCombo, EnvironmentIcon, EnvironmentLabel, environmentFullTitle } from "../environments/EnvironmentLabel"; import { ButtonContent } from "../primitives/Buttons"; import { Header2 } from "../primitives/Headers"; import { Paragraph } from "../primitives/Paragraph"; @@ -51,16 +52,35 @@ export function EnvironmentSelector({ const hasStaging = project.environments.some((env) => env.type === "STAGING"); + const trigger = ( +
+ + + + +
+ ); + if (isCollapsed) { return ( - -
- } + button={trigger} content={environmentFullTitle(environment)} side="right" + buttonClassName="!h-8 block" /> ); } @@ -71,9 +91,22 @@ export function EnvironmentSelector({ isOpen={isMenuOpen} overflowHidden fullWidth - className={cn("h-7 overflow-hidden py-1 pl-1.5", className)} + className={cn("h-8 overflow-hidden py-1 pl-0.5", className)} > - + + + + + +
- {!isCollapsed && isAdmin && !user.isImpersonating ? ( - - - - - - - Admin dashboard - - - - ) : !isCollapsed && isAdmin && user.isImpersonating ? ( - + {isAdmin && !user.isImpersonating ? ( + + + + + + + + Admin dashboard + + + + + ) : isAdmin && user.isImpersonating ? ( + + + ) : null}
-
+
- {!isCollapsed && } +
- {!isCollapsed && environment.type === "DEVELOPMENT" && project.engine === "V2" && ( - - - - -
- -
-
- - {isConnected === undefined - ? "Checking connection..." - : isConnected - ? "Your dev server is connected" - : "Your dev server is not connected"} - -
-
- -
+ {environment.type === "DEVELOPMENT" && project.engine === "V2" && ( + + + + + +
+ +
+
+ + {isConnected === undefined + ? "Checking connection..." + : isConnected + ? "Your dev server is connected" + : "Your dev server is not connected"} + +
+
+ +
+
)}
@@ -402,16 +409,20 @@ export function SideMenu({
- {!isCollapsed && } + + +
- {!isCollapsed && isFreeUser && ( - + {isFreeUser && ( + + + )}
@@ -448,14 +459,34 @@ function ProjectSelector({ setOrgMenuOpen(false); }, [navigation.location?.pathname]); - if (isCollapsed) { - return ( - setOrgMenuOpen(open)} open={isOrgMenuOpen}> + const triggerContent = ( + + + + + + {project.name ?? "Select a project"} + + + + ); + + return ( + setOrgMenuOpen(open)} open={isOrgMenuOpen}> + {isCollapsed ? ( - - + + {triggerContent} @@ -463,135 +494,15 @@ function ProjectSelector({ - -
-
- - -
- -
- -
- {organization.title} -
- {plan && ( - - {plan} plan - - )} - {simplur`${organization.membersCount} member[|s]`} -
-
-
-
- - - Settings - - {isManagedCloud && ( - - - Usage - - )} -
-
-
- {organization.projects.map((p) => { - const isSelected = p.id === project.id; - return ( - - {p.name} -
- } - isSelected={isSelected} - icon={isSelected ? FolderOpenIcon : FolderIcon} - leadingIconClassName="text-indigo-500" - /> - ); - })} - -
-
- {organizations.length > 1 ? ( - - ) : ( - - )} -
-
- -
-
- -
- - - ); - } - - return ( - setOrgMenuOpen(open)} open={isOrgMenuOpen}> - - - - - - {project.name ?? "Select a project"} - - - + {triggerContent} + + )} + {children} + + ); +} + +/** Helper component that fades out and collapses height completely */ +function CollapsibleHeight({ + isCollapsed, + children, + className, +}: { + isCollapsed: boolean; + children: ReactNode; + className?: string; +}) { + return ( + + {children} + + ); +} + function HelpAndAI({ isCollapsed }: { isCollapsed: boolean }) { return ( <> - {!isCollapsed && } - {!isCollapsed && } + + + + + + ); } diff --git a/apps/webapp/app/components/navigation/SideMenuHeader.tsx b/apps/webapp/app/components/navigation/SideMenuHeader.tsx index 83741a6c7a..9ef4ab489c 100644 --- a/apps/webapp/app/components/navigation/SideMenuHeader.tsx +++ b/apps/webapp/app/components/navigation/SideMenuHeader.tsx @@ -1,9 +1,18 @@ import { useNavigation } from "@remix-run/react"; import { useEffect, useState } from "react"; +import { motion } from "framer-motion"; import { Popover, PopoverContent, PopoverCustomTrigger } from "../primitives/Popover"; import { EllipsisHorizontalIcon } from "@heroicons/react/20/solid"; -export function SideMenuHeader({ title, children }: { title: string; children?: React.ReactNode }) { +export function SideMenuHeader({ + title, + children, + isCollapsed = false, +}: { + title: string; + children?: React.ReactNode; + isCollapsed?: boolean; +}) { const [isHeaderMenuOpen, setHeaderMenuOpen] = useState(false); const navigation = useNavigation(); @@ -12,8 +21,16 @@ export function SideMenuHeader({ title, children }: { title: string; children?: }, [navigation.location?.pathname]); return ( -
-

{title}

+ +

{title}

{children !== undefined ? ( setHeaderMenuOpen(open)} open={isHeaderMenuOpen}> @@ -27,6 +44,6 @@ export function SideMenuHeader({ title, children }: { title: string; children?: ) : null} -
+ ); } diff --git a/apps/webapp/app/components/navigation/SideMenuItem.tsx b/apps/webapp/app/components/navigation/SideMenuItem.tsx index c4cc4abdfb..1ff5c653dc 100644 --- a/apps/webapp/app/components/navigation/SideMenuItem.tsx +++ b/apps/webapp/app/components/navigation/SideMenuItem.tsx @@ -1,8 +1,8 @@ import { type AnchorHTMLAttributes, type ReactNode } from "react"; import { Link } from "@remix-run/react"; +import { motion } from "framer-motion"; import { usePathName } from "~/hooks/usePathName"; import { cn } from "~/utils/cn"; -import { LinkButton } from "../primitives/Buttons"; import { type RenderIcon, Icon } from "../primitives/Icon"; import { SimpleTooltip } from "../primitives/Tooltip"; @@ -10,8 +10,6 @@ export function SideMenuItem({ icon, activeIconColor, inactiveIconColor, - trailingIcon, - trailingIconClassName, name, to, badge, @@ -21,8 +19,6 @@ export function SideMenuItem({ icon?: RenderIcon; activeIconColor?: string; inactiveIconColor?: string; - trailingIcon?: RenderIcon; - trailingIconClassName?: string; name: string; to: string; badge?: ReactNode; @@ -32,54 +28,47 @@ export function SideMenuItem({ const pathName = usePathName(); const isActive = pathName === to; + const content = ( + + + + {name} + {badge &&
{badge}
} +
+ + ); + if (isCollapsed) { return ( - - - - } - content={name} + ); } - return ( - -
- {name} -
{badge !== undefined && badge}
-
-
- ); + return content; } diff --git a/apps/webapp/app/components/navigation/SideMenuSection.tsx b/apps/webapp/app/components/navigation/SideMenuSection.tsx index ac800f125e..7a5bf6893c 100644 --- a/apps/webapp/app/components/navigation/SideMenuSection.tsx +++ b/apps/webapp/app/components/navigation/SideMenuSection.tsx @@ -29,18 +29,20 @@ export function SideMenuSection({ onCollapseToggle?.(newIsCollapsed); }, [isCollapsed, onCollapseToggle]); - // When the side menu is collapsed, just render the children without the header - if (isSideMenuCollapsed) { - return
{children}
; - } - return (
-
-

{title}

+

{title}

-
+ Date: Tue, 20 Jan 2026 21:54:15 +0000 Subject: [PATCH 05/39] Improvements to the section dividers --- .../navigation/EnvironmentSelector.tsx | 2 +- .../app/components/navigation/SideMenu.tsx | 4 +- .../components/navigation/SideMenuItem.tsx | 25 ++++++++-- .../components/navigation/SideMenuSection.tsx | 49 ++++++++++++------- 4 files changed, 56 insertions(+), 24 deletions(-) diff --git a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx index 0678c50e94..1b9589a1cf 100644 --- a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx +++ b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx @@ -91,7 +91,7 @@ export function EnvironmentSelector({ isOpen={isMenuOpen} overflowHidden fullWidth - className={cn("h-8 overflow-hidden py-1 pl-0.5", className)} + className={cn("h-8 overflow-hidden py-1 pl-1.5", className)} > diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index ed7fb8dc0a..b35b88155e 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -156,7 +156,7 @@ export function SideMenu({
setIsCollapsed(!isCollapsed)} /> @@ -240,7 +240,7 @@ export function SideMenu({
-
+
- {name} - {badge &&
{badge}
} + + {name} + + {badge && ( + + {badge} + + )} ); diff --git a/apps/webapp/app/components/navigation/SideMenuSection.tsx b/apps/webapp/app/components/navigation/SideMenuSection.tsx index 7a5bf6893c..d0815a18d2 100644 --- a/apps/webapp/app/components/navigation/SideMenuSection.tsx +++ b/apps/webapp/app/components/navigation/SideMenuSection.tsx @@ -30,27 +30,39 @@ export function SideMenuSection({ }, [isCollapsed, onCollapseToggle]); return ( -
- -

{title}

+
+ {/* Header container - stays in DOM to preserve height */} +
+ {/* Header - fades out when sidebar is collapsed */} - +

{title}

+ + +
- + {/* Divider - absolutely positioned, visible when sidebar is collapsed */} + +
Date: Wed, 21 Jan 2026 09:33:18 +0000 Subject: [PATCH 06/39] Use framer motion instead to animate the svg --- .../app/components/navigation/SideMenu.tsx | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index b35b88155e..cfebac3e49 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -820,20 +820,21 @@ function AnimatedChevron({ }; return ( - {/* Top segment */} - {/* Bottom segment */} - - + ); } @@ -878,7 +881,7 @@ function CollapseToggle({ return (
{/* Vertical line to mask the side menu border */} -
+
From 999c8d107fd439a6e85e2bbee88e67a6c6c33178 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 21 Jan 2026 09:33:29 +0000 Subject: [PATCH 07/39] Animate the opacity too --- apps/webapp/app/components/navigation/SideMenuItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/navigation/SideMenuItem.tsx b/apps/webapp/app/components/navigation/SideMenuItem.tsx index 7c2d73977a..ca6a2aefbc 100644 --- a/apps/webapp/app/components/navigation/SideMenuItem.tsx +++ b/apps/webapp/app/components/navigation/SideMenuItem.tsx @@ -62,12 +62,12 @@ export function SideMenuItem({ > {name} - {badge && ( + {badge && !isCollapsed && ( From 2376c52ab4fa72115c860e1308ef45cf2304a4c4 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 21 Jan 2026 17:32:25 +0000 Subject: [PATCH 08/39] Improves the menu item widths in the collapsed state --- .../navigation/EnvironmentSelector.tsx | 2 +- .../app/components/navigation/SideMenu.tsx | 108 +++++++++--------- .../components/navigation/SideMenuHeader.tsx | 28 ++++- .../components/navigation/SideMenuItem.tsx | 99 +++++++--------- .../components/navigation/SideMenuSection.tsx | 9 +- 5 files changed, 127 insertions(+), 119 deletions(-) diff --git a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx index 1b9589a1cf..3585b03ddd 100644 --- a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx +++ b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx @@ -55,7 +55,7 @@ export function EnvironmentSelector({ const trigger = (
diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index cfebac3e49..6ad01b1459 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -5,7 +5,6 @@ import { BeakerIcon, BellAlertIcon, ChartBarIcon, - ChevronLeftIcon, ChevronRightIcon, ClockIcon, Cog8ToothIcon, @@ -28,6 +27,7 @@ import { motion } from "framer-motion"; import { useEffect, useRef, useState, type ReactNode } from "react"; import simplur from "simplur"; import { ConcurrencyIcon } from "~/assets/icons/ConcurrencyIcon"; +import { DropdownIcon } from "~/assets/icons/DropdownIcon"; import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons"; import { ListCheckedIcon } from "~/assets/icons/ListCheckedIcon"; import { LogsIcon } from "~/assets/icons/LogsIcon"; @@ -88,13 +88,12 @@ import { Dialog, DialogTrigger } from "../primitives/Dialog"; import { Paragraph } from "../primitives/Paragraph"; import { Popover, - PopoverArrowTrigger, PopoverContent, PopoverMenuItem, - PopoverTrigger, + PopoverTrigger } from "../primitives/Popover"; import { TextLink } from "../primitives/TextLink"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip"; +import { SimpleTooltip, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip"; import { ShortcutsAutoOpen } from "../Shortcuts"; import { UserProfilePhoto } from "../UserProfilePhoto"; import { V4Badge } from "../V4Badge"; @@ -155,18 +154,18 @@ export function SideMenu({ return (
setIsCollapsed(!isCollapsed)} />
-
+
-
-
- +
+
+
-
+
- - - - - {project.name ?? "Select a project"} - - - - ); - return ( setOrgMenuOpen(open)} open={isOrgMenuOpen}> - {isCollapsed ? ( - - - - - {triggerContent} - - - - {organization.title} / {project.name} - - - - ) : ( - - {triggerContent} - - )} + + + + + + + {project.name ?? "Select a project"} + + + + + + + + } + content={`${organization.title} / ${project.name}`} + side="right" + sideOffset={8} + hidden={!isCollapsed} + buttonClassName="!h-8" + asChild + /> diff --git a/apps/webapp/app/components/navigation/SideMenuHeader.tsx b/apps/webapp/app/components/navigation/SideMenuHeader.tsx index 9ef4ab489c..8d975cba43 100644 --- a/apps/webapp/app/components/navigation/SideMenuHeader.tsx +++ b/apps/webapp/app/components/navigation/SideMenuHeader.tsx @@ -8,10 +8,13 @@ export function SideMenuHeader({ title, children, isCollapsed = false, + collapsedTitle, }: { title: string; children?: React.ReactNode; isCollapsed?: boolean; + /** When provided, this text stays visible when collapsed and the rest fades out */ + collapsedTitle?: string; }) { const [isHeaderMenuOpen, setHeaderMenuOpen] = useState(false); const navigation = useNavigation(); @@ -20,17 +23,34 @@ export function SideMenuHeader({ setHeaderMenuOpen(false); }, [navigation.location?.pathname]); + // If collapsedTitle is provided and title starts with it, split the title + const hasCollapsedTitle = collapsedTitle && title.startsWith(collapsedTitle); + const visiblePart = hasCollapsedTitle ? collapsedTitle : title; + const fadingPart = hasCollapsedTitle ? title.slice(collapsedTitle.length) : ""; + return ( -

{title}

+

+ {visiblePart} + {fadingPart && ( + + {fadingPart} + + )} +

{children !== undefined ? ( setHeaderMenuOpen(open)} open={isHeaderMenuOpen}> diff --git a/apps/webapp/app/components/navigation/SideMenuItem.tsx b/apps/webapp/app/components/navigation/SideMenuItem.tsx index ca6a2aefbc..4b0ea0b3cc 100644 --- a/apps/webapp/app/components/navigation/SideMenuItem.tsx +++ b/apps/webapp/app/components/navigation/SideMenuItem.tsx @@ -28,66 +28,55 @@ export function SideMenuItem({ const pathName = usePathName(); const isActive = pathName === to; - const content = ( - - - - - {name} - - {badge && !isCollapsed && ( - + - {badge} + {name} + {badge && !isCollapsed && ( + + {badge} + + )} - )} - - + + } + content={name} + side="right" + sideOffset={8} + buttonClassName="!h-8 block w-full" + hidden={!isCollapsed} + asChild + /> ); - - if (isCollapsed) { - return ( - - ); - } - - return content; } diff --git a/apps/webapp/app/components/navigation/SideMenuSection.tsx b/apps/webapp/app/components/navigation/SideMenuSection.tsx index d0815a18d2..0eab5c26bf 100644 --- a/apps/webapp/app/components/navigation/SideMenuSection.tsx +++ b/apps/webapp/app/components/navigation/SideMenuSection.tsx @@ -30,9 +30,9 @@ export function SideMenuSection({ }, [isCollapsed, onCollapseToggle]); return ( -
+
{/* Header container - stays in DOM to preserve height */} -
+
{/* Header - fades out when sidebar is collapsed */} {/* Divider - absolutely positioned, visible when sidebar is collapsed */} Date: Wed, 21 Jan 2026 17:52:51 +0000 Subject: [PATCH 09/39] Adds shortcut to toggle side menu --- apps/webapp/app/components/Shortcuts.tsx | 4 ++++ apps/webapp/app/components/navigation/SideMenu.tsx | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/components/Shortcuts.tsx b/apps/webapp/app/components/Shortcuts.tsx index cd25d45e06..6fedb3f764 100644 --- a/apps/webapp/app/components/Shortcuts.tsx +++ b/apps/webapp/app/components/Shortcuts.tsx @@ -82,6 +82,10 @@ function ShortcutContent() { + + + + diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 6ad01b1459..24c16966f3 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -41,6 +41,8 @@ import { useFeatures } from "~/hooks/useFeatures"; import { type MatchedOrganization } from "~/hooks/useOrganizations"; import { type MatchedProject } from "~/hooks/useProject"; import { useHasAdminAccess } from "~/hooks/useUser"; +import { useShortcutKeys } from "~/hooks/useShortcutKeys"; +import { ShortcutKey } from "../primitives/ShortcutKey"; import { type User } from "~/models/user.server"; import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route"; import { type FeedbackType } from "~/routes/resources.feedback"; @@ -137,6 +139,11 @@ export function SideMenu({ const { isManagedCloud } = useFeatures(); const featureFlags = useFeatureFlags(); + useShortcutKeys({ + shortcut: { modifiers: ["mod"], key: "b" }, + action: () => setIsCollapsed((prev) => !prev), + }); + useEffect(() => { const handleScroll = () => { if (borderRef.current) { @@ -897,8 +904,12 @@ function CollapseToggle({ - + {isCollapsed ? "Expand" : "Collapse"} + + + + From ae6abd7fe26553bc90d23be22ef32a0b3b9ecbf0 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 21 Jan 2026 17:58:28 +0000 Subject: [PATCH 10/39] Fixes width issue with environment selector --- .../navigation/EnvironmentSelector.tsx | 92 ++++++++----------- 1 file changed, 38 insertions(+), 54 deletions(-) diff --git a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx index 3585b03ddd..78e0865402 100644 --- a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx +++ b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx @@ -1,6 +1,6 @@ import { ChevronRightIcon, Cog8ToothIcon } from "@heroicons/react/20/solid"; +import { DropdownIcon } from "~/assets/icons/DropdownIcon"; import { useNavigation } from "@remix-run/react"; -import { motion } from "framer-motion"; import { useEffect, useRef, useState } from "react"; import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons"; import { useEnvironment } from "~/hooks/useEnvironment"; @@ -16,7 +16,6 @@ import { Header2 } from "../primitives/Headers"; import { Paragraph } from "../primitives/Paragraph"; import { Popover, - PopoverArrowTrigger, PopoverContent, PopoverMenuItem, PopoverSectionHeader, @@ -52,64 +51,49 @@ export function EnvironmentSelector({ const hasStaging = project.environments.some((env) => env.type === "STAGING"); - const trigger = ( -
- - - - -
- ); - - if (isCollapsed) { - return ( + return ( + setIsMenuOpen(open)} open={isMenuOpen}> + + + + + + + + + + + } content={environmentFullTitle(environment)} side="right" - buttonClassName="!h-8 block" + sideOffset={8} + hidden={!isCollapsed} + buttonClassName="!h-8 w-full" + asChild /> - ); - } - - return ( - setIsMenuOpen(open)} open={isMenuOpen}> - - - - - - - - From e086fe12ad5c67832cd9e604d571427092a9d0a8 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 21 Jan 2026 18:21:23 +0000 Subject: [PATCH 11/39] Smooth transition the env selector --- .../app/components/navigation/SideMenu.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 24c16966f3..e0578f4731 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -739,17 +739,15 @@ function CollapsibleElement({ className?: string; }) { return ( - {children} - +
); } From dcc5d0fb52965dbe16b358868f8e42d70df2a11c Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 21 Jan 2026 18:25:41 +0000 Subject: [PATCH 12/39] Adds trailingIcon props --- apps/webapp/app/components/navigation/SideMenuItem.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/webapp/app/components/navigation/SideMenuItem.tsx b/apps/webapp/app/components/navigation/SideMenuItem.tsx index 4b0ea0b3cc..a4a0f04a7a 100644 --- a/apps/webapp/app/components/navigation/SideMenuItem.tsx +++ b/apps/webapp/app/components/navigation/SideMenuItem.tsx @@ -10,6 +10,8 @@ export function SideMenuItem({ icon, activeIconColor, inactiveIconColor, + trailingIcon, + trailingIconClassName, name, to, badge, @@ -19,6 +21,8 @@ export function SideMenuItem({ icon?: RenderIcon; activeIconColor?: string; inactiveIconColor?: string; + trailingIcon?: RenderIcon; + trailingIconClassName?: string; name: string; to: string; badge?: ReactNode; @@ -68,6 +72,12 @@ export function SideMenuItem({ {badge} )} + {trailingIcon && !isCollapsed && ( + + )} } From 2280babd92560527490e248efb9c2392aa925b94 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 21 Jan 2026 18:53:37 +0000 Subject: [PATCH 13/39] Improvements to the help and feedback button transition --- apps/webapp/app/components/AskAI.tsx | 38 +++++++++++-------- .../navigation/HelpAndFeedbackPopover.tsx | 25 ++++++++++-- .../app/components/navigation/SideMenu.tsx | 8 +--- .../app/components/primitives/Popover.tsx | 10 +++-- 4 files changed, 52 insertions(+), 29 deletions(-) diff --git a/apps/webapp/app/components/AskAI.tsx b/apps/webapp/app/components/AskAI.tsx index 39cc4cdaaf..6086691586 100644 --- a/apps/webapp/app/components/AskAI.tsx +++ b/apps/webapp/app/components/AskAI.tsx @@ -5,6 +5,7 @@ import { HandThumbUpIcon, StopIcon, } from "@heroicons/react/20/solid"; +import { cn } from "~/utils/cn"; import { type FeedbackComment, KapaProvider, type QA, useChat } from "@kapaai/react-sdk"; import { useSearchParams } from "@remix-run/react"; import DOMPurify from "dompurify"; @@ -37,7 +38,7 @@ function useKapaWebsiteId() { return routeMatch?.kapa.websiteId; } -export function AskAI() { +export function AskAI({ isCollapsed = false }: { isCollapsed?: boolean }) { const { isManagedCloud } = useFeatures(); const websiteId = useKapaWebsiteId(); @@ -46,21 +47,28 @@ export function AskAI() { } return ( - - - - } +
- {() => } - + + + + } + > + {() => } + +
); } diff --git a/apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx b/apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx index 93f2843de5..f7aee2df57 100644 --- a/apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx +++ b/apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx @@ -8,6 +8,7 @@ import { SignalIcon, StarIcon, } from "@heroicons/react/20/solid"; +import { cn } from "~/utils/cn"; import { DiscordIcon, SlackIcon } from "@trigger.dev/companyicons"; import { Fragment, useState } from "react"; import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route"; @@ -24,7 +25,13 @@ import { StepNumber } from "../primitives/StepNumber"; import { SideMenuItem } from "./SideMenuItem"; import { Badge } from "../primitives/Badge"; -export function HelpAndFeedback({ disableShortcut = false }: { disableShortcut?: boolean }) { +export function HelpAndFeedback({ + disableShortcut = false, + isCollapsed = false, +}: { + disableShortcut?: boolean; + isCollapsed?: boolean; +}) { const [isHelpMenuOpen, setHelpMenuOpen] = useState(false); const currentPlan = useCurrentPlan(); @@ -33,16 +40,26 @@ export function HelpAndFeedback({ disableShortcut = false }: { disableShortcut?:
- - Help & Feedback + + + Help & Feedback +
diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index e0578f4731..07425d11b4 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -780,12 +780,8 @@ function HelpAndAI({ isCollapsed }: { isCollapsed: boolean }) { return ( <> - - - - - - + + ); } diff --git a/apps/webapp/app/components/primitives/Popover.tsx b/apps/webapp/app/components/primitives/Popover.tsx index 02454864c4..c7a7499e50 100644 --- a/apps/webapp/app/components/primitives/Popover.tsx +++ b/apps/webapp/app/components/primitives/Popover.tsx @@ -154,10 +154,12 @@ function PopoverSideMenuTrigger({ children, className, shortcut, + hideShortcutKey = false, ...props }: { isOpen?: boolean; shortcut?: useShortcutKeys.ShortcutDefinition; + hideShortcutKey?: boolean; } & React.ComponentPropsWithoutRef) { const ref = React.useRef(null); useShortcutKeys.useShortcutKeys({ @@ -176,14 +178,14 @@ function PopoverSideMenuTrigger({ {...props} ref={ref} className={cn( - "flex h-[1.8rem] shrink-0 select-none items-center gap-x-1.5 rounded-sm bg-transparent px-[0.4rem] text-center font-sans text-2sm font-normal text-text-bright transition duration-150 focus-custom hover:bg-charcoal-750", - shortcut ? "justify-between" : "", + "flex h-[1.8rem] shrink-0 select-none items-center rounded-sm bg-transparent px-[0.4rem] text-center font-sans text-2sm font-normal text-text-bright transition duration-150 focus-custom hover:bg-charcoal-750", + shortcut && !hideShortcutKey ? "justify-between gap-x-1.5" : "", className )} > {children} - {shortcut && ( - + {shortcut && !hideShortcutKey && ( + )} ); From 8872d479d0805b53e4ef948e8b7706e1e9f2e422 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 21 Jan 2026 20:48:25 +0000 Subject: [PATCH 14/39] Adds disableHoverableContent --- apps/webapp/app/components/navigation/EnvironmentSelector.tsx | 1 + apps/webapp/app/components/navigation/SideMenu.tsx | 1 + apps/webapp/app/components/navigation/SideMenuItem.tsx | 1 + 3 files changed, 3 insertions(+) diff --git a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx index 78e0865402..97400af454 100644 --- a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx +++ b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx @@ -89,6 +89,7 @@ export function EnvironmentSelector({ hidden={!isCollapsed} buttonClassName="!h-8 w-full" asChild + disableHoverableContent />