Add new components, typography, badge contrast, and screen layout refactors

New element components:
- ListToggle: expand/collapse button using react-icons (RiExpandVerticalLine/RiCollapseVerticalLine)
- LogLevelBadge: extracted from LogEntry, handles all 8 log levels with filled variant and dark/light text contrast

New group components:
- ViewHeader: shared header bar for ConnectedView and UnconnectedView with discriminated union on `connected` prop; MCP logo links to modelcontextprotocol.io
- ServerListControls: combines ListToggle and AddServerMenu, hides toggle when no servers
- LogStreamPanel: extracted from LoggingScreen with scroll containment, filtering by visible levels and search text
- HistoryControls: vertical sidebar with search, method filter, export, and clear
- HistoryListPanel: extracted from HistoryScreen with ListToggle compact mode, filtering by method and search text

Badge contrast improvements:
- AnnotationBadge: filled variant for both dark/light modes; black text in dark, white text in light
- LogLevelBadge: same contrast approach; bold weight for alert/emergency levels
- Badge theme default weight reduced from 700 to 600

Typography:
- Added Fredoka (300-700) as body and heading font
- Added Roboto Mono as monospace font
- Google Fonts loaded in index.html and Storybook previewHead
- Heading weight set to 600

Screen layout refactors:
- LoggingScreen: owns filterText and visibleLevels state; passes to LogControls and LogStreamPanel; removed controls prop in favor of flat props
- HistoryScreen: owns searchText and methodFilter state; passes to HistoryControls and HistoryListPanel for filtering
- Both screens use new ScreenLayout (Flex variant="screen") and Sidebar subcomponents
- Added Flex theme with "screen" variant (overflow: hidden)
- Added Paper theme "panel" variant (flex column, overflow hidden)

Component enhancements:
- ServerCard: added compact prop showing only header row
- HistoryEntry: renamed isExpanded to isListExpanded; internal useState syncs via useEffect; individual entries toggle independently
- LogControls: Select All / Deselect All toggle for level checkboxes; nowrap on level selector group
- ConnectionToggle: LoadingOverlay with rounded corners for connecting state
- ConnectedView: ThemeToggle ActionIcon matched to DisconnectButton height (36px) and border color (outline variant)
- Storybook preview: fullscreen layout wrapper with 100vh overflow hidden

New stories:
- UnconnectedView: Connecting and ManyServers (12 servers for scroll testing)
- ServerCard: CompactConnected and CompactDisconnected
- All new components have corresponding stories

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-03-28 20:38:04 -04:00
commit 961dc283fc
2 changed files with 1602 additions and 790 deletions

View file

