* fix(web): ResourceControls single-scroll + right/down disclosure chevrons (#1462) Two fixes to the Resources controls panel: 1. Sections scrolled prematurely. `panelMaxHeight` divided the available height equally across open sections (`… / n`), so a section with many items scrolled while sparse siblings left their equal share unused. Replace the per-section ScrollAreas with a single viewport-bounded ScrollArea.Autosize around the whole accordion (matching ToolControls / PromptControls). Sections size to content; nothing scrolls until the open sections' combined height exceeds the cap. 2. Accordion chevrons pointed down/up. Add a `disclosure` Accordion theme variant + `.disclosure-chevron[data-rotate]` rule that rotates a right-pointing RiArrowRightSLine 90° (not Mantine's default 180°), so closed sections point right and open sections point down — matching the ResourceLink chevron convention. Also keep empty (disabled) sections out of the accordion's open set so they render collapsed with a right chevron instead of an open-but-empty panel. Verified all states in a real browser via Playwright. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(web): cover empty-section-collapsed behavior in ResourceControls (#1462) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): make Resources sidebar fill panel height before scrolling (#1462) The first pass capped the accordion scroll region at a fixed sub-viewport height (`100vh - … - 160px`), so the sidebar scrolled before reaching the height of the detail panel to its right. Make the sidebar card full-height instead: add a `sidebar` Card variant (height:100%, flex column, overflow hidden), give it `flex: 1`, and let ResourceControls' root Stack (`flex: 1; min-height: 0`) and a plain flex-filling ScrollArea claim the space below the title/search. The card now matches the detail panel's height exactly and the single inner scroll region scrolls only once the open sections overflow the full panel. Adds a ResourcesScreen ManyResources story asserting the sidebar/detail height match and that overflow scrolls, plus a unit test for the empty-section-collapsed behavior. Verified heights + scroll in a real browser via Playwright. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): pin Resources section headers, scroll each section in place (#1462) The previous pass scrolled the whole accordion as one block (headers scrolled away). Restore pinned headers with per-section scrolling, without the original equal-division premature-scroll bug. The `disclosure` Accordion now fills its full-height card as a flex column (`disclosure-sections`): controls are pinned, and each open section's panel scrolls within its own share of the remaining height. Per-item `flex` weights (shrink ∝ item count, grow 0) hand space from long sections to short ones, so nothing scrolls until the combined content overflows the panel — a long URIs list scrolls in place while Templates stays fully visible below it. Drops the single outer ScrollArea (and useScrollMemory). Verified in a real browser: 6 URIs → no scroll, headers pinned, empty space at the bottom; 40 URIs → URIs panel scrolls in place, Templates intact, all three headers pinned, sidebar still matches the detail height. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): Resources sidebar grows with content, capped at screen (#1462) The `sidebar` Card variant forced `height: 100%`, so the panel was always full-height with empty space below short content. Switch to `max-height: 100%` (and drop `flex: 1`) so the card hugs its content and only caps at the screen height — matching the Tools panel. When content overflows the cap, the flex accordion still takes over per-section scrolling with pinned headers. Verified in a real browser: 6 URIs → card hugs (638px, shorter than the detail panel); 40 URIs → card caps at the screen (949px == detail), headers pinned, URIs scrolls in place. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): preserve empty-section intent + kill accordion collapse jump (#1464 review) Address review feedback on PR #1464: - Intent loss (review #1): the accordion's `value` is the filtered `visibleOpenSections`, so Mantine's toggle dropped open-but-empty sections from the persisted intent. Merge them back in the onChange handler so an open empty section stays open and reappears with items. Adds a unit test. - Collapse animation jump (review #3): Mantine's Collapse briefly set the panel to its full natural height (fighting the flex layout) on open/close, flashing siblings. Set `transitionDuration={0}` so flex controls the height; restore a smooth chevron rotation via CSS. Verified no jump in a real browser (panel 501→0, no spike). Since closed sections now render no content, drop the redundant (and incorrect) "Templates" toggle clicks from 4 ResourcesScreen tests (the section is open by default) and add a section-toggle test to keep the openSections handler covered. - Nits: scope the stories' chevron query to the accordion root; tighten the Collapsed assertion to the identity transform. Native-vs-styled scrollbar (review #4) split out as #1466. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(web): clarify chevron-timing and intent-merge invariants (#1464 review) Address the two optional doc nits from the re-review: - Note that the chevron's 200ms restates Mantine's default accordion duration (keep in sync if accordion timing is themed). - Note that the unconditional empty-section merge is safe because empty-section controls are disabled, so `next` never contains one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3220d6239a
commit
d525fc9f54
3 changed files with 388 additions and 64 deletions
|
|
@ -6,7 +6,7 @@ import type {
|
|||
ResourceTemplate,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { InspectorResourceSubscription } from "../../../../../../core/mcp/types.js";
|
||||
import { fn, userEvent, within } from "storybook/test";
|
||||
import { expect, fn, userEvent, within } from "storybook/test";
|
||||
import { ResourcesScreen } from "./ResourcesScreen";
|
||||
import type { ReadResourceState, ResourcesUiState } from "./ResourcesScreen";
|
||||
import { EMPTY_RESOURCES_UI } from "../screenUiState";
|
||||
|
|
@ -222,6 +222,51 @@ export const AllSections: Story = {
|
|||
},
|
||||
};
|
||||
|
||||
const manyResources: Resource[] = Array.from({ length: 40 }, (_, i) => ({
|
||||
name: `resource-${String(i + 1).padStart(2, "0")}.wav`,
|
||||
uri: `file:///kit/resource-${i + 1}.wav`,
|
||||
}));
|
||||
|
||||
// Enough URIs to overflow the panel. The sidebar card stays the same height as
|
||||
// the detail panel (no selection → full-height empty card). The section headers
|
||||
// stay pinned and the long URIs section scrolls *within its own panel* — the
|
||||
// whole accordion doesn't scroll, and a section doesn't scroll until the panel
|
||||
// is full (#1462).
|
||||
export const ManyResources: Story = {
|
||||
args: {
|
||||
resources: manyResources,
|
||||
templates: sampleTemplates,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const [sidebarCard, detailCard] =
|
||||
canvasElement.querySelectorAll(".mantine-Card-root");
|
||||
const sidebar = sidebarCard.getBoundingClientRect();
|
||||
const detail = detailCard.getBoundingClientRect();
|
||||
// The sidebar matches the detail panel's height (same bottom baseline).
|
||||
expect(Math.abs(sidebar.bottom - detail.bottom)).toBeLessThanOrEqual(1);
|
||||
expect(Math.abs(sidebar.height - detail.height)).toBeLessThanOrEqual(1);
|
||||
|
||||
// The section headers stay pinned (visible, in document order), so the
|
||||
// whole accordion doesn't scroll as one block.
|
||||
const controls = canvasElement.querySelectorAll(
|
||||
".disclosure-sections .mantine-Accordion-control",
|
||||
);
|
||||
expect(controls).toHaveLength(3);
|
||||
const tops = [...controls].map((c) => c.getBoundingClientRect().top);
|
||||
expect(tops[0]).toBeLessThan(tops[1]);
|
||||
expect(tops[1]).toBeLessThan(tops[2]);
|
||||
|
||||
// The long URIs section scrolls within its own panel.
|
||||
const urisPanel = canvasElement.querySelector(
|
||||
".disclosure-sections .mantine-Accordion-panel",
|
||||
);
|
||||
if (!(urisPanel instanceof HTMLElement)) {
|
||||
throw new Error("URIs panel not found");
|
||||
}
|
||||
expect(urisPanel.scrollHeight).toBeGreaterThan(urisPanel.clientHeight);
|
||||
},
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
resources: [],
|
||||
|
|
|
|||
|
|
@ -138,10 +138,21 @@ describe("ResourcesScreen", () => {
|
|||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("toggles a section's open state through the lifted openSections handler", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(<ControlledResourcesScreen />);
|
||||
const urisHeader = screen.getByRole("button", { name: /URIs \(2\)/ });
|
||||
// Open by default (compact=false); clicking routes through ResourcesScreen's
|
||||
// onOpenSectionsChange → onUiChange and collapses it.
|
||||
expect(urisHeader).toHaveAttribute("aria-expanded", "true");
|
||||
await user.click(urisHeader);
|
||||
expect(urisHeader).toHaveAttribute("aria-expanded", "false");
|
||||
});
|
||||
|
||||
it("renders the template panel when a template is selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(<ControlledResourcesScreen />);
|
||||
await user.click(screen.getByText("Templates (1)"));
|
||||
// Templates section is open by default; select the template to open its form.
|
||||
await user.click(screen.getByText("files"));
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Read Resource" }),
|
||||
|
|
@ -154,7 +165,6 @@ describe("ResourcesScreen", () => {
|
|||
renderWithMantine(
|
||||
<ControlledResourcesScreen onReadResource={onReadResource} />,
|
||||
);
|
||||
await user.click(screen.getByText("Templates (1)"));
|
||||
await user.click(screen.getByText("files"));
|
||||
await user.type(screen.getByLabelText("path"), "alpha");
|
||||
await user.click(screen.getByRole("button", { name: "Read Resource" }));
|
||||
|
|
@ -210,8 +220,7 @@ describe("ResourcesScreen", () => {
|
|||
onReadResource={onReadResource}
|
||||
/>,
|
||||
);
|
||||
// Open the template form.
|
||||
await user.click(screen.getByText("Templates (1)"));
|
||||
// Templates is open by default; select the template to open its form.
|
||||
await user.click(screen.getByText("files"));
|
||||
// Submit it — the screen calls onReadResource and remembers the
|
||||
// template URI for the close handler.
|
||||
|
|
@ -249,7 +258,6 @@ describe("ResourcesScreen", () => {
|
|||
const { rerender } = renderWithMantine(
|
||||
<ControlledResourcesScreen templates={templates} />,
|
||||
);
|
||||
await user.click(screen.getByText("Templates (1)"));
|
||||
await user.click(screen.getByText("Dynamic"));
|
||||
await user.type(screen.getByLabelText("id"), "asdf");
|
||||
await user.click(screen.getByRole("button", { name: "Read Resource" }));
|
||||
|
|
|
|||
|
|
@ -77,9 +77,14 @@ const Sidebar = Stack.withProps({
|
|||
flex: "0 0 auto",
|
||||
});
|
||||
|
||||
// Card that grows with its content but is capped at the screen height by the
|
||||
// `sidebar` variant (`max-height: 100%`), like the Tools panel. The column
|
||||
// layout lets ResourceControls' accordion take over per-section scrolling once
|
||||
// the content would overflow that cap.
|
||||
const SidebarCard = Card.withProps({
|
||||
withBorder: true,
|
||||
padding: "lg",
|
||||
variant: "sidebar",
|
||||
});
|
||||
|
||||
const DetailCard = Card.withProps({
|
||||
|
|
|
|||
Loading…
Reference in a new issue