Merge remote-tracking branch 'origin/v2/main' into v2/sdk-v2-migration-1624
# Conflicts: # clients/web/src/components/groups/ResourceLink/ResourceLink.tsx # clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.stories.tsx # clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx
This commit is contained in:
commit
39362dd5ff
7 changed files with 591 additions and 197 deletions
|
|
@ -122,14 +122,16 @@ describe("ContentViewer", () => {
|
|||
type: "resource_link",
|
||||
uri: "ui://app",
|
||||
name: "Cool App",
|
||||
description: "An app",
|
||||
mimeType: "text/html",
|
||||
};
|
||||
renderWithMantine(<ContentViewer block={block} />);
|
||||
expect(screen.getByText("Cool App")).toBeInTheDocument();
|
||||
expect(screen.getByText("An app")).toBeInTheDocument();
|
||||
expect(screen.getByText("text/html")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
||||
// The URI copy button is present, but there's no expand control.
|
||||
expect(screen.getByRole("button", { name: "Copy" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /^Expand resource/ }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders nothing for unknown block types", () => {
|
||||
|
|
|
|||
|
|
@ -367,7 +367,6 @@ function BlockContent({
|
|||
<ResourceLinkInfo
|
||||
uri={block.uri}
|
||||
name={block.name}
|
||||
description={block.description}
|
||||
mimeType={block.mimeType}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -23,4 +23,30 @@ describe("ExpandToggle", () => {
|
|||
await user.click(screen.getByRole("button", { name: "Expand" }));
|
||||
expect(onToggle).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("uses ariaLabel as the accessible name when provided", () => {
|
||||
renderWithMantine(
|
||||
<ExpandToggle
|
||||
expanded={false}
|
||||
onToggle={vi.fn()}
|
||||
ariaLabel="Expand resource demo://r/1"
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Expand resource demo://r/1" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("exposes aria-expanded=false when collapsed", () => {
|
||||
renderWithMantine(<ExpandToggle expanded={false} onToggle={vi.fn()} />);
|
||||
expect(screen.getByRole("button")).toHaveAttribute(
|
||||
"aria-expanded",
|
||||
"false",
|
||||
);
|
||||
});
|
||||
|
||||
it("exposes aria-expanded=true when expanded", () => {
|
||||
renderWithMantine(<ExpandToggle expanded={true} onToggle={vi.fn()} />);
|
||||
expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,16 +5,28 @@ export interface ExpandToggleProps {
|
|||
/** Whether the owning entry is currently expanded. */
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
/**
|
||||
* Overrides the accessible name (aria-label). Defaults to the tooltip text
|
||||
* ("Expand"/"Collapse"). Pass a per-entry name (e.g. including the resource
|
||||
* URI) when several toggles sit in one list, so assistive tech can tell them
|
||||
* apart; the visible tooltip stays the plain verb.
|
||||
*/
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* ListToggle: collapsed shows the expand icon, expanded shows the collapse
|
||||
* icon. The aria-label stays "Expand"/"Collapse" so it reads the same as the
|
||||
* text button it replaced.
|
||||
* icon. The tooltip stays "Expand"/"Collapse" (the same verb as the text button
|
||||
* it replaced); `aria-expanded` exposes the disclosure state and `ariaLabel`
|
||||
* can distinguish sibling toggles.
|
||||
*/
|
||||
export function ExpandToggle({ expanded, onToggle }: ExpandToggleProps) {
|
||||
export function ExpandToggle({
|
||||
expanded,
|
||||
onToggle,
|
||||
ariaLabel,
|
||||
}: ExpandToggleProps) {
|
||||
const Icon = expanded ? RiCollapseVerticalLine : RiExpandVerticalLine;
|
||||
const label = expanded ? "Collapse" : "Expand";
|
||||
return (
|
||||
|
|
@ -23,7 +35,8 @@ export function ExpandToggle({ expanded, onToggle }: ExpandToggleProps) {
|
|||
variant="subtle"
|
||||
color="gray"
|
||||
size="md"
|
||||
aria-label={label}
|
||||
aria-label={ariaLabel ?? label}
|
||||
aria-expanded={expanded}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<Icon size={16} />
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ export const Full: Story = {
|
|||
args: {
|
||||
uri: "file:///docs/readme.md",
|
||||
name: "Readme",
|
||||
description: "Project documentation",
|
||||
mimeType: "text/markdown",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,18 +5,12 @@ import { ResourceLinkInfo } from "./ResourceLinkInfo";
|
|||
const URI = "file:///docs/readme.md";
|
||||
|
||||
describe("ResourceLinkInfo", () => {
|
||||
it("renders uri, name, description, and mimeType", () => {
|
||||
it("renders uri, name, and mimeType", () => {
|
||||
renderWithMantine(
|
||||
<ResourceLinkInfo
|
||||
uri={URI}
|
||||
name="Readme"
|
||||
description="The project readme"
|
||||
mimeType="text/markdown"
|
||||
/>,
|
||||
<ResourceLinkInfo uri={URI} name="Readme" mimeType="text/markdown" />,
|
||||
);
|
||||
expect(screen.getByText(URI)).toBeInTheDocument();
|
||||
expect(screen.getByText("Readme")).toBeInTheDocument();
|
||||
expect(screen.getByText("The project readme")).toBeInTheDocument();
|
||||
expect(screen.getByText("text/markdown")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import type { ReactNode } from "react";
|
||||
import { Badge, Group, Stack, Text } from "@mantine/core";
|
||||
import { CopyButton } from "../CopyButton/CopyButton";
|
||||
|
||||
export interface ResourceLinkInfoProps {
|
||||
/** The linked resource's URI (always shown). */
|
||||
/** The linked resource's URI (always shown, with a copy button). */
|
||||
uri: string;
|
||||
/** Optional human-friendly name shown beneath the URI. */
|
||||
/** Optional human-friendly name shown above the URI. */
|
||||
name?: string;
|
||||
/** Optional description shown beneath the name. */
|
||||
description?: string;
|
||||
/** Optional MIME type shown as a badge. */
|
||||
mimeType?: string;
|
||||
/**
|
||||
* Optional trailing element placed at the end of the URI row — e.g. an
|
||||
* expand/collapse indicator supplied by an interactive wrapper.
|
||||
* Optional trailing element placed at the end of the URI row (opposite the
|
||||
* copy button) — e.g. an expand/collapse control supplied by an interactive
|
||||
* wrapper. Mirrors ProtocolEntry, whose toggle sits on the row below the
|
||||
* badges.
|
||||
*/
|
||||
action?: ReactNode;
|
||||
}
|
||||
|
|
@ -21,70 +22,97 @@ const HeaderStack = Stack.withProps({
|
|||
gap: 4,
|
||||
});
|
||||
|
||||
// Name (left) + MIME badge (right). `justify` is set per-instance: spread when
|
||||
// a name is present, otherwise the badge hugs the right.
|
||||
const HeaderRow = Group.withProps({
|
||||
wrap: "nowrap",
|
||||
gap: "xs",
|
||||
align: "center",
|
||||
});
|
||||
|
||||
// Copy control + URI (left) and the optional expand/collapse control (right),
|
||||
// on the line below the header — mirroring ProtocolEntry's controls row.
|
||||
const UriRow = Group.withProps({
|
||||
justify: "space-between",
|
||||
wrap: "nowrap",
|
||||
gap: "xs",
|
||||
align: "flex-start",
|
||||
align: "center",
|
||||
});
|
||||
|
||||
// Copy button + URI cluster; flexes so the URI fills and the action stays right.
|
||||
const UriCluster = Group.withProps({
|
||||
wrap: "nowrap",
|
||||
gap: "xs",
|
||||
align: "center",
|
||||
flex: 1,
|
||||
miw: 0,
|
||||
});
|
||||
|
||||
// Match how ProtocolEntry/NetworkEntry render a URL: `sm` / `fw: 500`, in the
|
||||
// default sans-serif face and text color (not a blue monospace "link"). The
|
||||
// `monoBreak` variant only adds `word-break: break-all` so a long URI wraps
|
||||
// within the card instead of overflowing.
|
||||
const UriText = Text.withProps({
|
||||
size: "sm",
|
||||
c: "blue",
|
||||
ff: "monospace",
|
||||
fw: 500,
|
||||
variant: "monoBreak",
|
||||
flex: 1,
|
||||
miw: 0,
|
||||
});
|
||||
|
||||
const MetaGroup = Group.withProps({
|
||||
gap: "xs",
|
||||
wrap: "nowrap",
|
||||
});
|
||||
|
||||
const MimeBadge = Badge.withProps({
|
||||
size: "sm",
|
||||
variant: "light",
|
||||
color: "blue",
|
||||
// Match the point size of the ProtocolEntry method/status badges; the
|
||||
// lowercase MIME text reads smaller than their uppercase labels at `sm`.
|
||||
size: "md",
|
||||
radius: "sm",
|
||||
// MIME types are conventionally lowercase; keep them as-is rather than
|
||||
// letting Badge's default uppercase transform mangle them.
|
||||
tt: "none",
|
||||
autoContrast: false,
|
||||
// Light mode: the tinted blue-light chip (unchanged). Dark mode: a solid
|
||||
// dark-blue fill with white text — matching the solid ProtocolEntry badges
|
||||
// rather than a washed-out translucent tint.
|
||||
bg: "light-dark(var(--mantine-color-blue-light), var(--mantine-color-blue-9))",
|
||||
c: "light-dark(var(--mantine-color-blue-light-color), var(--mantine-color-white))",
|
||||
});
|
||||
|
||||
const NameText = Text.withProps({
|
||||
size: "sm",
|
||||
fw: 600,
|
||||
});
|
||||
|
||||
const DescriptionText = Text.withProps({
|
||||
size: "sm",
|
||||
c: "dimmed",
|
||||
flex: 1,
|
||||
miw: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* Pure-display metadata for a `resource_link`: the URI (monospace, link-styled),
|
||||
* optional name / description, and a MIME-type badge. The optional `action`
|
||||
* slot lets an interactive wrapper (e.g. {@link ResourceLink}) place an
|
||||
* expand/collapse indicator in the URI row.
|
||||
* Pure-display metadata for a `resource_link`: an optional name and MIME-type
|
||||
* badge on the header row, then the URI on the line below with a copy button.
|
||||
* The URI is styled like ProtocolEntry/NetworkEntry URLs (`sm` / `fw: 500`,
|
||||
* default sans-serif face and color), not a blue monospace link. The optional
|
||||
* `action` slot lets an interactive wrapper (e.g. {@link ResourceLink}) place
|
||||
* an expand/collapse control at the end of the URI row.
|
||||
*/
|
||||
export function ResourceLinkInfo({
|
||||
uri,
|
||||
name,
|
||||
description,
|
||||
mimeType,
|
||||
action,
|
||||
}: ResourceLinkInfoProps) {
|
||||
const hasHeader = Boolean(name || mimeType);
|
||||
return (
|
||||
<HeaderStack>
|
||||
<UriRow>
|
||||
<UriText>{uri}</UriText>
|
||||
<MetaGroup>
|
||||
{hasHeader && (
|
||||
<HeaderRow justify={name ? "space-between" : "flex-end"}>
|
||||
{name && <NameText>{name}</NameText>}
|
||||
{mimeType && <MimeBadge>{mimeType}</MimeBadge>}
|
||||
{action}
|
||||
</MetaGroup>
|
||||
</HeaderRow>
|
||||
)}
|
||||
<UriRow>
|
||||
<UriCluster>
|
||||
<CopyButton value={uri} />
|
||||
<UriText>{uri}</UriText>
|
||||
</UriCluster>
|
||||
{action}
|
||||
</UriRow>
|
||||
{name && <NameText>{name}</NameText>}
|
||||
{description && <DescriptionText>{description}</DescriptionText>}
|
||||
</HeaderStack>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue