From 4833cbea8cb2b00cf3869d67f763278e968a330c Mon Sep 17 00:00:00 2001 From: Alena Batitskaia Date: Thu, 20 Aug 2026 13:26:06 +0100 Subject: [PATCH 1/3] feat(CopySnippet): add a wrap prop to soft-wrap long content By default CopySnippet keeps each line on one line and scrolls horizontally, which pushes long error messages and logs off to the right. `wrap` lays the content out on multiple lines instead: the block grows vertically to fit (so even a single very long line is fully readable rather than clamped to the collapsed height), and unbreakable runs like URLs, tokens and identifiers wrap too (overflow-wrap: anywhere), not just spaces. It reuses the existing multiline block layout and only adds white-space handling on the inner , so behaviour is unchanged when the prop is unused. `nowrap` still wins. `wrap` is omitted from the inherited Card container styles (it maps to flex-wrap, meaningless on this grid-based component) so the name can carry the boolean prop. Also fixes the docs, which showed a non-existent `multiline` prop. Co-Authored-By: Claude Fable 5 --- .changeset/copysnippet-wrap.md | 13 +++++++ .../content/CopySnippet/CopySnippet.docs.mdx | 10 +++--- .../CopySnippet/CopySnippet.stories.tsx | 10 ++++++ .../content/CopySnippet/CopySnippet.tsx | 35 +++++++++++++++++-- 4 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 .changeset/copysnippet-wrap.md diff --git a/.changeset/copysnippet-wrap.md b/.changeset/copysnippet-wrap.md new file mode 100644 index 000000000..0e1bedc08 --- /dev/null +++ b/.changeset/copysnippet-wrap.md @@ -0,0 +1,13 @@ +--- +'@cube-dev/ui-kit': minor +--- + +`CopySnippet` takes a `wrap` prop that soft-wraps long content instead of scrolling it sideways. + +By default a `CopySnippet` keeps each line on one line and scrolls horizontally, which buries long error messages and logs off to the right. `wrap` lays the content out on multiple lines instead: the block grows vertically to fit — so even a single very long line is fully readable rather than clamped to the collapsed height — and unbreakable runs like URLs, tokens and identifiers break too (`overflow-wrap: anywhere`), not just spaces. + +```jsx + +``` + +It has no effect when `nowrap` is set (that still forces a single scrolling line), and the copy button and syntax highlighting are unchanged. The `wrap` container style inherited from `Card` (`flex-wrap`, meaningless on this grid-based component) is no longer part of the public props. diff --git a/src/components/content/CopySnippet/CopySnippet.docs.mdx b/src/components/content/CopySnippet/CopySnippet.docs.mdx index 0a83e4e19..49aa407ac 100644 --- a/src/components/content/CopySnippet/CopySnippet.docs.mdx +++ b/src/components/content/CopySnippet/CopySnippet.docs.mdx @@ -21,6 +21,7 @@ A code block with copy-to-clipboard functionality and syntax highlighting. Exten - **`title`** `string` (default: `'Code example'`) — Accessible title used in the copy toast message - **`prefix`** `string` (default: `''`) — Prefix for each line of code (e.g. `'$ '` for bash snippets) - **`nowrap`** `boolean` — Force single-line display even for multi-line code +- **`wrap`** `boolean` — Soft-wrap long content onto multiple lines instead of scrolling it horizontally. The block grows to fit, so even a single long line stays fully readable, and unbreakable runs (URLs, tokens, identifiers) wrap too. Useful for error messages and logs. Ignored when `nowrap` is set. - **`serif`** `boolean` — Use serif (non-monospace) font for the code - **`hideText`** `string | string[] | boolean` — Hide sensitive text with bullet characters. `true` hides all text, a string hides that substring, an array hides multiple substrings. A toggle button appears to reveal hidden content. - **`actions`** `ReactNode` — Additional action buttons displayed alongside the copy button. Use `CopySnippet.Button` for consistent styling. @@ -55,14 +56,13 @@ These properties allow direct style application without using the `styles` prop: ```jsx - + Save'} language="html" /> + +// Wrap a long single-line error instead of scrolling it sideways + ``` diff --git a/src/components/content/CopySnippet/CopySnippet.stories.tsx b/src/components/content/CopySnippet/CopySnippet.stories.tsx index b31ee5e9c..6f9f79488 100644 --- a/src/components/content/CopySnippet/CopySnippet.stories.tsx +++ b/src/components/content/CopySnippet/CopySnippet.stories.tsx @@ -65,6 +65,16 @@ WithScroll.args = { }, }; +export const Wrap = Template.bind({}); +Wrap.args = { + code: 'XMLA Internal Error: Arrow error: External error: Database Execution Error: Internal: Error during planning: Error decoding LogicalPlanNode.logical_plan_type:SubqueryAliasNode.input as protobuf message', + language: 'bash', + wrap: true, + styles: { + width: 'max 400px', + }, +}; + export const JavascriptSyntax = Template.bind({}); JavascriptSyntax.args = { language: 'javascript', diff --git a/src/components/content/CopySnippet/CopySnippet.tsx b/src/components/content/CopySnippet/CopySnippet.tsx index 0f5d8e367..8ea3f2dfa 100644 --- a/src/components/content/CopySnippet/CopySnippet.tsx +++ b/src/components/content/CopySnippet/CopySnippet.tsx @@ -62,6 +62,18 @@ const StyledBlock = tasty({ '': 'monospace', serif: true, }, + // The `` renders as `white-space: pre`, so setting `pre-wrap` on the + // surrounding block alone does not wrap it — the wrap has to live here. + // `overflow-wrap: anywhere` also breaks runs with no spaces (URLs, tokens, + // identifiers) that `pre-wrap` on its own would let overflow. + whiteSpace: { + '': 'pre', + wrap: 'pre-wrap', + }, + overflowWrap: { + '': 'normal', + wrap: 'anywhere', + }, }, }, }); @@ -153,13 +165,24 @@ const CopyButton = tasty(ActionButton, { const ShowButton = tasty(ActionButton, {}); -export interface CubeCopySnippetProps extends CubeCardProps { +// `wrap` is omitted from the inherited container styles (it maps to `flex-wrap`, +// which is meaningless on this grid-based component) so the name can carry the +// boolean "soft-wrap the code" prop below. +export interface CubeCopySnippetProps extends Omit { /** The code snippet */ code: string; /** The title of the snippet */ title?: string; /** Whether the snippet is single-lined */ nowrap?: boolean; + /** + * Soft-wrap long content onto multiple lines instead of scrolling it + * horizontally. The block grows vertically to fit (so even a single long line + * is fully readable rather than clamped), and unbreakable runs like URLs, + * tokens and identifiers wrap too. Useful for error messages and logs. + * Has no effect when `nowrap` is set. + */ + wrap?: boolean; /** The prefix for each line of code. Useful for bash snippets. */ prefix?: string; /** The code language of the snippet */ @@ -186,6 +209,7 @@ function CopySnippet(allProps: CubeCopySnippetProps) { code = '', title = t('copySnippet.title', 'Code example'), nowrap, + wrap, prefix = '', language, serif, @@ -208,6 +232,10 @@ function CopySnippet(allProps: CubeCopySnippetProps) { const pristineCode = code.replace(/\n$/, ''); const multiline = pristineCode.includes('\n') && !nowrap; + // `wrap` reuses the multiline block layout (auto height, no right fade, copy + // button on top) so wrapped content grows vertically instead of being clamped + // to the single-line height. `nowrap` (force one scrolling line) wins over it. + const shouldWrap = !!wrap && !nowrap; let formattedCode = pristineCode .replace(/\r/g, '') .split(/\n/g) @@ -236,11 +264,12 @@ function CopySnippet(allProps: CubeCopySnippetProps) { const mods = useMemo(() => { return { nowrap, - multiline, + multiline: multiline || shouldWrap, + wrap: shouldWrap, serif, hidden: !!hideText, }; - }, [nowrap, multiline, hideText, serif]); + }, [nowrap, multiline, shouldWrap, hideText, serif]); const Snippet = ( From 535dfcabaf6f4b5e1b74690d3bf5b8064e5c0f27 Mon Sep 17 00:00:00 2001 From: Alena Batitskaia Date: Thu, 20 Aug 2026 13:45:25 +0100 Subject: [PATCH 2/3] refactor(CopySnippet): rename wrap prop to isWrapped per boolean naming convention --- .changeset/copysnippet-wrap.md | 8 +++---- .../content/CopySnippet/CopySnippet.docs.mdx | 4 ++-- .../CopySnippet/CopySnippet.stories.tsx | 6 ++--- .../content/CopySnippet/CopySnippet.tsx | 22 +++++++++---------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.changeset/copysnippet-wrap.md b/.changeset/copysnippet-wrap.md index 0e1bedc08..68dea49e4 100644 --- a/.changeset/copysnippet-wrap.md +++ b/.changeset/copysnippet-wrap.md @@ -2,12 +2,12 @@ '@cube-dev/ui-kit': minor --- -`CopySnippet` takes a `wrap` prop that soft-wraps long content instead of scrolling it sideways. +`CopySnippet` takes an `isWrapped` prop that soft-wraps long content instead of scrolling it sideways. -By default a `CopySnippet` keeps each line on one line and scrolls horizontally, which buries long error messages and logs off to the right. `wrap` lays the content out on multiple lines instead: the block grows vertically to fit — so even a single very long line is fully readable rather than clamped to the collapsed height — and unbreakable runs like URLs, tokens and identifiers break too (`overflow-wrap: anywhere`), not just spaces. +By default a `CopySnippet` keeps each line on one line and scrolls horizontally, which buries long error messages and logs off to the right. `isWrapped` lays the content out on multiple lines instead: the block grows vertically to fit — so even a single very long line is fully readable rather than clamped to the collapsed height — and unbreakable runs like URLs, tokens and identifiers break too (`overflow-wrap: anywhere`), not just spaces. ```jsx - + ``` -It has no effect when `nowrap` is set (that still forces a single scrolling line), and the copy button and syntax highlighting are unchanged. The `wrap` container style inherited from `Card` (`flex-wrap`, meaningless on this grid-based component) is no longer part of the public props. +It is a different axis from `nowrap` — `nowrap` collapses real newlines into one scrolling line, `isWrapped` breaks long lines — and `nowrap` wins when both are set. The copy button and syntax highlighting are unchanged. diff --git a/src/components/content/CopySnippet/CopySnippet.docs.mdx b/src/components/content/CopySnippet/CopySnippet.docs.mdx index 49aa407ac..7aaa141e0 100644 --- a/src/components/content/CopySnippet/CopySnippet.docs.mdx +++ b/src/components/content/CopySnippet/CopySnippet.docs.mdx @@ -21,7 +21,7 @@ A code block with copy-to-clipboard functionality and syntax highlighting. Exten - **`title`** `string` (default: `'Code example'`) — Accessible title used in the copy toast message - **`prefix`** `string` (default: `''`) — Prefix for each line of code (e.g. `'$ '` for bash snippets) - **`nowrap`** `boolean` — Force single-line display even for multi-line code -- **`wrap`** `boolean` — Soft-wrap long content onto multiple lines instead of scrolling it horizontally. The block grows to fit, so even a single long line stays fully readable, and unbreakable runs (URLs, tokens, identifiers) wrap too. Useful for error messages and logs. Ignored when `nowrap` is set. +- **`isWrapped`** `boolean` — Soft-wrap long content onto multiple lines instead of scrolling it horizontally. The block grows to fit, so even a single long line stays fully readable, and unbreakable runs (URLs, tokens, identifiers) wrap too. Useful for error messages and logs. Ignored when `nowrap` is set (a different axis: `nowrap` collapses newlines into one scrolling line, `isWrapped` breaks long lines). - **`serif`** `boolean` — Use serif (non-monospace) font for the code - **`hideText`** `string | string[] | boolean` — Hide sensitive text with bullet characters. `true` hides all text, a string hides that substring, an array hides multiple substrings. A toggle button appears to reveal hidden content. - **`actions`** `ReactNode` — Additional action buttons displayed alongside the copy button. Use `CopySnippet.Button` for consistent styling. @@ -64,5 +64,5 @@ These properties allow direct style application without using the `styles` prop: /> // Wrap a long single-line error instead of scrolling it sideways - + ``` diff --git a/src/components/content/CopySnippet/CopySnippet.stories.tsx b/src/components/content/CopySnippet/CopySnippet.stories.tsx index 6f9f79488..3b744aa10 100644 --- a/src/components/content/CopySnippet/CopySnippet.stories.tsx +++ b/src/components/content/CopySnippet/CopySnippet.stories.tsx @@ -65,11 +65,11 @@ WithScroll.args = { }, }; -export const Wrap = Template.bind({}); -Wrap.args = { +export const Wrapped = Template.bind({}); +Wrapped.args = { code: 'XMLA Internal Error: Arrow error: External error: Database Execution Error: Internal: Error during planning: Error decoding LogicalPlanNode.logical_plan_type:SubqueryAliasNode.input as protobuf message', language: 'bash', - wrap: true, + isWrapped: true, styles: { width: 'max 400px', }, diff --git a/src/components/content/CopySnippet/CopySnippet.tsx b/src/components/content/CopySnippet/CopySnippet.tsx index 8ea3f2dfa..a789ce068 100644 --- a/src/components/content/CopySnippet/CopySnippet.tsx +++ b/src/components/content/CopySnippet/CopySnippet.tsx @@ -165,10 +165,7 @@ const CopyButton = tasty(ActionButton, { const ShowButton = tasty(ActionButton, {}); -// `wrap` is omitted from the inherited container styles (it maps to `flex-wrap`, -// which is meaningless on this grid-based component) so the name can carry the -// boolean "soft-wrap the code" prop below. -export interface CubeCopySnippetProps extends Omit { +export interface CubeCopySnippetProps extends CubeCardProps { /** The code snippet */ code: string; /** The title of the snippet */ @@ -180,9 +177,11 @@ export interface CubeCopySnippetProps extends Omit { * horizontally. The block grows vertically to fit (so even a single long line * is fully readable rather than clamped), and unbreakable runs like URLs, * tokens and identifiers wrap too. Useful for error messages and logs. - * Has no effect when `nowrap` is set. + * Has no effect when `nowrap` is set. Note this is a different axis from + * `nowrap`: `nowrap` collapses real newlines into one scrolling line, while + * `isWrapped` breaks long lines that would otherwise scroll. */ - wrap?: boolean; + isWrapped?: boolean; /** The prefix for each line of code. Useful for bash snippets. */ prefix?: string; /** The code language of the snippet */ @@ -209,7 +208,7 @@ function CopySnippet(allProps: CubeCopySnippetProps) { code = '', title = t('copySnippet.title', 'Code example'), nowrap, - wrap, + isWrapped, prefix = '', language, serif, @@ -232,10 +231,11 @@ function CopySnippet(allProps: CubeCopySnippetProps) { const pristineCode = code.replace(/\n$/, ''); const multiline = pristineCode.includes('\n') && !nowrap; - // `wrap` reuses the multiline block layout (auto height, no right fade, copy - // button on top) so wrapped content grows vertically instead of being clamped - // to the single-line height. `nowrap` (force one scrolling line) wins over it. - const shouldWrap = !!wrap && !nowrap; + // `isWrapped` reuses the multiline block layout (auto height, no right fade, + // copy button on top) so wrapped content grows vertically instead of being + // clamped to the single-line height. `nowrap` (force one scrolling line) wins + // over it. + const shouldWrap = !!isWrapped && !nowrap; let formattedCode = pristineCode .replace(/\r/g, '') .split(/\n/g) From c508ffc5d7ccae20485f65c75025e9e7bdce3989 Mon Sep 17 00:00:00 2001 From: Alena Batitskaia Date: Thu, 20 Aug 2026 14:22:00 +0100 Subject: [PATCH 3/3] feat(PrismCode): add an isWrapped prop and forward it from CopySnippet --- .changeset/copysnippet-wrap.md | 7 +++--- .../content/CopySnippet/CopySnippet.tsx | 17 ++++--------- .../content/PrismCode/PrismCode.docs.mdx | 1 + .../content/PrismCode/PrismCode.stories.tsx | 10 ++++++++ .../content/PrismCode/PrismCode.tsx | 24 +++++++++++++++++-- .../PrismCode/__tests__/PrismCode.test.tsx | 14 +++++++++++ 6 files changed, 55 insertions(+), 18 deletions(-) diff --git a/.changeset/copysnippet-wrap.md b/.changeset/copysnippet-wrap.md index 68dea49e4..ad42fb582 100644 --- a/.changeset/copysnippet-wrap.md +++ b/.changeset/copysnippet-wrap.md @@ -2,12 +2,13 @@ '@cube-dev/ui-kit': minor --- -`CopySnippet` takes an `isWrapped` prop that soft-wraps long content instead of scrolling it sideways. +`PrismCode` and `CopySnippet` take an `isWrapped` prop that soft-wraps long content instead of scrolling it sideways. -By default a `CopySnippet` keeps each line on one line and scrolls horizontally, which buries long error messages and logs off to the right. `isWrapped` lays the content out on multiple lines instead: the block grows vertically to fit — so even a single very long line is fully readable rather than clamped to the collapsed height — and unbreakable runs like URLs, tokens and identifiers break too (`overflow-wrap: anywhere`), not just spaces. +By default both components keep each line on one line and scroll horizontally, which buries long error messages and logs off to the right. `isWrapped` lays the content out on multiple lines instead: unbreakable runs like URLs, tokens and identifiers break too (`overflow-wrap: anywhere`), not just spaces. On `CopySnippet` the block additionally grows vertically to fit — so even a single very long line is fully readable rather than clamped to the collapsed height — and the prop is forwarded to the inner `PrismCode`, which owns the wrapping itself. ```jsx + ``` -It is a different axis from `nowrap` — `nowrap` collapses real newlines into one scrolling line, `isWrapped` breaks long lines — and `nowrap` wins when both are set. The copy button and syntax highlighting are unchanged. +On `CopySnippet` it is a different axis from `nowrap` — `nowrap` collapses real newlines into one scrolling line, `isWrapped` breaks long lines — and `nowrap` wins when both are set. The copy button and syntax highlighting are unchanged. diff --git a/src/components/content/CopySnippet/CopySnippet.tsx b/src/components/content/CopySnippet/CopySnippet.tsx index a789ce068..0db4dfcab 100644 --- a/src/components/content/CopySnippet/CopySnippet.tsx +++ b/src/components/content/CopySnippet/CopySnippet.tsx @@ -62,18 +62,9 @@ const StyledBlock = tasty({ '': 'monospace', serif: true, }, - // The `` renders as `white-space: pre`, so setting `pre-wrap` on the - // surrounding block alone does not wrap it — the wrap has to live here. - // `overflow-wrap: anywhere` also breaks runs with no spaces (URLs, tokens, - // identifiers) that `pre-wrap` on its own would let overflow. - whiteSpace: { - '': 'pre', - wrap: 'pre-wrap', - }, - overflowWrap: { - '': 'normal', - wrap: 'anywhere', - }, + // The wrapping itself (`white-space` / `overflow-wrap` on the ``) + // is owned by `PrismCode` via its `isWrapped` prop, which this component + // forwards. }, }, }); @@ -265,7 +256,6 @@ function CopySnippet(allProps: CubeCopySnippetProps) { return { nowrap, multiline: multiline || shouldWrap, - wrap: shouldWrap, serif, hidden: !!hideText, }; @@ -279,6 +269,7 @@ function CopySnippet(allProps: CubeCopySnippetProps) { style={{ margin: 0, overflow: 'visible' }} code={formattedCode} language={language || 'javascript'} + isWrapped={shouldWrap} /> diff --git a/src/components/content/PrismCode/PrismCode.docs.mdx b/src/components/content/PrismCode/PrismCode.docs.mdx index 21720ad93..56359f9f7 100644 --- a/src/components/content/PrismCode/PrismCode.docs.mdx +++ b/src/components/content/PrismCode/PrismCode.docs.mdx @@ -18,6 +18,7 @@ Syntax-highlighted code block using Prism. Supports multiple languages. - **`code`** `string` (default: `''`) — The code snippet to highlight - **`language`** `string` (default: `'javascript'`) — Language for syntax highlighting (`javascript`, `typescript`, `json`, `yaml`, `bash`, `sql`, `css`, `html`, etc.) +- **`isWrapped`** `boolean` — Soft-wrap long lines onto multiple lines instead of scrolling them horizontally. Unbreakable runs (URLs, tokens, identifiers) wrap too. Useful for error messages and logs. ### Style Defaults diff --git a/src/components/content/PrismCode/PrismCode.stories.tsx b/src/components/content/PrismCode/PrismCode.stories.tsx index 846bd4950..d1f8f7b28 100644 --- a/src/components/content/PrismCode/PrismCode.stories.tsx +++ b/src/components/content/PrismCode/PrismCode.stories.tsx @@ -30,6 +30,16 @@ export const MultiLine = { }, }; +export const Wrapped = { + render: Template, + args: { + language: 'bash', + isWrapped: true, + width: 'max 400px', + code: 'XMLA Internal Error: Arrow error: External error: Database Execution Error: Internal: Error during planning: Error decoding LogicalPlanNode.logical_plan_type:SubqueryAliasNode.input as protobuf message', + }, +}; + export const JavascriptSyntax = { render: Template, args: { diff --git a/src/components/content/PrismCode/PrismCode.tsx b/src/components/content/PrismCode/PrismCode.tsx index 7d21d4f6f..0e33623c0 100644 --- a/src/components/content/PrismCode/PrismCode.tsx +++ b/src/components/content/PrismCode/PrismCode.tsx @@ -31,6 +31,20 @@ const PreElement = tasty({ Code: { display: 'block', preset: 's3', + // The global Prism CSS pins `white-space: pre` on + // `code[class*="language-"]`, so the wrap has to be re-declared right on + // the `` element — `pre-wrap` on the surrounding block alone does + // not cascade past it. `overflow-wrap: anywhere` also breaks runs with no + // spaces (URLs, tokens, identifiers) that `pre-wrap` on its own would let + // overflow. + whiteSpace: { + '': 'pre', + wrapped: 'pre-wrap', + }, + overflowWrap: { + '': 'normal', + wrapped: 'anywhere', + }, }, }, }); @@ -39,6 +53,12 @@ export interface CubePrismCodeProps extends ContainerStyleProps { /** The CSS style map */ style?: BaseProps['style']; styles?: Styles; + /** + * Soft-wrap long lines onto multiple lines instead of scrolling them + * horizontally. Unbreakable runs like URLs, tokens and identifiers wrap too. + * Useful for error messages and logs. + */ + isWrapped?: boolean; /** The code snippet */ code?: string; /** The language of the code snippet */ @@ -82,7 +102,7 @@ function isDiffCode(code: string): boolean { } function PrismCode(props: CubePrismCodeProps, ref) { - let { code = '', language = 'javascript', ...otherProps } = props; + let { code = '', language = 'javascript', isWrapped, ...otherProps } = props; if (!code) { code = ''; @@ -109,7 +129,7 @@ function PrismCode(props: CubePrismCodeProps, ref) { } return ( - + {({ className, style, tokens, getLineProps, getTokenProps }) => { return ( diff --git a/src/components/content/PrismCode/__tests__/PrismCode.test.tsx b/src/components/content/PrismCode/__tests__/PrismCode.test.tsx index 099302696..7f3512191 100644 --- a/src/components/content/PrismCode/__tests__/PrismCode.test.tsx +++ b/src/components/content/PrismCode/__tests__/PrismCode.test.tsx @@ -93,4 +93,18 @@ describe('PrismCode component', () => { expect(codeElement?.querySelector('.token.keyword')).toBeInTheDocument(); expect(codeElement?.querySelector('.token.string')).toBeInTheDocument(); }); + + test('sets the wrapped mod only when isWrapped is passed', () => { + const code = 'a very long single-line error message'; + + const { container: plain } = render( + , + ); + const { container: wrapped } = render( + , + ); + + expect(plain.querySelector('pre')).not.toHaveAttribute('data-wrapped'); + expect(wrapped.querySelector('pre')).toHaveAttribute('data-wrapped'); + }); });