diff --git a/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx b/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx index 4af024d5..7c2733a3 100644 --- a/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx +++ b/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx @@ -120,6 +120,34 @@ describe("ContentViewer", () => { expect(screen.queryByRole("button")).not.toBeInTheDocument(); }); + it("renders plain text in a wrapping code block by default", () => { + const block: ContentBlock = { type: "text", text: "some long command" }; + const { container } = renderWithMantine(); + expect(container.querySelector(".mantine-Code-root")).toHaveAttribute( + "data-variant", + "wrapping", + ); + }); + + it("renders plain text in a non-wrapping code block when wrap is false", () => { + const block: ContentBlock = { type: "text", text: "some long command" }; + const { container } = renderWithMantine( + , + ); + const code = container.querySelector(".mantine-Code-root"); + expect(code).toHaveAttribute("data-variant", "nowrap"); + // Full value exposed on hover since it may be clipped with an ellipsis. + expect(code).toHaveAttribute("title", "some long command"); + }); + + it("does not set a title tooltip when wrapping (default)", () => { + const block: ContentBlock = { type: "text", text: "some long command" }; + const { container } = renderWithMantine(); + expect(container.querySelector(".mantine-Code-root")).not.toHaveAttribute( + "title", + ); + }); + it("renders text as markdown when mimeType is text/markdown", () => { const block: ContentBlock = { type: "text", diff --git a/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx b/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx index 8ab5a60b..be37a0fa 100644 --- a/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx +++ b/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx @@ -14,6 +14,19 @@ export interface ContentViewerProps { * instead of as preformatted code. */ mimeType?: string; + /** + * Whether long plain-text content wraps onto multiple lines. When `false`, + * text is kept to a single line (overflow clipped with an ellipsis) so the + * viewer keeps a fixed height — used by hosts like the server card where the + * box height must stay constant regardless of command/URL length. The full + * value remains available via the copy button (and a native `title` + * tooltip). Defaults to `true`. + * + * Intended for single-line values only: `false` applies `white-space: + * nowrap`, which collapses embedded newlines (e.g. pretty-printed JSON) onto + * one line — don't pass it for multi-line content. + */ + wrap?: boolean; } function formatJson(content: string): string { @@ -66,6 +79,7 @@ export function ContentViewer({ block, copyable = false, mimeType, + wrap = true, }: ContentViewerProps) { switch (block.type) { case "text": { @@ -93,7 +107,14 @@ export function ContentViewer({ return ( - + {displayText} {copyable && ( diff --git a/clients/web/src/components/groups/ServerCard/ServerCard.tsx b/clients/web/src/components/groups/ServerCard/ServerCard.tsx index 864ccf6a..1c3c3482 100644 --- a/clients/web/src/components/groups/ServerCard/ServerCard.tsx +++ b/clients/web/src/components/groups/ServerCard/ServerCard.tsx @@ -202,6 +202,7 @@ export function ServerCard({ diff --git a/clients/web/src/components/screens/ServerListScreen/ServerListScreen.tsx b/clients/web/src/components/screens/ServerListScreen/ServerListScreen.tsx index e0e31e1f..4a94fbfc 100644 --- a/clients/web/src/components/screens/ServerListScreen/ServerListScreen.tsx +++ b/clients/web/src/components/screens/ServerListScreen/ServerListScreen.tsx @@ -147,7 +147,17 @@ export function ServerListScreen({ const grid = ( diff --git a/clients/web/src/theme/Code.ts b/clients/web/src/theme/Code.ts index 77609c59..7910c7f7 100644 --- a/clients/web/src/theme/Code.ts +++ b/clients/web/src/theme/Code.ts @@ -9,6 +9,14 @@ export const ThemeCode = Code.extend({ root.wordBreak = "break-all"; root.whiteSpace = "pre-wrap"; } + if (props.variant === "nowrap") { + // Single line, fixed height: clip overflow with an ellipsis rather than + // wrapping onto new lines (which would grow the container's height). The + // full value stays available via the adjacent copy button. + root.whiteSpace = "nowrap"; + root.overflow = "hidden"; + root.textOverflow = "ellipsis"; + } return { root }; }, });