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. + +![Modal scrolled to the top — header pinned, Options section visible](ssm-1698-header-pinned-top.png) + +At the top of the scroll: the **Server Settings** header (collapse-all toggle, +title, close) sits above the first section. + +![Modal scrolled down — same header still pinned while the body shows Custom Headers / Request Metadata](ssm-1698-header-pinned-scrolled.png) + +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