storybook component fixes and improvements

- Add @storybook/addon-actions for action logging in stories
- Add onTestCapability callback to ExperimentalFeaturesPanel
- Use CopyButton overlay in ResourcePreviewPanel and ResultPanel
- Replace includeContext checkbox with badge in SamplingRequestPanel
- Add LongServerName story for ServerCard
- Fix long tool name overflow with truncation and contained Paper variant
- Add optional title prop to ToolListItem and ToolDetailPanel
- Move tool annotations from list to detail panel form
- Vertically center static text in ResourceTemplateInput
- Fix InlineError blank space when no details

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-03-22 20:04:28 -04:00
commit 1da76c53ef
18 changed files with 167 additions and 110 deletions

View file

@ -7,6 +7,7 @@ const config: StorybookConfig = {
], ],
"addons": [ "addons": [
"@chromatic-com/storybook", "@chromatic-com/storybook",
"@storybook/addon-actions",
"@storybook/addon-vitest", "@storybook/addon-vitest",
"@storybook/addon-a11y", "@storybook/addon-a11y",
"@storybook/addon-docs", "@storybook/addon-docs",

View file

@ -20,6 +20,7 @@
"@chromatic-com/storybook": "^5.0.1", "@chromatic-com/storybook": "^5.0.1",
"@eslint/js": "^9.39.4", "@eslint/js": "^9.39.4",
"@storybook/addon-a11y": "^10.2.19", "@storybook/addon-a11y": "^10.2.19",
"@storybook/addon-actions": "^9.0.8",
"@storybook/addon-docs": "^10.2.19", "@storybook/addon-docs": "^10.2.19",
"@storybook/addon-onboarding": "^10.2.19", "@storybook/addon-onboarding": "^10.2.19",
"@storybook/addon-vitest": "^10.2.19", "@storybook/addon-vitest": "^10.2.19",
@ -1695,6 +1696,17 @@
"storybook": "^10.2.19" "storybook": "^10.2.19"
} }
}, },
"node_modules/@storybook/addon-actions": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-9.0.8.tgz",
"integrity": "sha512-LFePu7PPnWN0Il/uoUpmA5T0J0C7d6haJIbg0pXrjxW2MQVSYXE4S4LSUz8fOImltBDV3xAl6tLPYHFj6VcrOA==",
"dev": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
}
},
"node_modules/@storybook/addon-docs": { "node_modules/@storybook/addon-docs": {
"version": "10.2.19", "version": "10.2.19",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.2.19.tgz", "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.2.19.tgz",

View file

@ -23,30 +23,31 @@
"react-dom": "^19.2.4" "react-dom": "^19.2.4"
}, },
"devDependencies": { "devDependencies": {
"@chromatic-com/storybook": "^5.0.1",
"@eslint/js": "^9.39.4", "@eslint/js": "^9.39.4",
"@storybook/addon-a11y": "^10.2.19",
"@storybook/addon-actions": "^9.0.8",
"@storybook/addon-docs": "^10.2.19",
"@storybook/addon-onboarding": "^10.2.19",
"@storybook/addon-vitest": "^10.2.19",
"@storybook/react-vite": "^10.2.19",
"@types/node": "^24.12.0", "@types/node": "^24.12.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.0", "@vitejs/plugin-react": "^6.0.0",
"@vitest/browser-playwright": "^4.1.0",
"@vitest/coverage-v8": "^4.1.0",
"eslint": "^9.39.4", "eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-react-refresh": "^0.5.2",
"eslint-plugin-storybook": "^10.2.19",
"globals": "^17.4.0", "globals": "^17.4.0",
"playwright": "^1.58.2",
"prettier": "^3.8.1",
"storybook": "^10.2.19",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"typescript-eslint": "^8.56.1", "typescript-eslint": "^8.56.1",
"prettier": "^3.8.1",
"vite": "^8.0.0", "vite": "^8.0.0",
"storybook": "^10.2.19", "vitest": "^4.1.0"
"@storybook/react-vite": "^10.2.19",
"@chromatic-com/storybook": "^5.0.1",
"@storybook/addon-vitest": "^10.2.19",
"@storybook/addon-a11y": "^10.2.19",
"@storybook/addon-docs": "^10.2.19",
"@storybook/addon-onboarding": "^10.2.19",
"eslint-plugin-storybook": "^10.2.19",
"vitest": "^4.1.0",
"playwright": "^1.58.2",
"@vitest/browser-playwright": "^4.1.0",
"@vitest/coverage-v8": "^4.1.0"
} }
} }

View file

@ -13,6 +13,7 @@ const meta: Meta<typeof ExperimentalFeaturesPanel> = {
onRemoveHeader: fn(), onRemoveHeader: fn(),
onHeaderChange: fn(), onHeaderChange: fn(),
onCopyResponse: fn(), onCopyResponse: fn(),
onTestCapability: fn(),
clientCapabilities: [ clientCapabilities: [
{ name: "experimental/customSampling", enabled: false }, { name: "experimental/customSampling", enabled: false },
{ name: "experimental/batchRequests", enabled: true }, { name: "experimental/batchRequests", enabled: true },

View file

@ -51,6 +51,7 @@ export interface ExperimentalFeaturesPanelProps {
onRemoveHeader: (index: number) => void; onRemoveHeader: (index: number) => void;
onHeaderChange: (index: number, key: string, value: string) => void; onHeaderChange: (index: number, key: string, value: string) => void;
onCopyResponse: () => void; onCopyResponse: () => void;
onTestCapability: (name: string) => void;
} }
export function ExperimentalFeaturesPanel({ export function ExperimentalFeaturesPanel({
@ -67,6 +68,7 @@ export function ExperimentalFeaturesPanel({
onRemoveHeader, onRemoveHeader,
onHeaderChange, onHeaderChange,
onCopyResponse, onCopyResponse,
onTestCapability,
}: ExperimentalFeaturesPanelProps) { }: ExperimentalFeaturesPanelProps) {
return ( return (
<Stack gap="md"> <Stack gap="md">
@ -94,7 +96,11 @@ export function ExperimentalFeaturesPanel({
</Text> </Text>
)} )}
<Group> <Group>
<Button size="xs" variant="light"> <Button
size="xs"
variant="light"
onClick={() => onTestCapability(cap.name)}
>
Test → Test →
</Button> </Button>
</Group> </Group>

View file

@ -6,7 +6,6 @@ const meta: Meta<typeof ResourcePreviewPanel> = {
title: "Molecules/ResourcePreviewPanel", title: "Molecules/ResourcePreviewPanel",
component: ResourcePreviewPanel, component: ResourcePreviewPanel,
args: { args: {
onCopy: fn(),
onSubscribe: fn(), onSubscribe: fn(),
onUnsubscribe: fn(), onUnsubscribe: fn(),
}, },

View file

@ -8,7 +8,6 @@ export interface ResourcePreviewPanelProps {
content: string; content: string;
lastUpdated?: string; lastUpdated?: string;
isSubscribed: boolean; isSubscribed: boolean;
onCopy: () => void;
onSubscribe: () => void; onSubscribe: () => void;
onUnsubscribe: () => void; onUnsubscribe: () => void;
} }
@ -26,7 +25,6 @@ export function ResourcePreviewPanel({
content, content,
lastUpdated, lastUpdated,
isSubscribed, isSubscribed,
onCopy,
onSubscribe, onSubscribe,
onUnsubscribe, onUnsubscribe,
}: ResourcePreviewPanelProps) { }: ResourcePreviewPanelProps) {
@ -61,11 +59,9 @@ export function ResourcePreviewPanel({
type={resolveContentType(mimeType)} type={resolveContentType(mimeType)}
content={content} content={content}
mimeType={mimeType} mimeType={mimeType}
copyable
/> />
<Group> <Group>
<Button variant="light" size="sm" onClick={onCopy}>
Copy
</Button>
<Button <Button
variant="light" variant="light"
size="sm" size="sm"

View file

@ -48,7 +48,7 @@ export function ResourceTemplateInput({
<Group gap="xs" align="flex-end"> <Group gap="xs" align="flex-end">
{parts.map((part, index) => {parts.map((part, index) =>
part.type === "static" ? ( part.type === "static" ? (
<Text key={index} size="sm"> <Text key={index} size="sm" pb={6}>
{part.value} {part.value}
</Text> </Text>
) : ( ) : (

View file

@ -6,7 +6,6 @@ const meta: Meta<typeof ResultPanel> = {
title: "Molecules/ResultPanel", title: "Molecules/ResultPanel",
component: ResultPanel, component: ResultPanel,
args: { args: {
onCopy: fn(),
onClear: fn(), onClear: fn(),
}, },
}; };

View file

@ -10,11 +10,10 @@ export interface ResultContentItem {
export interface ResultPanelProps { export interface ResultPanelProps {
content: ResultContentItem[]; content: ResultContentItem[];
onCopy: () => void;
onClear: () => void; onClear: () => void;
} }
export function ResultPanel({ content, onCopy, onClear }: ResultPanelProps) { export function ResultPanel({ content, onClear }: ResultPanelProps) {
return ( return (
<Stack> <Stack>
<Title order={4}>Results</Title> <Title order={4}>Results</Title>
@ -29,6 +28,7 @@ export function ResultPanel({ content, onCopy, onClear }: ResultPanelProps) {
key={index} key={index}
type={item.type === "text" ? "text" : "json"} type={item.type === "text" ? "text" : "json"}
content={item.text} content={item.text}
copyable
/> />
); );
} }
@ -47,9 +47,6 @@ export function ResultPanel({ content, onCopy, onClear }: ResultPanelProps) {
</> </>
)} )}
<Group> <Group>
<Button variant="light" size="sm" onClick={onCopy}>
Copy
</Button>
<Button variant="light" size="sm" onClick={onClear}> <Button variant="light" size="sm" onClick={onClear}>
Clear Clear
</Button> </Button>

View file

@ -1,7 +1,6 @@
import { import {
Badge, Badge,
Button, Button,
Checkbox,
Divider, Divider,
Group, Group,
Paper, Paper,
@ -134,11 +133,10 @@ export function SamplingRequestPanel({
</Text> </Text>
{includeContext && ( {includeContext && (
<Checkbox <Group gap="xs">
label={`Include Context: ${includeContext}`} <Text size="sm">Include Context:</Text>
checked <Badge>{includeContext}</Badge>
readOnly </Group>
/>
)} )}
{tools && tools.length > 0 && ( {tools && tools.length > 0 && (

View file

@ -89,6 +89,19 @@ export const WithClientFeatures: Story = {
}, },
}; };
export const LongServerName: Story = {
args: {
name: "my-organization-super-long-experimental-mcp-server-with-many-features-v2",
version: "1.0.0-beta.42",
transport: "stdio",
connectionMode: "Subprocess",
command:
"npx -y @my-organization/super-long-experimental-mcp-server-with-many-features-v2",
status: "connected",
canTestClientFeatures: true,
},
};
export const HttpDirect: Story = { export const HttpDirect: Story = {
args: { args: {
name: "Remote API Server", name: "Remote API Server",

View file

@ -1,4 +1,4 @@
import { Button, Divider, Group, Stack, Text, Title } from "@mantine/core"; import { Button, Divider, Group, Stack, Text } from "@mantine/core";
import { AnnotationBadge } from "../../atoms/AnnotationBadge/AnnotationBadge"; import { AnnotationBadge } from "../../atoms/AnnotationBadge/AnnotationBadge";
import { ProgressDisplay } from "../../atoms/ProgressDisplay/ProgressDisplay"; import { ProgressDisplay } from "../../atoms/ProgressDisplay/ProgressDisplay";
import { SchemaForm } from "../SchemaForm/SchemaForm"; import { SchemaForm } from "../SchemaForm/SchemaForm";
@ -14,6 +14,7 @@ export interface ToolAnnotations {
export interface ToolDetailPanelProps { export interface ToolDetailPanelProps {
name: string; name: string;
title?: string;
description?: string; description?: string;
annotations?: ToolAnnotations; annotations?: ToolAnnotations;
schema: JsonSchema; schema: JsonSchema;
@ -27,6 +28,7 @@ export interface ToolDetailPanelProps {
export function ToolDetailPanel({ export function ToolDetailPanel({
name, name,
title,
description, description,
annotations, annotations,
schema, schema,
@ -46,8 +48,39 @@ export function ToolDetailPanel({
annotations.hints); annotations.hints);
return ( return (
<Stack gap="md"> <Stack gap="md" miw={0}>
<Title order={4}>Tool: {name}</Title> <Text fw={700} size="lg" truncate="end">
{title ?? name}
</Text>
{title && (
<Text size="sm" c="dimmed" truncate="end">
{name}
</Text>
)}
{hasAnnotations && (
<Group gap="xs">
{annotations.audience && (
<AnnotationBadge
label={annotations.audience}
variant="audience"
/>
)}
{annotations.readOnly && (
<AnnotationBadge label="read-only" variant="readOnly" />
)}
{annotations.destructive && (
<AnnotationBadge label="destructive" variant="destructive" />
)}
{annotations.longRunning && (
<AnnotationBadge label="long-run" variant="longRun" />
)}
{annotations.hints && (
<Text size="xs" c="dimmed" fs="italic">
{annotations.hints}
</Text>
)}
</Group>
)}
{description && ( {description && (
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
@ -55,34 +88,6 @@ export function ToolDetailPanel({
</Text> </Text>
)} )}
{hasAnnotations && (
<>
<Text size="sm">Annotations:</Text>
<Group gap="xs">
{annotations.audience && (
<AnnotationBadge
label={annotations.audience}
variant="audience"
/>
)}
{annotations.readOnly && (
<AnnotationBadge label="read-only" variant="readOnly" />
)}
{annotations.destructive && (
<AnnotationBadge label="destructive" variant="destructive" />
)}
{annotations.longRunning && (
<AnnotationBadge label="long-run" variant="longRun" />
)}
{annotations.hints && (
<Text size="xs" c="dimmed" fs="italic">
{annotations.hints}
</Text>
)}
</Group>
</>
)}
<Divider /> <Divider />
<SchemaForm <SchemaForm

View file

@ -27,27 +27,11 @@ export const Selected: Story = {
}, },
}; };
export const WithAnnotations: Story = { export const WithTitle: Story = {
args: { args: {
name: "get_weather", name: "get_weather",
title: "Get Weather",
selected: false, selected: false,
annotations: [
{ label: "user", variant: "audience" },
{ label: "read-only", variant: "readOnly" },
],
},
};
export const MultipleAnnotations: Story = {
args: {
name: "update_database",
selected: false,
annotations: [
{ label: "user", variant: "audience" },
{ label: "read-only", variant: "readOnly" },
{ label: "destructive", variant: "destructive" },
{ label: "long-run", variant: "longRun" },
],
}, },
}; };

View file

@ -1,22 +1,15 @@
import { Group, Stack, Text, UnstyledButton } from "@mantine/core"; import { Stack, Text, UnstyledButton } from "@mantine/core";
import { AnnotationBadge } from "../../atoms/AnnotationBadge/AnnotationBadge";
import type { AnnotationVariant } from "../../atoms/AnnotationBadge/AnnotationBadge";
export interface ToolAnnotation {
label: string;
variant?: AnnotationVariant;
}
export interface ToolListItemProps { export interface ToolListItemProps {
name: string; name: string;
annotations?: ToolAnnotation[]; title?: string;
selected: boolean; selected: boolean;
onClick: () => void; onClick: () => void;
} }
export function ToolListItem({ export function ToolListItem({
name, name,
annotations, title,
selected, selected,
onClick, onClick,
}: ToolListItemProps) { }: ToolListItemProps) {
@ -28,18 +21,14 @@ export function ToolListItem({
style={{ borderRadius: "var(--mantine-radius-md)" }} style={{ borderRadius: "var(--mantine-radius-md)" }}
onClick={onClick} onClick={onClick}
> >
<Stack gap="xs"> <Stack gap={2}>
<Text fw={500}>{name}</Text> <Text fw={500} truncate>
{annotations && annotations.length > 0 && ( {title ?? name}
<Group gap="xs"> </Text>
{annotations.map((annotation) => ( {title && (
<AnnotationBadge <Text size="xs" c="dimmed" truncate>
key={annotation.label} {name}
label={annotation.label} </Text>
variant={annotation.variant}
/>
))}
</Group>
)} )}
</Stack> </Stack>
</UnstyledButton> </UnstyledButton>

View file

@ -21,23 +21,26 @@ export default meta;
type Story = StoryObj<typeof ToolsScreen>; type Story = StoryObj<typeof ToolsScreen>;
const sampleTools: ToolListItemProps[] = [ const sampleTools: ToolListItemProps[] = [
{ name: "send_message", selected: false, onClick: fn() }, {
name: "send_message",
title: "Send Message",
selected: false,
onClick: fn(),
},
{ {
name: "create_record", name: "create_record",
annotations: [{ label: "read-only" }], title: "Create Record",
selected: false, selected: false,
onClick: fn(), onClick: fn(),
}, },
{ {
name: "delete_records", name: "delete_records",
annotations: [{ label: "destructive", variant: "destructive" }],
selected: false, selected: false,
onClick: fn(), onClick: fn(),
}, },
{ name: "list_users", selected: false, onClick: fn() }, { name: "list_users", selected: false, onClick: fn() },
{ {
name: "batch_process", name: "batch_process",
annotations: [{ label: "long-run", variant: "longRun" }],
selected: false, selected: false,
onClick: fn(), onClick: fn(),
}, },
@ -45,6 +48,7 @@ const sampleTools: ToolListItemProps[] = [
const selectedToolData: ToolDetailPanelProps = { const selectedToolData: ToolDetailPanelProps = {
name: "create_record", name: "create_record",
title: "Create Record",
description: "Creates a new record with the given parameters", description: "Creates a new record with the given parameters",
schema: { schema: {
type: "object", type: "object",
@ -79,7 +83,6 @@ const resultData: ResultPanelProps = {
), ),
}, },
], ],
onCopy: fn(),
onClear: fn(), onClear: fn(),
}; };
@ -111,6 +114,51 @@ export const WithResult: Story = {
}, },
}; };
export const LongToolName: Story = {
args: {
tools: [
...sampleTools,
{
name: "organization_internal_database_complex_multi_table_join_query_with_aggregation_and_filtering",
selected: true,
onClick: fn(),
},
],
selectedTool: {
name: "organization_internal_database_complex_multi_table_join_query_with_aggregation_and_filtering",
annotations: {
readOnly: true,
longRunning: true,
},
description:
"Executes a complex multi-table join query across the organization's internal database with support for aggregation functions, nested filtering, and pagination of large result sets.",
schema: {
type: "object",
properties: {
primary_table_name: {
type: "string",
description: "The main table to query from",
},
join_configuration: {
type: "string",
description: "JSON configuration for table joins",
},
aggregation_functions: {
type: "string",
description: "Comma-separated list of aggregation functions to apply",
},
},
required: ["primary_table_name"],
},
formValues: {},
isExecuting: false,
onFormChange: fn(),
onExecute: fn(),
onCancel: fn(),
},
},
};
export const WithListChanged: Story = { export const WithListChanged: Story = {
args: { args: {
tools: sampleTools, tools: sampleTools,

View file

@ -60,7 +60,7 @@ export function ToolsScreen({
</Grid.Col> </Grid.Col>
<Grid.Col span={5}> <Grid.Col span={5}>
<Paper withBorder p="md"> <Paper withBorder p="md" variant="contained">
{selectedTool ? ( {selectedTool ? (
<ToolDetailPanel {...selectedTool} /> <ToolDetailPanel {...selectedTool} />
) : ( ) : (

View file

@ -17,6 +17,14 @@ export const ThemePaper = Paper.extend({
}, },
}; };
} }
if (props.variant === "contained") {
return {
root: {
overflow: "hidden",
minWidth: 0,
},
};
}
return { root: {} }; return { root: {} };
}, },
}); });