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

@ -1,39 +1,20 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { LoggingScreen } from "./LoggingScreen";
import type { LogControlsProps } from "../../groups/LogControls/LogControls";
import type { LogEntryProps, LogLevel } from "../../elements/LogEntry/LogEntry";
function makeControls(): LogControlsProps {
return {
currentLevel: "info",
filterText: "",
visibleLevels: {
debug: true,
info: true,
notice: true,
warning: true,
error: true,
critical: true,
alert: true,
emergency: true,
},
onSetLevel: fn(),
onFilterChange: fn(),
onToggleLevel: fn(),
onClear: fn(),
onExport: fn(),
};
}
const meta: Meta<typeof LoggingScreen> = {
title: "Screens/LoggingScreen",
component: LoggingScreen,
parameters: { layout: "fullscreen" },
args: {
currentLevel: "info",
onSetLevel: fn(),
onClear: fn(),
onExport: fn(),
autoScroll: true,
onToggleAutoScroll: fn(),
onCopyAll: fn(),
autoScroll: true,
},
};
@ -42,7 +23,6 @@ type Story = StoryObj<typeof LoggingScreen>;
export const Empty: Story = {
args: {
controls: makeControls(),
entries: [],
},
};
@ -137,7 +117,8 @@ const logMessages: { level: LogLevel; message: string; logger?: string }[] = [
},
{
level: "info",
message: "Session initialized with capabilities: tools, resources, prompts",
message:
"Session initialized with capabilities: tools, resources, prompts",
},
{
level: "debug",
@ -183,7 +164,6 @@ const mixedEntries: LogEntryProps[] = Array.from({ length: 50 }, (_, i) => {
export const WithEntries: Story = {
args: {
controls: makeControls(),
entries: mixedEntries,
},
};
@ -240,7 +220,6 @@ const allLevelEntries: LogEntryProps[] = [
export const MixedLevels: Story = {
args: {
controls: makeControls(),
entries: allLevelEntries,
},
};

View file

@ -1,31 +1,41 @@
import {
Button,
Card,
Checkbox,
Container,
Grid,
Group,
ScrollArea,
Stack,
Text,
Title,
} from "@mantine/core";
import { useState } from "react";
import { Card, Flex, Stack } from "@mantine/core";
import { LogControls } from "../../groups/LogControls/LogControls";
import { LogEntry } from "../../elements/LogEntry/LogEntry";
import type { LogControlsProps } from "../../groups/LogControls/LogControls";
import { LogStreamPanel } from "../../groups/LogStreamPanel/LogStreamPanel";
import type { LogEntryProps } from "../../elements/LogEntry/LogEntry";
export interface LoggingScreenProps {
controls: LogControlsProps;
entries: LogEntryProps[];
currentLevel: string;
onSetLevel: (level: string) => void;
onClear: () => void;
onExport: () => void;
autoScroll: boolean;
onToggleAutoScroll: () => void;
onCopyAll: () => void;
}
const PageContainer = Container.withProps({
size: "xl",
py: "xl",
const ALL_LEVELS_VISIBLE: Record<string, boolean> = {
debug: true,
info: true,
notice: true,
warning: true,
error: true,
critical: true,
alert: true,
emergency: true,
};
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100vh - var(--app-shell-header-height, 0px))",
gap: "xl",
p: "xl",
});
const Sidebar = Stack.withProps({
w: 280,
flex: "0 0 auto",
});
const SidebarCard = Card.withProps({
@ -33,74 +43,58 @@ const SidebarCard = Card.withProps({
padding: "lg",
});
const FullHeightCard = Card.withProps({
withBorder: true,
padding: "lg",
h: "100%",
});
const FullHeightStack = Stack.withProps({
gap: "sm",
h: "100%",
});
const ToolbarButton = Button.withProps({
variant: "light",
size: "sm",
});
const EmptyCenter = Stack.withProps({
flex: 1,
align: "center",
justify: "center",
});
export function LoggingScreen({
controls,
entries,
currentLevel,
onSetLevel,
onClear,
onExport,
autoScroll,
onToggleAutoScroll,
onCopyAll,
}: LoggingScreenProps) {
const [filterText, setFilterText] = useState("");
const [visibleLevels, setVisibleLevels] =
useState<Record<string, boolean>>(ALL_LEVELS_VISIBLE);
function handleToggleLevel(level: string, visible: boolean) {
setVisibleLevels((prev) => ({ ...prev, [level]: visible }));
}
const NO_LEVELS_VISIBLE: Record<string, boolean> = Object.fromEntries(
Object.keys(ALL_LEVELS_VISIBLE).map((k) => [k, false]),
);
function handleToggleAllLevels() {
const allSelected = Object.values(visibleLevels).every(Boolean);
setVisibleLevels(allSelected ? NO_LEVELS_VISIBLE : ALL_LEVELS_VISIBLE);
}
return (
<PageContainer>
<Grid align="stretch">
<Grid.Col span={3}>
<SidebarCard>
<LogControls {...controls} />
</SidebarCard>
</Grid.Col>
<Grid.Col span={9}>
<FullHeightCard>
<FullHeightStack>
<Group justify="space-between">
<Title order={4}>Log Stream</Title>
<Group>
<Checkbox
label="Auto-scroll"
checked={autoScroll}
onChange={onToggleAutoScroll}
/>
<ToolbarButton onClick={onCopyAll}>Copy All</ToolbarButton>
</Group>
</Group>
{entries.length > 0 ? (
<ScrollArea flex={1}>
<Stack gap="xs">
{entries.map((entry, index) => (
<LogEntry key={index} {...entry} />
))}
</Stack>
</ScrollArea>
) : (
<EmptyCenter>
<Text c="dimmed">No log entries</Text>
</EmptyCenter>
)}
</FullHeightStack>
</FullHeightCard>
</Grid.Col>
</Grid>
</PageContainer>
<ScreenLayout>
<Sidebar>
<SidebarCard>
<LogControls
currentLevel={currentLevel}
filterText={filterText}
visibleLevels={visibleLevels}
onSetLevel={onSetLevel}
onFilterChange={setFilterText}
onToggleLevel={handleToggleLevel}
onToggleAllLevels={handleToggleAllLevels}
onClear={onClear}
onExport={onExport}
/>
</SidebarCard>
</Sidebar>
<LogStreamPanel
entries={entries}
filterText={filterText}
visibleLevels={visibleLevels}
autoScroll={autoScroll}
onToggleAutoScroll={onToggleAutoScroll}
onCopyAll={onCopyAll}
/>
</ScreenLayout>
);
}