feat(web): add copyright notice + footer/toolbar polish (#1639)

- Add a grey CopyrightBadge ("Copyright © Model Context Protocol a Series of LF
  Projects, LLC.") to the footer. Version (bottom-left) and copyright
  (bottom-right) share the bottom `xl` margin band: vertically centered in it
  and inset by `xl` to line up with the content's left/right margins.
- Move the list expand/collapse toggle to the right of the "Add Servers" button
  in ServerListControls.
- Make the monitoring column's close-sidebar control match the open-sidebar
  PinColumnButton theme (Button size sm / variant subtle) instead of a gray
  ActionIcon, so the open/close affordances read as a pair.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
This commit is contained in:
cliffhall 2026-07-10 20:11:52 -04:00
commit 5d024ab54f
9 changed files with 88 additions and 18 deletions

View file

@ -102,6 +102,7 @@ import { useInspectorVersion } from "@inspector/core/react/useInspectorVersion.j
import { usePendingClientRequests } from "@inspector/core/react/usePendingClientRequests.js";
import { InspectorView } from "./components/views/InspectorView/InspectorView";
import { VersionBadge } from "./components/elements/VersionBadge/VersionBadge";
import { CopyrightBadge } from "./components/elements/CopyrightBadge/CopyrightBadge";
import type {
ToolCallState,
ToolsUiState,
@ -3824,6 +3825,7 @@ function App() {
onRefreshApps={onRefreshTools}
/>
</Box>
<CopyrightBadge />
<VersionBadge version={inspectorVersion} />
<ServerConfigModal
opened={configModal !== null}

View file

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, within } from "storybook/test";
import { CopyrightBadge, COPYRIGHT_NOTICE } from "./CopyrightBadge";
const meta: Meta<typeof CopyrightBadge> = {
title: "Elements/CopyrightBadge",
component: CopyrightBadge,
};
export default meta;
type Story = StoryObj<typeof CopyrightBadge>;
// The grey copyright notice pinned to the lower-left corner of the viewport.
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText(COPYRIGHT_NOTICE)).toBeInTheDocument();
},
};

View file

@ -0,0 +1,17 @@
import { describe, it, expect } from "vitest";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { CopyrightBadge, COPYRIGHT_NOTICE } from "./CopyrightBadge";
describe("CopyrightBadge", () => {
it("renders the project copyright notice", () => {
renderWithMantine(<CopyrightBadge />);
expect(screen.getByText(COPYRIGHT_NOTICE)).toBeInTheDocument();
});
it("names the Model Context Protocol and LF Projects", () => {
renderWithMantine(<CopyrightBadge />);
expect(
screen.getByText(/Model Context Protocol.*Series of LF Projects, LLC\./),
).toBeInTheDocument();
});
});

View file

@ -0,0 +1,18 @@
import { Text } from "@mantine/core";
/** The project's copyright notice. */
export const COPYRIGHT_NOTICE =
"Copyright © Model Context Protocol a Series of LF Projects, LLC.";
// The `copyrightBadge` variant (src/theme/Text.ts) pins it to the lower-right
// corner in grey, on the same row as the version badge, and non-interactive.
const CopyrightText = Text.withProps({ variant: "copyrightBadge" });
/**
* The project copyright notice, fixed to the lower-right corner of the screen
* (#1639) — the grey, non-interactive twin of the lower-left version badge,
* sharing its row.
*/
export function CopyrightBadge() {
return <CopyrightText>{COPYRIGHT_NOTICE}</CopyrightText>;
}

View file

@ -5,12 +5,12 @@ export interface VersionBadgeProps {
version?: string;
}
// The `versionBadge` variant (src/theme/Text.ts) pins it to the lower-right
// The `versionBadge` variant (src/theme/Text.ts) pins it to the lower-left
// corner in grey and makes it non-interactive.
const VersionText = Text.withProps({ variant: "versionBadge" });
/**
* A small, unobtrusive build-version label fixed to the lower-right corner of
* A small, unobtrusive build-version label fixed to the lower-left corner of
* the screen (#1639). Sourced from the root `package.json` via the backend's
* `GET /api/config`; renders nothing until the version is known (or on a legacy
* backend that omits it).

View file

@ -1,4 +1,4 @@
import { ActionIcon, Group, SegmentedControl, TextInput } from "@mantine/core";
import { Button, Group, SegmentedControl, TextInput } from "@mantine/core";
import { TbLayoutSidebarRightCollapse } from "react-icons/tb";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
@ -69,15 +69,14 @@ export function MonitoringControls({
) : null
}
/>
<ActionIcon
<Button
size="sm"
variant="subtle"
color="gray"
size="lg"
aria-label="Close monitoring column"
onClick={onClose}
>
<TbLayoutSidebarRightCollapse size={20} />
</ActionIcon>
</Button>
</ControlsBar>
);
}

View file

@ -39,8 +39,9 @@ describe("ServerListControls", () => {
onToggleList={onToggleList}
/>,
);
const buttons = screen.getAllByRole("button");
await user.click(buttons[0]);
await user.click(
screen.getByRole("button", { name: /Expand all|Collapse all/ }),
);
expect(onToggleList).toHaveBeenCalledTimes(1);
});

View file

@ -27,9 +27,6 @@ export function ServerListControls({
}: ServerListControlsProps) {
return (
<Group justify="flex-end">
{serverCount > 0 && (
<ListToggle compact={compact} onToggle={onToggleList} />
)}
<Button variant="default" onClick={onExport} disabled={serverCount === 0}>
Export
</Button>
@ -40,6 +37,9 @@ export function ServerListControls({
onImportServerJson={onImportServerJson}
/>
)}
{serverCount > 0 && (
<ListToggle compact={compact} onToggle={onToggleList} />
)}
</Group>
);
}

View file

@ -37,15 +37,29 @@ export const ThemeText = Text.extend({
},
};
}
// A small, unobtrusive build-version label pinned to the lower-right corner
// of the screen (#1639): grey, non-interactive (clicks pass through), and
// out of the tab/selection flow so it never interferes with the UI beneath.
if (props.variant === "versionBadge") {
// Two small, unobtrusive labels pinned to the bottom corners of the screen
// on the same row (#1639): the build version (bottom-left) and the
// copyright notice (bottom-right). Each occupies the full-height `xl` bottom
// margin band (`height: xl` + flex centering) so the text sits vertically
// centered in it, and insets by `xl` horizontally to line up with the
// content's left/right margins. Both are grey, non-interactive (clicks pass
// through), and out of the tab/selection flow.
if (
props.variant === "versionBadge" ||
props.variant === "copyrightBadge"
) {
const horizontal =
props.variant === "versionBadge"
? { left: "var(--mantine-spacing-xl)" }
: { right: "var(--mantine-spacing-xl)" };
return {
root: {
position: "fixed",
bottom: "0.35rem",
right: "0.6rem",
bottom: 0,
height: "var(--mantine-spacing-xl)",
display: "flex",
alignItems: "center",
...horizontal,
zIndex: 100,
color: "var(--inspector-text-secondary)",
fontSize: "var(--mantine-font-size-xs)",