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:
parent
8aba3122c5
commit
da37692485
28 changed files with 1567 additions and 991 deletions
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ const NoteText = Text.withProps({
|
|||
c: "dimmed",
|
||||
});
|
||||
|
||||
const OriginBadge = Badge.withProps({
|
||||
variant: "outline",
|
||||
color: "blue",
|
||||
});
|
||||
|
||||
// The two modern origins differ in HOW the answer is delivered: an MRTR round
|
||||
// retries the original call with the answer (SEP-2322); a task round submits it
|
||||
// via a separate `tasks/update` request (SEP-2663). The note is accurate to each
|
||||
|
|
@ -50,9 +55,7 @@ export function MrtrOriginNote({
|
|||
if (!note) return null;
|
||||
return (
|
||||
<NoteRow>
|
||||
<Badge variant="outline" color="blue">
|
||||
input_required
|
||||
</Badge>
|
||||
<OriginBadge>input_required</OriginBadge>
|
||||
<NoteText>{note}</NoteText>
|
||||
</NoteRow>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@ export interface PinToggleProps {
|
|||
onToggle: () => void;
|
||||
}
|
||||
|
||||
const PinActionIcon = ActionIcon.withProps({
|
||||
variant: "subtle",
|
||||
color: "gray",
|
||||
size: "md",
|
||||
});
|
||||
|
||||
/**
|
||||
* Icon toggle for pinning an entry. Unpinned shows an outline pin; pinned shows
|
||||
* a filled pin. The aria-label stays "Pin"/"Unpin" so it reads the same as the
|
||||
|
|
@ -17,15 +23,9 @@ export function PinToggle({ pinned, onToggle }: PinToggleProps) {
|
|||
const label = pinned ? "Unpin" : "Pin";
|
||||
return (
|
||||
<Tooltip label={label}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="md"
|
||||
aria-label={label}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<PinActionIcon aria-label={label} onClick={onToggle}>
|
||||
<Icon size={18} />
|
||||
</ActionIcon>
|
||||
</PinActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,13 @@ export interface ReplayButtonProps {
|
|||
onReplay: () => void;
|
||||
}
|
||||
|
||||
const ReplayActionIcon = ActionIcon.withProps({
|
||||
variant: "subtle",
|
||||
color: "gray",
|
||||
size: "md",
|
||||
"aria-label": "Replay",
|
||||
});
|
||||
|
||||
/**
|
||||
* Icon form of the "Replay" action, used in the compact (column) ProtocolEntry
|
||||
* layout where the text button is replaced by a replay icon sitting next to the
|
||||
|
|
@ -14,15 +21,9 @@ export interface ReplayButtonProps {
|
|||
export function ReplayButton({ onReplay }: ReplayButtonProps) {
|
||||
return (
|
||||
<Tooltip label="Replay">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="md"
|
||||
aria-label="Replay"
|
||||
onClick={onReplay}
|
||||
>
|
||||
<ReplayActionIcon onClick={onReplay}>
|
||||
<MdReplay size={18} />
|
||||
</ActionIcon>
|
||||
</ReplayActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue