diff --git a/.changeset/copysnippet-wrap.md b/.changeset/copysnippet-wrap.md
new file mode 100644
index 000000000..ad42fb582
--- /dev/null
+++ b/.changeset/copysnippet-wrap.md
@@ -0,0 +1,14 @@
+---
+'@cube-dev/ui-kit': minor
+---
+
+`PrismCode` and `CopySnippet` take an `isWrapped` prop that soft-wraps long content instead of scrolling it sideways.
+
+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
+
+
+```
+
+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.docs.mdx b/src/components/content/CopySnippet/CopySnippet.docs.mdx
index 0a83e4e19..7aaa141e0 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
+- **`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.
@@ -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..3b744aa10 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 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',
+ isWrapped: 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..0db4dfcab 100644
--- a/src/components/content/CopySnippet/CopySnippet.tsx
+++ b/src/components/content/CopySnippet/CopySnippet.tsx
@@ -62,6 +62,9 @@ const StyledBlock = tasty({
'': 'monospace',
serif: true,
},
+ // The wrapping itself (`white-space` / `overflow-wrap` on the ``)
+ // is owned by `PrismCode` via its `isWrapped` prop, which this component
+ // forwards.
},
},
});
@@ -160,6 +163,16 @@ export interface CubeCopySnippetProps extends CubeCardProps {
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. 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.
+ */
+ isWrapped?: boolean;
/** The prefix for each line of code. Useful for bash snippets. */
prefix?: string;
/** The code language of the snippet */
@@ -186,6 +199,7 @@ function CopySnippet(allProps: CubeCopySnippetProps) {
code = '',
title = t('copySnippet.title', 'Code example'),
nowrap,
+ isWrapped,
prefix = '',
language,
serif,
@@ -208,6 +222,11 @@ function CopySnippet(allProps: CubeCopySnippetProps) {
const pristineCode = code.replace(/\n$/, '');
const multiline = pristineCode.includes('\n') && !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)
@@ -236,11 +255,11 @@ function CopySnippet(allProps: CubeCopySnippetProps) {
const mods = useMemo(() => {
return {
nowrap,
- multiline,
+ multiline: multiline || shouldWrap,
serif,
hidden: !!hideText,
};
- }, [nowrap, multiline, hideText, serif]);
+ }, [nowrap, multiline, shouldWrap, hideText, serif]);
const Snippet = (
@@ -250,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');
+ });
});