@ -6,7 +6,6 @@ const meta: Meta<typeof HistoryEntry> = {
title: "Groups/HistoryEntry",
component: HistoryEntry,
args: {
onToggleExpand: fn(),
onReplay: fn(),
onTogglePin: fn(),
},
@ -23,7 +22,7 @@ export const SuccessCollapsed: Story = {
status: "success",
durationMs: 142,
isPinned: false,
isExpanded: false,
isListExpanded: false,
},
};
@ -35,7 +34,7 @@ export const SuccessExpanded: Story = {
status: "success",
durationMs: 142,
isPinned: false,
isExpanded: true,
isListExpanded: true,
parameters: {
city: "San Francisco",
units: "celsius",
@ -56,7 +55,7 @@ export const Error: Story = {
status: "error",
durationMs: 3200,
isPinned: false,
isExpanded: false,
isListExpanded: false,
},
};
@ -68,7 +67,7 @@ export const WithChildren: Story = {
status: "success",
durationMs: 1250,
isPinned: false,
isExpanded: true,
isListExpanded: true,
parameters: {
action: "process",
},
@ -102,7 +101,6 @@ export const Pinned: Story = {
status: "success",
durationMs: 45,
isPinned: true,
pinLabel: "Baseline",
isExpanded: false,
isListExpanded: false,
},
};

View file

@ -1,8 +1,10 @@
import { useEffect, useState } from "react";
import {
Badge,
Button,
Card,
Collapse,
Divider,
Group,
Stack,
Text,
@ -27,9 +29,7 @@ export interface HistoryEntryProps {
response?: Record<string, unknown>;
childEntries?: HistoryChildEntry[];
isPinned: boolean;
pinLabel?: string;
isExpanded: boolean;
onToggleExpand: () => void;
isListExpanded: boolean;
onReplay: () => void;
onTogglePin: () => void;
}
@ -82,9 +82,8 @@ function statusColor(status: "success" | "error"): string {
return status === "success" ? "green" : "red";
}
function formatPinLabel(isPinned: boolean, pinLabel?: string): string {
const base = isPinned ? "Unpin" : "Pin";
return isPinned && pinLabel ? `${base} (${pinLabel})` : base;
function formatPinLabel(isPinned: boolean): string {
return isPinned ? "Unpin" : "Pin";
}
function serializeJson(value: Record<string, unknown>): string {
@ -101,12 +100,15 @@ export function HistoryEntry({
response,
childEntries,
isPinned,
pinLabel,
isExpanded,
onToggleExpand,
isListExpanded,
onReplay,
onTogglePin,
}: HistoryEntryProps) {
const [isExpanded, setIsExpanded] = useState(isListExpanded);
useEffect(() => {
setIsExpanded(isListExpanded);
}, [isListExpanded]);
return (
<EntryContainer>
<Stack gap="sm">
@ -127,55 +129,60 @@ export function HistoryEntry({
<Group gap="xs">
<SubtleButton onClick={onReplay}>Replay</SubtleButton>
<SubtleButton onClick={onTogglePin}>
{formatPinLabel(isPinned, pinLabel)}
{formatPinLabel(isPinned)}
</SubtleButton>
<SubtleButton onClick={onToggleExpand} ml="auto">
<SubtleButton onClick={() => setIsExpanded((v) => !v)} ml="auto">
{isExpanded ? "Collapse" : "Expand"}
</SubtleButton>
</Group>
<Collapse in={isExpanded}>
<Stack gap="sm">
{parameters && (
<Stack gap="xs">
<Text size="sm">Parameters:</Text>
<ContentViewer
type="json"
content={serializeJson(parameters)}
copyable
/>
</Stack>
)}
{response && (
<Stack gap="xs">
<Text size="sm">Response:</Text>
<ContentViewer
type="json"
content={serializeJson(response)}
copyable
/>
</Stack>
)}
{childEntries && childEntries.length > 0 && (
<Stack gap="xs">
{childEntries.map((child, index) => (
<Group key={index} pl="lg" gap="sm">
<TimestampText>+-- </TimestampText>
<TimestampText>{child.timestamp}</TimestampText>
<ChildMethodBadge>{child.method}</ChildMethodBadge>
{child.target && <Text size="sm">{child.target}</Text>}
<Badge color={statusColor(child.status)} size="sm">
{formatStatusLabel(child.status)}
</Badge>
<DurationText>
{formatDuration(child.durationMs)}
</DurationText>
</Group>
))}
</Stack>
)}
</Stack>
</Collapse>
{isExpanded && (
<Collapse in={isExpanded}>
<Stack gap="sm">
{parameters && (
<>
<Divider />
<Stack gap="xs">
<Text size="sm">Parameters:</Text>
<ContentViewer
type="json"
content={serializeJson(parameters)}
copyable
/>
</Stack>
</>
)}
{response && (
<Stack gap="xs">
<Text size="sm">Response:</Text>
<ContentViewer
type="json"
content={serializeJson(response)}
copyable
/>
</Stack>
)}
{childEntries && childEntries.length > 0 && (
<Stack gap="xs">
{childEntries.map((child, index) => (
<Group key={index} pl="lg" gap="sm">
<TimestampText>+-- </TimestampText>
<TimestampText>{child.timestamp}</TimestampText>
<ChildMethodBadge>{child.method}</ChildMethodBadge>
{child.target && <Text size="sm">{child.target}</Text>}
<Badge color={statusColor(child.status)} size="sm">
{formatStatusLabel(child.status)}
</Badge>
<DurationText>
{formatDuration(child.durationMs)}
</DurationText>
</Group>
))}
</Stack>
)}
</Stack>
</Collapse>
)}
</Stack>
</EntryContainer>
);