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

@ -0,0 +1,77 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { LogStreamPanel } from "./LogStreamPanel";
import type { LogEntryProps, LogLevel } from "../../elements/LogEntry/LogEntry";
const meta: Meta<typeof LogStreamPanel> = {
title: "Groups/LogStreamPanel",
component: LogStreamPanel,
args: {
filterText: "",
visibleLevels: {
debug: true,
info: true,
notice: true,
warning: true,
error: true,
critical: true,
alert: true,
emergency: true,
},
autoScroll: true,
onToggleAutoScroll: fn(),
onCopyAll: fn(),
},
};
export default meta;
type Story = StoryObj<typeof LogStreamPanel>;
export const Empty: Story = {
args: {
entries: [],
},
};
const logMessages: { level: LogLevel; message: string; logger?: string }[] = [
{ level: "info", message: "Server started on port 3000" },
{ level: "debug", message: "Loading configuration", logger: "config" },
{
level: "warning",
message: "Deprecated API endpoint called",
logger: "http",
},
{ level: "error", message: "Failed to read resource", logger: "resources" },
{
level: "info",
message: "Tool execution completed (245ms)",
logger: "tools",
},
{
level: "critical",
message: "Database connection pool exhausted",
logger: "db",
},
{ level: "alert", message: "Memory usage exceeds 95%", logger: "system" },
{ level: "emergency", message: "System unresponsive", logger: "system" },
];
function pad(n: number): string {
return n.toString().padStart(2, "0");
}
const entries: LogEntryProps[] = Array.from({ length: 30 }, (_, i) => {
const src = logMessages[i % logMessages.length];
return {
timestamp: `2026-03-17T10:00:${pad(i + 1)}Z`,
level: src.level,
message: src.message,
logger: src.logger,
};
});
export const WithEntries: Story = {
args: {
entries,
},
};

View file

@ -0,0 +1,98 @@
import { useMemo } from "react";
import {
Button,
Checkbox,
Group,
Paper,
ScrollArea,
Stack,
Text,
Title,
} from "@mantine/core";
import { LogEntry } from "../../elements/LogEntry/LogEntry";
import type { LogEntryProps } from "../../elements/LogEntry/LogEntry";
export interface LogStreamPanelProps {
entries: LogEntryProps[];
filterText: string;
visibleLevels: Record<string, boolean>;
autoScroll: boolean;
onToggleAutoScroll: () => void;
onCopyAll: () => void;
}
const PanelContainer = Paper.withProps({
withBorder: true,
p: "lg",
flex: 1,
variant: "panel",
});
const ToolbarButton = Button.withProps({
variant: "light",
size: "sm",
});
const EmptyCenter = Stack.withProps({
flex: 1,
align: "center",
justify: "center",
});
function matchesFilters(
entry: LogEntryProps,
filterText: string,
visibleLevels: Record<string, boolean>,
): boolean {
if (!visibleLevels[entry.level]) return false;
if (filterText) {
const term = filterText.toLowerCase();
const searchable =
`${entry.message} ${entry.logger ?? ""} ${entry.level}`.toLowerCase();
if (!searchable.includes(term)) return false;
}
return true;
}
export function LogStreamPanel({
entries,
filterText,
visibleLevels,
autoScroll,
onToggleAutoScroll,
onCopyAll,
}: LogStreamPanelProps) {
const filteredEntries = useMemo(
() => entries.filter((e) => matchesFilters(e, filterText, visibleLevels)),
[entries, filterText, visibleLevels],
);
return (
<PanelContainer>
<Group justify="space-between" mb="sm">
<Title order={4}>Log Stream</Title>
<Group>
<Checkbox
label="Auto-scroll"
checked={autoScroll}
onChange={onToggleAutoScroll}
/>
<ToolbarButton onClick={onCopyAll}>Copy All</ToolbarButton>
</Group>
</Group>
{filteredEntries.length > 0 ? (
<ScrollArea.Autosize mah="calc(100vh - var(--app-shell-header-height, 0px) - 150px)">
<Stack gap="xs">
{filteredEntries.map((entry, index) => (
<LogEntry key={index} {...entry} />
))}
</Stack>
</ScrollArea.Autosize>
) : (
<EmptyCenter>
<Text c="dimmed">No log entries</Text>
</EmptyCenter>
)}
</PanelContainer>
);
}