Normalize screen layouts: extract control groups, unify sidebar widths, and refactor PromptsScreen

- Extract ToolControls group from ToolsScreen (search, list, list-changed indicator)
- Extract PromptControls and PromptListItem groups for PromptsScreen sidebar
- Refactor PromptsScreen to sidebar + detail layout (matching ResourcesScreen pattern)
- Refactor PromptArgumentsForm to accept a single prompt instead of a dropdown
- Normalize all screen sidebar widths to 340px (Tools, Resources, Prompts, Logs, Tasks, History)
- Convert ToolsScreen from Grid-only to sidebar + Grid layout
- Replace LogControls checkboxes with full-width toggle buttons
- Move Clear/Export buttons from LogControls to LogStreamPanel toolbar
- Make PromptArgumentsForm "Get Prompt" button right-aligned and compact
- Update all affected stories and ConnectedView

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-03-31 17:54:20 -04:00
commit a6724626b7
6 changed files with 627 additions and 233 deletions

View file

@ -19,7 +19,7 @@ const ScreenLayout = Flex.withProps({
});
const Sidebar = Stack.withProps({
w: 280,
w: 340,
flex: "0 0 auto",
});

View file

@ -34,7 +34,7 @@ const ScreenLayout = Flex.withProps({
});
const Sidebar = Stack.withProps({
w: 280,
w: 340,
flex: "0 0 auto",
});
@ -82,8 +82,6 @@ export function LoggingScreen({
onFilterChange={setFilterText}
onToggleLevel={handleToggleLevel}
onToggleAllLevels={handleToggleAllLevels}
onClear={onClear}
onExport={onExport}
/>
</SidebarCard>
</Sidebar>
@ -94,6 +92,8 @@ export function LoggingScreen({
autoScroll={autoScroll}
onToggleAutoScroll={onToggleAutoScroll}
onCopyAll={onCopyAll}
onClear={onClear}
onExport={onExport}
/>
</ScreenLayout>
);

View file

@ -1,69 +1,90 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { PromptsScreen } from "./PromptsScreen";
import type { PromptItem, SelectedPrompt } from "./PromptsScreen";
const meta: Meta<typeof PromptsScreen> = {
title: "Screens/PromptsScreen",
component: PromptsScreen,
parameters: { layout: "fullscreen" },
args: {
searchText: "",
onSearchChange: fn(),
onRefreshList: fn(),
onSelectPrompt: fn(),
onArgumentChange: fn(),
onGetPrompt: fn(),
listChanged: false,
},
};
export default meta;
type Story = StoryObj<typeof PromptsScreen>;
const samplePrompts: PromptItem[] = [
{
name: "summarize",
description: "Summarize the given text into key points",
selected: false,
},
{
name: "translate",
description: "Translate text from one language to another",
selected: false,
},
{
name: "code-review",
description: "Review code for issues",
selected: false,
},
{
name: "analyze",
description: "Analyze sentiment and tone of the text",
selected: false,
},
{ name: "refactor", selected: false },
];
const selectedTranslate: SelectedPrompt = {
name: "translate",
description: "Translate text from one language to another",
arguments: [
{ name: "text", required: true, description: "The text to translate" },
{
name: "targetLanguage",
required: true,
description: "Target language code",
},
],
argumentValues: {},
};
export const NoSelection: Story = {
args: {
promptForm: {
prompts: [
{ name: "summarize", description: "Summarize a document" },
{
name: "translate",
description: "Translate text to another language",
},
{ name: "code-review", description: "Review code for issues" },
],
selectedPrompt: undefined,
arguments: [],
argumentValues: {},
onSelectPrompt: fn(),
onArgumentChange: fn(),
onGetPrompt: fn(),
},
messages: undefined,
listChanged: false,
prompts: samplePrompts,
},
};
export const PromptSelected: Story = {
args: {
prompts: samplePrompts.map((p) =>
p.name === "translate" ? { ...p, selected: true } : p,
),
selectedPrompt: selectedTranslate,
},
};
export const WithResult: Story = {
args: {
promptForm: {
prompts: [
{ name: "summarize", description: "Summarize a document" },
{
name: "translate",
description: "Translate text to another language",
},
{ name: "code-review", description: "Review code for issues" },
],
selectedPrompt: "translate",
arguments: [
{ name: "text", required: true, description: "The text to translate" },
{
name: "targetLanguage",
required: true,
description: "Target language code",
},
],
prompts: samplePrompts.map((p) =>
p.name === "translate" ? { ...p, selected: true } : p,
),
selectedPrompt: {
...selectedTranslate,
argumentValues: {
text: "Hello, how are you?",
targetLanguage: "es",
},
onSelectPrompt: fn(),
onArgumentChange: fn(),
onGetPrompt: fn(),
},
messages: {
onCopyAll: fn(),
@ -79,6 +100,18 @@ export const WithResult: Story = {
},
],
},
listChanged: false,
},
};
export const ListChanged: Story = {
args: {
prompts: samplePrompts,
listChanged: true,
},
};
export const Empty: Story = {
args: {
prompts: [],
},
};

View file

@ -1,26 +1,56 @@
import { Card, Container, Grid, Stack, Text } from "@mantine/core";
import { Card, Flex, ScrollArea, Stack, Text } from "@mantine/core";
import { PromptControls } from "../../groups/PromptControls/PromptControls";
import { PromptArgumentsForm } from "../../groups/PromptArgumentsForm/PromptArgumentsForm";
import { PromptMessagesDisplay } from "../../groups/PromptMessagesDisplay/PromptMessagesDisplay";
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import type { PromptArgumentsFormProps } from "../../groups/PromptArgumentsForm/PromptArgumentsForm";
import type { PromptArgument } from "../../groups/PromptArgumentsForm/PromptArgumentsForm";
import type { PromptMessagesDisplayProps } from "../../groups/PromptMessagesDisplay/PromptMessagesDisplay";
export interface PromptsScreenProps {
promptForm: PromptArgumentsFormProps;
messages?: PromptMessagesDisplayProps;
listChanged: boolean;
onRefreshList: () => void;
export interface PromptItem {
name: string;
description?: string;
selected: boolean;
}
const PageContainer = Container.withProps({
size: "xl",
py: "xl",
export interface SelectedPrompt {
name: string;
description?: string;
arguments: PromptArgument[];
argumentValues: Record<string, string>;
}
export interface PromptsScreenProps {
prompts: PromptItem[];
selectedPrompt?: SelectedPrompt;
messages?: PromptMessagesDisplayProps;
listChanged: boolean;
searchText: string;
onSearchChange: (text: string) => void;
onRefreshList: () => void;
onSelectPrompt: (name: string) => void;
onArgumentChange: (name: string, value: string) => void;
onGetPrompt: () => void;
}
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100vh - var(--app-shell-header-height, 0px))",
gap: "xl",
p: "xl",
});
const FullHeightCard = Card.withProps({
const Sidebar = Stack.withProps({
w: 340,
flex: "0 0 auto",
});
const SidebarCard = Card.withProps({
withBorder: true,
padding: "lg",
});
const DetailCard = Card.withProps({
withBorder: true,
padding: "lg",
h: "100%",
});
const EmptyState = Text.withProps({
@ -30,37 +60,62 @@ const EmptyState = Text.withProps({
});
export function PromptsScreen({
promptForm,
prompts,
selectedPrompt,
messages,
listChanged,
searchText,
onSearchChange,
onRefreshList,
onSelectPrompt,
onArgumentChange,
onGetPrompt,
}: PromptsScreenProps) {
return (
<PageContainer>
<Grid align="stretch">
<Grid.Col span={5}>
<FullHeightCard>
<Stack gap="md">
<ListChangedIndicator
visible={listChanged}
onRefresh={onRefreshList}
/>
<PromptArgumentsForm {...promptForm} />
</Stack>
</FullHeightCard>
</Grid.Col>
<Grid.Col span={7}>
<FullHeightCard>
{messages ? (
<PromptMessagesDisplay {...messages} />
) : (
<EmptyState>
Select a prompt and click Get Prompt to see messages
</EmptyState>
)}
</FullHeightCard>
</Grid.Col>
</Grid>
</PageContainer>
<ScreenLayout>
<Sidebar>
<SidebarCard>
<PromptControls
prompts={prompts}
listChanged={listChanged}
searchText={searchText}
onSearchChange={onSearchChange}
onRefreshList={onRefreshList}
onSelectPrompt={onSelectPrompt}
/>
</SidebarCard>
</Sidebar>
<ScrollArea.Autosize
flex={1}
mah="calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2)"
>
<Stack gap="md">
{selectedPrompt ? (
<>
<DetailCard>
<PromptArgumentsForm
name={selectedPrompt.name}
description={selectedPrompt.description}
arguments={selectedPrompt.arguments}
argumentValues={selectedPrompt.argumentValues}
onArgumentChange={onArgumentChange}
onGetPrompt={onGetPrompt}
/>
</DetailCard>
{messages && (
<DetailCard>
<PromptMessagesDisplay {...messages} />
</DetailCard>
)}
</>
) : (
<DetailCard>
<EmptyState>Select a prompt to view details</EmptyState>
</DetailCard>
)}
</Stack>
</ScrollArea.Autosize>
</ScreenLayout>
);
}

View file

@ -18,7 +18,7 @@ const ScreenLayout = Flex.withProps({
});
const Sidebar = Stack.withProps({
w: 280,
w: 340,
flex: "0 0 auto",
});

View file

@ -1,14 +1,5 @@
import {
Card,
Container,
Grid,
Stack,
Text,
TextInput,
Title,
} from "@mantine/core";
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import { ToolListItem } from "../../groups/ToolListItem/ToolListItem";
import { Card, Flex, Grid, Stack, Text } from "@mantine/core";
import { ToolControls } from "../../groups/ToolControls/ToolControls";
import { ToolDetailPanel } from "../../groups/ToolDetailPanel/ToolDetailPanel";
import { ResultPanel } from "../../groups/ResultPanel/ResultPanel";
import type { ToolListItemProps } from "../../groups/ToolListItem/ToolListItem";
@ -26,9 +17,21 @@ export interface ToolsScreenProps {
onSelectTool: (name: string) => void;
}
const PageContainer = Container.withProps({
size: "xl",
py: "xl",
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100vh - var(--app-shell-header-height, 0px))",
gap: "xl",
p: "xl",
});
const Sidebar = Stack.withProps({
w: 340,
flex: "0 0 auto",
});
const SidebarCard = Card.withProps({
withBorder: true,
padding: "lg",
});
const FullHeightCard = Card.withProps({
@ -53,40 +56,23 @@ export function ToolsScreen({
onRefreshList,
onSelectTool,
}: ToolsScreenProps) {
const filteredTools = searchText
? tools.filter((tool) =>
tool.name.toLowerCase().includes(searchText.toLowerCase()),
)
: tools;
return (
<PageContainer>
<Grid align="stretch">
<Grid.Col span={3}>
<FullHeightCard>
<Stack gap="sm">
<Title order={4}>Tools</Title>
<ListChangedIndicator
visible={listChanged}
onRefresh={onRefreshList}
/>
<TextInput
placeholder="Search tools..."
value={searchText}
onChange={(event) => onSearchChange(event.currentTarget.value)}
/>
{filteredTools.map((tool) => (
<ToolListItem
key={tool.name}
{...tool}
onClick={() => onSelectTool(tool.name)}
/>
))}
</Stack>
</FullHeightCard>
</Grid.Col>
<ScreenLayout>
<Sidebar>
<SidebarCard>
<ToolControls
tools={tools}
listChanged={listChanged}
searchText={searchText}
onSearchChange={onSearchChange}
onRefreshList={onRefreshList}
onSelectTool={onSelectTool}
/>
</SidebarCard>
</Sidebar>
<Grid.Col span={5}>
<Grid align="stretch" flex={1}>
<Grid.Col span={7}>
<FullHeightCard>
{selectedTool ? (
<ToolDetailPanel {...selectedTool} />
@ -96,7 +82,7 @@ export function ToolsScreen({
</FullHeightCard>
</Grid.Col>
<Grid.Col span={4}>
<Grid.Col span={5}>
<FullHeightCard>
{result ? (
<ResultPanel {...result} />
@ -106,6 +92,6 @@ export function ToolsScreen({
</FullHeightCard>
</Grid.Col>
</Grid>
</PageContainer>
</ScreenLayout>
);
}