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:
parent
9553fb9c8a
commit
4e99e6393f
2 changed files with 52 additions and 17 deletions
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue