refactor(web): sweep AGENTS.md TypeScript + Mantine rule violations (#1771)

Codebase-wide sweep bringing the web client into strict compliance with the AGENTS.md TypeScript and Mantine/React conventions: ~170 inline 2+-static-prop elements extracted to named .withProps() constants across ~77 files, the AGENTS.md rule wording tightened to match (static-prop boundary + Accordion/Transition/data-*/Box exemptions), a real ScreenStage Box->Flex layout regression fixed (direction: column) with a Storybook regression guard, and an App.tsx double-cast tightened. Closes #1762.
This commit is contained in:
Cliff Hall 2026-07-25 11:38:49 -04:00 • committed by GitHub
commit da37692485
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
87 changed files with 1567 additions and 991 deletions

View file

@ -251,8 +251,9 @@ gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$ITEM_ID" --field-i
- NEVER use inline code; instead extract to functions in the same file, exported or located in a shared location if immediately reusable.
- In a component's file, for sub-components:
- ALWAYS use Mantine components for layout and content, configured with props for styling and behavior.
- ALWAYS declare a meaningfully named subcomponent as a constant using `.withProps()` if a component has two or more props.
- NEVER use `Box` for subcomponent constants — `Box` does not support `.withProps()`. Use `Group`, `Stack`, `Flex`, `Text`, `Paper`, `UnstyledButton`, or `Image` instead. Pick the component that best matches the purpose: `Paper` for bordered/surfaced containers, `Text` for any text or content wrapper, `Stack`/`Group`/`Flex` for layout.
- ALWAYS declare a meaningfully named subcomponent as a constant using `.withProps()` if an inline Mantine element carries two or more **static** props. A *static* prop is one whose value is a literal that configures the element's **styling, layout, or behavior** (`size="sm"`, `c="dimmed"`, `fw={500}`, `gap="xs"`, `justify="space-between"`, `variant="light"`, `withBorder`, `readOnly`, `striped`, …); dynamic props (`value`, `onChange`/`on*`, `children`, `key`, `ref`, and anything whose value is a variable/expression) do **not** count toward the two and are passed at the call site, not baked into the constant. Purely per-instance **content/accessibility** literals — `label`, `description`, `placeholder`, `title`, `aria-label`, `role` — likewise do **not** count toward the two (a `<Checkbox label="…" description="…">` with no styling/layout/behavior props stays inline); they may be baked into a constant when it already qualifies and doing so aids reuse, but they never by themselves trigger extraction. This rule applies in **all** cases: "repeated pattern" is NOT the bar — a single-use element with two or more static styling/layout/behavior props must still be extracted. Bake the static props into the `.withProps()` constant and pass the dynamic ones where it's rendered.
- The following **cannot** be expressed via `.withProps()` and so stay inline (like `Box` below), each with a one-line comment saying why: **`Accordion`** (a compound, `multiple`-discriminated generic — `.withProps({ multiple: true, … })` loses its JSX call signature and fails to type); **headless, non-`factory()` Mantine components** such as **`Transition`** (plain function components with no Styles API — they have no `.withProps` static at all, e.g. `Transition.withProps` is a TS2339); and **`data-*` attributes** (not part of a component's typed props object, so excess-property-checked out of a `withProps` literal — pass them at the call site). The rule targets factory-based (Styles-API) Mantine components; anything that isn't one is out of scope entirely — a third-party element (a `react-icons` glyph, another library's component) **and** a first-party component that isn't a Mantine factory (a dumb `export function` like `ContentViewer`, which has no `.withProps` static of its own).
- NEVER use `Box` for subcomponent constants — `Box` does not support `.withProps()`. Use `Group`, `Stack`, `Flex`, `Text`, `Paper`, `UnstyledButton`, or `Image` instead. Pick the component that best matches the purpose: `Paper` for bordered/surfaced containers, `Text` for any text or content wrapper, `Stack`/`Group`/`Flex` for layout. A `Box` that genuinely needs a non-flex primitive it can't provide — `component="iframe"`, or `display="grid"` (no Mantine flex primitive is a CSS grid) — stays a `Box` inline, with a one-line comment saying why.
- NEVER use a CSS class on a subcomponent constant when the styles can be expressed as a Mantine theme variant instead. Define variants in `src/theme/<Component>.ts` using `Component.extend({ styles: (_theme, props) => { ... } })` and reference them with `variant="variantName"` on the component or in `.withProps()`.
- CSS classes are ONLY acceptable on subcomponents for styles that cannot be expressed as flat CSS-in-JS properties in the theme — specifically: pseudo-selectors (`:hover`, `:focus`), cross-component hover relationships (`.parent:hover .child`), nested child-element selectors (`.wrapper p`, `.wrapper code`), `@keyframes` definitions, and native HTML elements (`img`, `iframe`) that are not Mantine components.
- When a theme variant needs a CSS class for nested/pseudo selectors, use `classNames` in the theme extension to auto-assign it — never add `className` manually in JSX for theme-styled components.

View file

@ -266,15 +266,6 @@ body {
}
}
@keyframes inspector-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Both motions travel downward: enter descends from above into place, exit
descends from place to below — used for the header crossfade (#1450). */
@keyframes inspector-fade-slide-in {
@ -318,10 +309,6 @@ body {
animation: inspector-pulse 1.5s ease-in-out infinite;
}
.inspector-spin {
animation: inspector-spin 1s linear infinite;
}
/* Applied (via the `tabGlow` Text variant) to a tab label; the glow starts full
and fades, running only while `data-glow="on"` on a freshly-appeared tab. */
.tab-glow[data-glow="on"] {
@ -343,13 +330,6 @@ body {
animation: inspector-fade-slide-out 300ms ease both;
}
/* The title and tab bar additionally share one grid cell so one replaces the
other in place during the center crossfade. */
.header-stack-cell {
grid-area: 1 / 1;
place-self: center;
}
/* Honor reduced-motion for the header animations this PR adds (#1450): skip the
fade/slide and the new-tab glow, leaving the elements at their resting state.
Selectors match the originals' specificity so they win inside the query. */
@ -528,12 +508,6 @@ body {
background-color: var(--mantine-primary-color-light-hover);
}
/* ── Grid alignment ─────────────────────────────────────── */
.grid-align-start {
align-items: start;
}
/* ── Markdown content (third-party HTML from react-markdown) ───── */
.markdown-content {
@ -583,16 +557,6 @@ body {
cursor: grabbing;
}
/*
* Sticky re-auth banner bar. Positioning (pos/top) and background are set via
* Mantine props on the Box; z-index and box-shadow have no Mantine style-prop
* shorthand, so they live here.
*/
.reauth-banner-bar {
z-index: 200;
box-shadow: var(--mantine-shadow-sm);
}
/*
* Draggable vertical divider between the primary screen area and the pinned
* monitoring column (#1616). The col-resize cursor, the hover/active accent,

View file

@ -3,6 +3,7 @@ import {
Anchor,
Box,
List,
Paper,
Stack,
Text,
useComputedColorScheme,
@ -270,10 +271,12 @@ function messagesToLogEntries(messages: MessageEntry[]): LogEntryData[] {
for (const m of messages) {
if (m.direction !== "notification") continue;
// MessageEntry.message is a JSONRPC union; notifications have `method`
// but not `id`. Narrow with an `in` check before the cast.
// but not `id`. Narrow with an `in` check, then confirm the method.
if (!("method" in m.message)) continue;
if (m.message.method !== "notifications/message") continue;
const params = (m.message as unknown as LoggingMessageNotification).params;
// The method check pins this to a logging notification; its `params` are
// only generically typed on the JSONRPC union, so cast just that value.
const params = m.message.params as LoggingMessageNotification["params"];
out.push({
receivedAt: m.timestamp,
params,
@ -381,6 +384,34 @@ function bodyDroppedToastId(serverId: string): string {
const CLIENT_CONFIG_LOAD_ERROR_NOTIFICATION_ID = "client-config-load-error";
// Shared "list of likely causes" styling for the warning-toast bodies below.
const ToastCauseList = List.withProps({ size: "sm", spacing: 2 });
// The "open the relevant settings/details" link rendered at the bottom of each
// warning-toast body. Same static shape across all three toasts; each passes
// its own `onClick`.
const ToastLinkButton = Anchor.withProps({
component: "button",
type: "button",
size: "sm",
});
// Sticky re-auth banner bar. A `Paper` so every static style is a prop: `shadow`
// emits `var(--mantine-shadow-sm)` (identical to the old CSS), and the stacking
// order goes through `styles.root` since Mantine has no `z` prop.
const ReAuthBannerBar = Paper.withProps({
px: "md",
pt: "xs",
pos: "sticky",
top: 60,
bg: "var(--mantine-color-body)",
shadow: "sm",
styles: { root: { zIndex: 200 } },
// Paper's default `radius: "md"` would round this full-bleed sticky bar's
// corners; the bar it replaced (a Box) had none.
radius: 0,
});
// Body of the "response body dropped" warning toast: a one-line summary of what
// happened, the likely causes, and a link that opens this server's settings
// (on the Options section) so the user can raise the Network Log Size if it's
@ -399,7 +430,7 @@ const FetchBodyDroppedToastMessage = ({
out (the log hit its {maxFetchRequests}-request limit), so the body
couldn&apos;t be shown. This usually indicates:
</Text>
<List size="sm" spacing={2}>
<ToastCauseList>
<List.Item>
a chatty or misbehaving server (notification storms, rapid polling)
</List.Item>
@ -410,10 +441,10 @@ const FetchBodyDroppedToastMessage = ({
<List.Item>
the Network Log Size set too low for this server&apos;s traffic
</List.Item>
</List>
<Anchor component="button" type="button" size="sm" onClick={onAdjust}>
</ToastCauseList>
<ToastLinkButton onClick={onAdjust}>
Adjust Network Log Size for this server
</Anchor>
</ToastLinkButton>
</Stack>
);
@ -431,9 +462,9 @@ const OutputValidationToastMessage = ({
tool&apos;s outputSchema. The inspector renders it anyway, but strict MCP
clients may not.
</Text>
<Anchor component="button" type="button" size="sm" onClick={onViewDetails}>
<ToastLinkButton onClick={onViewDetails}>
View validation details
</Anchor>
</ToastLinkButton>
</Stack>
);
@ -450,9 +481,9 @@ const UrlElicitationErrorToastMessage = ({
The server reported a URLElicitationRequired error but listed no required
elicitations, so there&apos;s nothing to open.
</Text>
<Anchor component="button" type="button" size="sm" onClick={onViewDetails}>
<ToastLinkButton onClick={onViewDetails}>
View error details
</Anchor>
</ToastLinkButton>
</Stack>
);
@ -4183,20 +4214,13 @@ function App() {
<>
<Box>
{reAuthBanner ? (
<Box
className="reauth-banner-bar"
px="md"
pt="xs"
pos="sticky"
top={60}
bg="var(--mantine-color-body)"
>
<ReAuthBannerBar>
<ReAuthBanner
message={reAuthBanner.message}
onReauthenticate={onReauthenticateFromBanner}
onDismiss={() => setReAuthBanner(null)}
/>
</Box>
</ReAuthBannerBar>
) : null}
<InspectorView
deepLink={deepLink}

View file

@ -26,6 +26,12 @@ const colorMap: Record<AnnotationFacet, string> = {
longRunHint: "yellow",
};
const FilledBadge = Badge.withProps({
variant: "filled",
fw: 500,
autoContrast: true,
});
function formatLabel(
facet: AnnotationFacet,
value: Role[] | number | boolean,
@ -59,9 +65,5 @@ export function AnnotationBadge({ facet, value }: AnnotationBadgeProps) {
// fills and the darker dark-mode `-filled` shades — unlike a fixed
// scheme→black/white mapping, which inverted the contrast in dark mode.
// Amber fills are pinned to shade 5 first (see `filledBadgeColor`).
return (
<Badge color={color} variant="filled" fw={500} autoContrast>
{formatLabel(facet, value)}
</Badge>
);
return <FilledBadge color={color}>{formatLabel(facet, value)}</FilledBadge>;
}

View file

@ -522,6 +522,8 @@ export function AppRenderer({
// iframe. Sandboxing this outer frame would block the postMessage bridge
// that `AppBridge` relies on.
return (
// Box+iframe is a native element (not a Mantine primitive), so the
// `.withProps()` extraction rule doesn't apply.
<Box
component="iframe"
ref={iframeRef}

View file

@ -5,6 +5,8 @@ const ContentWrapper = Flex.withProps({
direction: "column",
});
const NoticeCode = Code.withProps({ block: true, p: 36 });
/**
* Fallback shown when content can't be previewed — an unsupported binary MIME
* type, or a blob whose base64 fails to decode. Kept in its own module so blob
@ -15,9 +17,9 @@ export function BinaryNotice({ mimeType }: { mimeType: string }) {
return (
<Stack gap="xs">
<ContentWrapper>
<Code block p={36}>
<NoticeCode>
{`[Binary content (${mimeType}) — preview not supported]`}
</Code>
</NoticeCode>
</ContentWrapper>
</Stack>
);

View file

@ -86,6 +86,11 @@ const PreviewImage = Image.withProps({
radius: "md",
});
const CodeBlock = Code.withProps({
block: true,
p: 36,
});
// Markdown anchors are constrained to a safe-scheme allowlist: a non-matching
// href (e.g. `javascript:`, protocol-relative `//evil.com`) renders as inert
// text so user-supplied markdown can't smuggle a script-bearing link.
@ -168,9 +173,7 @@ function PlainTextContent({
const displayText = looksLikeJson(text) ? formatJson(text) : text;
return (
<CopyableWrapper copyable={copyable} copyValue={text}>
<Code
block
p={36}
<CodeBlock
// A `block` Code has `overflow: auto`, so make it keyboard-scrollable
// (WCAG SC 2.1.1) — it holds only text, with no focusable child.
tabIndex={0}
@ -180,7 +183,7 @@ function PlainTextContent({
title={wrap ? undefined : displayText}
>
{displayText}
</Code>
</CodeBlock>
</CopyableWrapper>
);
}
@ -351,11 +354,11 @@ function BlockContent({
return (
<Stack gap="xs">
<ContentWrapper>
<Code block p={36}>
<CodeBlock>
{"text" in block.resource
? block.resource.text
: `[blob: ${block.resource.uri}]`}
</Code>
</CodeBlock>
</ContentWrapper>
</Stack>
);

View file

@ -18,6 +18,13 @@ export const MAX_ROWS = 100;
const PlainCode = Code.withProps({ block: true, variant: "wrapping" });
const CsvGrid = Table.withProps({
striped: true,
highlightOnHover: true,
withTableBorder: true,
withColumnBorders: true,
});
interface ParsedCsv {
fields: string[];
rows: string[][];
@ -49,7 +56,7 @@ export function CsvTable({ text }: CsvTableProps) {
const truncated = parsed.total > MAX_ROWS;
return (
<Table striped highlightOnHover withTableBorder withColumnBorders>
<CsvGrid>
{truncated && (
<Table.Caption>
{`Showing first ${MAX_ROWS} of ${parsed.total} rows`}
@ -71,6 +78,6 @@ export function CsvTable({ text }: CsvTableProps) {
</Table.Tr>
))}
</Table.Tbody>
</Table>
</CsvGrid>
);
}

View file

@ -25,6 +25,8 @@ export function HtmlFrame({ html }: HtmlFrameProps) {
);
const url = useObjectUrl(blob);
return (
// Box+iframe is a native element (not a Mantine primitive), so the
// `.withProps()` extraction rule doesn't apply.
<Box
component="iframe"
title="HTML preview"

View file

@ -30,6 +30,8 @@ export function PdfFrame({ data }: PdfFrameProps) {
return <BinaryNotice mimeType="application/pdf" />;
}
return (
// Box+iframe is a native element (not a Mantine primitive), so the
// `.withProps()` extraction rule doesn't apply.
<Box
component="iframe"
title="PDF preview"

View file

@ -13,21 +13,24 @@ export interface CopyButtonProps {
flush?: boolean;
}
const CopyActionIcon = ActionIcon.withProps({
variant: "subtle",
fz: 24,
});
export function CopyButton({ value, flush = false }: CopyButtonProps) {
return (
<MantineCopyButton value={value}>
{({ copied, copy }) => (
<Tooltip label={copied ? "Copied" : "Copy"}>
<ActionIcon
variant="subtle"
<CopyActionIcon
color={copied ? "green" : "var(--inspector-text-primary)"}
onClick={copy}
fz={24}
aria-label={copied ? "Copied" : "Copy"}
{...(flush && { p: 0, h: "auto", w: "auto" })}
>
{copied ? "\u2713" : "\u2398"}
</ActionIcon>
</CopyActionIcon>
</Tooltip>
)}
</MantineCopyButton>

View file

@ -34,6 +34,18 @@ export interface EmbeddableScrollAreaProps {
// grow wider than its viewport; see `constrainContentWidth`.
const CONSTRAIN_CONTENT_STYLES = { content: { minWidth: 0 } } as const;
// Shared scroll region for both hosts; the differing props (viewportRef, mah,
// styles) are passed at each call site.
const StreamScrollArea = ScrollArea.Autosize.withProps({
type: "scroll",
offsetScrollbars: true,
viewportProps: { tabIndex: 0 },
});
// Fill-height wrapper for the embedded host, so the inner scroll region can claim
// the remaining space and scroll instead of overflowing.
const EmbeddedColumn = Stack.withProps({ flex: 1, mih: 0, gap: 0 });
/**
* The scroll region shared by the Logs / Protocol / Network stream panels, which
* render both full-size (their own tab) and embedded (the monitoring sidebar).
@ -48,30 +60,20 @@ export function EmbeddableScrollArea({
const styles = constrainContentWidth ? CONSTRAIN_CONTENT_STYLES : undefined;
if (embedded) {
return (
<Stack flex={1} mih={0} gap={0}>
<ScrollArea.Autosize
viewportRef={viewportRef}
mah="100%"
type="scroll"
offsetScrollbars
styles={styles}
viewportProps={{ tabIndex: 0 }}
>
<EmbeddedColumn>
<StreamScrollArea viewportRef={viewportRef} mah="100%" styles={styles}>
{children}
</ScrollArea.Autosize>
</Stack>
</StreamScrollArea>
</EmbeddedColumn>
);
}
return (
<ScrollArea.Autosize
<StreamScrollArea
viewportRef={viewportRef}
mah={FULLSIZE_MAH}
type="scroll"
offsetScrollbars
styles={styles}
viewportProps={{ tabIndex: 0 }}
>
{children}
</ScrollArea.Autosize>
</StreamScrollArea>
);
}

View file

@ -14,6 +14,12 @@ export interface ExpandToggleProps {
ariaLabel?: string;
}
const ExpandActionIcon = ActionIcon.withProps({
variant: "subtle",
color: "gray",
size: "md",
});
/**
* Icon toggle for a per-entry expand/collapse control (Protocol, Network, and
* Task cards). Uses the same expand/collapse-vertical icons as the list-level
@ -31,16 +37,13 @@ export function ExpandToggle({
const label = expanded ? "Collapse" : "Expand";
return (
<Tooltip label={label}>
<ActionIcon
variant="subtle"
color="gray"
size="md"
<ExpandActionIcon
aria-label={ariaLabel ?? label}
aria-expanded={expanded}
onClick={onToggle}
>
<Icon size={16} />
</ActionIcon>
</ExpandActionIcon>
</Tooltip>
);
}

View file

@ -17,6 +17,12 @@ const ToggleLabel = Text.withProps({
fw: 500,
});
const ToggleButton = UnstyledButton.withProps({
w: "100%",
p: "sm",
variant: "filterToggle",
});
/**
* A single full-width filter toggle used by the Logging, Protocol, and Network
* controls. The `filterToggle` theme variant + `.filter-toggle` rules own the
@ -32,14 +38,8 @@ export function FilterToggleButton({
onToggle,
}: FilterToggleButtonProps) {
return (
<UnstyledButton
w="100%"
p="sm"
variant="filterToggle"
aria-pressed={active}
onClick={() => onToggle(!active)}
>
<ToggleButton aria-pressed={active} onClick={() => onToggle(!active)}>
<ToggleLabel c={accessibleTextColor(color)}>{label}</ToggleLabel>
</UnstyledButton>
</ToggleButton>
);
}

View file

@ -7,6 +7,19 @@ export interface ListToggleProps {
variant?: "default" | "subtle";
}
const SubtleActionIcon = ActionIcon.withProps({
variant: "subtle",
color: "gray",
size: "md",
});
// `size={36}` matches the header's theme / client-settings ActionIcons so the
// toolbar's toggle reads as the same size icon button.
const ToolbarActionIcon = ActionIcon.withProps({
variant: "subtle",
size: 36,
});
export function ListToggle({
compact,
onToggle,
@ -18,31 +31,18 @@ export function ListToggle({
if (variant === "subtle") {
return (
<Tooltip label={label}>
<ActionIcon
variant="subtle"
color="gray"
size="md"
aria-label={label}
onClick={onToggle}
>
<SubtleActionIcon aria-label={label} onClick={onToggle}>
<Icon size={16} />
</ActionIcon>
</SubtleActionIcon>
</Tooltip>
);
}
// `size={36}` matches the header's theme / client-settings ActionIcons so the
// toolbar's toggle reads as the same size icon button.
return (
<Tooltip label={label}>
<ActionIcon
variant="subtle"
size={36}
aria-label={label}
onClick={onToggle}
>
<ToolbarActionIcon aria-label={label} onClick={onToggle}>
<Icon size={20} />
</ActionIcon>
</ToolbarActionIcon>
</Tooltip>
);
}

View file

@ -81,6 +81,12 @@ const MetaRow = Group.withProps({
align: "center",
});
// The single-line row (full Logs screen): meta + message on one row.
const LogRow = Group.withProps({
gap: "sm",
wrap: "nowrap",
});
export function LogEntry({ entry, compact = false }: LogEntryProps) {
const { receivedAt, params } = entry;
const message = formatData(params.data);
@ -106,13 +112,13 @@ export function LogEntry({ entry, compact = false }: LogEntryProps) {
}
return (
<Group gap="sm" wrap="nowrap">
<LogRow>
<TimestampText>{formatTimestamp(receivedAt)}</TimestampText>
<LogLevelBadge level={params.level} />
{logger}
<MessageText c={accessibleTextColor(levelMessageColor[params.level])}>
{message}
</MessageText>
</Group>
</LogRow>
);
}

View file

@ -19,19 +19,19 @@ const levelColor: Record<LoggingLevel, string> = {
const boldLevels: Set<LoggingLevel> = new Set(["alert", "emergency"]);
const FilledBadge = Badge.withProps({
variant: "filled",
autoContrast: true,
});
export function LogLevelBadge({ level }: LogLevelBadgeProps) {
const fw = boldLevels.has(level) ? 500 : undefined;
// `autoContrast` keeps the label legible (WCAG AA) on both the light-mode
// fills and the darker dark-mode `-filled` shades — see AnnotationBadge.
return (
<Badge
color={filledBadgeColor(levelColor[level])}
variant="filled"
fw={fw}
autoContrast
>
<FilledBadge color={filledBadgeColor(levelColor[level])} fw={fw}>
{level}
</Badge>
</FilledBadge>
);
}

View file

@ -20,6 +20,21 @@ const COLOR_BY_CODE: Record<number, string> = {
[-32601]: "yellow", // MethodNotFound (modern 404)
};
const SpecErrorBadge = Badge.withProps({
variant: "filled",
autoContrast: true,
// Keep the spec/SDK identifier's own casing (e.g. "UnsupportedProtocolVersion")
// rather than Mantine's default uppercase, which runs these long
// PascalCase names together and hurts readability.
tt: "none",
});
const DescriptionTooltip = Tooltip.withProps({
multiline: true,
w: 280,
withArrow: true,
});
/**
* Distinct badge for one of the modern Streamable HTTP spec error codes shown in
* the Protocol tab. Labels the code and spec name (e.g. "-32020 HeaderMismatch")
@ -31,22 +46,10 @@ const COLOR_BY_CODE: Record<number, string> = {
*/
export function McpErrorBadge({ code, name, description }: McpErrorBadgeProps) {
const badge = (
<Badge
color={filledBadgeColor(COLOR_BY_CODE[code] ?? "red")}
variant="filled"
autoContrast
// Keep the spec/SDK identifier's own casing (e.g. "UnsupportedProtocolVersion")
// rather than Mantine's default uppercase, which runs these long
// PascalCase names together and hurts readability.
tt="none"
>
<SpecErrorBadge color={filledBadgeColor(COLOR_BY_CODE[code] ?? "red")}>
{code} {name}
</Badge>
</SpecErrorBadge>
);
if (!description) return badge;
return (
<Tooltip label={description} multiline w={280} withArrow>
{badge}
</Tooltip>
);
return <DescriptionTooltip label={description}>{badge}</DescriptionTooltip>;
}

View file

@ -5,19 +5,17 @@ export interface MethodBadgeProps {
method: string;
}
const MethodChip = Badge.withProps({
autoContrast: false,
bg: "var(--inspector-badge-method-bg)",
c: "var(--inspector-badge-method-fg)",
});
/**
* Badge labelling a Protocol/Network entry's method. A neutral charcoal chip with
* light text, driven by `--inspector-badge-method-*` so it stays legible (not a
* washed-out pale fill) in dark mode. Shared by `ProtocolEntry` and `NetworkEntry`.
*/
export function MethodBadge({ method }: MethodBadgeProps) {
return (
<Badge
autoContrast={false}
bg="var(--inspector-badge-method-bg)"
c="var(--inspector-badge-method-fg)"
>
{method}
</Badge>
);
return <MethodChip>{method}</MethodChip>;
}

View file

@ -11,6 +11,12 @@ export interface MonitoringToggleProps {
onToggle: () => void;
}
// `size={36}` matches the header's theme / client-settings ActionIcons.
const MonitoringActionIcon = ActionIcon.withProps({
variant: "subtle",
size: 36,
});
/**
* The single header affordance for the monitoring sidebar (#1661). It replaces
* the per-screen pin buttons, the server-list open-sidebar button, and the
@ -19,7 +25,7 @@ export interface MonitoringToggleProps {
* label reflect the current state (expand when closed, collapse when open). The
* caller only mounts it when the sidebar is available (connected, or a failed
* connect attempt, on a wide viewport), so it never appears with nothing to
* toggle. `size={36}` matches the header's theme / client-settings ActionIcons.
* toggle.
*/
export function MonitoringToggle({ open, onToggle }: MonitoringToggleProps) {
const Icon = open ? TbLayoutSidebarRightCollapse : TbLayoutSidebarRightExpand;
@ -27,14 +33,9 @@ export function MonitoringToggle({ open, onToggle }: MonitoringToggleProps) {
return (
<Tooltip label={label}>
<ActionIcon
variant="subtle"
size={36}
aria-label={label}
onClick={onToggle}
>
<MonitoringActionIcon aria-label={label} onClick={onToggle}>
<Icon size={20} />
</ActionIcon>
</MonitoringActionIcon>
</Tooltip>
);
}

Some files were not shown because too many files have changed in this diff Show more