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

View file

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