Skip to content

Commit b92fe12

Browse files
committed
fix(webapp): fade overflowing side menu selector labels
The org, project, and environment selector rows each rendered their own plain span, so long names hard-clipped mid-character (and the environment name ended in an ellipsis). They now use SideMenuLabel, which masks the right edge only while the text actually overflows, matching the rest of the menu. The mask moved into a shared labelOverflowFade module so EnvironmentLabel can reuse it; that component already measured overflow for its tooltip, so no second observer was needed.
1 parent 205bdc3 commit b92fe12

6 files changed

Lines changed: 35 additions & 17 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Long organization, project, and environment names in the side menu now fade out at the right edge instead of being cut off mid-character or ending in an ellipsis.

apps/webapp/app/components/environments/EnvironmentLabel.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "~/assets/icons/EnvironmentIcons";
77
import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
88
import { cn } from "~/utils/cn";
9+
import { labelOverflowFadeStyle } from "~/components/primitives/labelOverflowFade";
910
import { SimpleTooltip } from "~/components/primitives/Tooltip";
1011
import { useEffect, useRef, useState } from "react";
1112

@@ -88,7 +89,10 @@ export function EnvironmentLabel({
8889
tooltipSideOffset?: number;
8990
tooltipSide?: "top" | "right" | "bottom" | "left";
9091
disableTooltip?: boolean;
91-
/** When false, the label clips without an ellipsis (side menu fades it in place). Defaults true. */
92+
/**
93+
* When false, an overflowing label fades out at its right edge instead of ending in an ellipsis,
94+
* matching the other side menu labels. Defaults true.
95+
*/
9296
truncate?: boolean;
9397
}) {
9498
const spanRef = useRef<HTMLSpanElement>(null);
@@ -122,6 +126,7 @@ export function EnvironmentLabel({
122126
environmentTextClassName(environment),
123127
className
124128
)}
129+
style={truncate ? undefined : labelOverflowFadeStyle(isTruncated)}
125130
>
126131
{text}
127132
</span>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export function EnvironmentSelector({
9494
fades in place and scales its width to 0 so it never holds width mid-drag. The
9595
selector is also reused outside the side menu (BlankStatePanels, limits) where the var
9696
is unset — the 0.2 max-width fallback pins a ~200px cap (0.2 * 1000px) so long names
97-
ellipsis-truncate there instead of widening the control, while opacity stays 1.
97+
fade out there instead of widening the control, while opacity stays 1.
9898
*/}
9999
<span
100100
className="flex min-w-0 items-center overflow-hidden"
@@ -105,7 +105,7 @@ export function EnvironmentSelector({
105105
>
106106
<EnvironmentLabel
107107
environment={environment}
108-
className="text-ellipsis text-[0.90625rem] font-medium tracking-[-0.01em]"
108+
className="text-[0.90625rem] font-medium tracking-[-0.01em]"
109109
disableTooltip
110110
truncate={false}
111111
/>

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1682,9 +1682,9 @@ function OrgSelector({
16821682
className="flex min-w-0 items-center gap-1.5 overflow-hidden"
16831683
style={SIDE_MENU_SELECTOR_LABEL_STYLE}
16841684
>
1685-
<span className="overflow-hidden whitespace-nowrap text-[0.90625rem] font-medium tracking-[-0.01em] text-text-bright">
1685+
<SideMenuLabel className="text-[0.90625rem] font-medium tracking-[-0.01em] text-text-bright">
16861686
{organization.title}
1687-
</span>
1687+
</SideMenuLabel>
16881688
</span>
16891689
</span>
16901690
<span
@@ -1987,9 +1987,9 @@ function ProjectSelector({
19871987
className="flex min-w-0 items-center overflow-hidden"
19881988
style={SIDE_MENU_SELECTOR_LABEL_STYLE}
19891989
>
1990-
<span className="overflow-hidden whitespace-nowrap text-[0.90625rem] font-medium tracking-[-0.01em] text-text-bright">
1990+
<SideMenuLabel className="text-[0.90625rem] font-medium tracking-[-0.01em] text-text-bright">
19911991
{project.name ?? "Select a project"}
1992-
</span>
1992+
</SideMenuLabel>
19931993
</span>
19941994
</span>
19951995
<span
@@ -2041,7 +2041,7 @@ function ProjectSelector({
20412041
to={v3ProjectPath(organization, p)}
20422042
title={
20432043
<div className="flex w-full items-center justify-between text-text-bright">
2044-
<span className="grow truncate text-left">{p.name}</span>
2044+
<SideMenuLabel className="min-w-0 grow text-left">{p.name}</SideMenuLabel>
20452045
</div>
20462046
}
20472047
isSelected={isSelected}

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,10 @@ import { motion } from "framer-motion";
1212
import { usePathName } from "~/hooks/usePathName";
1313
import { cn } from "~/utils/cn";
1414
import { type RenderIcon, Icon } from "../primitives/Icon";
15+
import { labelOverflowFadeStyle } from "../primitives/labelOverflowFade";
1516
import { SimpleTooltip } from "../primitives/Tooltip";
1617
import { useActiveFavoriteId } from "./favoritePages";
1718

18-
/** Right-edge fade shown instead of a hard clip, only while the label actually overflows. */
19-
const LABEL_OVERFLOW_MASK = "linear-gradient(to right, black calc(100% - 1.5rem), transparent)";
20-
2119
/**
2220
* A menu label that fades out at its right edge when (and only when) the text overflows. Text
2321
* that fits renders exactly as before, with no mask. Overflow is re-measured when the element
@@ -56,12 +54,7 @@ export function SideMenuLabel({
5654
<span
5755
ref={ref}
5856
className={cn("overflow-hidden whitespace-nowrap", className)}
59-
style={{
60-
...style,
61-
...(isOverflowing
62-
? { maskImage: LABEL_OVERFLOW_MASK, WebkitMaskImage: LABEL_OVERFLOW_MASK }
63-
: undefined),
64-
}}
57+
style={{ ...style, ...labelOverflowFadeStyle(isOverflowing) }}
6558
>
6659
{children}
6760
</span>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Right-edge gradient fade for labels that clip, used instead of a hard cut or an ellipsis. Apply
3+
* it only while the text actually overflows — masking a label that fits would fade its last
4+
* characters for no reason.
5+
*/
6+
export const LABEL_OVERFLOW_MASK =
7+
"linear-gradient(to right, black calc(100% - 1.5rem), transparent)";
8+
9+
/** Mask style for a clipping label, or nothing when the text fits. Spread into a `style` prop. */
10+
export function labelOverflowFadeStyle(isOverflowing: boolean) {
11+
return isOverflowing
12+
? { maskImage: LABEL_OVERFLOW_MASK, WebkitMaskImage: LABEL_OVERFLOW_MASK }
13+
: undefined;
14+
}

0 commit comments

Comments
 (0)