Skip to content

Commit

Permalink
✨ feat: snippet almost done
Browse files Browse the repository at this point in the history
  • Loading branch information
ONLY-yours committed Nov 1, 2023
1 parent 724874b commit fc0b503
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 28 deletions.
3 changes: 2 additions & 1 deletion src/Highlight/components/HighLighter/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* 如果没有在 https://github.com/highlightjs/highlight.js/tree/master/src/languages 中查找是否支持,然后添加
* 优先支持主流语言,没有import在代码中使用的不会打包
*/
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 +19,7 @@ export type ShikiProps = Pick<
>;

const HighLighter: React.FC<ShikiProps> = (props) => {
const { children, lineNumber = false, theme, language, prefixCls } = props;
const { children, lineNumber = false, theme, 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
11 changes: 10 additions & 1 deletion src/Snippet/demos/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { Snippet } from '@ant-design/pro-editor';

export default () => {
return <Snippet />;
return (
<Snippet
language="sh"
style={{
width: '350px',
}}
>
pnpm install @ant-design/pro-chat
</Snippet>
);
};
48 changes: 29 additions & 19 deletions src/Snippet/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import HighLighter from '@/Highlight/components/HighLighter';
import CopyButton from '@/components/CopyButton';
import Spotlight from '@/components/Spotlight';
import { memo } from 'react';

import { Highlight } from '@ant-design/pro-editor';

import { DivProps } from 'react-layout-kit';
import { getPrefixCls } from '..';
import { useStyles } from './style';

export interface SnippetProps extends DivProps {
Expand Down Expand Up @@ -34,28 +35,37 @@ export interface SnippetProps extends DivProps {
* @default 'ghost'
*/
type?: 'ghost' | 'block';

prefixCls?: string;
}

const Snippet = memo<SnippetProps>(
({
symbol,
const Snippet = memo<SnippetProps>((props) => {
const {
symbol = '$',
language = 'tsx',
children,
// copyable = true,
copyable = true,
prefixCls: customPrefixCls,
type = 'ghost',
spotlight,
className,
...props
}) => {
const { styles, cx } = useStyles(type);
return (
<div className={cx(styles.container, className)} {...props}>
{/* {spotlight && <Spotlight />} */}
<Highlight language={language}>{[symbol, children].filter(Boolean).join(' ')}</Highlight>
{/* {copyable && <CopyButton content={children} size={{ blockSize: 24, fontSize: 14 }} />} */}
</div>
);
},
);
...rest
} = props;
const prefixCls = getPrefixCls('snippet', customPrefixCls);

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

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

export const useStyles = createStyles(({ css, cx, token, prefixCls }, type: 'ghost' | 'block') => {
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;
Expand All @@ -16,7 +17,6 @@ export const useStyles = createStyles(({ css, cx, token, prefixCls }, type: 'gho
display: flex;
gap: 8px;
align-items: center;
max-width: 100%;
height: 38px;
padding: 0 8px 0 12px;
Expand All @@ -29,7 +29,7 @@ export const useStyles = createStyles(({ css, cx, token, prefixCls }, type: 'gho
background-color: ${token.colorFillTertiary};
}
.${prefixCls}-highlighter-shiki {
.${prefixCls}-shiki {
position: relative;
overflow: hidden;
flex: 1;
Expand Down
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 copy from 'copy-to-clipboard';
import { Copy } from 'lucide-react';
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={<Copy 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;
30 changes: 30 additions & 0 deletions src/components/Spotlight/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createStyles } from 'antd-style';

export const useStyles = createStyles(
(
{ css, token, isDarkMode },
{ offset, outside, size }: { offset: { x: number; y: number }; outside: boolean; size: number },
) => {
const spotlightX = (offset?.x ?? 0) + 'px';
const spotlightY = (offset?.y ?? 0) + 'px';
const spotlightOpacity = outside ? '0' : '.1';
const spotlightSize = size + 'px';
return css`
pointer-events: none;
position: absolute;
z-index: 1;
inset: 0;
opacity: ${spotlightOpacity};
background: radial-gradient(
${spotlightSize} circle at ${spotlightX} ${spotlightY},
${isDarkMode ? token.colorText : '#fff'},
${isDarkMode ? 'transparent' : token.colorTextQuaternary}
);
border-radius: inherit;
transition: all 0.2s;
`;
},
);
21 changes: 21 additions & 0 deletions src/hooks/useCopied.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useCallback, useEffect, useMemo, useState } from 'react';

export const useCopied = () => {
const [copied, setCopy] = useState(false);

useEffect(() => {
if (!copied) return;

const timer = setTimeout(() => {
setCopy(false);
}, 2000);

return () => {
clearTimeout(timer);
};
}, [copied]);

const setCopied = useCallback(() => setCopy(true), []);

return useMemo(() => ({ copied, setCopied }), [copied]);
};

0 comments on commit fc0b503

Please sign in to comment.