feat: word-wrap toggle for code blocks
Add a persistent wrap/nowrap toggle button in the code block header bar, consistent with the existing tool call content viewer. Persisted in localStorage. Default is nowrap (existing behavior).
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useCallback, type HTMLAttributes, type ReactElement } from 'react';
|
import { useState, useCallback, type HTMLAttributes, type ReactElement } from 'react';
|
||||||
import { Check, Copy, Hash } from 'lucide-react';
|
import { Check, Copy, Hash, WrapText, AlignLeft } from 'lucide-react';
|
||||||
|
|
||||||
/** Extract the language from the nested <code> element's className (e.g. "language-ts"). */
|
/** Extract the language from the nested <code> element's className (e.g. "language-ts"). */
|
||||||
function extractLanguage(children: React.ReactNode): string | null {
|
function extractLanguage(children: React.ReactNode): string | null {
|
||||||
@@ -43,6 +43,7 @@ function formatLanguage(lang: string): string {
|
|||||||
* Wraps code blocks with a language label and a floating copy button.
|
* Wraps code blocks with a language label and a floating copy button.
|
||||||
*/
|
*/
|
||||||
const LINE_NUMBER_KEY = 'pinchchat-line-numbers';
|
const LINE_NUMBER_KEY = 'pinchchat-line-numbers';
|
||||||
|
const WRAP_KEY = 'pinchchat-code-wrap';
|
||||||
const LINE_THRESHOLD = 3; // Only show line numbers for blocks with more than this many lines
|
const LINE_THRESHOLD = 3; // Only show line numbers for blocks with more than this many lines
|
||||||
|
|
||||||
export function CodeBlock(props: HTMLAttributes<HTMLPreElement>) {
|
export function CodeBlock(props: HTMLAttributes<HTMLPreElement>) {
|
||||||
@@ -51,6 +52,10 @@ export function CodeBlock(props: HTMLAttributes<HTMLPreElement>) {
|
|||||||
const stored = localStorage.getItem(LINE_NUMBER_KEY);
|
const stored = localStorage.getItem(LINE_NUMBER_KEY);
|
||||||
return stored === null ? true : stored === 'true';
|
return stored === null ? true : stored === 'true';
|
||||||
});
|
});
|
||||||
|
const [wordWrap, setWordWrap] = useState(() => {
|
||||||
|
const stored = localStorage.getItem(WRAP_KEY);
|
||||||
|
return stored === 'true';
|
||||||
|
});
|
||||||
const language = extractLanguage(props.children);
|
const language = extractLanguage(props.children);
|
||||||
|
|
||||||
const code = (props.children as ReactElement<{ children?: string }> | undefined)?.props?.children;
|
const code = (props.children as ReactElement<{ children?: string }> | undefined)?.props?.children;
|
||||||
@@ -74,6 +79,14 @@ export function CodeBlock(props: HTMLAttributes<HTMLPreElement>) {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const toggleWrap = useCallback(() => {
|
||||||
|
setWordWrap(prev => {
|
||||||
|
const next = !prev;
|
||||||
|
localStorage.setItem(WRAP_KEY, String(next));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const shouldShowNumbers = showLineNumbers && hasEnoughLines;
|
const shouldShowNumbers = showLineNumbers && hasEnoughLines;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -81,17 +94,27 @@ export function CodeBlock(props: HTMLAttributes<HTMLPreElement>) {
|
|||||||
{language && (
|
{language && (
|
||||||
<div className="flex items-center justify-between px-4 py-1.5 bg-pc-elevated/80 border-b border-pc-border rounded-t-lg text-[11px] text-pc-text-muted font-mono select-none">
|
<div className="flex items-center justify-between px-4 py-1.5 bg-pc-elevated/80 border-b border-pc-border rounded-t-lg text-[11px] text-pc-text-muted font-mono select-none">
|
||||||
<span>{formatLanguage(language)}</span>
|
<span>{formatLanguage(language)}</span>
|
||||||
{hasEnoughLines && (
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={toggleLineNumbers}
|
onClick={toggleWrap}
|
||||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded hover:bg-pc-border/40 transition-colors text-pc-text-muted hover:text-pc-text-secondary"
|
className="flex items-center gap-1 px-1.5 py-0.5 rounded hover:bg-pc-border/40 transition-colors text-pc-text-muted hover:text-pc-text-secondary"
|
||||||
title={showLineNumbers ? 'Hide line numbers' : 'Show line numbers'}
|
title={wordWrap ? 'Disable word wrap' : 'Enable word wrap'}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Hash className="h-3 w-3" />
|
{wordWrap ? <AlignLeft className="h-3 w-3" /> : <WrapText className="h-3 w-3" />}
|
||||||
<span className="text-[10px]">{lines.length}</span>
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
{hasEnoughLines && (
|
||||||
|
<button
|
||||||
|
onClick={toggleLineNumbers}
|
||||||
|
className="flex items-center gap-1 px-1.5 py-0.5 rounded hover:bg-pc-border/40 transition-colors text-pc-text-muted hover:text-pc-text-secondary"
|
||||||
|
title={showLineNumbers ? 'Hide line numbers' : 'Show line numbers'}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Hash className="h-3 w-3" />
|
||||||
|
<span className="text-[10px]">{lines.length}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{shouldShowNumbers ? (
|
{shouldShowNumbers ? (
|
||||||
@@ -104,10 +127,10 @@ export function CodeBlock(props: HTMLAttributes<HTMLPreElement>) {
|
|||||||
<div key={i}>{i + 1}</div>
|
<div key={i}>{i + 1}</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<pre {...props} className={`${props.className || ''} flex-1 !rounded-none !mt-0 min-w-0`} />
|
<pre {...props} className={`${props.className || ''} flex-1 !rounded-none !mt-0 min-w-0`} style={wordWrap ? { whiteSpace: 'pre-wrap', overflowWrap: 'break-word', wordBreak: 'break-word' } : undefined} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<pre {...props} className={`${props.className || ''} ${language ? '!rounded-t-none !mt-0' : ''}`} />
|
<pre {...props} className={`${props.className || ''} ${language ? '!rounded-t-none !mt-0' : ''}`} style={wordWrap ? { whiteSpace: 'pre-wrap', overflowWrap: 'break-word', wordBreak: 'break-word' } : undefined} />
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
|
|||||||
Reference in New Issue
Block a user