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": [
"@chromatic-com/storybook",
"@storybook/addon-actions",
"@storybook/addon-vitest",
"@storybook/addon-a11y",
"@storybook/addon-docs",

View file

@ -20,6 +20,7 @@
"@chromatic-com/storybook": "^5.0.1",
"@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",
@ -1695,6 +1696,17 @@
"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": {
"version": "10.2.19",
"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"
},
"devDependencies": {
"@chromatic-com/storybook": "^5.0.1",
"@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/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.0",
"@vitest/browser-playwright": "^4.1.0",
"@vitest/coverage-v8": "^4.1.0",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"eslint-plugin-storybook": "^10.2.19",
"globals": "^17.4.0",
"playwright": "^1.58.2",
"prettier": "^3.8.1",
"storybook": "^10.2.19",
"typescript": "~5.9.3",
"typescript-eslint": "^8.56.1",
"prettier": "^3.8.1",
"vite": "^8.0.0",
"storybook": "^10.2.19",
"@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"
"vitest": "^4.1.0"
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
import {
Badge,
Button,
Checkbox,
Divider,
Group,
Paper,
@ -134,11 +133,10 @@ export function SamplingRequestPanel({
</Text>
{includeContext && (
<Checkbox
label={`Include Context: ${includeContext}`}
checked
readOnly
/>
<Group gap="xs">
<Text size="sm">Include Context:</Text>
<Badge>{includeContext}</Badge>
</Group>
)}
{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 = {
args: {
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 { ProgressDisplay } from "../../atoms/ProgressDisplay/ProgressDisplay";
import { SchemaForm } from "../SchemaForm/SchemaForm";
@ -14,6 +14,7 @@ export interface ToolAnnotations {
export interface ToolDetailPanelProps {
name: string;
title?: string;
description?: string;
annotations?: ToolAnnotations;
schema: JsonSchema;
@ -27,6 +28,7 @@ export interface ToolDetailPanelProps {
export function ToolDetailPanel({
name,
title,
description,
annotations,
schema,
@ -46,8 +48,39 @@ export function ToolDetailPanel({
annotations.hints);
return (
<Stack gap="md">
<Title order={4}>Tool: {name}</Title>
<Stack gap="md" miw={0}>
<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 && (
<Text size="sm" c="dimmed">
@ -55,34 +88,6 @@ export function ToolDetailPanel({
</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 />
<SchemaForm

View file

@ -27,27 +27,11 @@ export const Selected: Story = {
},
};
export const WithAnnotations: Story = {
export const WithTitle: Story = {
args: {
name: "get_weather",
title: "Get Weather",
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 { AnnotationBadge } from "../../atoms/AnnotationBadge/AnnotationBadge";
import type { AnnotationVariant } from "../../atoms/AnnotationBadge/AnnotationBadge";
export interface ToolAnnotation {
label: string;
variant?: AnnotationVariant;
}
import { Stack, Text, UnstyledButton } from "@mantine/core";
export interface ToolListItemProps {
name: string;
annotations?: ToolAnnotation[];
title?: string;
selected: boolean;
onClick: () => void;
}
export function ToolListItem({
name,
annotations,
title,
selected,
onClick,
}: ToolListItemProps) {
@ -28,18 +21,14 @@ export function ToolListItem({
style={{ borderRadius: "var(--mantine-radius-md)" }}
onClick={onClick}
>
<Stack gap="xs">
<Text fw={500}>{name}</Text>
{annotations && annotations.length > 0 && (
<Group gap="xs">
{annotations.map((annotation) => (
<AnnotationBadge
key={annotation.label}
label={annotation.label}
variant={annotation.variant}
/>
))}
</Group>
<Stack gap={2}>
<Text fw={500} truncate>
{title ?? name}
</Text>
{title && (
<Text size="xs" c="dimmed" truncate>
{name}
</Text>
)}
</Stack>
</UnstyledButton>

View file

@ -21,23 +21,26 @@ export default meta;
type Story = StoryObj<typeof ToolsScreen>;
const sampleTools: ToolListItemProps[] = [
{ name: "send_message", selected: false, onClick: fn() },
{
name: "send_message",
title: "Send Message",
selected: false,
onClick: fn(),
},
{
name: "create_record",
annotations: [{ label: "read-only" }],
title: "Create Record",
selected: false,
onClick: fn(),
},
{
name: "delete_records",
annotations: [{ label: "destructive", variant: "destructive" }],
selected: false,
onClick: fn(),
},
{ name: "list_users", selected: false, onClick: fn() },
{
name: "batch_process",
annotations: [{ label: "long-run", variant: "longRun" }],
selected: false,
onClick: fn(),
},
@ -45,6 +48,7 @@ const sampleTools: ToolListItemProps[] = [
const selectedToolData: ToolDetailPanelProps = {
name: "create_record",
title: "Create Record",
description: "Creates a new record with the given parameters",
schema: {
type: "object",
@ -79,7 +83,6 @@ const resultData: ResultPanelProps = {
),
},
],
onCopy: 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 = {
args: {
tools: sampleTools,

View file

@ -60,7 +60,7 @@ export function ToolsScreen({
</Grid.Col>
<Grid.Col span={5}>
<Paper withBorder p="md">
<Paper withBorder p="md" variant="contained">
{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: {} };
},
});