mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-09-13 22:32:26 +03:00
Add code block language header and improve styles (#2403)
Some checks failed
Deploy to Netlify (dev) / Deploy to Netlify (push) Has been cancelled
Some checks failed
Deploy to Netlify (dev) / Deploy to Netlify (push) Has been cancelled
* add code block language header and improve styles * improve codeblock fallback text * move floating expand button to code block header * reduce code font size
This commit is contained in:
parent
d8d4714370
commit
31942b1114
3 changed files with 123 additions and 99 deletions
|
@ -157,12 +157,12 @@ export function RenderElement({ attributes, element, children }: RenderElementPr
|
||||||
<Text as="pre" className={css.CodeBlock} {...attributes}>
|
<Text as="pre" className={css.CodeBlock} {...attributes}>
|
||||||
<Scroll
|
<Scroll
|
||||||
direction="Horizontal"
|
direction="Horizontal"
|
||||||
variant="Secondary"
|
variant="SurfaceVariant"
|
||||||
size="300"
|
size="300"
|
||||||
visibility="Hover"
|
visibility="Hover"
|
||||||
hideTrack
|
hideTrack
|
||||||
>
|
>
|
||||||
<div className={css.CodeBlockInternal()}>{children}</div>
|
<div className={css.CodeBlockInternal}>{children}</div>
|
||||||
</Scroll>
|
</Scroll>
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,6 @@ import React, {
|
||||||
ReactEventHandler,
|
ReactEventHandler,
|
||||||
Suspense,
|
Suspense,
|
||||||
lazy,
|
lazy,
|
||||||
useCallback,
|
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
@ -17,7 +16,7 @@ import {
|
||||||
} from 'html-react-parser';
|
} from 'html-react-parser';
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Icon, IconButton, Icons, Scroll, Text } from 'folds';
|
import { Box, Chip, config, Header, Icon, IconButton, Icons, Scroll, Text, toRem } from 'folds';
|
||||||
import { IntermediateRepresentation, Opts as LinkifyOpts, OptFn } from 'linkifyjs';
|
import { IntermediateRepresentation, Opts as LinkifyOpts, OptFn } from 'linkifyjs';
|
||||||
import Linkify from 'linkify-react';
|
import Linkify from 'linkify-react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
@ -205,16 +204,13 @@ export const highlightText = (
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export function CodeBlock(children: ChildNode[], opts: HTMLReactParserOptions) {
|
|
||||||
const LINE_LIMIT = 14;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively extracts and concatenates all text content from an array of ChildNode objects.
|
* Recursively extracts and concatenates all text content from an array of ChildNode objects.
|
||||||
*
|
*
|
||||||
* @param {ChildNode[]} nodes - An array of ChildNode objects to extract text from.
|
* @param {ChildNode[]} nodes - An array of ChildNode objects to extract text from.
|
||||||
* @returns {string} The concatenated plain text content of all descendant text nodes.
|
* @returns {string} The concatenated plain text content of all descendant text nodes.
|
||||||
*/
|
*/
|
||||||
const extractTextFromChildren = useCallback((nodes: ChildNode[]): string => {
|
const extractTextFromChildren = (nodes: ChildNode[]): string => {
|
||||||
let text = '';
|
let text = '';
|
||||||
|
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
|
@ -226,58 +222,90 @@ export function CodeBlock(children: ChildNode[], opts: HTMLReactParserOptions) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const [copied, setCopied] = useTimeoutToggle();
|
export function CodeBlock({
|
||||||
const collapsible = useMemo(
|
children,
|
||||||
|
opts,
|
||||||
|
}: {
|
||||||
|
children: ChildNode[];
|
||||||
|
opts: HTMLReactParserOptions;
|
||||||
|
}) {
|
||||||
|
const code = children[0];
|
||||||
|
const languageClass =
|
||||||
|
code instanceof Element && code.name === 'code' ? code.attribs.class : undefined;
|
||||||
|
const language =
|
||||||
|
languageClass && languageClass.startsWith('language-')
|
||||||
|
? languageClass.replace('language-', '')
|
||||||
|
: languageClass;
|
||||||
|
|
||||||
|
const LINE_LIMIT = 14;
|
||||||
|
const largeCodeBlock = useMemo(
|
||||||
() => extractTextFromChildren(children).split('\n').length > LINE_LIMIT,
|
() => extractTextFromChildren(children).split('\n').length > LINE_LIMIT,
|
||||||
[children, extractTextFromChildren]
|
[children]
|
||||||
);
|
);
|
||||||
const [collapsed, setCollapsed] = useState(collapsible);
|
|
||||||
|
|
||||||
const handleCopy = useCallback(() => {
|
const [expanded, setExpand] = useState(false);
|
||||||
|
const [copied, setCopied] = useTimeoutToggle();
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
copyToClipboard(extractTextFromChildren(children));
|
copyToClipboard(extractTextFromChildren(children));
|
||||||
setCopied();
|
setCopied();
|
||||||
}, [children, extractTextFromChildren, setCopied]);
|
};
|
||||||
|
|
||||||
const toggleCollapse = useCallback(() => {
|
const toggleExpand = () => {
|
||||||
setCollapsed((prev) => !prev);
|
setExpand(!expanded);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Text size="T300" as="pre" className={css.CodeBlock}>
|
||||||
<div className={css.CodeBlockControls}>
|
<Header variant="Surface" size="400" className={css.CodeBlockHeader}>
|
||||||
<IconButton
|
<Box grow="Yes">
|
||||||
variant="Secondary" // Needs a better copy icon
|
<Text size="L400" truncate>
|
||||||
size="300"
|
{language ?? 'Code'}
|
||||||
radii="300"
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box shrink="No" gap="200">
|
||||||
|
<Chip
|
||||||
|
variant={copied ? 'Success' : 'Surface'}
|
||||||
|
fill="None"
|
||||||
|
radii="Pill"
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
aria-label="Copy Code Block"
|
before={copied && <Icon size="50" src={Icons.Check} />}
|
||||||
>
|
>
|
||||||
<Icon src={copied ? Icons.Check : Icons.File} size="50" />
|
<Text size="B300">{copied ? 'Copied' : 'Copy'}</Text>
|
||||||
</IconButton>
|
</Chip>
|
||||||
{collapsible && (
|
{largeCodeBlock && (
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="Secondary"
|
|
||||||
size="300"
|
size="300"
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
outlined
|
||||||
radii="300"
|
radii="300"
|
||||||
onClick={toggleCollapse}
|
onClick={toggleExpand}
|
||||||
aria-expanded={!collapsed}
|
aria-label={expanded ? 'Collapse' : 'Expand'}
|
||||||
aria-pressed={!collapsed}
|
|
||||||
aria-controls="code-block-content"
|
|
||||||
aria-label={collapsed ? 'Show Full Code Block' : 'Show Code Block Preview'}
|
|
||||||
style={collapsed ? { visibility: 'visible' } : {}}
|
|
||||||
>
|
>
|
||||||
<Icon src={collapsed ? Icons.ChevronBottom : Icons.ChevronTop} size="50" />
|
<Icon size="50" src={expanded ? Icons.ChevronTop : Icons.ChevronBottom} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Box>
|
||||||
<Scroll direction="Both" variant="Secondary" size="300" visibility="Hover" hideTrack>
|
</Header>
|
||||||
<div id="code-block-content" className={css.CodeBlockInternal({ collapsed })}>
|
<Scroll
|
||||||
|
style={{
|
||||||
|
maxHeight: largeCodeBlock && !expanded ? toRem(300) : undefined,
|
||||||
|
paddingBottom: largeCodeBlock ? config.space.S400 : undefined,
|
||||||
|
}}
|
||||||
|
direction="Both"
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
size="300"
|
||||||
|
visibility="Hover"
|
||||||
|
hideTrack
|
||||||
|
>
|
||||||
|
<div id="code-block-content" className={css.CodeBlockInternal}>
|
||||||
{domToReact(children, opts)}
|
{domToReact(children, opts)}
|
||||||
</div>
|
</div>
|
||||||
</Scroll>
|
</Scroll>
|
||||||
</>
|
{largeCodeBlock && !expanded && <Box className={css.CodeBlockBottomShadow} />}
|
||||||
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -355,11 +383,7 @@ export const getReactCustomHtmlParser = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'pre') {
|
if (name === 'pre') {
|
||||||
return (
|
return <CodeBlock opts={opts}>{children}</CodeBlock>;
|
||||||
<Text {...props} as="pre" className={css.CodeBlock}>
|
|
||||||
{CodeBlock(children, opts)}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'blockquote') {
|
if (name === 'blockquote') {
|
||||||
|
@ -409,9 +433,9 @@ export const getReactCustomHtmlParser = (
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<code className={css.Code} {...props}>
|
<Text as="code" size="T300" className={css.Code} {...props}>
|
||||||
{domToReact(children, opts)}
|
{domToReact(children, opts)}
|
||||||
</code>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,16 +41,19 @@ export const BlockQuote = style([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const BaseCode = style({
|
const BaseCode = style({
|
||||||
fontFamily: 'monospace',
|
color: color.SurfaceVariant.OnContainer,
|
||||||
color: color.Secondary.OnContainer,
|
background: color.SurfaceVariant.Container,
|
||||||
background: color.Secondary.Container,
|
border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
|
||||||
border: `${config.borderWidth.B300} solid ${color.Secondary.ContainerLine}`,
|
|
||||||
borderRadius: config.radii.R300,
|
borderRadius: config.radii.R300,
|
||||||
});
|
});
|
||||||
|
const CodeFont = style({
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
});
|
||||||
|
|
||||||
export const Code = style([
|
export const Code = style([
|
||||||
DefaultReset,
|
DefaultReset,
|
||||||
BaseCode,
|
BaseCode,
|
||||||
|
CodeFont,
|
||||||
{
|
{
|
||||||
padding: `0 ${config.space.S100}`,
|
padding: `0 ${config.space.S100}`,
|
||||||
},
|
},
|
||||||
|
@ -86,34 +89,31 @@ export const CodeBlock = style([
|
||||||
{
|
{
|
||||||
fontStyle: 'normal',
|
fontStyle: 'normal',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
export const CodeBlockInternal = recipe({
|
export const CodeBlockHeader = style({
|
||||||
base: {
|
padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
|
||||||
padding: `${config.space.S200} ${config.space.S200} 0`,
|
borderBottomWidth: config.borderWidth.B300,
|
||||||
minWidth: toRem(100),
|
gap: config.space.S200,
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
collapsed: {
|
|
||||||
true: {
|
|
||||||
maxHeight: `calc(${config.lineHeight.T400} * 9.6)`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
export const CodeBlockControls = style({
|
export const CodeBlockInternal = style([
|
||||||
|
CodeFont,
|
||||||
|
{
|
||||||
|
padding: `${config.space.S200} ${config.space.S200} 0`,
|
||||||
|
minWidth: toRem(200),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CodeBlockBottomShadow = style({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: config.space.S200,
|
bottom: 0,
|
||||||
right: config.space.S200,
|
left: 0,
|
||||||
visibility: 'hidden',
|
right: 0,
|
||||||
selectors: {
|
pointerEvents: 'none',
|
||||||
[`${CodeBlock}:hover &`]: {
|
|
||||||
visibility: 'visible',
|
height: config.space.S400,
|
||||||
},
|
background: `linear-gradient(to top, #00000022, #00000000)`,
|
||||||
[`${CodeBlock}:focus-within &`]: {
|
|
||||||
visibility: 'visible',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const List = style([
|
export const List = style([
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue