Make ViewHeader and ServerStatusIndicator responsive

Header now adapts cleanly across widths instead of wrapping or clipping:

- LeftSection/RightSection are symmetric (`flex: 1, miw: 0, wrap: nowrap`),
  so the natural-width CenterSection sits visually centered between them.
  Server name truncates with ellipsis when the left slice is too narrow.
- Below `md` (992px) the SegmentedControl swaps for a Select dropdown so
  the tab labels never have to shrink or wrap.
- Below `sm` (768px) the "Disconnect" button collapses to an MdLinkOff
  ActionIcon (with `aria-label` + `title` for the tooltip).
- Below `lg` (1200px) ServerStatusIndicator hides its text label and shows
  only the colored dot (full label still available as a tooltip on hover),
  freeing space so the Disconnect button stops getting truncated at typical
  desktop widths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-04-25 18:34:07 -04:00
commit 4e99e6393f
2 changed files with 52 additions and 17 deletions

View file

@ -1,4 +1,5 @@
import { Group, Paper, Text } from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks";
import type { ConnectionStatus } from "@inspector/core/mcp/types.js";
export interface ServerStatusIndicatorProps {
@ -42,13 +43,16 @@ export function ServerStatusIndicator({
latencyMs,
retryCount,
}: ServerStatusIndicatorProps) {
const showLabel = useMediaQuery("(min-width: 1200px)");
const label = getLabel(status, latencyMs, retryCount);
return (
<Group gap="xs">
<Dot
bg={statusColorVar[status]}
className={status === "connecting" ? "inspector-pulse" : undefined}
title={showLabel ? undefined : label}
/>
<Text size="sm">{getLabel(status, latencyMs, retryCount)}</Text>
{showLabel && <Text size="sm">{label}</Text>}
</Group>
);
}

View file

@ -5,12 +5,14 @@ import {
Group,
Image,
SegmentedControl,
Select,
Text,
Title,
useComputedColorScheme,
} from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks";
import type { Implementation } from "@modelcontextprotocol/sdk/types.js";
import { MdLightMode, MdDarkMode } from "react-icons/md";
import { MdLightMode, MdDarkMode, MdLinkOff } from "react-icons/md";
import type { ConnectionStatus } from "@inspector/core/mcp/types.js";
import { ServerStatusIndicator } from "../../elements/ServerStatusIndicator/ServerStatusIndicator";
import mcpLogo from "../../../theme/assets/MCP.svg";
@ -39,13 +41,13 @@ const HeaderBar = Group.withProps({
h: "100%",
px: "md",
wrap: "nowrap",
gap: 0,
gap: "md",
});
const LeftSection = Group.withProps({
gap: "md",
wrap: "nowrap",
w: "33.33%",
flex: 1,
miw: 0,
});
@ -66,17 +68,20 @@ const ServerName = Text.withProps({
fw: 600,
size: "lg",
truncate: "end",
maw: "calc(100% - 40px)",
miw: 0,
flex: 1,
});
const CenterSection = Group.withProps({
w: "33.33%",
justify: "center",
wrap: "nowrap",
flex: "0 0 auto",
});
const RightSection = Group.withProps({
gap: "sm",
w: "33.33%",
wrap: "nowrap",
flex: 1,
miw: 0,
justify: "flex-end",
});
@ -86,6 +91,13 @@ const DisconnectButton = Button.withProps({
size: "sm",
});
const DisconnectIcon = ActionIcon.withProps({
variant: "subtle",
c: "red",
size: 36,
"aria-label": "Disconnect",
});
const ThemeToggle = ActionIcon.withProps({
variant: "subtle",
size: 36,
@ -101,6 +113,8 @@ const UnconnectedBar = Group.withProps({
export function ViewHeader(props: ViewHeaderProps) {
const colorScheme = useComputedColorScheme();
const ThemeIcon = colorScheme === "dark" ? MdLightMode : MdDarkMode;
const showSegmented = useMediaQuery("(min-width: 992px)");
const showDisconnectLabel = useMediaQuery("(min-width: 768px)");
if (!props.connected) {
return (
@ -126,12 +140,23 @@ export function ViewHeader(props: ViewHeaderProps) {
</LeftSection>
<CenterSection>
<SegmentedControl
value={props.activeTab}
onChange={props.onTabChange}
data={props.availableTabs}
size="sm"
/>
{showSegmented ? (
<SegmentedControl
value={props.activeTab}
onChange={props.onTabChange}
data={props.availableTabs}
size="sm"
/>
) : (
<Select
value={props.activeTab}
onChange={(value) => value && props.onTabChange(value)}
data={props.availableTabs}
size="sm"
allowDeselect={false}
w={140}
/>
)}
</CenterSection>
<RightSection>
@ -139,9 +164,15 @@ export function ViewHeader(props: ViewHeaderProps) {
status={props.status}
latencyMs={props.latencyMs}
/>
<DisconnectButton onClick={props.onDisconnect}>
Disconnect
</DisconnectButton>
{showDisconnectLabel ? (
<DisconnectButton onClick={props.onDisconnect}>
Disconnect
</DisconnectButton>
) : (
<DisconnectIcon onClick={props.onDisconnect} title="Disconnect">
<MdLinkOff size={20} />
</DisconnectIcon>
)}
<ThemeToggle onClick={props.onToggleTheme}>
<ThemeIcon size={20} />
</ThemeToggle>