Skip to content

Commit adb5f29

Browse files
cliffhallclaude
andcommitted
Auto-hide template after read; size preview to content with cap
- After the user clicks Read Resource on the template form, handleReadResource now clears selectedTemplateUri so the screen swaps from the template form to the resource preview. Previously both panels stayed mounted side-by-side. - The preview no longer hard-fills the viewport. The Card uses a new "preview" theme variant (overflow: hidden) and is content-sized, capped at SCROLL_MAX_HEIGHT. ResourcePreviewPanel's flex column marks the header / meta / footer rows as `flex: 0 0 auto` and the ContentScroll as `flex: 0 1 auto` with `mih: 0`, so: - short content → card hugs it, footer sits right under the body - long content → card caps at viewport, ContentScroll shrinks and scrolls internally, footer stays pinned at the cap - ScrollArea / Group imports in ResourcesScreen pruned to match the simpler single-pane layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a77027b commit adb5f29

4 files changed

Lines changed: 87 additions & 64 deletions

File tree

clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ function formatLastUpdated(date: Date): string {
5454
const HeaderRow = Group.withProps({
5555
justify: "space-between",
5656
wrap: "nowrap",
57+
flex: "0 0 auto",
5758
});
5859

5960
const UriGroup = Group.withProps({
@@ -70,6 +71,7 @@ const UriText = Text.withProps({
7071
const MetaRow = Group.withProps({
7172
justify: "space-between",
7273
wrap: "nowrap",
74+
flex: "0 0 auto",
7375
});
7476

7577
const TimestampText = Text.withProps({
@@ -84,6 +86,7 @@ const MimeText = Text.withProps({
8486

8587
const FooterRow = Group.withProps({
8688
justify: "space-between",
89+
flex: "0 0 auto",
8790
});
8891

8992
const AnnotationGroup = Group.withProps({
@@ -96,22 +99,25 @@ const ActionGroup = Group.withProps({
9699

97100
const Spacer = Flex.withProps({});
98101

99-
// Outer container fills the parent (a fixed-height Card in the resource
100-
// branch of ResourcesScreen) so the header/footer can pin to its edges
101-
// while the content area scrolls.
102+
// The panel sizes to its content: when the resource body is short the
103+
// Card hugs it; when the body would overflow the Card's `mah`, the
104+
// browser shrinks shrinkable flex items (only ContentScroll, since the
105+
// header / meta / footer rows opt out with `flex: 0 0 auto`) and the
106+
// inner ScrollArea takes over scrolling — keeping the subscribe button
107+
// pinned at the bottom edge of the cap.
102108
const PanelStack = Stack.withProps({
103109
gap: "md",
104-
h: "100%",
105-
flex: 1,
106110
miw: 0,
111+
mih: 0,
107112
});
108113

109-
// The middle scroll region. flex=1 lets it absorb the height left over
110-
// after the header / meta / footer rows; miw=0 prevents wide markdown
111-
// (tables, long links) from pushing the panel past the viewport.
114+
// Middle scroll region: basis sized to its own content, can shrink to
115+
// fit the available space when content overflows, never grows past its
116+
// content (so a short resource body doesn't push the footer down).
112117
const ContentScroll = ScrollArea.withProps({
113-
flex: 1,
118+
flex: "0 1 auto",
114119
miw: 0,
120+
mih: 0,
115121
type: "auto",
116122
scrollbars: "y",
117123
offsetScrollbars: true,

clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.test.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,27 @@ describe("ResourcesScreen", () => {
110110
await user.click(screen.getByText("Templates (1)"));
111111
await user.click(screen.getByText("files"));
112112
expect(
113-
screen.getByText("Enter a URI and click Read to preview"),
113+
screen.getByRole("button", { name: "Read Resource" }),
114114
).toBeInTheDocument();
115115
});
116116

117+
it("hides the template panel once the user reads the resource", async () => {
118+
const user = userEvent.setup();
119+
const onReadResource = vi.fn();
120+
renderWithMantine(
121+
<ResourcesScreen {...baseProps} onReadResource={onReadResource} />,
122+
);
123+
await user.click(screen.getByText("Templates (1)"));
124+
await user.click(screen.getByText("files"));
125+
await user.type(screen.getByLabelText("path"), "alpha");
126+
await user.click(screen.getByRole("button", { name: "Read Resource" }));
127+
expect(onReadResource).toHaveBeenCalledWith("file:///alpha");
128+
// After read, the template form is gone and the preview branch is active.
129+
expect(
130+
screen.queryByRole("button", { name: "Read Resource" }),
131+
).not.toBeInTheDocument();
132+
});
133+
117134
it("auto-reads when a resource is clicked in the sidebar", async () => {
118135
const user = userEvent.setup();
119136
const onReadResource = vi.fn();

clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx

Lines changed: 41 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,5 @@
11
import { useState } from "react";
2-
import {
3-
Alert,
4-
Card,
5-
Flex,
6-
Group,
7-
Loader,
8-
ScrollArea,
9-
Stack,
10-
Text,
11-
} from "@mantine/core";
2+
import { Alert, Card, Flex, Loader, Stack, Text } from "@mantine/core";
123
import type {
134
ReadResourceResult,
145
Resource,
@@ -62,22 +53,24 @@ const DetailCard = Card.withProps({
6253
padding: "lg",
6354
});
6455

65-
// Same as DetailCard but stretched to fill its parent's height. Used in
66-
// the preview pane so the ResourcePreviewPanel can pin its header/footer
67-
// to the card's edges while the content scrolls in the middle.
68-
const FillDetailCard = Card.withProps({
56+
// Card that sizes to its content but caps at the screen's available
57+
// height. When content fits, the card stays compact (footer sits right
58+
// under the body); when content would overflow, the inner ScrollArea
59+
// inside ResourcePreviewPanel shrinks and scrolls.
60+
const PreviewCard = Card.withProps({
6961
withBorder: true,
7062
padding: "lg",
71-
h: "100%",
63+
variant: "preview",
7264
});
7365

74-
// Fixed-height column that hosts the FillDetailCard. Replaces the prior
75-
// ScrollArea.Autosize wrapping so the panel's internal scroll region —
76-
// not the whole card — handles overflow.
66+
// Column that pins the preview card to the top of the available space
67+
// and bounds its growth via the consumer-set `mah`. The card inside
68+
// keeps its natural height up to that cap.
7769
const PreviewPane = Flex.withProps({
7870
flex: 1,
7971
miw: 0,
8072
direction: "column",
73+
align: "stretch",
8174
});
8275

8376
const EmptyState = Text.withProps({
@@ -134,6 +127,11 @@ export function ResourcesScreen({
134127
}
135128

136129
function handleReadResource(uri: string) {
130+
// Once the user reads (either from the template form or a refresh
131+
// inside the preview panel), hand the screen over to the preview:
132+
// clearing the template selection hides the template form so only
133+
// the rendered resource is shown.
134+
setSelectedTemplateUri(undefined);
137135
setSelectedResourceUri(uri);
138136
onReadResource(uri);
139137
}
@@ -143,28 +141,28 @@ export function ResourcesScreen({
143141

144142
if (readState.status === "pending") {
145143
return (
146-
<FillDetailCard>
144+
<PreviewCard>
147145
<Stack align="center" py="xl">
148146
<Loader size="sm" />
149147
<Text c="dimmed">Reading resource...</Text>
150148
</Stack>
151-
</FillDetailCard>
149+
</PreviewCard>
152150
);
153151
}
154152

155153
if (readState.status === "error") {
156154
return (
157-
<FillDetailCard>
155+
<PreviewCard>
158156
<Alert color="red" variant="light" title="Read Error">
159157
{readState.error ?? "Failed to read resource"}
160158
</Alert>
161-
</FillDetailCard>
159+
</PreviewCard>
162160
);
163161
}
164162

165163
if (readState.result && readResource) {
166164
return (
167-
<FillDetailCard>
165+
<PreviewCard>
168166
<ResourcePreviewPanel
169167
resource={readResource}
170168
contents={readState.result.contents}
@@ -174,7 +172,7 @@ export function ResourcesScreen({
174172
onSubscribe={() => onSubscribeResource(readResource.uri)}
175173
onUnsubscribe={() => onUnsubscribeResource(readResource.uri)}
176174
/>
177-
</FillDetailCard>
175+
</PreviewCard>
178176
);
179177
}
180178

@@ -201,36 +199,25 @@ export function ResourcesScreen({
201199
</Sidebar>
202200

203201
{selectedTemplate ? (
204-
<Group
205-
flex={1}
206-
miw={0}
207-
mah={SCROLL_MAX_HEIGHT}
208-
gap="md"
209-
align="stretch"
210-
wrap="nowrap"
211-
>
212-
<ScrollArea.Autosize flex={1} miw={0} mah={SCROLL_MAX_HEIGHT}>
213-
<DetailCard>
214-
<ResourceTemplatePanel
215-
template={selectedTemplate}
216-
onReadResource={handleReadResource}
217-
/>
218-
</DetailCard>
219-
</ScrollArea.Autosize>
220-
<PreviewPane>
221-
{renderReadState() ?? (
222-
<FillDetailCard>
223-
<EmptyState>Enter a URI and click Read to preview</EmptyState>
224-
</FillDetailCard>
225-
)}
226-
</PreviewPane>
227-
</Group>
228-
) : selectedResource ? (
229-
// Fixed-height column lets the preview panel pin its header and
230-
// subscribe/refresh footer to the card's edges while the resource
231-
// body scrolls inside the panel. miw=0 prevents wide content
232-
// (long unbroken lines, tables) from pushing the pane past the
233-
// viewport's right edge.
202+
// Template form only — once the user clicks Read Resource,
203+
// handleReadResource clears the template selection so the
204+
// resource branch takes over and the preview is shown alone.
205+
<PreviewPane mah={SCROLL_MAX_HEIGHT}>
206+
<PreviewCard>
207+
<ResourceTemplatePanel
208+
template={selectedTemplate}
209+
onReadResource={handleReadResource}
210+
/>
211+
</PreviewCard>
212+
</PreviewPane>
213+
) : readResource ? (
214+
// Sized-to-content preview pane, capped at the screen's available
215+
// height. When the resource body fits, the card hugs its content
216+
// and the subscribe/refresh row sits right under it. When the body
217+
// would overflow, the inner ScrollArea inside ResourcePreviewPanel
218+
// shrinks and scrolls, keeping the footer pinned at the cap.
219+
// miw=0 prevents wide content (long unbroken lines, tables) from
220+
// pushing the pane past the viewport's right edge.
234221
<PreviewPane mah={SCROLL_MAX_HEIGHT}>{renderReadState()}</PreviewPane>
235222
) : (
236223
<DetailCard flex={1}>

clients/web/src/theme/Card.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ export const ThemeCard = Card.extend({
2020
},
2121
};
2222
}
23+
if (props.variant === "preview") {
24+
// Container for the resource preview / template form panels: sizes to
25+
// content (no forced height) but caps at the screen's available area
26+
// via consumer-set `mah`. `overflow: hidden` lets a flex-shrunk inner
27+
// ScrollArea take over scrolling when content exceeds the cap, instead
28+
// of the whole card bleeding past the viewport.
29+
return {
30+
root: {
31+
backgroundColor: "var(--inspector-surface-card)",
32+
overflow: "hidden",
33+
},
34+
};
35+
}
2336
return {
2437
root: { backgroundColor: "var(--inspector-surface-card)" },
2538
};

0 commit comments

Comments
 (0)