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 { usePendingClientRequests } from "@inspector/core/react/usePendingClientRequests.js";
import { InspectorView } from "./components/views/InspectorView/InspectorView"; import { InspectorView } from "./components/views/InspectorView/InspectorView";
import { VersionBadge } from "./components/elements/VersionBadge/VersionBadge"; import { VersionBadge } from "./components/elements/VersionBadge/VersionBadge";
import { CopyrightBadge } from "./components/elements/CopyrightBadge/CopyrightBadge";
import type { import type {
ToolCallState, ToolCallState,
ToolsUiState, ToolsUiState,
@ -3824,6 +3825,7 @@ function App() {
onRefreshApps={onRefreshTools} onRefreshApps={onRefreshTools}
/> />
</Box> </Box>
<CopyrightBadge />
<VersionBadge version={inspectorVersion} /> <VersionBadge version={inspectorVersion} />
<ServerConfigModal <ServerConfigModal
opened={configModal !== null} 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; 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. // corner in grey and makes it non-interactive.
const VersionText = Text.withProps({ variant: "versionBadge" }); 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 * 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 * `GET /api/config`; renders nothing until the version is known (or on a legacy
* backend that omits it). * 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 { TbLayoutSidebarRightCollapse } from "react-icons/tb";
import { ClearButton } from "../../elements/ClearButton/ClearButton"; import { ClearButton } from "../../elements/ClearButton/ClearButton";
@ -69,15 +69,14 @@ export function MonitoringControls({
) : null ) : null
} }
/> />
<ActionIcon <Button
size="sm"
variant="subtle" variant="subtle"
color="gray"
size="lg"
aria-label="Close monitoring column" aria-label="Close monitoring column"
onClick={onClose} onClick={onClose}
> >
<TbLayoutSidebarRightCollapse size={20} /> <TbLayoutSidebarRightCollapse size={20} />
</ActionIcon> </Button>
</ControlsBar> </ControlsBar>
); );
} }

View file

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

View file

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

View file

@ -37,15 +37,29 @@ export const ThemeText = Text.extend({
}, },
}; };
} }
// A small, unobtrusive build-version label pinned to the lower-right corner // Two small, unobtrusive labels pinned to the bottom corners of the screen
// of the screen (#1639): grey, non-interactive (clicks pass through), and // on the same row (#1639): the build version (bottom-left) and the
// out of the tab/selection flow so it never interferes with the UI beneath. // copyright notice (bottom-right). Each occupies the full-height `xl` bottom
if (props.variant === "versionBadge") { // 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 { return {
root: { root: {
position: "fixed", position: "fixed",
bottom: "0.35rem", bottom: 0,
right: "0.6rem", height: "var(--mantine-spacing-xl)",
display: "flex",
alignItems: "center",
...horizontal,
zIndex: 100, zIndex: 100,
color: "var(--inspector-text-secondary)", color: "var(--inspector-text-secondary)",
fontSize: "var(--mantine-font-size-xs)", fontSize: "var(--mantine-font-size-xs)",