Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -61,36 +61,46 @@ export function ClientSettingsModal({
}

return (
<Modal
// Compound Modal so the header lives in `Modal.Header` (sticky by design)
// while `scrollAreaComponent` confines overflow to `Modal.Body` (#1698 —
// kept consistent with ServerSettingsModal for when this grows enough to
// scroll). The fade-down transition `<Modal>` defaults to (but `Modal.Root`
// doesn't inherit) is supplied app-wide by `ThemeModalRoot`.
<Modal.Root
opened={opened}
onClose={handleClose}
withCloseButton={false}
size="lg"
centered
scrollAreaComponent={ScrollArea.Autosize}
>
<Stack gap="md">
<Group justify="space-between" wrap="nowrap">
<ListToggle
compact={!allExpanded}
variant="subtle"
onToggle={handleToggleAll}
<Modal.Overlay />
<Modal.Content>
<Modal.Header>
<Group justify="space-between" wrap="nowrap" w="100%">
<ListToggle
compact={!allExpanded}
variant="subtle"
onToggle={handleToggleAll}
/>
{/* `Modal.Title` names the dialog (wires `aria-labelledby`). */}
<Modal.Title ta="center" flex={1}>
Client Settings
</Modal.Title>
<CloseButton aria-label="Close" onClick={handleClose} />
</Group>
</Modal.Header>
<Modal.Body>
<ClientSettingsForm
settings={settings}
expandedSections={expandedSections}
onExpandedSectionsChange={setExpandedSections}
onSettingsChange={onSettingsChange}
emaIdpLoginState={emaIdpLoginState}
onEmaIdpLogout={onEmaIdpLogout}
revealErrors={revealErrors}
/>
{/* `Modal.Title` names the dialog (wires `aria-labelledby`). */}
<Modal.Title ta="center" flex={1}>
Client Settings
</Modal.Title>
<CloseButton aria-label="Close" onClick={handleClose} />
</Group>
<ClientSettingsForm
settings={settings}
expandedSections={expandedSections}
onExpandedSectionsChange={setExpandedSections}
onSettingsChange={onSettingsChange}
emaIdpLoginState={emaIdpLoginState}
onEmaIdpLogout={onEmaIdpLogout}
revealErrors={revealErrors}
/>
</Stack>
</Modal>
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
}
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -232,57 +232,68 @@ export function ServerSettingsModal({
}

return (
<Modal
// Compound Modal so the header lives in `Modal.Header` (sticky by design)
// while `scrollAreaComponent` confines overflow to `Modal.Body` — otherwise
// expanding enough accordion sections grows the whole modal past the
// viewport and scrolls the header out of view (#1698). The fade-down
// transition `<Modal>` defaults to (but `Modal.Root` doesn't inherit) is
// supplied app-wide by `ThemeModalRoot`.
<Modal.Root
opened={opened}
onClose={onClose}
withCloseButton={false}
size="lg"
centered
scrollAreaComponent={ScrollArea.Autosize}
>
<Stack gap="md">
<Group justify="space-between" wrap="nowrap">
<ListToggle
compact={!allExpanded}
variant="subtle"
onToggle={handleToggleAll}
<Modal.Overlay />
<Modal.Content>
<Modal.Header>
<Group justify="space-between" wrap="nowrap" w="100%">
<ListToggle
compact={!allExpanded}
variant="subtle"
onToggle={handleToggleAll}
/>
{/* `Modal.Title` names the dialog (wires `aria-labelledby`). */}
<Modal.Title ta="center" flex={1}>
Server Settings
</Modal.Title>
<CloseButton aria-label="Close" onClick={onClose} />
</Group>
</Modal.Header>
<Modal.Body>
<ServerSettingsForm
settings={settings}
serverType={serverType}
isStdio={isStdio}
expandedSections={expandedSections}
onExpandedSectionsChange={setExpandedSections}
onAddHeader={handleAddHeader}
onRemoveHeader={handleRemoveHeader}
onHeaderChange={handleHeaderChange}
onAddEnv={handleAddEnv}
onRemoveEnv={handleRemoveEnv}
onEnvChange={handleEnvChange}
onCwdChange={handleCwdChange}
onAddMetadata={handleAddMetadata}
onRemoveMetadata={handleRemoveMetadata}
onMetadataChange={handleMetadataChange}
onTimeoutChange={handleTimeoutChange}
onAutoRefreshChange={handleAutoRefreshChange}
onPaginatedListsChange={handlePaginatedListsChange}
onAdvertisedExtensionChange={handleAdvertisedExtensionChange}
onMaxFetchRequestsChange={handleMaxFetchRequestsChange}
onProtocolEraChange={handleProtocolEraChange}
onModernLogLevelChange={handleModernLogLevelChange}
negotiatedEra={negotiatedEra}
onOAuthChange={handleOAuthChange}
onClearStoredOAuth={onClearStoredOAuth}
onAddRoot={handleAddRoot}
onRemoveRoot={handleRemoveRoot}
onRootChange={handleRootChange}
/>
{/* `Modal.Title` names the dialog (wires `aria-labelledby`). */}
<Modal.Title ta="center" flex={1}>
Server Settings
</Modal.Title>
<CloseButton aria-label="Close" onClick={onClose} />
</Group>
<ServerSettingsForm
settings={settings}
serverType={serverType}
isStdio={isStdio}
expandedSections={expandedSections}
onExpandedSectionsChange={setExpandedSections}
onAddHeader={handleAddHeader}
onRemoveHeader={handleRemoveHeader}
onHeaderChange={handleHeaderChange}
onAddEnv={handleAddEnv}
onRemoveEnv={handleRemoveEnv}
onEnvChange={handleEnvChange}
onCwdChange={handleCwdChange}
onAddMetadata={handleAddMetadata}
onRemoveMetadata={handleRemoveMetadata}
onMetadataChange={handleMetadataChange}
onTimeoutChange={handleTimeoutChange}
onAutoRefreshChange={handleAutoRefreshChange}
onPaginatedListsChange={handlePaginatedListsChange}
onAdvertisedExtensionChange={handleAdvertisedExtensionChange}
onMaxFetchRequestsChange={handleMaxFetchRequestsChange}
onProtocolEraChange={handleProtocolEraChange}
onModernLogLevelChange={handleModernLogLevelChange}
negotiatedEra={negotiatedEra}
onOAuthChange={handleOAuthChange}
onClearStoredOAuth={onClearStoredOAuth}
onAddRoot={handleAddRoot}
onRemoveRoot={handleRemoveRoot}
onRootChange={handleRootChange}
/>
</Stack>
</Modal>
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
}
15 changes: 15 additions & 0 deletions clients/web/src/theme/Modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Modal>` 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 `<Modal>`. 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 },
},
});
2 changes: 1 addition & 1 deletion clients/web/src/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 2 additions & 0 deletions clients/web/src/theme/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ThemeGroup,
ThemeInput,
ThemeModal,
ThemeModalRoot,
ThemePaper,
ThemeScrollArea,
ThemeScrollAreaAutosize,
Expand Down Expand Up @@ -194,6 +195,7 @@ export const theme = createTheme({
Group: ThemeGroup,
Input: ThemeInput,
Modal: ThemeModal,
ModalRoot: ThemeModalRoot,
Paper: ThemePaper,
ScrollArea: ThemeScrollArea,
ScrollAreaAutosize: ThemeScrollAreaAutosize,
Expand Down
24 changes: 24 additions & 0 deletions pr-screenshots/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pr-screenshots/ssm-1698-header-pinned-top.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading