Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ feat: new Components Snippet #97

Merged
merged 6 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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