Skip to content

Commit

Permalink
Merge pull request #97 from ant-design/feat/Snippet
Browse files Browse the repository at this point in the history
✨ feat: new Components Snippet
  • Loading branch information
rdmclin2 authored Nov 6, 2023
2 parents 6096f12 + efa1bb1 commit 25bb6ec
Show file tree
Hide file tree
Showing 17 changed files with 701 additions and 90 deletions.
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
"react-rnd": "^10.4.1",
"reactflow": "^11.8.3",
"rxjs": "^7.8.1",
"shiki-es": "~0.2.0",
"shikiji": "^0.6.12",
"type-fest": "^3.13.1",
"umi-request": "^1.4.0",
"use-merge-value": "^1.2.0",
Expand Down Expand Up @@ -141,7 +141,6 @@
"husky": "^8.0.3",
"jsdom": "^22.1.0",
"lint-staged": "^13.3.0",
"lucide-react": "latest",
"prettier-plugin-organize-imports": "^3.2.3",
"prettier-plugin-packagejson": "^2.4.5",
"react": "^18.2.0",
Expand Down
5 changes: 2 additions & 3 deletions src/ContextMenu/demos/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { CopyOutlined, ZoomInOutlined, ZoomOutOutlined } from '@ant-design/icons';
import { CopyOutlined, ExpandOutlined, ZoomInOutlined, ZoomOutOutlined } from '@ant-design/icons';
import { ContextMenu } from '@ant-design/pro-editor';
import { BoxSelectIcon } from 'lucide-react';

