feat: Add HomeLayout and ConnectedLayout components with Storybook stories

- HomeLayout: AppShell with header (title + theme toggle) for disconnected state
- ConnectedLayout: AppShell with server name, status indicator, tab navigation,
  theme toggle, and disconnect button for connected state

All 6 phases of the Storybook component plan are now complete:
12 atoms, 25 molecules, 7 organisms, 2 layouts = 46 components total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-03-17 10:47:52 -04:00
commit d53c3d338f
4 changed files with 211 additions and 0 deletions

View file

@ -0,0 +1,53 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import { Text } from '@mantine/core';
import { ConnectedLayout } from './ConnectedLayout';
const allTabs = ['Tools', 'Resources', 'Prompts', 'Logs', 'Tasks', 'History'];
const meta: Meta<typeof ConnectedLayout> = {
title: 'Layouts/ConnectedLayout',
component: ConnectedLayout,
parameters: { layout: 'fullscreen' },
args: {
serverName: 'my-mcp-server',
status: 'connected',
latencyMs: 23,
availableTabs: allTabs,
activeTab: 'Tools',
onTabChange: fn(),
onDisconnect: fn(),
onToggleTheme: fn(),
},
};
export default meta;
type Story = StoryObj<typeof ConnectedLayout>;
export const ToolsActive: Story = {
args: {
activeTab: 'Tools',
children: <Text>Tools screen content</Text>,
},
};
export const ResourcesActive: Story = {
args: {
activeTab: 'Resources',
children: <Text>Resources screen content</Text>,
},
};
export const LimitedTabs: Story = {
args: {
availableTabs: ['Tools', 'Resources', 'Prompts'],
children: <Text>Tools screen content</Text>,
},
};
export const LongServerName: Story = {
args: {
serverName: 'my-very-long-server-name-that-might-overflow',
children: <Text>Tools screen content</Text>,
},
};

View file

@ -0,0 +1,83 @@
import type { ReactNode } from 'react';
import { ActionIcon, AppShell, Button, Group, Tabs, Text } from '@mantine/core';
import { useComputedColorScheme } from '@mantine/core';
import { StatusIndicator } from '../../atoms/StatusIndicator/StatusIndicator';
export interface ConnectedLayoutProps {
serverName: string;
status: 'connected' | 'connecting' | 'failed';
latencyMs?: number;
activeTab: string;
availableTabs: string[];
onTabChange: (tab: string) => void;
onDisconnect: () => void;
onToggleTheme: () => void;
children: ReactNode;
}
const ServerName = Text.withProps({
fw: 600,
size: 'lg',
});
export function ConnectedLayout({
serverName,
status,
latencyMs,
activeTab,
availableTabs,
onTabChange,
onDisconnect,
onToggleTheme,
children,
}: ConnectedLayoutProps) {
const colorScheme = useComputedColorScheme();
return (
<AppShell header={{ height: 60 }} padding="md">
<AppShell.Header>
<Group h="100%" px="md" justify="space-between" wrap="nowrap">
<Group gap="md" wrap="nowrap">
<ServerName>{serverName}</ServerName>
<StatusIndicator status={status} latencyMs={latencyMs} />
</Group>
<Tabs
value={activeTab}
onChange={(value) => value && onTabChange(value)}
variant="default"
>
<Tabs.List>
{availableTabs.map((tab) => (
<Tabs.Tab key={tab} value={tab}>
{tab}
</Tabs.Tab>
))}
</Tabs.List>
</Tabs>
<Group gap="sm">
<ActionIcon
variant="default"
size="lg"
aria-label="Toggle color scheme"
onClick={onToggleTheme}
>
{colorScheme === 'dark' ? '\u2600' : '\u263E'}
</ActionIcon>
<Button
variant="outline"
color="red"
size="sm"
onClick={onDisconnect}
>
Disconnect
</Button>
</Group>
</Group>
</AppShell.Header>
<AppShell.Main>{children}</AppShell.Main>
</AppShell>
);
}

View file

@ -0,0 +1,45 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import { Container, Paper, Stack, Text } from '@mantine/core';
import { HomeLayout } from './HomeLayout';
const meta: Meta<typeof HomeLayout> = {
title: 'Layouts/HomeLayout',
component: HomeLayout,
parameters: { layout: 'fullscreen' },
args: {
onToggleTheme: fn(),
},
};
export default meta;
type Story = StoryObj<typeof HomeLayout>;
export const Light: Story = {
args: {
children: <Text>Page content goes here</Text>,
},
};
export const WithContent: Story = {
args: {
children: (
<Container>
<Stack>
<Paper p="md" shadow="xs">
<Text fw={500}>Local Dev Server</Text>
<Text size="sm" c="dimmed">npx @modelcontextprotocol/server-filesystem</Text>
</Paper>
<Paper p="md" shadow="xs">
<Text fw={500}>Database Tools</Text>
<Text size="sm" c="dimmed">python -m mcp_server_sqlite</Text>
</Paper>
<Paper p="md" shadow="xs">
<Text fw={500}>Remote API Server</Text>
<Text size="sm" c="dimmed">https://api.example.com/mcp</Text>
</Paper>
</Stack>
</Container>
),
},
};

View file

@ -0,0 +1,30 @@
import type { ReactNode } from 'react';
import { ActionIcon, AppShell, Group, Title, useComputedColorScheme } from '@mantine/core';
export interface HomeLayoutProps {
children: ReactNode;
onToggleTheme: () => void;
}
export function HomeLayout({ children, onToggleTheme }: HomeLayoutProps) {
const colorScheme = useComputedColorScheme();
return (
<AppShell header={{ height: 60 }} padding="md">
<AppShell.Header>
<Group h="100%" px="md" justify="space-between">
<Title order={2}>MCP Inspector</Title>
<ActionIcon
variant="default"
size="lg"
aria-label="Toggle color scheme"
onClick={onToggleTheme}
>
{colorScheme === 'dark' ? '\u2600' : '\u263E'}
</ActionIcon>
</Group>
</AppShell.Header>
<AppShell.Main>{children}</AppShell.Main>
</AppShell>
);
}