Skip to content

Commit a77027b

Browse files
cliffhallclaude
andcommitted
Contain resource preview: sticky header/footer, scrollable body
Restructure ResourcePreviewPanel into a fixed-height column with the resource title + URI pinned to the top and the timestamp / annotations / subscribe-refresh actions pinned to the bottom. The content viewer area in the middle now owns its own ScrollArea, so a long markdown body scrolls within the panel instead of pushing the subscribe button below the viewport. In ResourcesScreen, the selectedResource branch (and the template branch's right pane) now hosts the panel inside a PreviewPane Flex column with the screen's max-height, and renderReadState wraps in a FillDetailCard sized to fill that column. The legacy outer ScrollArea.Autosize is removed for these panes since scrolling is internal now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ee4ea54 commit a77027b

2 files changed

Lines changed: 90 additions & 28 deletions

File tree

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

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { Button, Flex, Group, Stack, Text, Title } from "@mantine/core";
1+
import {
2+
Button,
3+
Flex,
4+
Group,
5+
ScrollArea,
6+
Stack,
7+
Text,
8+
Title,
9+
} from "@mantine/core";
210
import type {
311
BlobResourceContents,
412
ContentBlock,
@@ -88,6 +96,31 @@ const ActionGroup = Group.withProps({
8896

8997
const Spacer = Flex.withProps({});
9098

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+
const PanelStack = Stack.withProps({
103+
gap: "md",
104+
h: "100%",
105+
flex: 1,
106+
miw: 0,
107+
});
108+
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.
112+
const ContentScroll = ScrollArea.withProps({
113+
flex: 1,
114+
miw: 0,
115+
type: "auto",
116+
scrollbars: "y",
117+
offsetScrollbars: true,
118+
});
119+
120+
const ContentStack = Stack.withProps({
121+
gap: "md",
122+
});
123+
91124
// Infer a markdown MIME from the URI when the server didn't supply one.
92125
// MCP servers often return `text/plain` (or omit mimeType entirely) for
93126
// `.md` resources; the file extension is the most reliable fallback signal.
@@ -125,22 +158,26 @@ export function ResourcePreviewPanel({
125158
const mimeType = effectiveMime(contents[0]?.mimeType, resource);
126159

127160
return (
128-
<Stack gap="md">
161+
<PanelStack>
129162
<HeaderRow>
130163
<Title order={4}>Resource</Title>
131164
<UriGroup>
132165
<UriText>{uri}</UriText>
133166
<CopyButton value={uri} />
134167
</UriGroup>
135168
</HeaderRow>
136-
{contents.map((item, index) => (
137-
<ContentViewer
138-
key={index}
139-
block={toContentBlock(item)}
140-
mimeType={effectiveMime(item.mimeType, resource)}
141-
copyable
142-
/>
143-
))}
169+
<ContentScroll>
170+
<ContentStack>
171+
{contents.map((item, index) => (
172+
<ContentViewer
173+
key={index}
174+
block={toContentBlock(item)}
175+
mimeType={effectiveMime(item.mimeType, resource)}
176+
copyable
177+
/>
178+
))}
179+
</ContentStack>
180+
</ContentScroll>
144181
<MetaRow>
145182
{lastUpdated ? (
146183
<TimestampText>{formatLastUpdated(lastUpdated)}</TimestampText>
@@ -168,6 +205,6 @@ export function ResourcePreviewPanel({
168205
/>
169206
</ActionGroup>
170207
</FooterRow>
171-
</Stack>
208+
</PanelStack>
172209
);
173210
}

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

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,24 @@ const DetailCard = Card.withProps({
6262
padding: "lg",
6363
});
6464

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({
69+
withBorder: true,
70+
padding: "lg",
71+
h: "100%",
72+
});
73+
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.
77+
const PreviewPane = Flex.withProps({
78+
flex: 1,
79+
miw: 0,
80+
direction: "column",
81+
});
82+
6583
const EmptyState = Text.withProps({
6684
c: "dimmed",
6785
ta: "center",
@@ -125,28 +143,28 @@ export function ResourcesScreen({
125143

126144
if (readState.status === "pending") {
127145
return (
128-
<DetailCard>
146+
<FillDetailCard>
129147
<Stack align="center" py="xl">
130148
<Loader size="sm" />
131149
<Text c="dimmed">Reading resource...</Text>
132150
</Stack>
133-
</DetailCard>
151+
</FillDetailCard>
134152
);
135153
}
136154

137155
if (readState.status === "error") {
138156
return (
139-
<DetailCard>
157+
<FillDetailCard>
140158
<Alert color="red" variant="light" title="Read Error">
141159
{readState.error ?? "Failed to read resource"}
142160
</Alert>
143-
</DetailCard>
161+
</FillDetailCard>
144162
);
145163
}
146164

147165
if (readState.result && readResource) {
148166
return (
149-
<DetailCard>
167+
<FillDetailCard>
150168
<ResourcePreviewPanel
151169
resource={readResource}
152170
contents={readState.result.contents}
@@ -156,7 +174,7 @@ export function ResourcesScreen({
156174
onSubscribe={() => onSubscribeResource(readResource.uri)}
157175
onUnsubscribe={() => onUnsubscribeResource(readResource.uri)}
158176
/>
159-
</DetailCard>
177+
</FillDetailCard>
160178
);
161179
}
162180

@@ -183,7 +201,14 @@ export function ResourcesScreen({
183201
</Sidebar>
184202

185203
{selectedTemplate ? (
186-
<Group flex={1} miw={0} gap="md" align="flex-start" wrap="nowrap">
204+
<Group
205+
flex={1}
206+
miw={0}
207+
mah={SCROLL_MAX_HEIGHT}
208+
gap="md"
209+
align="stretch"
210+
wrap="nowrap"
211+
>
187212
<ScrollArea.Autosize flex={1} miw={0} mah={SCROLL_MAX_HEIGHT}>
188213
<DetailCard>
189214
<ResourceTemplatePanel
@@ -192,21 +217,21 @@ export function ResourcesScreen({
192217
/>
193218
</DetailCard>
194219
</ScrollArea.Autosize>
195-
<ScrollArea.Autosize flex={1} miw={0} mah={SCROLL_MAX_HEIGHT}>
220+
<PreviewPane>
196221
{renderReadState() ?? (
197-
<DetailCard>
222+
<FillDetailCard>
198223
<EmptyState>Enter a URI and click Read to preview</EmptyState>
199-
</DetailCard>
224+
</FillDetailCard>
200225
)}
201-
</ScrollArea.Autosize>
226+
</PreviewPane>
202227
</Group>
203228
) : selectedResource ? (
204-
// miw=0 lets the flex item shrink below its content's intrinsic
205-
// width; without it a single long unwrappable line in the resource
206-
// body would push the panel past the viewport's right edge.
207-
<ScrollArea.Autosize flex={1} miw={0} mah={SCROLL_MAX_HEIGHT}>
208-
{renderReadState()}
209-
</ScrollArea.Autosize>
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.
234+
<PreviewPane mah={SCROLL_MAX_HEIGHT}>{renderReadState()}</PreviewPane>
210235
) : (
211236
<DetailCard flex={1}>
212237
<EmptyState>Select a resource to preview</EmptyState>

0 commit comments

Comments
 (0)