export default () => {
return (
Expand All @@ -17,7 +16,7 @@ export default () => {
{
key: 'selectAll',
label: '选择全部',
icon: <BoxSelectIcon width={'1em'} height={'1em'} />,
icon: <ExpandOutlined width={'1em'} height={'1em'} />,
shortcut: ['meta', 'A'],
},
{ label: '放大', key: 'zoomIn', icon: <ZoomInOutlined /> },
Expand Down
10 changes: 9 additions & 1 deletion src/Highlight/components/HighLighter/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* 如果没有在 https://github.com/highlightjs/highlight.js/tree/master/src/languages 中查找是否支持,然后添加
* 优先支持主流语言,没有import在代码中使用的不会打包
*/
import { THEME_LIGHT } from '@/Highlight/theme';
import { STUDIO_UI_PREFIX } from '@/theme';
import { Loading3QuartersOutlined as Loading } from '@ant-design/icons';
import classNames from 'classnames';
import { Center } from 'react-layout-kit';
Expand All @@ -18,7 +20,13 @@ export type ShikiProps = Pick<
>;

const HighLighter: React.FC<ShikiProps> = (props) => {
const { children, lineNumber = false, theme, language, prefixCls } = props;
const {
children,
lineNumber = false,
theme = THEME_LIGHT,
language,
prefixCls = STUDIO_UI_PREFIX,
} = props;
const { styles } = useStyles({ prefixCls, lineNumber, theme });
const { renderShiki, loading } = useShiki(language, theme);

Expand Down
5 changes: 5 additions & 0 deletions src/Highlight/demos/config.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 9 additions & 4 deletions src/Highlight/hooks/useHighlight.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import hljs from 'highlight.js/lib/core';
import { useEffect } from 'react';

import { default as bash, default as sh } from 'highlight.js/lib/languages/bash';
import css from 'highlight.js/lib/languages/css';
import java from 'highlight.js/lib/languages/java';
import { default as javascript, default as jsx } from 'highlight.js/lib/languages/javascript';
Expand All @@ -26,6 +27,8 @@ export const languageMap = {
java,
python,
sql,
bash,
sh,
};

export const useHighlight = (language) => {
Expand All @@ -41,10 +44,12 @@ export const useHighlight = (language) => {
}, [language]);

const renderHighlight = (content) => {
const result = (
language ? hljs.highlight(language, content || '') : hljs.highlightAuto(content)
)?.value;

let result = null;
if (language & languageMap[language]) {
result = hljs.highlight(language, content || '').value;
} else {
result = hljs.highlightAuto(content).value;
}
return result;
};
return { renderHighlight };
Expand Down
8 changes: 2 additions & 6 deletions src/Highlight/hooks/useShiki.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { useEffect, useState } from 'react';
import { getHighlighter, setCDN, type Highlighter } from 'shiki-es';
import { getHighlighter, type Highlighter } from 'shikiji';
import { themeConfig } from '../theme';

// 国内使用 CDN 加速, 测试环境为 node,会加载失败
if (process.env.NODE_ENV !== 'test') {
setCDN('https://npm.elemecdn.com/shiki-es/dist/assets');
}

// 目前支持的语言列表
export const languageMap = [
'javascript',
Expand All @@ -21,6 +16,7 @@ export const languageMap = [
'java',
'python',
'sql',
'sh',
];

export const useShiki = (language, theme) => {
Expand Down
4 changes: 2 additions & 2 deletions src/Highlight/wrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DownOutlined, RightOutlined } from '@ant-design/icons';
import { ActionIcon, Button, Select, type SelectProps } from '@ant-design/pro-editor';
import classNames from 'classnames';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { memo, useState } from 'react';
import { DivProps, Flexbox } from 'react-layout-kit';
import { getPrefixCls } from '..';
Expand Down Expand Up @@ -49,7 +49,7 @@ export const FullFeatureWrapper = memo<HighlighterWrapperProps & HighlightProps>
<Flexbox align={'center'} className={styles.header} horizontal justify={'space-between'}>
<ActionIcon
className={styles.expandIcon}
icon={expand ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
icon={expand ? <DownOutlined size={14} /> : <RightOutlined size={14} />}
onClick={() => setExpand(!expand)}
size={24}
/>
Expand Down
5 changes: 5 additions & 0 deletions src/Snippet/demos/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Snippet } from '@ant-design/pro-editor';

export default () => {
return <Snippet language="sh">pnpm install @ant-design/pro-chat</Snippet>;
};
14 changes: 14 additions & 0 deletions src/Snippet/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
nav: 组件
group: Content
title: Snippet
description: The Snippet component is used to display a code snippet with syntax highlighting. It can be customized with a symbol before the content and a language for syntax highlighting. The component is also copyable with a CopyButton included by default.
---

## Default

<code src="./demos/index.tsx" nopadding></code>

## APIs

<API></API>
73 changes: 73 additions & 0 deletions src/Snippet/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import HighLighter from '@/Highlight/components/HighLighter';
import CopyButton from '@/components/CopyButton';
import Spotlight from '@/components/Spotlight';
import { useThemeMode } from 'antd-style';
import { memo } from 'react';
import { DivProps } from 'react-layout-kit';
import { getPrefixCls } from '..';
import { useStyles } from './style';

export interface SnippetProps extends DivProps {
/**
* @description The content to be displayed inside the Snippet component
*/
children: string;
/**
* @description Whether the Snippet component is copyable or not
* @default true
*/
copyable?: boolean;
/**
* @description The language of the content inside the Snippet component
* @default 'tsx'
*/
language?: string;
/**
* @description Whether add spotlight background
* @default false
*/
spotlight?: boolean;
/**
* @description The symbol to be displayed before the content inside the Snippet component
*/
symbol?: string;
/**
* @description The type of the Snippet component
* @default 'ghost'
*/
type?: 'ghost' | 'block';

prefixCls?: string;
}

const Snippet = memo<SnippetProps>((props) => {
const {
symbol = '$',
language = 'tsx',
children,
copyable = true,
prefixCls: customPrefixCls,
type = 'ghost',
spotlight,
className,
...rest
} = props;
const prefixCls = getPrefixCls('snippet', customPrefixCls);
const { isDarkMode } = useThemeMode();

const { styles, cx } = useStyles({
type,
prefixCls,
});
return (
<div className={cx(styles.container, className)} {...rest}>
{spotlight && <Spotlight />}
<HighLighter language={language} prefixCls={prefixCls} theme={isDarkMode ? 'dark' : 'light'}>
{[symbol, children].filter(Boolean).join(' ')}
</HighLighter>
{copyable && <CopyButton content={children} />}
</div>
);
});

export { Snippet };
58 changes: 58 additions & 0 deletions src/Snippet/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { createStyles } from 'antd-style';

export const useStyles = createStyles(({ css, cx, token }, { type, prefixCls }) => {
const typeStylish = css`
background-color: ${type === 'block' ? token.colorFillTertiary : 'transparent'};
border: 1px solid ${type === 'block' ? 'transparent' : token.colorBorder};
`;

return {
container: cx(
`${prefixCls}-container`,
typeStylish,
css`
position: relative;
overflow: hidden;
display: inline-flex;
gap: 8px;
align-items: center;
max-width: 100%;
height: 38px;
padding: 0 8px 0 12px;
border-radius: ${token.borderRadius}px;
transition: background-color 100ms ${token.motionEaseOut};
&:hover {
background-color: ${token.colorFillTertiary};
}
.${prefixCls}-shiki {
position: relative;
overflow: hidden;
flex: 1;
}
pre {
overflow-x: auto !important;
overflow-y: hidden !important;
display: flex;
align-items: center;
width: 100%;
height: 36px !important;
margin: 0 !important;
line-height: 1;
background: none !important;
}
code[class*='language-'] {
background: none !important;
}
`,
),
};
});
47 changes: 47 additions & 0 deletions src/components/CopyButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { CopyOutlined } from '@ant-design/icons';
import copy from 'copy-to-clipboard';
import { memo } from 'react';

import ActionIcon from '@/ActionIcon';
import { useCopied } from '@/hooks/useCopied';
import { type TooltipProps } from 'antd';
import { DivProps } from 'react-layout-kit';

export interface CopyButtonProps extends DivProps {
/**
* @description Additional class name
*/
className?: string;
/**
* @description The text content to be copied
*/
content: string;
/**
* @description The placement of the tooltip
* @enum ['top', 'left', 'right', 'bottom', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight', 'leftTop', 'leftBottom', 'rightTop', 'rightBottom']
* @default 'right'
*/
placement?: TooltipProps['placement'];
}

const CopyButton = memo<CopyButtonProps>(
({ content, className, placement = 'right', ...props }) => {
const { copied, setCopied } = useCopied();

return (
<ActionIcon
{...props}
className={className}
icon={<CopyOutlined size={12} />}
onClick={() => {
copy(content);
setCopied();
}}
placement={placement}
title={copied ? '✅ Success' : 'Copy'}
/>
);
},
);

export default CopyButton;
51 changes: 51 additions & 0 deletions src/components/Spotlight/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { memo, useEffect, useRef, useState } from 'react';
import { DivProps } from 'react-layout-kit';
import { useStyles } from './style';

const useMouseOffset = (): any => {
const [offset, setOffset] = useState<{ x: number; y: number }>();
const [outside, setOutside] = useState(true);
const reference = useRef<HTMLDivElement>();

useEffect(() => {
if (reference.current && reference.current.parentElement) {
const element = reference.current.parentElement;

// debounce?
const onMouseMove = (e: MouseEvent) => {
const bound = element.getBoundingClientRect();
setOffset({ x: e.clientX - bound.x, y: e.clientY - bound.y });
setOutside(false);
};

const onMouseLeave = () => {
setOutside(true);
};
element.addEventListener('mousemove', onMouseMove);
element.addEventListener('mouseleave', onMouseLeave);
return () => {
element.removeEventListener('mousemove', onMouseMove);
element.removeEventListener('mouseleave', onMouseLeave);
};
}
}, []);

return [offset, outside, reference] as const;
};

export interface SpotlightProps extends DivProps {
/**
* @description The size of the spotlight circle
* @default 64
*/
size?: number;
}

const Spotlight = memo<SpotlightProps>(({ className, size = 64, ...properties }) => {
const [offset, outside, reference] = useMouseOffset();
const { styles, cx } = useStyles({ offset, outside, size });

return <div className={cx(styles, className)} ref={reference} {...properties} />;
});

export default Spotlight;
Loading

0 comments on commit 25bb6ec

Please sign in to comment.