diff --git a/clients/web/src/components/groups/ClientSettingsModal/ClientSettingsModal.tsx b/clients/web/src/components/groups/ClientSettingsModal/ClientSettingsModal.tsx
index 33b39b007..c823dbde7 100644
--- a/clients/web/src/components/groups/ClientSettingsModal/ClientSettingsModal.tsx
+++ b/clients/web/src/components/groups/ClientSettingsModal/ClientSettingsModal.tsx
@@ -1,5 +1,5 @@
import { useState } from "react";
-import { CloseButton, Group, Modal, Stack } from "@mantine/core";
+import { CloseButton, Group, Modal, ScrollArea } from "@mantine/core";
import { ListToggle } from "../../elements/ListToggle/ListToggle";
import {
ClientSettingsForm,
@@ -61,36 +61,46 @@ export function ClientSettingsModal({
}
return (
- ` defaults to (but `Modal.Root`
+ // doesn't inherit) is supplied app-wide by `ThemeModalRoot`.
+
-
-
-
+
+
+
+
+ {/* `Modal.Title` names the dialog (wires `aria-labelledby`). */}
+
+ Client Settings
+
+
+
+
+
+
- {/* `Modal.Title` names the dialog (wires `aria-labelledby`). */}
-
- Client Settings
-
-
-
-
-
-
+
+
+
);
}
diff --git a/clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.tsx b/clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.tsx
index e91d43c8a..6e29abd44 100644
--- a/clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.tsx
+++ b/clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.tsx
@@ -1,5 +1,5 @@
import { useState } from "react";
-import { CloseButton, Group, Modal, Stack } from "@mantine/core";
+import { CloseButton, Group, Modal, ScrollArea } from "@mantine/core";
import type { ProtocolEra } from "@modelcontextprotocol/client";
import type {
InspectorServerSettings,
@@ -232,57 +232,68 @@ export function ServerSettingsModal({
}
return (
- ` defaults to (but `Modal.Root` doesn't inherit) is
+ // supplied app-wide by `ThemeModalRoot`.
+
-
-
-
+
+
+
+
+ {/* `Modal.Title` names the dialog (wires `aria-labelledby`). */}
+
+ Server Settings
+
+
+
+
+
+
- {/* `Modal.Title` names the dialog (wires `aria-labelledby`). */}
-
- Server Settings
-
-
-
-
-
-
+
+
+
);
}
diff --git a/clients/web/src/theme/Modal.ts b/clients/web/src/theme/Modal.ts
index baaf84f52..bfdd97d88 100644
--- a/clients/web/src/theme/Modal.ts
+++ b/clients/web/src/theme/Modal.ts
@@ -12,3 +12,18 @@ export const ThemeModal = Modal.extend({
closeButtonProps: { "aria-label": "Close" },
},
});
+
+/**
+ * Restore the fade-down open/close animation on the compound `Modal.Root` API.
+ * The default `` wrapper hardcodes `transitionProps: { transition:
+ * "fade-down", duration: 200 }`, but `Modal.Root` does not inherit it — so
+ * modals built from `Modal.Root` (e.g. Server/Client Settings, which need a
+ * sticky `Modal.Header` + scrollable `Modal.Body`, see #1698) would otherwise
+ * animate differently from every plain ``. Setting it here keeps them
+ * consistent app-wide without repeating the literal at each call site.
+ */
+export const ThemeModalRoot = Modal.Root.extend({
+ defaultProps: {
+ transitionProps: { transition: "fade-down", duration: 200 },
+ },
+});
diff --git a/clients/web/src/theme/index.ts b/clients/web/src/theme/index.ts
index 0e1eb95eb..37ec7a430 100644
--- a/clients/web/src/theme/index.ts
+++ b/clients/web/src/theme/index.ts
@@ -10,7 +10,7 @@ export { ThemeCode } from "./Code";
export { ThemeFlex } from "./Flex";
export { ThemeGroup } from "./Group";
export { ThemeInput } from "./Input";
-export { ThemeModal } from "./Modal";
+export { ThemeModal, ThemeModalRoot } from "./Modal";
export { ThemePaper } from "./Paper";
export { ThemeScrollArea, ThemeScrollAreaAutosize } from "./ScrollArea";
export { ThemeSelect } from "./Select";
diff --git a/clients/web/src/theme/theme.ts b/clients/web/src/theme/theme.ts
index d6f552475..c76fb3169 100644
--- a/clients/web/src/theme/theme.ts
+++ b/clients/web/src/theme/theme.ts
@@ -13,6 +13,7 @@ import {
ThemeGroup,
ThemeInput,
ThemeModal,
+ ThemeModalRoot,
ThemePaper,
ThemeScrollArea,
ThemeScrollAreaAutosize,
@@ -194,6 +195,7 @@ export const theme = createTheme({
Group: ThemeGroup,
Input: ThemeInput,
Modal: ThemeModal,
+ ModalRoot: ThemeModalRoot,
Paper: ThemePaper,
ScrollArea: ThemeScrollArea,
ScrollAreaAutosize: ThemeScrollAreaAutosize,
diff --git a/pr-screenshots/README.md b/pr-screenshots/README.md
index 1cd2017ac..cd46c2378 100644
--- a/pr-screenshots/README.md
+++ b/pr-screenshots/README.md
@@ -1,3 +1,27 @@
+# Server/Client Settings modal: sticky header while body scrolls (#1698) — proof screenshots
+
+Expanding enough accordion sections grew the Server Settings modal past the
+viewport, scrolling the whole modal and taking the header (title + close) out of
+view. The fix moves the header into a real `Modal.Header` (sticky by design) and
+adds `scrollAreaComponent={ScrollArea.Autosize}` so only `Modal.Body` scrolls.
+Captured from the `ServerSettingsModal` Storybook story (`FullyConfigured`, all
+sections expanded, ~680px viewport) via Playwright.
+
+
+
+At the top of the scroll: the **Server Settings** header (collapse-all toggle,
+title, close) sits above the first section.
+
+
+
+Scrolled down through the body (now showing Advertised Extensions → Custom
+Headers → Request Metadata): the **exact same header stays pinned** at the top.
+Measured in-page: the body scroll container is `scrollHeight` 2253 ×
+`clientHeight` 612 (scrolls internally), and the header is `position: sticky`
+with its top flush to the dialog top after scrolling.
+
+---
+
# TUI array-of-enum renders as a select on `items.enum` alone (#1751) — proof screenshots
Follow-up to #1691. The TUI `schemaToForm` array-of-enum branch was nested under
diff --git a/pr-screenshots/ssm-1698-header-pinned-scrolled.png b/pr-screenshots/ssm-1698-header-pinned-scrolled.png
new file mode 100644
index 000000000..327307d5b
Binary files /dev/null and b/pr-screenshots/ssm-1698-header-pinned-scrolled.png differ
diff --git a/pr-screenshots/ssm-1698-header-pinned-top.png b/pr-screenshots/ssm-1698-header-pinned-top.png
new file mode 100644
index 000000000..216b3a2e8
Binary files /dev/null and b/pr-screenshots/ssm-1698-header-pinned-top.png differ