fix(webapp): fade overflowing side menu selector labels (#4412)

Long organization, project, and environment names in the side menu were
cut off mid-character. They now fade out at the right edge like the rest
of the side menu items already did.

### Example of faded long names:
<img width="246" height="200" alt="CleanShot 2026-07-28 at 22 59 24"
src="https://github.com/user-attachments/assets/efa60b87-286f-4ab0-9d4e-490ef2de53e5"
/>
This commit is contained in:
James Ritchie
2026-07-29 10:16:45 +01:00
committed by GitHub
parent 1e14e29d71
commit a11e5ffbc6
5 changed files with 19 additions and 17 deletions
@@ -6,6 +6,7 @@ import {
} from "~/assets/icons/EnvironmentIcons";
import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
import { cn } from "~/utils/cn";
import { labelOverflowFadeStyle } from "~/components/primitives/labelOverflowFade";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { useEffect, useRef, useState } from "react";
@@ -88,7 +89,7 @@ export function EnvironmentLabel({
tooltipSideOffset?: number;
tooltipSide?: "top" | "right" | "bottom" | "left";
disableTooltip?: boolean;
/** When false, the label clips without an ellipsis (side menu fades it in place). Defaults true. */
/** When false, an overflowing label fades at its right edge instead of ending in an ellipsis. */
truncate?: boolean;
}) {
const spanRef = useRef<HTMLSpanElement>(null);
@@ -122,6 +123,7 @@ export function EnvironmentLabel({
environmentTextClassName(environment),
className
)}
style={labelOverflowFadeStyle(!truncate && isTruncated)}
>
{text}
</span>
@@ -94,7 +94,7 @@ export function EnvironmentSelector({
fades in place and scales its width to 0 so it never holds width mid-drag. The
selector is also reused outside the side menu (BlankStatePanels, limits) where the var
is unset — the 0.2 max-width fallback pins a ~200px cap (0.2 * 1000px) so long names
ellipsis-truncate there instead of widening the control, while opacity stays 1.
fade out there instead of widening the control, while opacity stays 1.
*/}
<span
className="flex min-w-0 items-center overflow-hidden"
@@ -105,7 +105,7 @@ export function EnvironmentSelector({
>
<EnvironmentLabel
environment={environment}
className="text-ellipsis text-[0.90625rem] font-medium tracking-[-0.01em]"
className="text-[0.90625rem] font-medium tracking-[-0.01em]"
disableTooltip
truncate={false}
/>
@@ -1682,9 +1682,9 @@ function OrgSelector({
className="flex min-w-0 items-center gap-1.5 overflow-hidden"
style={SIDE_MENU_SELECTOR_LABEL_STYLE}
>
<span className="overflow-hidden whitespace-nowrap text-[0.90625rem] font-medium tracking-[-0.01em] text-text-bright">
<SideMenuLabel className="text-[0.90625rem] font-medium tracking-[-0.01em] text-text-bright">
{organization.title}
</span>
</SideMenuLabel>
</span>
</span>
<span
@@ -1987,9 +1987,9 @@ function ProjectSelector({
className="flex min-w-0 items-center overflow-hidden"
style={SIDE_MENU_SELECTOR_LABEL_STYLE}
>
<span className="overflow-hidden whitespace-nowrap text-[0.90625rem] font-medium tracking-[-0.01em] text-text-bright">
<SideMenuLabel className="text-[0.90625rem] font-medium tracking-[-0.01em] text-text-bright">
{project.name ?? "Select a project"}
</span>
</SideMenuLabel>
</span>
</span>
<span
@@ -2041,7 +2041,7 @@ function ProjectSelector({
to={v3ProjectPath(organization, p)}
title={
<div className="flex w-full items-center justify-between text-text-bright">
<span className="grow truncate text-left">{p.name}</span>
<SideMenuLabel className="min-w-0 grow text-left">{p.name}</SideMenuLabel>
</div>
}
isSelected={isSelected}
@@ -12,12 +12,10 @@ import { motion } from "framer-motion";
import { usePathName } from "~/hooks/usePathName";
import { cn } from "~/utils/cn";
import { type RenderIcon, Icon } from "../primitives/Icon";
import { labelOverflowFadeStyle } from "../primitives/labelOverflowFade";
import { SimpleTooltip } from "../primitives/Tooltip";
import { useActiveFavoriteId } from "./favoritePages";
/** Right-edge fade shown instead of a hard clip, only while the label actually overflows. */
const LABEL_OVERFLOW_MASK = "linear-gradient(to right, black calc(100% - 1.5rem), transparent)";
/**
* A menu label that fades out at its right edge when (and only when) the text overflows. Text
* that fits renders exactly as before, with no mask. Overflow is re-measured when the element
@@ -56,12 +54,7 @@ export function SideMenuLabel({
<span
ref={ref}
className={cn("overflow-hidden whitespace-nowrap", className)}
style={{
...style,
...(isOverflowing
? { maskImage: LABEL_OVERFLOW_MASK, WebkitMaskImage: LABEL_OVERFLOW_MASK }
: undefined),
}}
style={{ ...style, ...labelOverflowFadeStyle(isOverflowing) }}
>
{children}
</span>
@@ -0,0 +1,7 @@
const LABEL_OVERFLOW_MASK = "linear-gradient(to right, black calc(100% - 1.5rem), transparent)";
export function labelOverflowFadeStyle(isOverflowing: boolean) {
return isOverflowing
? { maskImage: LABEL_OVERFLOW_MASK, WebkitMaskImage: LABEL_OVERFLOW_MASK }
: undefined;
}