Skip to content

Commit 2376c52

Browse files
committed
Improves the menu item widths in the collapsed state
1 parent 999c8d1 commit 2376c52

File tree

5 files changed

+127
-119
lines changed

5 files changed

+127
-119
lines changed

apps/webapp/app/components/navigation/EnvironmentSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function EnvironmentSelector({
5555
const trigger = (
5656
<div
5757
className={cn(
58-
"flex h-8 items-center gap-1.5 overflow-hidden rounded px-2 transition-colors hover:bg-charcoal-750",
58+
"flex h-8 w-full items-center gap-1.5 overflow-hidden rounded pl-1.5 pr-2 transition-colors hover:bg-charcoal-750",
5959
className
6060
)}
6161
>

apps/webapp/app/components/navigation/SideMenu.tsx

Lines changed: 53 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
BeakerIcon,
66
BellAlertIcon,
77
ChartBarIcon,
8-
ChevronLeftIcon,
98
ChevronRightIcon,
109
ClockIcon,
1110
Cog8ToothIcon,
@@ -28,6 +27,7 @@ import { motion } from "framer-motion";
2827
import { useEffect, useRef, useState, type ReactNode } from "react";
2928
import simplur from "simplur";
3029
import { ConcurrencyIcon } from "~/assets/icons/ConcurrencyIcon";
30+
import { DropdownIcon } from "~/assets/icons/DropdownIcon";
3131
import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons";
3232
import { ListCheckedIcon } from "~/assets/icons/ListCheckedIcon";
3333
import { LogsIcon } from "~/assets/icons/LogsIcon";
@@ -88,13 +88,12 @@ import { Dialog, DialogTrigger } from "../primitives/Dialog";
8888
import { Paragraph } from "../primitives/Paragraph";
8989
import {
9090
Popover,
91-
PopoverArrowTrigger,
9291
PopoverContent,
9392
PopoverMenuItem,
94-
PopoverTrigger,
93+
PopoverTrigger
9594
} from "../primitives/Popover";
9695
import { TextLink } from "../primitives/TextLink";
97-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
96+
import { SimpleTooltip, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
9897
import { ShortcutsAutoOpen } from "../Shortcuts";
9998
import { UserProfilePhoto } from "../UserProfilePhoto";
10099
import { V4Badge } from "../V4Badge";
@@ -155,18 +154,18 @@ export function SideMenu({
155154
return (
156155
<div
157156
className={cn(
158-
"relative grid h-full grid-rows-[2.5rem_1fr_auto] border-r border-grid-bright bg-background-bright transition-all duration-200",
159-
isCollapsed ? "w-12" : "w-56"
157+
"relative grid h-full grid-cols-[100%] grid-rows-[2.5rem_1fr_auto] border-r border-grid-bright bg-background-bright transition-all duration-200",
158+
isCollapsed ? "w-[2.75rem]" : "w-56"
160159
)}
161160
>
162161
<CollapseToggle isCollapsed={isCollapsed} onToggle={() => setIsCollapsed(!isCollapsed)} />
163162
<div
164163
className={cn(
165-
"flex items-center overflow-hidden border-b px-1 py-1 transition duration-300",
166-
showHeaderDivider ? "border-grid-bright" : "border-transparent"
164+
"flex min-w-0 items-center overflow-hidden border-b px-1 py-1 transition duration-300",
165+
showHeaderDivider || isCollapsed ? "border-grid-bright" : "border-transparent"
167166
)}
168167
>
169-
<div className="min-w-0 flex-1">
168+
<div className={cn("min-w-0", !isCollapsed && "flex-1")}>
170169
<ProjectSelector
171170
organizations={organizations}
172171
organization={organization}
@@ -198,9 +197,9 @@ export function SideMenu({
198197
className="overflow-hidden overflow-y-auto pt-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
199198
ref={borderRef}
200199
>
201-
<div className={cn("mb-6 flex flex-col gap-4 overflow-hidden", isCollapsed ? "px-0.5" : "px-1")}>
202-
<div className="space-y-1">
203-
<SideMenuHeader title={"Environment"} isCollapsed={isCollapsed} />
200+
<div className="mb-6 flex w-full flex-col gap-4 overflow-hidden px-1">
201+
<div className="w-full space-y-1">
202+
<SideMenuHeader title={"Environment"} isCollapsed={isCollapsed} collapsedTitle="Env" />
204203
<div className="flex items-center">
205204
<EnvironmentSelector
206205
organization={organization}
@@ -240,7 +239,7 @@ export function SideMenu({
240239
</div>
241240
</div>
242241

243-
<div className="space-y-px">
242+
<div className="w-full space-y-px">
244243
<SideMenuItem
245244
name="Tasks"
246245
icon={TaskIconSmall}
@@ -459,52 +458,51 @@ function ProjectSelector({
459458
setOrgMenuOpen(false);
460459
}, [navigation.location?.pathname]);
461460

462-
const triggerContent = (
463-
<span className="flex items-center gap-1.5 overflow-hidden">
464-
<Avatar avatar={organization.avatar} size={1.25} orgName={organization.title} />
465-
<motion.span
466-
className="flex items-center gap-1.5 overflow-hidden"
467-
initial={false}
468-
animate={{
469-
opacity: isCollapsed ? 0 : 1,
470-
width: isCollapsed ? 0 : "auto",
471-
}}
472-
transition={{ duration: 0.15, ease: "easeOut" }}
473-
>
474-
<SelectorDivider />
475-
<span className="truncate text-2sm font-normal text-text-bright">
476-
{project.name ?? "Select a project"}
477-
</span>
478-
</motion.span>
479-
</span>
480-
);
481-
482461
return (
483462
<Popover onOpenChange={(open) => setOrgMenuOpen(open)} open={isOrgMenuOpen}>
484-
{isCollapsed ? (
485-
<TooltipProvider disableHoverableContent>
486-
<Tooltip>
487-
<TooltipTrigger asChild>
488-
<PopoverTrigger className="flex h-8 w-full items-center justify-center rounded px-1.5 transition-colors hover:bg-charcoal-750">
489-
{triggerContent}
490-
</PopoverTrigger>
491-
</TooltipTrigger>
492-
<TooltipContent side="right" className="text-xs">
493-
{organization.title} / {project.name}
494-
</TooltipContent>
495-
</Tooltip>
496-
</TooltipProvider>
497-
) : (
498-
<PopoverArrowTrigger
499-
isOpen={isOrgMenuOpen}
500-
overflowHidden
501-
className="h-8 w-full justify-between py-1 pl-1.5"
502-
>
503-
{triggerContent}
504-
</PopoverArrowTrigger>
505-
)}
463+
<SimpleTooltip
464+
button={
465+
<PopoverTrigger
466+
className={cn(
467+
"group flex h-8 items-center rounded pl-[0.4375rem] transition-colors hover:bg-charcoal-750",
468+
isCollapsed ? "justify-center pr-0.5" : "w-full justify-between pr-1"
469+
)}
470+
>
471+
<span className="flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden">
472+
<Avatar avatar={organization.avatar} size={1.25} orgName={organization.title} />
473+
<span
474+
className={cn(
475+
"flex min-w-0 items-center gap-1.5 overflow-hidden transition-all duration-200",
476+
isCollapsed ? "max-w-0 opacity-0" : "max-w-[200px] opacity-100"
477+
)}
478+
>
479+
<SelectorDivider />
480+
<span className="truncate text-2sm font-normal text-text-bright">
481+
{project.name ?? "Select a project"}
482+
</span>
483+
</span>
484+
</span>
485+
<span
486+
className={cn(
487+
"overflow-hidden transition-all duration-200",
488+
isCollapsed ? "max-w-0 opacity-0" : "max-w-[16px] opacity-100"
489+
)}
490+
>
491+
<DropdownIcon className="size-4 min-w-4 text-text-dimmed transition group-hover:text-text-bright" />
492+
</span>
493+
</PopoverTrigger>
494+
}
495+
content={`${organization.title} / ${project.name}`}
496+
side="right"
497+
sideOffset={8}
498+
hidden={!isCollapsed}
499+
buttonClassName="!h-8"
500+
asChild
501+
/>
506502
<PopoverContent
507503
className="min-w-[16rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
504+
side={isCollapsed ? "right" : "bottom"}
505+
sideOffset={isCollapsed ? 8 : 4}
508506
align="start"
509507
style={{ maxHeight: `calc(var(--radix-popover-content-available-height) - 10vh)` }}
510508
>

apps/webapp/app/components/navigation/SideMenuHeader.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ export function SideMenuHeader({
88
title,
99
children,
1010
isCollapsed = false,
11+
collapsedTitle,
1112
}: {
1213
title: string;
1314
children?: React.ReactNode;
1415
isCollapsed?: boolean;
16+
/** When provided, this text stays visible when collapsed and the rest fades out */
17+
collapsedTitle?: string;
1518
}) {
1619
const [isHeaderMenuOpen, setHeaderMenuOpen] = useState(false);
1720
const navigation = useNavigation();
@@ -20,17 +23,34 @@ export function SideMenuHeader({
2023
setHeaderMenuOpen(false);
2124
}, [navigation.location?.pathname]);
2225

26+
// If collapsedTitle is provided and title starts with it, split the title
27+
const hasCollapsedTitle = collapsedTitle && title.startsWith(collapsedTitle);
28+
const visiblePart = hasCollapsedTitle ? collapsedTitle : title;
29+
const fadingPart = hasCollapsedTitle ? title.slice(collapsedTitle.length) : "";
30+
2331
return (
2432
<motion.div
25-
className="group flex items-center justify-between overflow-hidden pl-1.5"
33+
className="group flex h-4 items-center justify-between overflow-hidden pl-1.5"
2634
initial={false}
2735
animate={{
28-
height: isCollapsed ? 0 : "auto",
29-
opacity: isCollapsed ? 0 : 1,
36+
opacity: hasCollapsedTitle ? 1 : isCollapsed ? 0 : 1,
3037
}}
3138
transition={{ duration: 0.15, ease: "easeOut" }}
3239
>
33-
<h2 className="text-xs whitespace-nowrap">{title}</h2>
40+
<h2 className="text-xs whitespace-nowrap">
41+
{visiblePart}
42+
{fadingPart && (
43+
<motion.span
44+
initial={false}
45+
animate={{
46+
opacity: isCollapsed ? 0 : 1,
47+
}}
48+
transition={{ duration: 0.15, ease: "easeOut" }}
49+
>
50+
{fadingPart}
51+
</motion.span>
52+
)}
53+
</h2>
3454
{children !== undefined ? (
3555
<Popover onOpenChange={(open) => setHeaderMenuOpen(open)} open={isHeaderMenuOpen}>
3656
<PopoverCustomTrigger className="p-1">

apps/webapp/app/components/navigation/SideMenuItem.tsx

Lines changed: 44 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -28,66 +28,55 @@ export function SideMenuItem({
2828
const pathName = usePathName();
2929
const isActive = pathName === to;
3030

31-
const content = (
32-
<Link
33-
to={to}
34-
target={target}
35-
className={cn(
36-
"flex h-8 items-center gap-2 overflow-hidden rounded px-2 text-text-bright transition-colors hover:bg-charcoal-750",
37-
isActive ? "bg-tertiary" : ""
38-
)}
39-
>
40-
<Icon
41-
icon={icon}
42-
className={cn(
43-
"size-5 shrink-0",
44-
isActive ? activeIconColor : inactiveIconColor ?? "text-text-dimmed"
45-
)}
46-
/>
47-
<motion.div
48-
className="flex min-w-0 flex-1 items-center justify-between overflow-hidden"
49-
initial={false}
50-
animate={{
51-
width: isCollapsed ? 0 : "auto",
52-
}}
53-
transition={{ duration: 0.15, ease: "easeOut" }}
54-
>
55-
<motion.span
56-
className="truncate text-2sm"
57-
initial={false}
58-
animate={{
59-
opacity: isCollapsed ? 0 : 1,
60-
}}
61-
transition={{ duration: 0.15, ease: "easeOut" }}
31+
return (
32+
<SimpleTooltip
33+
button={
34+
<Link
35+
to={to}
36+
target={target}
37+
className={cn(
38+
"flex h-8 w-full items-center gap-2 overflow-hidden rounded pr-2 pl-[0.4375rem] text-text-bright transition-colors hover:bg-charcoal-750",
39+
isActive ? "bg-tertiary" : ""
40+
)}
6241
>
63-
{name}
64-
</motion.span>
65-
{badge && !isCollapsed && (
66-
<motion.div
67-
className="ml-1 flex shrink-0 items-center gap-1"
42+
<Icon
43+
icon={icon}
44+
className={cn(
45+
"size-5 shrink-0",
46+
isActive ? activeIconColor : inactiveIconColor ?? "text-text-dimmed"
47+
)}
48+
/>
49+
<motion.div
50+
className="flex min-w-0 flex-1 items-center justify-between overflow-hidden"
6851
initial={false}
6952
animate={{
70-
opacity: 1,
53+
width: isCollapsed ? 0 : "auto",
54+
opacity: isCollapsed ? 0 : 1,
7155
}}
72-
transition={{ duration: 0.15, ease: "easeOut" }}
56+
transition={{ duration: 0.2, ease: "easeOut" }}
7357
>
74-
{badge}
58+
<span className="truncate text-2sm">{name}</span>
59+
{badge && !isCollapsed && (
60+
<motion.div
61+
className="ml-1 flex shrink-0 items-center gap-1"
62+
initial={false}
63+
animate={{
64+
opacity: 1,
65+
}}
66+
transition={{ duration: 0.15, ease: "easeOut" }}
67+
>
68+
{badge}
69+
</motion.div>
70+
)}
7571
</motion.div>
76-
)}
77-
</motion.div>
78-
</Link>
72+
</Link>
73+
}
74+
content={name}
75+
side="right"
76+
sideOffset={8}
77+
buttonClassName="!h-8 block w-full"
78+
hidden={!isCollapsed}
79+
asChild
80+
/>
7981
);
80-
81-
if (isCollapsed) {
82-
return (
83-
<SimpleTooltip
84-
button={content}
85-
content={name}
86-
side="right"
87-
buttonClassName="!h-8 block"
88-
/>
89-
);
90-
}
91-
92-
return content;
9382
}

apps/webapp/app/components/navigation/SideMenuSection.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ export function SideMenuSection({
3030
}, [isCollapsed, onCollapseToggle]);
3131

3232
return (
33-
<div className="overflow-hidden">
33+
<div className="w-full overflow-hidden">
3434
{/* Header container - stays in DOM to preserve height */}
35-
<div className="relative">
35+
<div className="relative w-full">
3636
{/* Header - fades out when sidebar is collapsed */}
3737
<motion.div
3838
className="flex cursor-pointer items-center gap-1 overflow-hidden rounded-sm py-1 pl-1.5 text-text-dimmed transition hover:bg-charcoal-750 hover:text-text-bright"
@@ -55,7 +55,7 @@ export function SideMenuSection({
5555
</motion.div>
5656
{/* Divider - absolutely positioned, visible when sidebar is collapsed */}
5757
<motion.div
58-
className="absolute left-2 right-2 top-1 w-full h-px bg-grid-bright"
58+
className="absolute left-2 right-2 top-1 h-px bg-grid-bright"
5959
initial={false}
6060
animate={{
6161
opacity: isSideMenuCollapsed ? 1 : 0,
@@ -65,6 +65,7 @@ export function SideMenuSection({
6565
</div>
6666
<AnimatePresence initial={false}>
6767
<motion.div
68+
className="w-full"
6869
initial={isCollapsed ? "collapsed" : "expanded"}
6970
animate={isCollapsed && !isSideMenuCollapsed ? "collapsed" : "expanded"}
7071
exit="collapsed"
@@ -85,7 +86,7 @@ export function SideMenuSection({
8586
style={{ overflow: "hidden" }}
8687
>
8788
<motion.div
88-
className="space-y-px"
89+
className="w-full space-y-px"
8990
variants={{
9091
expanded: {
9192
translateY: 0,

0 commit comments

Comments
 (0)