diff --git a/src/Highlight/components/HighLighter/index.tsx b/src/Highlight/components/HighLighter/index.tsx index afb75221..f2d51ec9 100644 --- a/src/Highlight/components/HighLighter/index.tsx +++ b/src/Highlight/components/HighLighter/index.tsx @@ -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'; @@ -18,7 +19,7 @@ export type ShikiProps = Pick< >; const HighLighter: React.FC = (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); diff --git a/src/Highlight/demos/config.js b/src/Highlight/demos/config.js index b5406909..58e12d18 100644 --- a/src/Highlight/demos/config.js +++ b/src/Highlight/demos/config.js @@ -1,4 +1,9 @@ const configs = [ + { + language: 'bash', + text: 'Bash', + code: '#!/bin/bash\n\n###### CONFIG\nACCEPTED_HOSTS="/root/.hag_accepted.conf"\nBE_VERBOSE=false\n\nif [ "$UID" -ne 0 ]\nthen\n echo "Superuser rights required"\n exit 2\nfi\n\ngenApacheConf(){\n echo -e "# Host ${HOME_DIR}$1/$2 :"\n}\n\necho \'"quoted"\' | tr -d \\\\/" > text.txt\n\n', + }, { language: 'cpp', text: 'Cpp', diff --git a/src/Highlight/hooks/useHighlight.tsx b/src/Highlight/hooks/useHighlight.tsx index 970f51c9..55c7c0f9 100644 --- a/src/Highlight/hooks/useHighlight.tsx +++ b/src/Highlight/hooks/useHighlight.tsx @@ -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'; @@ -26,6 +27,8 @@ export const languageMap = { java, python, sql, + bash, + sh, }; export const useHighlight = (language) => { @@ -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 }; diff --git a/src/Snippet/demos/index.tsx b/src/Snippet/demos/index.tsx index a5019aad..efd3821c 100644 --- a/src/Snippet/demos/index.tsx +++ b/src/Snippet/demos/index.tsx @@ -1,5 +1,14 @@ import { Snippet } from '@ant-design/pro-editor'; export default () => { - return ; + return ( + + pnpm install @ant-design/pro-chat + + ); }; diff --git a/src/Snippet/index.tsx b/src/Snippet/index.tsx index c4045e0c..f1b0fd61 100644 --- a/src/Snippet/index.tsx +++ b/src/Snippet/index.tsx @@ -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 { @@ -34,28 +35,37 @@ export interface SnippetProps extends DivProps { * @default 'ghost' */ type?: 'ghost' | 'block'; + + prefixCls?: string; } -const Snippet = memo( - ({ - symbol, +const Snippet = memo((props) => { + const { + symbol = '$', language = 'tsx', children, - // copyable = true, + copyable = true, + prefixCls: customPrefixCls, type = 'ghost', spotlight, className, - ...props - }) => { - const { styles, cx } = useStyles(type); - return ( -
- {/* {spotlight && } */} - {[symbol, children].filter(Boolean).join(' ')} - {/* {copyable && } */} -
- ); - }, -); + ...rest + } = props; + const prefixCls = getPrefixCls('snippet', customPrefixCls); + + const { styles, cx } = useStyles({ + type, + prefixCls, + }); + return ( +
+ {spotlight && } + + {[symbol, children].filter(Boolean).join(' ')} + + {copyable && } +
+ ); +}); export { Snippet }; diff --git a/src/Snippet/style.ts b/src/Snippet/style.ts index 54e67c0e..74dd6430 100644 --- a/src/Snippet/style.ts +++ b/src/Snippet/style.ts @@ -1,6 +1,6 @@ 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}; @@ -8,6 +8,7 @@ export const useStyles = createStyles(({ css, cx, token, prefixCls }, type: 'gho return { container: cx( + `${prefixCls}-container`, typeStylish, css` position: relative; @@ -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; @@ -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; diff --git a/src/components/CopyButton/index.tsx b/src/components/CopyButton/index.tsx new file mode 100644 index 00000000..14a0e257 --- /dev/null +++ b/src/components/CopyButton/index.tsx @@ -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( + ({ content, className, placement = 'right', ...props }) => { + const { copied, setCopied } = useCopied(); + + return ( + } + onClick={() => { + copy(content); + setCopied(); + }} + placement={placement} + title={copied ? '✅ Success' : 'Copy'} + /> + ); + }, +); + +export default CopyButton; diff --git a/src/components/Spotlight/index.tsx b/src/components/Spotlight/index.tsx new file mode 100644 index 00000000..a79ecdac --- /dev/null +++ b/src/components/Spotlight/index.tsx @@ -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(); + + 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(({ className, size = 64, ...properties }) => { + const [offset, outside, reference] = useMouseOffset(); + const { styles, cx } = useStyles({ offset, outside, size }); + + return
; +}); + +export default Spotlight; diff --git a/src/components/Spotlight/style.ts b/src/components/Spotlight/style.ts new file mode 100644 index 00000000..279b5f7a --- /dev/null +++ b/src/components/Spotlight/style.ts @@ -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; + `; + }, +); diff --git a/src/hooks/useCopied.ts b/src/hooks/useCopied.ts new file mode 100644 index 00000000..360e45f4 --- /dev/null +++ b/src/hooks/useCopied.ts @@ -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]); +};