import React from 'react'; import { Col, ConfigProvider, Flex, Row, Tag, theme, Typography } from 'antd'; import { createStyles, css } from 'antd-style'; import classnames from 'classnames'; const MARK_BORDER_SIZE = 2; const useStyle = createStyles(({ token }, markPos: [number, number, number, number]) => ({ container: css` position: relative; `, colWrap: css` border-right: 1px solid ${token.colorBorderSecondary}; display: flex; justify-content: center; align-items: center; padding: ${token.paddingMD}px; overflow: hidden; `, listWrap: css` display: flex; flex-direction: column; list-style: none; margin: 0; padding: 0; overflow: hidden; `, listItem: css` cursor: pointer; padding: ${token.paddingSM}px; transition: background-color ${token.motionDurationFast} ease; &:hover { background-color: ${token.controlItemBgHover}; } &:not(:first-of-type) { border-top: 1px solid ${token.colorBorderSecondary}; } `, marker: css` position: absolute; border: ${MARK_BORDER_SIZE}px solid ${token.colorWarning}; box-sizing: border-box; z-index: 999999; box-shadow: 0 0 0 1px #fff; pointer-events: none; inset-inline-start: ${markPos[0] - MARK_BORDER_SIZE}px; top: ${markPos[1] - MARK_BORDER_SIZE}px; width: ${markPos[2] + MARK_BORDER_SIZE * 2}px; height: ${markPos[3] + MARK_BORDER_SIZE * 2}px; `, markerActive: css` opacity: 1; `, markerNotActive: css` opacity: 0; `, markerMotion: css` transition: opacity ${token.motionDurationSlow} ease, all ${token.motionDurationSlow} ease; `, markerNotMotion: css` transition: opacity ${token.motionDurationSlow} ease; `, })); export interface SemanticPreviewProps { semantics: { name: string; desc: string; version?: string }[]; children: React.ReactElement; height?: number; } const SemanticPreview: React.FC = (props) => { const { semantics = [], children, height } = props; const { token } = theme.useToken(); // ======================= Semantic ======================= const getMarkClassName = React.useCallback( (semanticKey: string) => `semantic-mark-${semanticKey}`, [], ); const semanticClassNames = React.useMemo>(() => { const classNames: Record = {}; semantics.forEach((semantic) => { classNames[semantic.name] = getMarkClassName(semantic.name); }); return classNames; }, [semantics]); const cloneNode = React.cloneElement(children, { classNames: semanticClassNames, }); // ======================== Hover ========================= const containerRef = React.useRef(null); const timerRef = React.useRef>(); const [positionMotion, setPositionMotion] = React.useState(false); const [hoverSemantic, setHoverSemantic] = React.useState(null); const [markPos, setMarkPos] = React.useState<[number, number, number, number]>([0, 0, 0, 0]); const { styles } = useStyle(markPos); React.useEffect(() => { if (hoverSemantic) { const targetClassName = getMarkClassName(hoverSemantic); const targetElement = containerRef.current?.querySelector(`.${targetClassName}`); const containerRect = containerRef.current?.getBoundingClientRect(); const targetRect = targetElement?.getBoundingClientRect(); setMarkPos([ (targetRect?.left || 0) - (containerRect?.left || 0), (targetRect?.top || 0) - (containerRect?.top || 0), targetRect?.width || 0, targetRect?.height || 0, ]); timerRef.current = setTimeout(() => { setPositionMotion(true); }, 10); } else { timerRef.current = setTimeout(() => { setPositionMotion(false); }, 500); } return () => { if (timerRef.current) { clearTimeout(timerRef.current); } }; }, [hoverSemantic]); // ======================== Render ======================== return (
{cloneNode}
    {semantics.map((semantic) => (
  • setHoverSemantic(semantic.name)} onMouseLeave={() => setHoverSemantic(null)} > {semantic.name} {semantic.version && {semantic.version}} {semantic.desc}
  • ))}
); }; export default SemanticPreview;