浏览代码

feat: support userGuide component (#2733)

* feat: support userGuide component

* feat: userGuide in dev

* feat: support userGuide component

* feat: support userGuide component

* docs: add userGuide doc

* feat: add locale to UserGuide and add no in view logic

* feat: add getScrollbarWidth func
YannLynn 7 月之前
父节点
当前提交
573124c9e1
共有 59 个文件被更改,包括 2755 次插入22 次删除
  1. 1 1
      content/feedback/banner/index-en-US.md
  2. 1 1
      content/feedback/banner/index.md
  3. 1 1
      content/feedback/notification/index-en-US.md
  4. 1 1
      content/feedback/notification/index.md
  5. 1 1
      content/feedback/popconfirm/index-en-US.md
  6. 1 1
      content/feedback/popconfirm/index.md
  7. 1 1
      content/feedback/progress/index-en-US.md
  8. 1 1
      content/feedback/progress/index.md
  9. 1 1
      content/feedback/skeleton/index-en-US.md
  10. 1 1
      content/feedback/skeleton/index.md
  11. 1 1
      content/feedback/spin/index-en-US.md
  12. 1 1
      content/feedback/spin/index.md
  13. 1 1
      content/feedback/toast/index-en-US.md
  14. 1 1
      content/feedback/toast/index.md
  15. 1 0
      content/order.js
  16. 1 1
      content/other/configprovider/index-en-US.md
  17. 1 1
      content/other/configprovider/index.md
  18. 1 1
      content/other/locale/index-en-US.md
  19. 1 1
      content/other/locale/index.md
  20. 1 1
      content/plus/audioPlayer/index-en-US.md
  21. 1 1
      content/plus/audioPlayer/index.md
  22. 1 1
      content/show/chart/index-en-US.md
  23. 1 1
      content/show/chart/index.md
  24. 553 0
      content/show/userGuide/index-en-US.md
  25. 553 0
      content/show/userGuide/index.md
  26. 1 0
      content/start/overview/index.md
  27. 2 0
      packages/semi-foundation/userGuide/animation.scss
  28. 35 0
      packages/semi-foundation/userGuide/constants.ts
  29. 87 0
      packages/semi-foundation/userGuide/foundation.ts
  30. 143 0
      packages/semi-foundation/userGuide/userGuide.scss
  31. 48 0
      packages/semi-foundation/userGuide/variables.scss
  32. 1 0
      packages/semi-ui/index.ts
  33. 6 0
      packages/semi-ui/locale/interface.ts
  34. 6 0
      packages/semi-ui/locale/source/ar.ts
  35. 6 0
      packages/semi-ui/locale/source/de.ts
  36. 6 0
      packages/semi-ui/locale/source/en_GB.ts
  37. 6 0
      packages/semi-ui/locale/source/en_US.ts
  38. 6 0
      packages/semi-ui/locale/source/es.ts
  39. 6 0
      packages/semi-ui/locale/source/fr.ts
  40. 6 0
      packages/semi-ui/locale/source/id_ID.ts
  41. 6 0
      packages/semi-ui/locale/source/it.ts
  42. 6 0
      packages/semi-ui/locale/source/ja_JP.ts
  43. 6 0
      packages/semi-ui/locale/source/ko_KR.ts
  44. 6 0
      packages/semi-ui/locale/source/ms_MY.ts
  45. 6 0
      packages/semi-ui/locale/source/nl_NL.ts
  46. 7 0
      packages/semi-ui/locale/source/pl_PL.ts
  47. 6 0
      packages/semi-ui/locale/source/pt_BR.ts
  48. 6 0
      packages/semi-ui/locale/source/ro.ts
  49. 6 0
      packages/semi-ui/locale/source/ru_RU.ts
  50. 6 0
      packages/semi-ui/locale/source/sv_SE.ts
  51. 6 0
      packages/semi-ui/locale/source/th_TH.ts
  52. 6 0
      packages/semi-ui/locale/source/tr_TR.ts
  53. 6 0
      packages/semi-ui/locale/source/vi_VN.ts
  54. 6 0
      packages/semi-ui/locale/source/zh_CN.ts
  55. 6 0
      packages/semi-ui/locale/source/zh_TW.ts
  56. 607 0
      packages/semi-ui/userGuide/_story/userGuide.stories.jsx
  57. 42 0
      packages/semi-ui/userGuide/_story/userGuide.stories.tsx
  58. 515 0
      packages/semi-ui/userGuide/index.tsx
  59. 6 0
      src/images/docIcons/doc-userGuide.svg

+ 1 - 1
content/feedback/banner/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 81
+order: 82
 category: Feedback
 title:  Banner
 subTitle: Banner

+ 1 - 1
content/feedback/banner/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 81
+order: 82
 category: 反馈类
 title:  Banner 通知横幅
 icon: doc-banner

+ 1 - 1
content/feedback/notification/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 82
+order: 83
 category: Feedback
 title:  Notification
 subTitle: Notification

+ 1 - 1
content/feedback/notification/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 82
+order: 83
 category: 反馈类
 title: Notification 通知
 icon: doc-notification

+ 1 - 1
content/feedback/popconfirm/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 83
+order: 84
 category: Feedback
 title:  Popconfirm
 subTitle: Popconfirm

+ 1 - 1
content/feedback/popconfirm/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 83
+order: 84
 category: 反馈类
 title:  Popconfirm 气泡确认框
 icon: doc-popconfirm

+ 1 - 1
content/feedback/progress/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 84
+order: 85
 category: Feedback
 title: Progress
 subTitle: Progress

+ 1 - 1
content/feedback/progress/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 84
+order: 85
 category: 反馈类
 title: Progress 进度条
 icon: doc-progress

+ 1 - 1
content/feedback/skeleton/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 85
+order: 86
 category: Feedback
 title: Skeleton
 subTitle: Skeleton

+ 1 - 1
content/feedback/skeleton/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 85
+order: 86
 category: 反馈类
 title: Skeleton 骨架屏
 icon: doc-skeleton

+ 1 - 1
content/feedback/spin/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 86
+order: 87
 category: Feedback
 title: Spin
 subTitle: Spin

+ 1 - 1
content/feedback/spin/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 86
+order: 87
 category: 反馈类
 title: Spin 加载器
 icon: doc-spin

+ 1 - 1
content/feedback/toast/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 87
+order: 88
 category: Feedback
 title: Toast
 subTitle: Toast

+ 1 - 1
content/feedback/toast/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 87
+order: 88
 category: 反馈类
 title: Toast 提示
 icon: doc-toast

+ 1 - 0
content/order.js

@@ -78,6 +78,7 @@ const order = [
     'tag',
     'timeline',
     'tooltip',
+    'userGuide',
     'chart',
     'banner',
     'notification',

+ 1 - 1
content/other/configprovider/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 88
+order: 89
 category: Other
 title: ConfigProvider
 icon: doc-configprovider

+ 1 - 1
content/other/configprovider/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 88
+order: 89
 category: 其他
 title:  ConfigProvider 全局配置
 icon: doc-configprovider

+ 1 - 1
content/other/locale/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 89
+order: 90
 category: Other
 title: LocaleProvider
 subTitle: LocaleProvider

+ 1 - 1
content/other/locale/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 89
+order: 90
 category: 其他
 title:  LocaleProvider 多语言
 icon: doc-i18n

+ 1 - 1
content/plus/audioPlayer/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 91
+order: 92
 category: Plus
 title: AudioPlayer
 icon: doc-audioplayer

+ 1 - 1
content/plus/audioPlayer/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 91
+order: 92
 category: Plus
 title: AudioPlayer 音频播放器
 icon: doc-audioplayer

+ 1 - 1
content/show/chart/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 80
+order: 81
 category: Show
 title: Data Visualization
 icon: doc-vchart

+ 1 - 1
content/show/chart/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 80
+order: 81
 category: 展示类
 title:  Data Visualization 数据可视化
 icon: doc-vchart

+ 553 - 0
content/show/userGuide/index-en-US.md

@@ -0,0 +1,553 @@
+---
+localeCode: en-US
+order: 80
+category: Show
+title: UserGuide
+icon: doc-userGuide
+brief: Used to guide new users through pages
+showNew: true
+---
+
+
+## Demos
+
+### How to import
+
+```jsx import
+import { UserGuide } from '@douyinfe/semi-ui';
+```
+
+### Basic Usage
+
+```jsx live=true
+import React from 'react';
+import { UserGuide, Button, Space, Tag, Switch } from '@douyinfe/semi-ui';
+
+() => {
+    const [visible, setVisible] = useState(false);
+    const showDialog = () => {
+        setVisible(true);
+    };
+    return (
+        <div>
+            <Button onClick={showDialog}>Start Guide</Button>
+            <br />
+            <br />
+            <Space>
+                <Switch id={'basic-demo-1'} defaultChecked={true}></Switch>
+                <Tag id={'basic-demo-2'}> Default Tag </Tag>
+                <Button id={'basic-demo-3'}>Confirm</Button>
+            </Space>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                visible={visible}
+                steps={[
+                    {
+                        target: document.querySelector('#basic-demo-1'),
+                        title: "Beginner's Guide",
+                        description: 'Hello ByteDancer!',
+                        position: 'bottom',
+                    },
+                    {
+                        target: document.querySelector('#basic-demo-2'),
+                        title: 'Switch',
+                        description: 'This is a Semi Switch',
+                        position: 'bottom',
+                    },
+                    {
+                        target: document.querySelector('#basic-demo-3'),
+                        title: 'Button',
+                        description: 'This is a Semi Button',
+                        position: 'bottom',
+                    },
+                ]}
+                onChange={(current) => {
+                    console.log('Current guide step', current);
+                }}
+                onNext={(current) => {
+                    console.log('Next guide step');
+                }}
+                onPrev={(current) => {
+                    console.log('Previous guide step');
+                }}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('Guide completed');
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('Skip guide');
+                }}
+            />  
+        </div>
+    );
+};
+```
+
+### Theme
+`popup` bubble card mode provides two themes `default` and `primary`, set by the `theme` property.
+```jsx live=true
+import React from 'react';
+import { UserGuide, Button, Space, Tag, Switch } from '@douyinfe/semi-ui';
+
+() => {
+    const [visible, setVisible] = useState(false);
+    const showDialog = () => {
+        setVisible(true);
+    };
+    return (
+        <div>
+            <Button onClick={showDialog}>Start Guide</Button>
+            <br />
+            <br />
+            <Space>
+                <Switch id={'theme-demo-1'} defaultChecked={true}></Switch>
+                <Tag id={'theme-demo-2'}> Default Tag </Tag>
+                <Button id={'theme-demo-3'}>Confirm</Button>
+            </Space>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                visible={visible}
+                theme="primary"
+                steps={[
+                    {
+                        target: document.querySelector('#theme-demo-1'),
+                        title: "Beginner's Guide",
+                        description: 'Hello ByteDancer!',
+                        position: 'bottom',
+                    },
+                    {
+                        target: document.querySelector('#theme-demo-2'),
+                        title: 'Switch',
+                        description: 'This is a Semi Switch',
+                        position: 'bottom',
+                    },
+                    {
+                        target: document.querySelector('#theme-demo-3'),
+                        title: 'Button',
+                        description: 'This is a Semi Button',
+                        position: 'bottom',
+                    },
+                ]}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成');
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('跳过引导');
+                }}
+            />  
+        </div>
+    );
+};
+```
+
+### Popup position
+`popup` bubble card mode provides 12 positions, optional values are `top`, `topLeft`, `topRight`, `left`, `leftTop`, `leftBottom`, `right`, `rightTop`, `rightBottom`, `bottom`, `bottomLeft`, `bottomRight`, and can be set by the `showArrow` property to display the arrow.
+
+```jsx live=true
+import React from 'react';
+import { UserGuide, Button, Space, Tag, Switch } from '@douyinfe/semi-ui';
+
+() => {
+    const [visible, setVisible] = useState(false);
+    const showDialog = () => {
+        setVisible(true);
+    };
+    return (
+        <div>
+            <Button id={'position-demo'} onClick={showDialog}>Start Guide</Button>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                visible={visible}
+                steps={[
+                    {
+                        target: document.querySelector('#position-demo'),
+                        title: "Beginner's Guide",
+                        description: 'Hello ByteDancer!',
+                        position: 'top',
+                    },
+                    {
+                        target: document.querySelector('#position-demo'),
+                        title: 'New Position',
+                        description: 'This is Right Position',
+                        position: 'right',
+                    },
+                    {
+                        target: document.querySelector('#position-demo'),
+                        title: 'Hide Arrow',
+                        description: 'We hide the arrow',
+                        position: 'bottom',
+                        showArrow: false,
+                    },
+                ]}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('Guide completed');
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('Skip guide');
+                }}
+            />  
+        </div>
+    );
+};
+```
+
+### Set the size of the highlight area
+Set by the `spotlightPadding` property.
+
+```jsx live=true
+import React from 'react';
+import { UserGuide, Button, Space, Tag, Switch } from '@douyinfe/semi-ui';
+
+() => {
+    const [visible, setVisible] = useState(false);
+    const showDialog = () => {
+        setVisible(true);
+    };
+    return (
+        <div>
+            <Button onClick={showDialog}>Start Guide</Button>
+            <br />
+            <br />
+            <Space>
+                <Switch id={'padding-demo-1'} defaultChecked={true}></Switch>
+                <Tag id={'padding-demo-2'}> Default Tag </Tag>
+                <Button id={'padding-demo-3'}>Confirm</Button>
+            </Space>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                visible={visible}
+                spotlightPadding={10}
+                steps={[
+                    {
+                        target: document.querySelector('#padding-demo-1'),
+                        title: "Beginner's Guide",
+                        description: 'Hello ByteDancer!',
+                    },
+                    {
+                        target: document.querySelector('#padding-demo-2'),
+                        title: 'New Padding',
+                        description: 'This is 10px padding',
+                    },
+                    {
+                        target: document.querySelector('#padding-demo-3'),
+                        title: 'Change Padding',
+                        spotlightPadding: 15,
+                        description: 'We change the Padding to 15px',
+                    },
+                ]}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('Guide completed');
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('Skip guide');
+                }}
+            />  
+        </div>
+    );
+};
+```
+
+### Customize the button
+Set by the `nextButtonProps` and `prevButtonProps` properties.
+```jsx live=true
+import React from 'react';
+import { UserGuide, Button, Space, Tag, Switch } from '@douyinfe/semi-ui';
+
+() => {
+    const [visible, setVisible] = useState(false);
+    const showDialog = () => {
+        setVisible(true);
+    };
+    return (
+        <div>
+            <Button onClick={showDialog}>Start Guide</Button>
+            <br />
+            <br />
+            <Space>
+                <Switch id={'button-demo-1'} defaultChecked={true}></Switch>
+                <Tag id={'button-demo-2'}> Default Tag </Tag>
+                <Button id={'button-demo-3'}>Confirm</Button>
+            </Space>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                visible={visible}
+                nextButtonProps={{
+                    children: 'Next',
+                }}
+                prevButtonProps={{
+                    children: 'Prev',
+                    theme: 'borderless',
+                }}
+                finishText="I know!"
+                steps={[
+                    {
+                        target: document.querySelector('#button-demo-1'),
+                        title: "Beginner's Guide",
+                        description: 'Hello ByteDancer!',
+                    },
+                    {
+                        target: document.querySelector('#button-demo-2'),
+                        title: 'New Button Style',
+                        description: 'Button text is Next',
+                    },
+                    {
+                        target: document.querySelector('#button-demo-3'),
+                        title: 'New finish button text',
+                        description: 'Button text is I know',
+                    },
+                ]}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('Guide completed');
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('Skip guide');
+                }}
+            />  
+        </div>
+    );
+};
+```
+
+### Controlled
+Set by the `current` property.
+
+```jsx live=true
+import React from 'react';
+import { UserGuide, Button, Space, Tag, Switch } from '@douyinfe/semi-ui';
+
+() => {
+    const [visible, setVisible] = useState(false);
+    const [current, setCurrent] = useState(0);
+
+    const showDialog = () => {
+        setVisible(true);
+    };
+    return (
+        <div>
+            <Button onClick={showDialog}>Start Guide</Button>
+            <br />
+            <br />
+            <Space>
+                <Switch id={'controlled-demo-1'} defaultChecked={true}></Switch>
+                <Tag id={'controlled-demo-2'}> Default Tag </Tag>
+                <Button id={'controlled-demo-3'}>Confirm</Button>
+            </Space>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                visible={visible}
+                current={current}
+                steps={[
+                    {
+                        target: document.querySelector('#controlled-demo-1'),
+                        title: "Beginner's Guide",
+                        description: 'Hello ByteDancer!',
+                        position: 'bottom',
+                    },
+                    {
+                        target: document.querySelector('#controlled-demo-2'),
+                        title: 'Switch',
+                        description: 'This is a Semi Switch',
+                        position: 'bottom',
+                    },
+                    {
+                        target: document.querySelector('#controlled-demo-3'),
+                        title: 'Button',
+                        description: 'This is a Semi Button',
+                        position: 'bottom',
+                    },
+                ]}
+                onChange={(current) => {
+                    setCurrent(current);
+                }}
+                onFinish={() => {
+                    setVisible(false);
+                    setCurrent(0);
+                    console.log('Guide completed');
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    setCurrent(0);
+                    console.log('Skip guide');
+                }}
+            />  
+        </div>
+    );
+};
+```
+
+
+### Modal guide
+Set by the `mode` property.
+
+```jsx live=true
+import React from 'react';
+import { UserGuide, Button, Space, Tag, Switch, Image, Typography } from '@douyinfe/semi-ui';
+
+() => {
+    const [visible, setVisible] = useState(false);
+    const showDialog = () => {
+        setVisible(true);
+    };
+    return (
+        <div>
+            <Button onClick={showDialog}>Start Guide</Button>
+            <UserGuide
+                mode="modal"
+                mask={true}
+                visible={visible}
+                steps={[
+                    {
+                        title: 'Welcome to Semi DSM!',
+                        description: <div>You can start from the published theme, or choose {<Typography.Text strong>Create Now</Typography.Text>} to create a new theme</div>,
+                        cover: <Image 
+                            width={'600px'} 
+                            height={'100%'} 
+                            src="https://lf9-static.bytednsdoc.com/obj/eden-cn/nuhpxphk/dsm/dsm_welcome.png"
+                        />,
+                        position: 'bottom',
+                    },
+                    {
+                        title: 'High-available color palette',
+                        description: 'After selecting the main color, our color algorithm will generate a high-available color palette for you',
+                        cover: <Image 
+                            width={'600px'} 
+                            height={'100%'} 
+                            src="https://lf9-static.bytednsdoc.com/obj/eden-cn/nuhpxphk/dsm/dsm_console.png"
+                        />,
+                        position: 'bottom',
+                    },
+                    {
+                        title: 'Customize freely',
+                        description: 'Start customizing your design system!',
+                        cover: <Image 
+                            width={'600px'} 
+                            height={'100%'} 
+                            src="https://lf9-static.bytednsdoc.com/obj/eden-cn/nuhpxphk/dsm/dsm_palette.png" 
+                        />,
+                        position: 'bottom',
+                    },
+                ]}
+                onChange={(current) => {
+                    console.log('Current guide step', current);
+                }}
+                onNext={(current) => {
+                    console.log('Next guide step');
+                }}
+                onPrev={(current) => {
+                    console.log('Previous guide step');
+                }}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('Guide completed');
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('Skip guide');
+                }}
+            />  
+        </div>
+    );
+};
+```
+
+### No mask
+Set by the `mask` property.
+
+```jsx live=true
+import React from 'react';
+import { UserGuide, Button, Space, Tag, Switch, Image } from '@douyinfe/semi-ui';
+
+() => {
+    const [visible, setVisible] = useState(false);
+    const showDialog = () => {
+        setVisible(true);
+    };
+    return (
+        <div>
+            <Button id={'mask-demo'} onClick={showDialog}>Start Guide</Button>
+            <UserGuide
+                mode="popup"
+                mask={false}
+                visible={visible}
+                steps={[
+                    {
+                        target: document.querySelector('#mask-demo'),
+                        title: 'No Mask',
+                        description: 'Hello ByteDancer!',
+                    },
+                ]}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('Guide completed');
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('Skip guide');
+                }}
+            />  
+        </div>
+    );
+};
+```
+
+## API Reference
+
+---
+| Properties | Instructions | Type | Default | Version |
+| --- | --- | --- | --- | --- |
+| className | Custom class name | string | - | |
+| current | Current step index | number | 0 | |
+| finishText | Text of the last step completion button | string | '完成' | |
+| mask | Whether to display the mask | boolean | true | |
+| mode | Guide mode, optional values: `popup` (bubble card) or `modal` (modal) | string | popup | |
+| nextButtonProps | The properties of the next button | ButtonProps | {} | |
+| onChange | Callback when the step changes | function(current: number) | () => void | |
+| onFinish | Callback when all steps are completed | function() | () => void | |
+| onNext | Callback when the next button is clicked | function(current: number) | () => void | |
+| onPrev | Callback when the previous button is clicked | function(current: number) | () => void | |
+| onSkip | Callback when the skip button is clicked | function() | () => void | |
+| position | The position of the pop-up layer relative to the target element, optional values: `top`, `topLeft`, `topRight`, `left`, `leftTop`, `leftBottom`, `right`, `rightTop`, `rightBottom`, `bottom`, `bottomLeft`, `bottomRight` | string | bottom | |
+| prevButtonProps | The properties of the previous button | ButtonProps | {} | |
+| showPrevButton | Whether to display the previous button | boolean | true | |
+| showSkipButton | Whether to display the skip button | boolean | true | |
+| spotlightPadding | The inner padding of the highlight area, in pixels | number | - | |
+| steps | Guide step configuration, required | StepItem[] | [] | |
+| style | Custom style | React.CSSProperties | - | |
+| theme | Theme style, optional values: `default` or `primary` | string | default | |
+| visible | Whether to display the guide | boolean | false | |
+| getPopupContainer | Specify the parent DOM, the pop-up layer will be rendered to the DOM | () => HTMLElement | - | |
+| zIndex | Pop-up layer level | number | 1030 | |
+
+### Steps.Step
+| Properties | Instructions | Type | Default | Version |
+| --- | --- | --- | --- | --- |
+| className | Custom class name | string | - | |
+| cover | The cover image of the step | ReactNode | - | |
+| target | The target element, the highlight area will focus on this element | (() => Element) \| Element | - | |
+| title | Step title | string \| ReactNode | - | |
+| description | Step description | ReactNode | - | |
+| mask | Whether to display the mask of this step, it will override the global configuration | boolean | - | |
+| showArrow | Whether to display the arrow (only valid when mode=`popup`) | boolean | true | | 
+| spotlightPadding | The inner padding of the highlight area of this step, it will override the global configuration | number | - | |
+| theme | The theme of this step, it will override the global configuration | `default` \| `primary` | - | |
+| position | The position of the pop-up layer of this step, it will override the global configuration | Position | - | |
+
+## Design Tokens
+
+<DesignToken/>
+

+ 553 - 0
content/show/userGuide/index.md

@@ -0,0 +1,553 @@
+---
+localeCode: zh-CN
+order: 80
+category: 展示类
+title:  UserGuide 用户引导
+icon: doc-userGuide
+brief: 用于页面对新用户进行功能引导
+showNew: true
+---
+
+
+## 代码演示
+
+### 如何引入
+
+```jsx import
+import { UserGuide } from '@douyinfe/semi-ui';
+```
+
+### 基本用法
+
+```jsx live=true
+import React from 'react';
+import { UserGuide, Button, Space, Tag, Switch } from '@douyinfe/semi-ui';
+
+() => {
+    const [visible, setVisible] = useState(false);
+    const showDialog = () => {
+        setVisible(true);
+    };
+    return (
+        <div>
+            <Button onClick={showDialog}>开始引导</Button>
+            <br />
+            <br />
+            <Space>
+                <Switch id={'basic-demo-1'} defaultChecked={true}></Switch>
+                <Tag id={'basic-demo-2'}> Default Tag </Tag>
+                <Button id={'basic-demo-3'}>确定</Button>
+            </Space>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                visible={visible}
+                steps={[
+                    {
+                        target: document.querySelector('#basic-demo-1'),
+                        title: '新手引导',
+                        description: 'Hello ByteDancer!',
+                        position: 'bottom',
+                    },
+                    {
+                        target: document.querySelector('#basic-demo-2'),
+                        title: 'Switch',
+                        description: 'This is a Semi Switch',
+                        position: 'bottom',
+                    },
+                    {
+                        target: document.querySelector('#basic-demo-3'),
+                        title: 'Button',
+                        description: 'This is a Semi Button',
+                        position: 'bottom',
+                    },
+                ]}
+                onChange={(current) => {
+                    console.log('当前引导步骤', current);
+                }}
+                onNext={(current) => {
+                    console.log('下一步引导');
+                }}
+                onPrev={(current) => {
+                    console.log('上一步引导');
+                }}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成');
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('跳过引导');
+                }}
+            />  
+        </div>
+    );
+};
+```
+
+### 主题
+`popup` 气泡卡片模式下提供两种主题 `default` 和 `primary`,通过 `theme` 属性设置。
+```jsx live=true
+import React from 'react';
+import { UserGuide, Button, Space, Tag, Switch } from '@douyinfe/semi-ui';
+
+() => {
+    const [visible, setVisible] = useState(false);
+    const showDialog = () => {
+        setVisible(true);
+    };
+    return (
+        <div>
+            <Button onClick={showDialog}>开始引导</Button>
+            <br />
+            <br />
+            <Space>
+                <Switch id={'theme-demo-1'} defaultChecked={true}></Switch>
+                <Tag id={'theme-demo-2'}> Default Tag </Tag>
+                <Button id={'theme-demo-3'}>确定</Button>
+            </Space>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                visible={visible}
+                theme="primary"
+                steps={[
+                    {
+                        target: document.querySelector('#theme-demo-1'),
+                        title: '新手引导',
+                        description: 'Hello ByteDancer!',
+                        position: 'bottom',
+                    },
+                    {
+                        target: document.querySelector('#theme-demo-2'),
+                        title: 'Switch',
+                        description: 'This is a Semi Switch',
+                        position: 'bottom',
+                    },
+                    {
+                        target: document.querySelector('#theme-demo-3'),
+                        title: 'Button',
+                        description: 'This is a Semi Button',
+                        position: 'bottom',
+                    },
+                ]}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成');
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('跳过引导');
+                }}
+            />  
+        </div>
+    );
+};
+```
+
+### 气泡卡片弹出位置
+`popup` 气泡卡片模式下提供 12 种弹出位置,可选值有`top`, `topLeft`, `topRight`, `left`, `leftTop`, `leftBottom`, `right`, `rightTop`, `rightBottom`, `bottom`, `bottomLeft`, `bottomRight`,还可以通过 `showArrow` 属性设置是否显示箭头。
+
+```jsx live=true
+import React from 'react';
+import { UserGuide, Button, Space, Tag, Switch } from '@douyinfe/semi-ui';
+
+() => {
+    const [visible, setVisible] = useState(false);
+    const showDialog = () => {
+        setVisible(true);
+    };
+    return (
+        <div>
+            <Button id={'position-demo'} onClick={showDialog}>开始引导</Button>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                visible={visible}
+                steps={[
+                    {
+                        target: document.querySelector('#position-demo'),
+                        title: '新手引导',
+                        description: 'Hello ByteDancer!',
+                        position: 'top',
+                    },
+                    {
+                        target: document.querySelector('#position-demo'),
+                        title: 'New Position',
+                        description: 'This is Right Position',
+                        position: 'right',
+                    },
+                    {
+                        target: document.querySelector('#position-demo'),
+                        title: 'Hide Arrow',
+                        description: 'We hide the arrow',
+                        position: 'bottom',
+                        showArrow: false,
+                    },
+                ]}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成');
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('跳过引导');
+                }}
+            />  
+        </div>
+    );
+};
+```
+
+### 设置高亮区域大小
+通过 `spotlightPadding` 属性设置。
+
+```jsx live=true
+import React from 'react';
+import { UserGuide, Button, Space, Tag, Switch } from '@douyinfe/semi-ui';
+
+() => {
+    const [visible, setVisible] = useState(false);
+    const showDialog = () => {
+        setVisible(true);
+    };
+    return (
+        <div>
+            <Button onClick={showDialog}>开始引导</Button>
+            <br />
+            <br />
+            <Space>
+                <Switch id={'padding-demo-1'} defaultChecked={true}></Switch>
+                <Tag id={'padding-demo-2'}> Default Tag </Tag>
+                <Button id={'padding-demo-3'}>确定</Button>
+            </Space>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                visible={visible}
+                spotlightPadding={10}
+                steps={[
+                    {
+                        target: document.querySelector('#padding-demo-1'),
+                        title: '新手引导',
+                        description: 'Hello ByteDancer!',
+                    },
+                    {
+                        target: document.querySelector('#padding-demo-2'),
+                        title: 'New Padding',
+                        description: 'This is 10px padding',
+                    },
+                    {
+                        target: document.querySelector('#padding-demo-3'),
+                        title: 'Change Padding',
+                        spotlightPadding: 15,
+                        description: 'We change the Padding to 15px',
+                    },
+                ]}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成');
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('跳过引导');
+                }}
+            />  
+        </div>
+    );
+};
+```
+
+### 定制按钮
+通过 `nextButtonProps` 和 `prevButtonProps` 属性设置按钮的样式。
+```jsx live=true
+import React from 'react';
+import { UserGuide, Button, Space, Tag, Switch } from '@douyinfe/semi-ui';
+
+() => {
+    const [visible, setVisible] = useState(false);
+    const showDialog = () => {
+        setVisible(true);
+    };
+    return (
+        <div>
+            <Button onClick={showDialog}>开始引导</Button>
+            <br />
+            <br />
+            <Space>
+                <Switch id={'button-demo-1'} defaultChecked={true}></Switch>
+                <Tag id={'button-demo-2'}> Default Tag </Tag>
+                <Button id={'button-demo-3'}>确定</Button>
+            </Space>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                visible={visible}
+                nextButtonProps={{
+                    children: 'Next',
+                }}
+                prevButtonProps={{
+                    children: 'Prev',
+                    theme: 'borderless',
+                }}
+                finishText="我知道啦!"
+                steps={[
+                    {
+                        target: document.querySelector('#button-demo-1'),
+                        title: '新手引导',
+                        description: 'Hello ByteDancer!',
+                    },
+                    {
+                        target: document.querySelector('#button-demo-2'),
+                        title: 'New Button Style',
+                        description: 'Button text is Next',
+                    },
+                    {
+                        target: document.querySelector('#button-demo-3'),
+                        title: 'New finish button text',
+                        description: 'Button text is I know',
+                    },
+                ]}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成');
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('跳过引导');
+                }}
+            />  
+        </div>
+    );
+};
+```
+
+### 受控
+通过 `current` 属性设置当前引导步骤。
+
+```jsx live=true
+import React from 'react';
+import { UserGuide, Button, Space, Tag, Switch } from '@douyinfe/semi-ui';
+
+() => {
+    const [visible, setVisible] = useState(false);
+    const [current, setCurrent] = useState(0);
+
+    const showDialog = () => {
+        setVisible(true);
+    };
+    return (
+        <div>
+            <Button onClick={showDialog}>开始引导</Button>
+            <br />
+            <br />
+            <Space>
+                <Switch id={'controlled-demo-1'} defaultChecked={true}></Switch>
+                <Tag id={'controlled-demo-2'}> Default Tag </Tag>
+                <Button id={'controlled-demo-3'}>确定</Button>
+            </Space>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                visible={visible}
+                current={current}
+                steps={[
+                    {
+                        target: document.querySelector('#controlled-demo-1'),
+                        title: '新手引导',
+                        description: 'Hello ByteDancer!',
+                        position: 'bottom',
+                    },
+                    {
+                        target: document.querySelector('#controlled-demo-2'),
+                        title: 'Switch',
+                        description: 'This is a Semi Switch',
+                        position: 'bottom',
+                    },
+                    {
+                        target: document.querySelector('#controlled-demo-3'),
+                        title: 'Button',
+                        description: 'This is a Semi Button',
+                        position: 'bottom',
+                    },
+                ]}
+                onChange={(current) => {
+                    setCurrent(current);
+                }}
+                onFinish={() => {
+                    setVisible(false);
+                    setCurrent(0);
+                    console.log('引导完成');
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    setCurrent(0);
+                    console.log('跳过引导');
+                }}
+            />  
+        </div>
+    );
+};
+```
+
+
+### 弹窗式引导
+通过 `mode` 属性设置为 `modal` 开启弹窗式引导。
+
+```jsx live=true
+import React from 'react';
+import { UserGuide, Button, Space, Tag, Switch, Image, Typography } from '@douyinfe/semi-ui';
+
+() => {
+    const [visible, setVisible] = useState(false);
+    const showDialog = () => {
+        setVisible(true);
+    };
+    return (
+        <div>
+            <Button onClick={showDialog}>开始引导</Button>
+            <UserGuide
+                mode="modal"
+                mask={true}
+                visible={visible}
+                steps={[
+                    {
+                        title: '欢迎使用 Semi DSM!',
+                        description: <div>你可以从已发布的主题出发,或者选择{<Typography.Text strong>立即创造</Typography.Text>}来创造一个新的主题</div>,
+                        cover: <Image 
+                            width={'600px'} 
+                            height={'100%'} 
+                            src="https://lf9-static.bytednsdoc.com/obj/eden-cn/nuhpxphk/dsm/dsm_welcome.png"
+                        />,
+                        position: 'bottom',
+                    },
+                    {
+                        title: '高可用的色盘',
+                        description: '选取主色后,我们的颜色算法会为你生成一套高可用的色盘',
+                        cover: <Image 
+                            width={'600px'} 
+                            height={'100%'} 
+                            src="https://lf9-static.bytednsdoc.com/obj/eden-cn/nuhpxphk/dsm/dsm_console.png"
+                        />,
+                        position: 'bottom',
+                    },
+                    {
+                        title: '自由定制',
+                        description: '开始定制属于你的设计系统吧!',
+                        cover: <Image 
+                            width={'600px'} 
+                            height={'100%'} 
+                            src="https://lf9-static.bytednsdoc.com/obj/eden-cn/nuhpxphk/dsm/dsm_palette.png" 
+                        />,
+                        position: 'bottom',
+                    },
+                ]}
+                onChange={(current) => {
+                    console.log('当前引导步骤', current);
+                }}
+                onNext={(current) => {
+                    console.log('下一步引导');
+                }}
+                onPrev={(current) => {
+                    console.log('上一步引导');
+                }}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成');
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('跳过引导');
+                }}
+            />  
+        </div>
+    );
+};
+```
+
+### 无遮罩
+通过 `mask` 属性设置为 `false` 开启无遮罩引导。
+
+```jsx live=true
+import React from 'react';
+import { UserGuide, Button, Space, Tag, Switch, Image } from '@douyinfe/semi-ui';
+
+() => {
+    const [visible, setVisible] = useState(false);
+    const showDialog = () => {
+        setVisible(true);
+    };
+    return (
+        <div>
+            <Button id={'mask-demo'} onClick={showDialog}>开始引导</Button>
+            <UserGuide
+                mode="popup"
+                mask={false}
+                visible={visible}
+                steps={[
+                    {
+                        target: document.querySelector('#mask-demo'),
+                        title: 'No Mask',
+                        description: 'Hello ByteDancer!',
+                    },
+                ]}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成');
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('跳过引导');
+                }}
+            />  
+        </div>
+    );
+};
+```
+
+## API 参考
+
+---
+| 属性 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| className | 自定义类名 | string | - | |
+| current | 当前步骤的索引 | number | 0 | |
+| finishText | 最后一步完成按钮的文本 | string | '完成' | |
+| mask | 是否显示蒙层 | boolean | true | |
+| mode | 引导模式,可选值:`popup`(气泡卡片)或 `modal`(弹窗式) | string | popup | |
+| nextButtonProps | 下一步按钮的属性 | ButtonProps | {} | |
+| onChange | 步骤改变时的回调 | function(current: number) | () => void | |
+| onFinish | 完成所有步骤时的回调 | function() | () => void | |
+| onNext | 点击下一步按钮时的回调 | function(current: number) | () => void | |
+| onPrev | 点击上一步按钮时的回调 | function(current: number) | () => void | |
+| onSkip | 点击跳过按钮时的回调 | function() | () => void | |
+| position | 弹出层相对于目标元素的位置,可选值:`top`, `topLeft`, `topRight`, `left`, `leftTop`, `leftBottom`, `right`, `rightTop`, `rightBottom`, `bottom`, `bottomLeft`, `bottomRight` | string | bottom | |
+| prevButtonProps | 上一步按钮的属性 | ButtonProps | {} | |
+| showPrevButton | 是否显示上一步按钮 | boolean | true | |
+| showSkipButton | 是否显示跳过按钮 | boolean | true | |
+| spotlightPadding | 高亮区域的内边距,单位为像素 | number | - | |
+| steps | 引导步骤配置,必填 | StepItem[] | [] | |
+| style | 自定义样式 | React.CSSProperties | - | |
+| theme | 主题样式,可选值:`default` 或 `primary` | string | default | |
+| visible | 是否显示引导 | boolean | false | |
+| getPopupContainer | 指定父级 DOM,弹层将会渲染至该 DOM 中 | () => HTMLElement | - | |
+| zIndex | 弹层层级 | number | 1030 | |
+
+### Steps.Step
+| 属性 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| className | 步骤的自定义类名 | string | - | |
+| cover | 步骤的封面图 | ReactNode | - | |
+| target | 目标元素,高亮区域会聚焦到这个元素上 | (() => Element) \| Element | - | |
+| title | 步骤标题 | string \| ReactNode | - | |
+| description | 步骤描述 | ReactNode | - | |
+| mask | 是否显示此步骤的蒙层,会覆盖全局配置 | boolean | - | |
+| showArrow | 是否显示箭头(仅在 mode=`popup` 时有效) | boolean | true | |
+| spotlightPadding | 此步骤高亮区域区域的内边距,会覆盖全局配置 | number | - | |
+| theme | 此步骤的主题,会覆盖全局配置 | `default` \| `primary` | - | |
+| position | 此步骤弹出层的位置,会覆盖全局配置 | Position | - | |
+
+## 设计变量
+
+<DesignToken/>
+

+ 1 - 0
content/start/overview/index.md

@@ -94,6 +94,7 @@ Table 表格,
 Tag 标签,
 Timeline 时间轴,
 Tooltip 工具提示,
+UserGuide 用户引导,
 VChart 图表
 ```
 

+ 2 - 0
packages/semi-foundation/userGuide/animation.scss

@@ -0,0 +1,2 @@
+$transition_duration-userGuide_spotLight: 200ms; // spotLight 动画持续时间
+$transition_function-userGuide_spotLight: cubic-bezier(0.4, 0, 0.2, 1); // spotLight 过渡曲线

+ 35 - 0
packages/semi-foundation/userGuide/constants.ts

@@ -0,0 +1,35 @@
+import { BASE_CLASS_PREFIX } from '../base/constants';
+
+const cssClasses = {
+    PREFIX: `${BASE_CLASS_PREFIX}-userGuide`,
+    PREFIX_MODAL: `${BASE_CLASS_PREFIX}-userGuide-modal`,
+};
+
+const strings = {
+    MODE: ['popup', 'modal'],
+    POSITION_SET: [
+        'top',
+        'topLeft',
+        'topRight',
+        'left',
+        'leftTop',
+        'leftBottom',
+        'right',
+        'rightTop',
+        'rightBottom',
+        'bottom',
+        'bottomLeft',
+        'bottomRight',
+        'leftTopOver',
+        'rightTopOver',
+    ],
+    THEME: ['default', 'primary'],
+};
+
+const numbers = {
+    DEFAULT_CURRENT: 0,
+    DEFAULT_SPOTLIGHT_PADDING: 5,
+    DEFAULT_Z_INDEX: 1030,
+};
+
+export { cssClasses, strings, numbers }; 

+ 87 - 0
packages/semi-foundation/userGuide/foundation.ts

@@ -0,0 +1,87 @@
+import BaseFoundation, { DefaultAdapter } from '../base/foundation';
+import { numbers } from './constants';
+
+export interface UserGuideAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S>{
+    notifyChange: (current: number) => void;
+    notifyPrev: (current: number) => void;
+    notifyNext: (current: number) => void;
+    notifySkip: () => void;
+    notifyFinish: () => void;
+    setCurrent: (current: number) => void;
+    disabledBodyScroll: () => void;
+    enabledBodyScroll: () => void
+}
+
+
+export default class UserGuideFoundation <P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<UserGuideAdapter<P, S>, P, S> {
+    constructor(adapter: UserGuideAdapter<P, S>) {
+        super({ ...adapter });
+    }
+
+    init() {
+    }
+
+    destroy() {
+        this._adapter.enabledBodyScroll();
+    }
+
+    _notifyChange(current: number): void {
+        const { current: stateCurrent } = this.getStates();
+        if (stateCurrent !== current) {
+            this._adapter.notifyChange(current);
+        }
+    }
+
+    getIsControlledComponent(): boolean {
+        return this._isInProps('current');
+    }
+
+    beforeShow() {
+        this._adapter.disabledBodyScroll();
+    }
+
+    afterHide() {
+        this._adapter.enabledBodyScroll();
+    }
+
+    getFinalPaading() {
+        const { spotlightPadding, steps } = this.getProps();
+        const { current } = this.getStates();
+        const stepPadding = steps[current]?.spotlightPadding;
+        const padding = typeof stepPadding === 'number' ? stepPadding : 
+            typeof spotlightPadding === 'number' ? spotlightPadding : 
+                numbers.DEFAULT_SPOTLIGHT_PADDING;
+        return padding;
+    }
+
+    handlePrev = () => {
+        const { current } = this.getStates();
+        const newCurrent = current - 1;
+        if (!this.getIsControlledComponent()) {
+            this._adapter.setCurrent(newCurrent);
+        } 
+        this._notifyChange(newCurrent);
+        this._adapter.notifyPrev(newCurrent);
+    };
+
+    handleNext = () => {
+        const { steps } = this.getProps();
+        const { current } = this.getStates();
+        const isLastStep = current === steps.length - 1;
+        const newCurrent = isLastStep ? current : current + 1;
+        if (isLastStep) {
+            this._adapter.notifyFinish();
+        } else {
+            this._notifyChange(newCurrent);
+            this._adapter.notifyNext(newCurrent);
+            if (!this.getIsControlledComponent()) {
+                this._adapter.setCurrent(newCurrent);
+            }
+        }
+    };
+
+    handleSkip = () => {
+        this._adapter.notifySkip();
+    };
+
+} 

+ 143 - 0
packages/semi-foundation/userGuide/userGuide.scss

@@ -0,0 +1,143 @@
+@import './variables.scss';
+@import './animation.scss';
+
+$module: #{$prefix}-userGuide;
+
+
+.#{$module} {
+
+    &-spotlight {
+        position: fixed;
+        top: 0;
+        left: 0;
+        width: 100vw;
+        height: 100vh;
+        pointer-events: none;
+
+        &-rect {
+            transition: all $transition_duration-userGuide_spotLight $transition_function-userGuide_spotLight;
+        }
+    }
+
+    &-popover {
+        max-width: fit-content;
+        width: $width-userGuide_popover-default;
+    }
+
+    &-popup-content {
+        color: $color-userGuide_popup-text-default;
+
+        &-primary {
+            color: $color-userGuide_popup-text-primary;
+        }
+
+        &-cover {
+            img {
+                display: block;
+                height: $height-userGuide_popup_content_cover-default;
+                width: 100%;
+                border-radius: $radius-userGuide_popup_content_cover;
+            }
+        }
+
+        &-body {
+            padding: $spacing-userGuide_popup_content_body-padding;
+        }
+    
+        &-title {
+            font-size: $font-userGuide_popup_content_title-fontSize;
+            font-weight: $font-userGuide_popup_content_title-fontWeight;
+            line-height: $font-userGuide_popup_content_title-lineHeight;
+            margin-bottom: $spacing-userGuide_popup_content_title-marginBottom;
+        }
+
+        &-description {
+            font-size: $font-userGuide_popup_content_description-fontSize;
+            line-height: $font-userGuide_popup_content_description-lineHeight;
+            margin-bottom: $spacing-userGuide_popup_content_description-marginBottom;
+        }
+
+        &-footer {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+    
+        &-buttons {
+            display: flex;
+            gap: $spacing-userGuide_popup_content_button-gap;
+            margin-left: $spacing-userGuide_popup_content_button-marginLeft;
+        }
+
+        &-indicator {
+            font-size: $font-size-regular; 
+            line-height: $font-userGuide_popup_content_indicator-lineHeight;
+        }
+    }
+
+    &-modal {
+
+        .#{$prefix}-modal-small {
+            width: fit-content;
+        }
+
+        // Override the modal's original padding
+        .#{$prefix}-modal-content {
+            padding: 0;
+            width: $width-userGuide_modal_content_cover-default;
+            max-width: fit-content;
+        }
+
+        &-cover {
+            height: $height-userGuide_modal_content_cover-default;
+        }
+
+        &-indicator {
+            height: $height-userGuide_modal_content_indicator-default;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            column-gap: $spacing-userGuide_popup_content_indicator-gap;
+
+            &-item {
+                width: $width-userGuide_modal_content_indicator_item-default;
+                height: $height-userGuide_modal_content_indicator_item-default;
+                border-radius: $radius-userGuide_modal_content_indicator;
+                background-color: $color-userGuide_modal_content_indicator-bg;
+
+                &-active {
+                    background: $color-userGuide_modal_content_indicator-bg-active
+                }
+            }
+        }
+
+        &-body {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            flex-direction: column;
+            padding: $spacing-userGuide_modal_content_body-padding;
+
+            &-title {
+                font-size: $font-userGuide_modal_content_title-fontSize;
+                font-weight: $font-userGuide_modal_content_title-fontWeight;
+                line-height:$font-userGuide_modal_content_title-lineHeight;
+                margin-bottom: $spacing-userGuide_modal_content_title-marginBottom;
+            }
+
+            &-description {
+                font-size: $font-userGuide_modal_content_description-fontSize;
+                line-height: $font-userGuide_modal_content_description-lineHeight;
+            }
+        }
+
+        &-footer {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            padding: $spacing-userGuide_modal_content_footer-padding;
+            column-gap: $spacing-userGuide_modal_content_button-gap
+        }
+    }
+} 
+

+ 48 - 0
packages/semi-foundation/userGuide/variables.scss

@@ -0,0 +1,48 @@
+// Color
+$color-userGuide_popup-text-default: var(--semi-color-text-0); // 气泡卡片文字颜色 - 默认
+$color-userGuide_popup-text-primary: var(--semi-color-tertiary-light-default); // 气泡卡片文字颜色 - primary
+$color-userGuide_modal_content_indicator-bg: var(--semi-color-primary-light-active); // 弹窗指示器背景颜色
+$color-userGuide_modal_content_indicator-bg-active: var(--semi-color-primary);  // 弹窗指示器背景颜色 - 选中态
+
+// Width/Height
+$width-userGuide_popover-default: 400px; // 默认气泡卡片宽度
+$height-userGuide_popup_content_cover-default: 200px; // 默认气泡卡片封面高度
+$width-userGuide_modal_content_cover-default: 600px; // 默认弹窗式卡片封面宽度
+$height-userGuide_modal_content_cover-default: 300px; // 默认弹窗式卡片封面高度
+$height-userGuide_modal_content_indicator-default: 30px; // 默认弹窗式卡片指示器整体高度
+$width-userGuide_modal_content_indicator_item-default: 6px; // 默认弹窗式卡片单个指示器宽度
+$height-userGuide_modal_content_indicator_item-default: 6px; // 默认弹窗式卡片单个指示器高度
+$font-userGuide_popup_content_title-lineHeight: 24px; // 气泡卡片标题行高
+$font-userGuide_popup_content_description-lineHeight: 20px; // 气泡卡片详情行高
+$font-userGuide_popup_content_indicator-lineHeight: 20px; // 气泡卡片指示器行高
+$font-userGuide_modal_content_title-lineHeight: 24px; // 弹窗式卡片标题行高
+$font-userGuide_modal_content_description-lineHeight: 20px; // 弹窗式卡片详情行高
+
+// Spacing
+$spacing-userGuide_popup_content_body-padding: 24px; // 气泡卡片内边距
+$spacing-userGuide_popup_content_title-marginBottom: 8px; // 气泡卡片标题底部外边距
+$spacing-userGuide_popup_content_description-marginBottom: 48px; // 气泡卡片详情底部外边距
+$spacing-userGuide_popup_content_button-gap: 12px; // 气泡卡片底部按钮间距
+$spacing-userGuide_popup_content_button-marginLeft: 120px; // 气泡卡片按钮组左边距
+$spacing-userGuide_modal_content_title-marginBottom: 8px; // 弹窗式卡片标题底部外边距
+$spacing-userGuide_modal_content_body-padding: 24px 48px; // 弹窗式卡片body内边距
+$spacing-userGuide_modal_content_footer-padding: 24px; // 弹窗式卡片footer内边距
+$spacing-userGuide_popup_content_indicator-gap: 8px; // 弹窗式片指示器间距
+$spacing-userGuide_modal_content_button-gap: 12px; // 弹窗式卡片底部按钮间距
+
+
+// Radius
+$radius-userGuide_popup_content_cover: var(--semi-border-radius-medium) var(--semi-border-radius-medium) 0 0; // 气泡卡片封面圆角
+$radius-userGuide_modal_content_indicator: var(--semi-border-radius-large); // 气泡卡片指示器圆角
+
+
+// font
+$font-userGuide_popup_content_title-fontSize: $font-size-header-5; // 气泡卡片标题字体大小
+$font-userGuide_popup_content_description-fontSize: $font-size-regular; // 气泡卡片详情字体大小
+$font-userGuide_popup_content_indicator-fontSize: $font-size-regular; // 气泡卡片指示器字体大小
+$font-userGuide_popup_content_title-fontWeight: $font-weight-bold; // 气泡卡片标题字重
+$font-userGuide_modal_content_title-fontSize: $font-size-header-5; // 弹窗式卡片标题字体大小
+$font-userGuide_modal_content_description-fontSize: $font-size-regular; // 弹窗式卡详情字体大小
+$font-userGuide_modal_content_title-fontWeight: $font-weight-bold; // 弹窗式卡片标题字重
+
+

+ 1 - 0
packages/semi-ui/index.ts

@@ -127,3 +127,4 @@ export { default as JsonViewer } from './jsonViewer';
 export { default as DragMove } from './dragMove';
 export { default as Cropper } from './cropper';
 export { default as AudioPlayer } from './audioPlayer';
+export { default as UserGuide } from './userGuide';

+ 6 - 0
packages/semi-ui/locale/interface.ts

@@ -182,6 +182,12 @@ export interface Locale {
         copied: string;
         dropAreaText: string
     };
+    UserGuide: {
+        skip: string;
+        next: string;
+        prev: string;
+        finish: string
+    };
     InputNumber: {
     }
 }

+ 6 - 0
packages/semi-ui/locale/source/ar.ts

@@ -183,6 +183,12 @@ const local: Locale = {
         copied: 'نسخ',
         dropAreaText: 'ضع الملف هنا',
     },
+    UserGuide: {
+        skip: 'تخطي',
+        next: 'التالي',
+        prev: 'السابق',
+        finish: 'إنهاء',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/de.ts

@@ -183,6 +183,12 @@ const local: Locale = {
         copied: 'Kopiert',
         dropAreaText: 'Datei hier ablegen',
     },
+    UserGuide: {
+        skip: 'Überspringen',
+        next: 'Weiter',
+        prev: 'Zurück',
+        finish: 'Fertig',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/en_GB.ts

@@ -183,6 +183,12 @@ const local: Locale = {
         copied: 'Copied',
         dropAreaText: 'Put the file here',
     },
+    UserGuide: {
+        skip: 'Skip',
+        next: 'Next',
+        prev: 'Prev',
+        finish: 'Finish',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/en_US.ts

@@ -183,6 +183,12 @@ const local: Locale = {
         copied: 'Copied',
         dropAreaText: 'Put the file here',
     },
+    UserGuide: {
+        skip: 'Skip',
+        next: 'Next',
+        prev: 'Prev',
+        finish: 'Finish',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/es.ts

@@ -188,6 +188,12 @@ const locale: Locale = {
         copied: 'Copiado',
         dropAreaText: 'Coloca el archivo aquí',
     },
+    UserGuide: {
+        skip: 'Omitir',
+        next: 'Siguiente',
+        prev: 'Anterior',
+        finish: 'Finalizar',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/fr.ts

@@ -183,6 +183,12 @@ const local: Locale = {
         copied: 'Copié',
         dropAreaText: 'Déposez le fichier ici',
     },
+    UserGuide: {
+        skip: 'Passer',
+        next: 'Suivant',
+        prev: 'Précédent',
+        finish: 'Terminer',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/id_ID.ts

@@ -183,6 +183,12 @@ const local: Locale = {
         copied: 'Disalin',
         dropAreaText: 'Letakkan file di sini',
     },
+    UserGuide: {
+        skip: 'Lewati',
+        next: 'Selanjutnya',
+        prev: 'Sebelumnya',
+        finish: 'Selesai',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/it.ts

@@ -183,6 +183,12 @@ const local: Locale = {
         copied: 'Copiato',
         dropAreaText: 'Metti il file qui',
     },
+    UserGuide: {
+        skip: 'Salta',
+        next: 'Avanti',
+        prev: 'Indietro',
+        finish: 'Fine',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/ja_JP.ts

@@ -184,6 +184,12 @@ const local: Locale = {
         copied: 'コピーしました',
         dropAreaText: 'ファイルをここに置いてください',
     },
+    UserGuide: {
+        skip: 'スキップ',
+        next: '次へ',
+        prev: '前へ',
+        finish: '完了',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/ko_KR.ts

@@ -184,6 +184,12 @@ const local: Locale = {
         copied: '복사했습니다',
         dropAreaText: '파일을 여기에 놓으세요',
     },
+    UserGuide: {
+        skip: '건너뛰기',
+        next: '다음',
+        prev: '이전',
+        finish: '완료',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/ms_MY.ts

@@ -183,6 +183,12 @@ const local: Locale = {
         copied: 'Disalin',
         dropAreaText: 'Letakkan fail di sini',
     },
+    UserGuide: {
+        skip: 'Lewati',
+        next: 'Selanjutnya',
+        prev: 'Sebelumnya',
+        finish: 'Selesai',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/nl_NL.ts

@@ -190,6 +190,12 @@ const local: Locale = {
         copied: 'Gekopieerd',
         dropAreaText: 'Plaats het bestand hier',
     },
+    UserGuide: {
+        skip: 'Overslaan',
+        next: 'Volgende',
+        prev: 'Vorige',
+        finish: 'Voltooien',
+    },  
     InputNumber: {}
 };
 

+ 7 - 0
packages/semi-ui/locale/source/pl_PL.ts

@@ -190,6 +190,13 @@ const local: Locale = {
         copy: 'Kopiuj',
         copied: 'Skopiowano',
         dropAreaText: 'Umieść plik tutaj',
+    },
+    UserGuide: {
+        skip: 'Pomiń',
+        next: 'Następny',
+        prev: 'Poprzedni',
+        finish: 'Zakończ',
+    },
     },  
     InputNumber: {}
 };

+ 6 - 0
packages/semi-ui/locale/source/pt_BR.ts

@@ -191,6 +191,12 @@ const local: Locale = {
         copied: 'Cópia bem sucedida',
         dropAreaText: 'Coloque o arquivo aqui',
     },
+    UserGuide: {
+        skip: 'Pular',
+        next: 'Próximo',
+        prev: 'Anterior',
+        finish: 'Finalizar',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/ro.ts

@@ -183,6 +183,12 @@ const local: Locale = {
         copied: 'Copiat',
         dropAreaText: 'Puneți fișierul aici',
     },
+    UserGuide: {
+        skip: 'Omite',
+        next: 'Următorul',
+        prev: 'Anterior',
+        finish: 'Finalizare',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/ru_RU.ts

@@ -186,6 +186,12 @@ const local: Locale = {
         copied: 'Скопировано',
         dropAreaText: 'Положите файл здесь',
     },
+    UserGuide: {
+        skip: 'Пропустить',
+        next: 'Следующий',
+        prev: 'Предыдущий',
+        finish: 'Завершить',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/sv_SE.ts

@@ -188,6 +188,12 @@ const local: Locale = {
         copied: 'Kopierad',
         dropAreaText: 'Placera filen här',   
     }, 
+    UserGuide: {
+        skip: 'Hoppa över',
+        next: 'Nästa',
+        prev: 'Föregående',
+        finish: 'Slutför',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/th_TH.ts

@@ -187,6 +187,12 @@ const local: Locale = {
         copied: 'คัดลอกสำเร็จ',
         dropAreaText: 'วางไฟล์ที่นี่',
     },
+    UserGuide: {
+        skip: 'ข้าม',
+        next: 'ถัดไป',
+        prev: 'ก่อนหน้า',
+        finish: 'สำเร็จ',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/tr_TR.ts

@@ -184,6 +184,12 @@ const local: Locale = {
         copied: 'Kopyalama başarılı',
         dropAreaText: 'Dosyayı buraya yerleştirin',
     },
+    UserGuide: {
+        skip: 'Atla',
+        next: 'Sonraki',
+        prev: 'Önceki',
+        finish: 'Tamamla',
+    },
     InputNumber: {},
 };
 

+ 6 - 0
packages/semi-ui/locale/source/vi_VN.ts

@@ -186,6 +186,12 @@ const local: Locale = {
         copied: 'Đã sao chép',
         dropAreaText: 'Đặt tệp vào đây',
     }, 
+    UserGuide: {
+        skip: 'Bỏ qua',
+        next: 'Tiếp theo',
+        prev: 'Trước đó',
+        finish: 'Hoàn tất',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/zh_CN.ts

@@ -184,6 +184,12 @@ const local: Locale = {
         copied: '复制成功',
         dropAreaText: '将文件放到这里',
     },
+    UserGuide: {
+        skip: '跳过',
+        next: '下一步',
+        prev: '上一步',
+        finish: '完成',
+    },
     InputNumber: {}
 };
 

+ 6 - 0
packages/semi-ui/locale/source/zh_TW.ts

@@ -184,6 +184,12 @@ const local: Locale = {
         copied: '複制成功',
         dropAreaText: '將文件放到這裡',
     },
+    UserGuide: {
+        skip: '跳過',
+        next: '下一步',
+        prev: '上一步',
+        finish: '完成',
+    },
     InputNumber: {}
 };
 

+ 607 - 0
packages/semi-ui/userGuide/_story/userGuide.stories.jsx

@@ -0,0 +1,607 @@
+/* argus-disable unPkgSensitiveInfo */
+import React, { useState } from 'react';
+import { Button, UserGuide, Toast, Tag, Switch, Space } from '@douyinfe/semi-ui/index';
+
+export default {
+    title: 'UserGuide',
+};
+
+export const BasicUsage = () => {
+    const [visible, setVisible] = useState(false);
+
+    return (
+        <div>
+            <Space spacing='medium'>
+                <Switch id={'step-1'} defaultChecked={true}/>     
+                <Button id={'step-2'} type="secondary">次要</Button>
+                <Button id={'step-3'} type="tertiary">第三</Button>
+                <Button id={'step-4'} type="warning">警告</Button>
+            </Space>
+            <div style={{ marginTop: '20px' }}>
+                <Button onClick={() => setVisible(true)}>显示引导</Button>
+            </div>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                steps={[
+                    {
+                        target: document.querySelector('#step-1'),
+                        cover: (
+                            <img 
+                                alt="example" 
+                                height={200}
+                                src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/card-cover-docs-demo.jpeg" 
+                            />
+                        ),
+                        title: "这里是标题1",
+                        description: "一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "bottom"
+                    },
+                    {
+                        target: document.querySelector('#step-2'),
+                        cover: (
+                            <img 
+                                alt="example"
+                                height={200} 
+                                width={'100%'}
+                                src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/card-cover-docs-demo.jpeg" 
+                            />
+                        ),
+                        title: "这里是标题2",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "right"
+                    },
+                    {
+                        target: document.querySelector('#step-3'),
+                        title: "这里是标题3",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "bottom"
+                    },
+                    {
+                        target: document.querySelector('#step-4'),
+                        title: "这里是标题4",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "right"
+                    }
+                ]}
+                visible={visible}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成')
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('引导跳过')
+                }}
+        />
+        </div>
+    );
+};
+
+export const PrimaryTheme = () => {
+    const [visible, setVisible] = useState(false);
+
+    return (
+        <div>
+            <Space spacing='medium'>
+                <Switch id={'step-1'} defaultChecked={true}/>     
+                <Button id={'step-2'} type="secondary">次要</Button>
+                <Button id={'step-3'} type="tertiary">第三</Button>
+                <Button id={'step-4'} type="warning">警告</Button>
+            </Space>
+            <div style={{ marginTop: '20px' }}>
+                <Button onClick={() => setVisible(true)}>显示引导</Button>
+            </div>
+            <UserGuide
+                mode="popup"
+                theme="primary"
+                mask={true}
+                steps={[
+                    {
+                        target: document.querySelector('#step-1'),
+                        cover: (
+                            <img 
+                                alt="example" 
+                                height={200}
+                                width={'100%'}
+                                src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/card-cover-docs-demo.jpeg" 
+                            />
+                        ),
+                        title: "这里是标题1",
+                        description: "一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "bottom"
+                    },
+                    {
+                        target: document.querySelector('#step-2'),
+                        cover: (
+                            <img 
+                                alt="example" 
+                                height={200}
+                                width={'100%'}
+                                src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/card-cover-docs-demo.jpeg" 
+                            />
+                        ),
+                        title: "这里是标题2",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "right"
+                    },
+                    {
+                        target: document.querySelector('#step-3'),
+                        title: "这里是标题3",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "bottom"
+                    },
+                    {
+                        target: document.querySelector('#step-4'),
+                        title: "这里是标题4",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "right"
+                    }
+                ]}
+                visible={visible}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成')
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('引导跳过')
+                }}
+        />
+        </div>
+    );
+};
+
+export const HideButtons = () => {
+    const [visible, setVisible] = useState(false);
+
+    return (
+        <div>
+            <Space spacing='medium'>
+                <Switch id={'step-1'} defaultChecked={true}/>     
+                <Button id={'step-2'} type="secondary">次要</Button>
+                <Button id={'step-3'} type="tertiary">第三</Button>
+                <Button id={'step-4'} type="warning">警告</Button>
+            </Space>
+            <div style={{ marginTop: '20px' }}>
+                <Button onClick={() => setVisible(true)}>显示引导</Button>
+            </div>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                showPrevButton={false}
+                showSkipButton={false}
+                steps={[
+                    {
+                        target: document.querySelector('#step-1'),
+                        cover: (
+                            <img 
+                                alt="example"
+                                height={200}
+                                width={'100%'} 
+                                src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/card-cover-docs-demo.jpeg" 
+                            />
+                        ),
+                        title: "这里是标题1",
+                        description: "一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "bottom"
+                    },
+                    {
+                        target: document.querySelector('#step-2'),
+                        cover: (
+                            <img 
+                                alt="example" 
+                                height={200}
+                                width={'100%'}
+                                src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/card-cover-docs-demo.jpeg" 
+                            />
+                        ),
+                        title: "这里是标题2",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "right"
+                    },
+                    {
+                        target: document.querySelector('#step-3'),
+                        title: "这里是标题3",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "bottom"
+                    },
+                    {
+                        target: document.querySelector('#step-4'),
+                        title: "这里是标题4",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "right"
+                    }
+                ]}
+                visible={visible}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成')
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('引导跳过')
+                }}
+        />
+        </div>
+    );
+};
+
+export const NoMask = () => {
+    const [visible, setVisible] = useState(false);
+
+    return (
+        <div>
+            <Space spacing='medium'>
+                <Switch id={'step-1'} defaultChecked={true}/>     
+                <Button id={'step-2'} type="secondary">次要</Button>
+                <Button id={'step-3'} type="tertiary">第三</Button>
+                <Button id={'step-4'} type="warning">警告</Button>
+            </Space>
+            <div style={{ marginTop: '20px' }}>
+                <Button onClick={() => setVisible(true)}>显示引导</Button>
+            </div>
+            <UserGuide
+                mode="popup"
+                theme="primary"
+                mask={false}
+                steps={[
+                    {
+                        target: document.querySelector('#step-1'),
+                        cover: (
+                            <img 
+                                alt="example" 
+                                height={200}
+                                width={'100%'}
+                                src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/card-cover-docs-demo.jpeg" 
+                            />
+                        ),
+                        title: "这里是标题1",
+                        description: "一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "bottom"
+                    },
+                    {
+                        target: document.querySelector('#step-2'),
+                        cover: (
+                            <img 
+                                alt="example" 
+                                height={200}
+                                width={'100%'}
+                                src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/card-cover-docs-demo.jpeg" 
+                            />
+                        ),
+                        title: "这里是标题2",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "right"
+                    },
+                    {
+                        target: document.querySelector('#step-3'),
+                        title: "这里是标题3",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "bottom"
+                    },
+                    {
+                        target: document.querySelector('#step-4'),
+                        title: "这里是标题4",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "right"
+                    }
+                ]}
+                visible={visible}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成')
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('引导跳过')
+                }}
+        />
+        </div>
+    );
+};
+
+export const ControlledDemo = () => {
+    const [visible, setVisible] = useState(false);
+    const [current, setCurrent] = useState(1);
+
+    return (
+        <div>
+            <Space spacing='medium'>
+                <Switch id={'step-1'} defaultChecked={true}/>     
+                <Button id={'step-2'} type="secondary">次要</Button>
+                <Button id={'step-3'} type="tertiary">第三</Button>
+                <Button id={'step-4'} type="warning">警告</Button>
+            </Space>
+            <div style={{ marginTop: '20px' }}>
+                <Button onClick={() => setVisible(true)}>显示引导</Button>
+            </div>
+            <UserGuide
+                mode="popup"
+                theme="primary"
+                current={current}
+                steps={[
+                    {
+                        target: document.querySelector('#step-1'),
+                        cover: (
+                            <img 
+                                alt="example" 
+                                height={200}
+                                width={'100%'}
+                                src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/card-cover-docs-demo.jpeg" 
+                            />
+                        ),
+                        title: "这里是标题1",
+                        description: "一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "bottom"
+                    },
+                    {
+                        target: document.querySelector('#step-2'),
+                        cover: (
+                            <img 
+                                alt="example" 
+                                height={200}
+                                width={'100%'}
+                                src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/card-cover-docs-demo.jpeg" 
+                            />
+                        ),
+                        title: "这里是标题2",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "right"
+                    },
+                    {
+                        target: document.querySelector('#step-3'),
+                        title: "这里是标题3",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "bottom"
+                    },
+                    {
+                        target: document.querySelector('#step-4'),
+                        title: "这里是标题4",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "right"
+                    }
+                ]}
+                visible={visible}
+                onPrev={(current) => {
+                    console.log('onPrev', current)
+                    setCurrent(current - 1);
+                }}
+                onNext={(current) => {
+                    console.log('onNext', current)
+                    setCurrent(current + 1);
+                }}
+                onFinish={(current) => {
+                    console.log('onFinish', current)
+                    setVisible(false);
+                    setCurrent(0);
+                    console.log('引导完成')
+                }}
+                onSkip={(current) => {
+                    console.log('onSkip', current)
+                    setVisible(false);
+                    setCurrent(0);
+                    console.log('引导跳过')
+                }}
+                onChange={(current) => {
+                    console.log('onChange', current)
+                    console.log('changel', current)
+                }}
+        />
+        </div>
+    );
+};
+
+export const ModalMode = () => {
+    const [visible, setVisible] = useState(false);
+
+    return (
+        <div>
+            <Space spacing='medium'>
+                <Switch id={'step-1'} defaultChecked={true}/>     
+                <Button id={'step-2'} type="secondary">次要</Button>
+                <Button id={'step-3'} type="tertiary">第三</Button>
+                <Button id={'step-4'} type="warning">警告</Button>
+            </Space>
+            <div style={{ marginTop: '20px' }}>
+                <Button onClick={() => setVisible(true)}>显示引导</Button>
+            </div>
+            <UserGuide
+                mode="modal"
+                mask={true}
+                steps={[
+                    {
+                        cover: (
+                            <img 
+                                alt="example" 
+                                height={'100%'}
+                                width={'100%'}
+                                src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/card-cover-docs-demo.jpeg" 
+                            />
+                        ),
+                        title: "这里是标题1",
+                        description: "一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                    },
+                    {
+                        cover: (
+                            <img 
+                                alt="example"
+                                height={'100%'} 
+                                width={'100%'}
+                                src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/card-cover-docs-demo2.jpeg" 
+                            />
+                        ),
+                        title: "这里是标题2",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                    },
+                    {
+                        cover: (
+                            <img 
+                                alt="example"
+                                height={'100%'} 
+                                width={'100%'}
+                                src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/card-cover-docs-demo2.jpeg" 
+                            />
+                        ),
+                        title: "这里是标题3",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                    },
+                    {
+                        cover: (
+                            <img 
+                                alt="example"
+                                height={'100%'} 
+                                width={'100%'}
+                                src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/card-cover-docs-demo2.jpeg" 
+                            />
+                        ),
+                        title: "这里是标题4",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                    }
+                ]}
+                visible={visible}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成')
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('引导跳过')
+                }}
+        />
+        </div>
+    );
+};
+
+export const CustomButton = () => {
+    const [visible, setVisible] = useState(false);
+
+    return (
+        <div>
+            <Space spacing='medium'>
+                <Switch id={'step-1'} defaultChecked={true}/>     
+                <Button id={'step-2'} type="secondary">次要</Button>
+                <Button id={'step-3'} type="tertiary">第三</Button>
+                <Button id={'step-4'} type="warning">警告</Button>
+            </Space>
+            <div style={{ marginTop: '20px' }}>
+                <Button onClick={() => setVisible(true)}>显示引导</Button>
+            </div>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                finishText='开始使用'
+                prevButtonProps={{ theme: 'outline', type: 'primary' }}
+                steps={[
+                    {
+                        target: document.querySelector('#step-1'),
+                        title: "这里是标题1",
+                        description: "一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "bottom"
+                    },
+                    {
+                        target: document.querySelector('#step-2'),
+                        title: "这里是标题2",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "right"
+                    },
+                ]}
+                visible={visible}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成')
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('引导跳过')
+                }}
+        />
+        </div>
+    );
+}
+
+export const MixedTheme = () => {
+    const [visible, setVisible] = useState(false);
+
+    return (
+        <div>
+            <Space spacing='medium'>
+                <Switch id={'step-1'} defaultChecked={true}/>     
+                <Button id={'step-2'} type="secondary">次要</Button>
+                <Button id={'step-3'} type="tertiary">第三</Button>
+                <Button id={'step-4'} type="warning">警告</Button>
+            </Space>
+            <div style={{ marginTop: '20px' }}>
+                <Button onClick={() => setVisible(true)}>显示引导</Button>
+            </div>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                steps={[
+                    {
+                        target: document.querySelector('#step-1'),
+                        theme: 'primary',
+                        title: "这里是标题1",
+                        description: "一些描述文案",
+                        position: "bottom"
+                    },
+                    {
+                        target: document.querySelector('#step-2'),
+                        title: "这里是标题2",
+                        description: "这是第二步的说明一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案一些描述文案",
+                        position: "right"
+                    },
+                ]}
+                visible={visible}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成')
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('引导跳过')
+                }}
+        />
+        </div>
+    );
+}
+
+export const OneStep = () => {
+    const [visible, setVisible] = useState(false);
+
+    return (
+        <div>
+            <Space spacing='medium'>
+                <Switch id={'step-1'} defaultChecked={true}/>     
+                <Button id={'step-2'} type="secondary">次要</Button>
+                <Button id={'step-3'} type="tertiary">第三</Button>
+                <Button id={'step-4'} type="warning">警告</Button>
+            </Space>
+            <div style={{ marginTop: '20px' }}>
+                <Button onClick={() => setVisible(true)}>显示引导</Button>
+            </div>
+            <UserGuide
+                mode="popup"
+                mask={true}
+                steps={[
+                    {
+                        target: document.querySelector('#step-1'),
+                        theme: 'primary',
+                        title: "这里是标题",
+                        description: "一些描述文案",
+                        position: "bottom"
+                    }
+                ]}
+                visible={visible}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成')
+                }}
+                onSkip={() => {
+                    setVisible(false);
+                    console.log('引导跳过')
+                }}
+            />
+        </div>
+    )
+}

+ 42 - 0
packages/semi-ui/userGuide/_story/userGuide.stories.tsx

@@ -0,0 +1,42 @@
+/* argus-disable unPkgSensitiveInfo */
+import React, { useState } from 'react';
+import { Button, UserGuide, Toast, Tag, Switch, SideSheet } from '@douyinfe/semi-ui/index';
+
+export default {
+    title: 'UserGuide',
+};
+
+export const BasicUsage = () => {
+    const [visible, setVisible] = useState(false);
+
+    return (
+        <div>
+            <Button onClick={() => setVisible(true)}>显示引导</Button>
+            <div className="step-1">第一步目标</div>
+            <div className="step-2">第二步目标</div>
+            <UserGuide
+                mode="popup"
+                steps={[
+                    {
+                        target: document.querySelector('#step-1'),
+                        title: "第一步",
+                        description: "这是第一步的说明",
+                        position: "bottom"
+                    },
+                    {
+                        target: document.querySelector('#step-2'),
+                        title: "第二步",
+                        description: "这是第二步的说明",
+                        position: "right"
+                    }
+                ]}
+                visible={visible}
+                onFinish={() => {
+                    setVisible(false);
+                    console.log('引导完成')
+                }}
+        />
+        </div>
+    );
+};
+

+ 515 - 0
packages/semi-ui/userGuide/index.tsx

@@ -0,0 +1,515 @@
+import React, { ReactNode } from 'react';
+import cls from 'classnames';
+import PropTypes from 'prop-types';
+import { cssClasses, numbers, strings } from '@douyinfe/semi-foundation/userGuide/constants';
+import UserGuideFoundation, { UserGuideAdapter } from '@douyinfe/semi-foundation/userGuide/foundation';
+import { Position } from '../tooltip/index';
+import BaseComponent from '../_base/baseComponent';
+import Popover from '../popover';
+import Button, { ButtonProps } from '../button';
+import Modal from '../modal';
+import { noop } from '@douyinfe/semi-foundation/utils/function';
+import '@douyinfe/semi-foundation/userGuide/userGuide.scss';
+import { BaseProps } from '../_base/baseComponent';
+import isNullOrUndefined from '@douyinfe/semi-foundation/utils/isNullOrUndefined';
+import { getUuidShort } from '@douyinfe/semi-foundation/utils/uuid';
+import { Locale } from '../locale/interface';
+import LocaleConsumer from '../locale/localeConsumer';
+import { getScrollbarWidth } from '../_utils';
+
+
+const prefixCls = cssClasses.PREFIX;
+
+export interface UserGuideProps extends BaseProps {
+    className?: string;
+    current?: number;
+    finishText?: string;
+    mask?: boolean;
+    mode?: 'popup' | 'modal';
+    nextButtonProps?: ButtonProps;
+    onChange?: (current: number) => void;
+    onFinish?: () => void;
+    onNext?: (current: number) => void;
+    onPrev?: (current: number) => void;
+    onSkip?: () => void;
+    position?: Position;
+    prevButtonProps?: ButtonProps;
+    showPrevButton?: boolean;
+    showSkipButton?: boolean;
+    spotlightPadding?: number;
+    steps: StepItem[];
+    style?: React.CSSProperties;
+    theme?: 'default' | 'primary';
+    visible?: boolean;
+    getPopupContainer?: () => HTMLElement;
+    zIndex?: number
+}
+
+export interface StepItem {
+    className?: string;
+    cover?: ReactNode;
+    target?: (() => Element) | Element;
+    title?: string | ReactNode;
+    description?: React.ReactNode;
+    mask?: boolean;
+    showArrow?: boolean;
+    spotlightPadding?: number;
+    theme?: 'default' | 'primary';
+    position?: Position
+}
+
+export interface UserGuideState {
+    current: number;
+    spotlightRect: DOMRect | null
+}
+
+class UserGuide extends BaseComponent<UserGuideProps, UserGuideState> {
+    static propTypes = {
+        mask: PropTypes.bool,
+        mode: PropTypes.oneOf(strings.MODE),
+        onChange: PropTypes.func,
+        onFinish: PropTypes.func,
+        onNext: PropTypes.func,
+        onPrev: PropTypes.func,
+        onSkip: PropTypes.func,
+        position: PropTypes.oneOf(strings.POSITION_SET),
+        showPrevButton: PropTypes.bool,
+        showSkipButton: PropTypes.bool,
+        theme: PropTypes.oneOf(strings.THEME),
+        visible: PropTypes.bool,
+        getPopupContainer: PropTypes.func,
+        zIndex: PropTypes.number,
+    };
+
+    static defaultProps: UserGuideProps = {
+        mask: true,
+        mode: 'popup',
+        nextButtonProps: {},
+        onChange: noop,
+        onFinish: noop,
+        onNext: noop,
+        onPrev: noop,
+        onSkip: noop,
+        position: 'bottom',
+        prevButtonProps: {},
+        showPrevButton: true,
+        showSkipButton: true,
+        steps: [],
+        theme: 'default',
+        visible: false,
+        zIndex: numbers.DEFAULT_Z_INDEX,
+    };
+
+    private bodyOverflow: string;
+    private scrollBarWidth: number;
+    private originBodyWidth: string;
+    foundation: UserGuideFoundation;
+    userGuideId: string;
+
+    constructor(props: UserGuideProps) {
+        super(props);
+        this.foundation = new UserGuideFoundation(this.adapter);
+        this.state = {
+            current: props.current || numbers.DEFAULT_CURRENT,
+            spotlightRect: null,
+        };
+        this.scrollBarWidth = 0;
+        this.userGuideId = '';
+    }
+
+    get adapter(): UserGuideAdapter<UserGuideProps, UserGuideState> {
+        return {
+            ...super.adapter,
+            disabledBodyScroll: () => {
+                const { getPopupContainer } = this.props;
+                this.bodyOverflow = document.body.style.overflow || '';
+                if (!getPopupContainer && this.bodyOverflow !== 'hidden') {
+                    document.body.style.overflow = 'hidden';
+                    document.body.style.width = `calc(${this.originBodyWidth || '100%'} - ${this.scrollBarWidth}px)`;
+                }
+            },
+            enabledBodyScroll: () => {
+                const { getPopupContainer } = this.props;
+                if (!getPopupContainer && this.bodyOverflow !== 'hidden') {
+                    document.body.style.overflow = this.bodyOverflow;
+                    document.body.style.width = this.originBodyWidth;
+                }
+            },
+            notifyChange: (current: number) => {
+                this.props.onChange(current);
+            },
+            notifyFinish: () => {
+                this.props.onFinish();
+            },
+            notifyNext: (current: number) => {
+                this.props.onNext(current);
+            },
+            notifyPrev: (current: number) => {
+                this.props.onPrev(current);
+            },
+            notifySkip: () => {
+                this.props.onSkip();
+            },
+            setCurrent: (current: number) => {
+                this.setState({ current });
+            }
+        };
+    }
+
+    static getDerivedStateFromProps(props: UserGuideProps, state: UserGuideState): Partial<UserGuideState> {
+        const states: Partial<UserGuideState> = {};
+        if (!isNullOrUndefined(props.current) && props.current !== state.current) {
+            states.current = props.current;
+        }
+        return states;
+    }
+
+    componentDidMount() {
+        this.foundation.init();
+        this.scrollBarWidth = getScrollbarWidth();
+        this.userGuideId = getUuidShort();
+    }
+
+    componentDidUpdate(prevProps: UserGuideProps, prevStates: UserGuideState) {
+        const { steps, mode, visible } = this.props;
+        const { current } = this.state;
+
+        if (visible !== prevProps.visible) {
+            if (visible) {
+                this.foundation.beforeShow();
+                this.setState({ current: 0 });
+            } else {
+                this.foundation.afterHide();
+            }
+        }
+
+        if (mode === 'popup' && (prevStates.current !== current) && steps[current] || (prevProps.visible !== visible)) {
+            this.updateSpotlightRect();
+        }
+    }
+
+    componentWillUnmount() {
+        this.foundation.destroy();
+    }
+
+    scrollTargetIntoViewIfNeeded(target: Element) {
+        if (!target) {
+            return ;
+        }
+        
+        const rect = target.getBoundingClientRect();
+        const isInViewport = 
+            rect.top >= 0 &&
+            rect.left >= 0 &&
+            rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
+            rect.right <= (window.innerWidth || document.documentElement.clientWidth);
+        
+        if (!isInViewport) {
+            target.scrollIntoView({
+                behavior: 'auto',
+                block: 'center'
+            });
+
+        }
+    }
+
+
+    async updateSpotlightRect() {
+        const { steps, spotlightPadding } = this.props;
+        const { current } = this.state;
+        const step = steps[current];
+
+        if (step.target) {
+            const target = typeof step.target === 'function' ? step.target() : step.target;
+            // Checks if the target element is within the viewport, and scrolls it into view if not
+            this.scrollTargetIntoViewIfNeeded(target);
+
+            const rect = target?.getBoundingClientRect();
+            const padding = step?.spotlightPadding || spotlightPadding || numbers.DEFAULT_SPOTLIGHT_PADDING;
+
+            const newRects = new DOMRect(
+                rect.x - padding,
+                rect.y - padding,
+                rect.width + padding * 2,
+                rect.height + padding * 2
+            );
+
+            requestAnimationFrame(() => {
+                this.setState({ spotlightRect: newRects });
+            });
+        }
+    }
+
+    renderPopupContent(step: StepItem, index: number) {
+        const { showPrevButton, showSkipButton, theme, steps, finishText, nextButtonProps, prevButtonProps } = this.props;
+        const { current } = this.state;
+
+        const isFirst = index === 0;
+        const isLast = index === steps.length - 1;
+        const popupPrefixCls = `${prefixCls}-popup-content`;
+        const isPrimaryTheme = theme === 'primary' || step?.theme === 'primary';
+        const { cover, title, description } = step;
+
+        return (
+            <LocaleConsumer componentName="UserGuide">
+                {(locale: Locale['UserGuide'], localeCode: Locale['code']) => (
+                    <div className={cls(`${popupPrefixCls}`, {
+                        [`${popupPrefixCls}-primary`]: isPrimaryTheme,
+                    })}
+                    >
+                        {cover && <div className={`${popupPrefixCls}-cover`}>{cover}</div>}
+                        <div className={`${popupPrefixCls}-body`}>
+                            {title && <div className={`${popupPrefixCls}-title`}>{title}</div>}
+                            {description && <div className={`${popupPrefixCls}-description`}>{description}</div>}
+                            <div className={`${popupPrefixCls}-footer`}>
+                                {steps.length > 1 && (
+                                    <div className={`${popupPrefixCls}-indicator`}>
+                                        {current + 1}/{steps.length}
+                                    </div>
+                                )}
+                                <div className={`${popupPrefixCls}-buttons`}>
+                                    {showSkipButton && !isLast && (
+                                        <Button 
+                                            style={isPrimaryTheme ? { backgroundColor: 'var(--semi-color-fill-2)' } : {}}
+                                            theme={isPrimaryTheme ? 'solid' : 'light'} 
+                                            type={isPrimaryTheme ? 'primary' : 'tertiary'} 
+                                            onClick={this.foundation.handleSkip}
+                                        >
+                                            {locale.skip}
+                                        </Button>
+                                    )}
+                                    {showPrevButton && !isFirst && (
+                                        <Button 
+                                            style={isPrimaryTheme ? { backgroundColor: 'var(--semi-color-fill-2)' } : {}}
+                                            theme={isPrimaryTheme ? 'solid' : 'light'} 
+                                            type={isPrimaryTheme ? 'primary' : 'tertiary'} 
+                                            onClick={this.foundation.handlePrev}
+                                            {...prevButtonProps}
+                                        >
+                                            {prevButtonProps?.children || locale.prev}
+                                        </Button>
+                                    )}
+                                    <Button 
+                                        style={isPrimaryTheme ? { backgroundColor: '#FFF' } : {}}
+                                        theme={isPrimaryTheme ? 'borderless' : 'solid'} 
+                                        type={'primary'} 
+                                        onClick={this.foundation.handleNext}
+                                        {...nextButtonProps}
+                                    >
+                                        {isLast ? (finishText || locale.finish) : (nextButtonProps?.children || locale.next)}
+                                    </Button>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                )}
+            </LocaleConsumer>
+        );
+    }
+
+    renderStep = (step: StepItem, index: number) => {
+        const { theme, position, visible, className, style, spotlightPadding } = this.props;
+        const { current } = this.state;
+
+        const isCurrentStep = current === index;
+        if (!step.target) {
+            return null;
+        }
+
+        const basePopoverStyle = { padding: 0 };
+
+        const target = typeof step.target === 'function' ? step.target() : step.target;
+        const rect = target.getBoundingClientRect();
+        const padding = step?.spotlightPadding || spotlightPadding || numbers.DEFAULT_SPOTLIGHT_PADDING;
+        const isPrimaryTheme = theme === 'primary' || step?.theme === 'primary';
+        const primaryStyle = isPrimaryTheme ? { backgroundColor: 'var(--semi-color-primary)' } : {};
+
+
+        return (
+            <Popover
+                key={`userGuide-popup-${index}`}
+                className={cls(`${prefixCls}-popover`, className)}
+                style={{ ...basePopoverStyle, ...primaryStyle, ...style }}
+                content={this.renderPopupContent(step, index)}
+                position={step.position || position}
+                trigger="custom"
+                visible={visible && isCurrentStep}
+                showArrow={step.showArrow !== false}
+            >
+                <div
+                    style={{
+                        position: 'fixed',
+                        left: rect.x - padding,
+                        top: rect.y - padding,
+                        width: rect.width + padding * 2,
+                        height: rect.height + padding * 2,
+                        pointerEvents: 'none',
+                    }}
+                >
+                </div>
+            </Popover>
+        );
+    };
+
+    renderSpotlight() {
+        const { steps, mask, zIndex } = this.props;
+        const { spotlightRect, current } = this.state;
+        const step = steps[current];
+
+        if (!step.target) {
+            return null;
+        }
+
+        if (!spotlightRect) {
+            this.updateSpotlightRect();
+        }
+
+        return (
+            <>
+                {
+                    spotlightRect ? (
+                        <svg className={`${prefixCls}-spotlight`} style={{ zIndex }}>
+                            <defs>
+                                <mask id={`spotlight-${this.userGuideId}`}>
+                                    <rect width="100%" height="100%" fill="white"/>
+                                    <rect
+                                        className={`${prefixCls}-spotlight-rect`}
+                                        x={spotlightRect.x}
+                                        y={spotlightRect.y}
+                                        width={spotlightRect.width}
+                                        height={spotlightRect.height}
+                                        rx={4}
+                                        fill="black"
+                                    />
+                                </mask>
+                            </defs>
+                            {
+                                mask && <rect
+                                    width="100%"
+                                    height="100%"
+                                    fill="var(--semi-color-overlay-bg)"
+                                    mask={`url(#spotlight-${this.userGuideId})`}
+                                />
+                            }
+                        </svg>
+                    ) : null
+                }
+            </>
+        );
+    }
+
+    renderIndicator = () => {
+        const { steps } = this.props;
+        const { current } = this.state;
+        const indicatorContent: ReactNode[] = [];
+        for (let i = 0; i < steps.length; i++) {
+            indicatorContent.push(
+                <span
+                    key={i}
+                    data-index={i}
+                    className={cls([`${cssClasses.PREFIX_MODAL}-indicator-item`], {
+                        [`${cssClasses.PREFIX_MODAL}-indicator-item-active`]: i === current
+                    })}
+                ></span>
+            );
+        }
+        return indicatorContent;
+    }
+
+    renderModal = () => {
+        const { visible, steps, showSkipButton, showPrevButton, finishText, nextButtonProps, prevButtonProps, mask } = this.props;
+        const { current } = this.state;
+        const step = steps[current];
+
+        const isFirst = current === 0;
+        const isLast = current === steps.length - 1;
+        const { cover, title, description } = step;
+
+        return (
+            <LocaleConsumer componentName="UserGuide">
+                {(locale: Locale['UserGuide'], localeCode: Locale['code']) => (
+                    <Modal
+                        className={cssClasses.PREFIX_MODAL}
+                        bodyStyle={{ padding: 0 }}
+                        header={null}
+                        visible={visible}
+                        maskClosable={false}
+                        mask={mask}
+                        centered
+                        footer={null}
+                    >
+                        {cover && 
+                        <>
+                            <div className={`${cssClasses.PREFIX_MODAL}-cover`}>
+                                {cover}
+                            </div>
+                            <div className={`${cssClasses.PREFIX_MODAL}-indicator`}>
+                                {this.renderIndicator()}
+                            </div>
+                        </>
+                        }
+                        {
+                            (title || description) && (
+                                <div className={`${cssClasses.PREFIX_MODAL}-body`}>
+                                    {title && <div className={`${cssClasses.PREFIX_MODAL}-body-title`}>{title}</div>}
+                                    {description && <div className={`${cssClasses.PREFIX_MODAL}-body-description`}>{description}</div>}
+                                </div>
+                            )
+                        }
+                        <div className={`${cssClasses.PREFIX_MODAL}-footer`}>
+                            {showSkipButton && !isLast && (
+                                <Button 
+                                    type='tertiary'
+                                    onClick={this.foundation.handleSkip}
+                                >
+                                    {locale.skip}
+                                </Button>
+                            )}
+                            {showPrevButton && !isFirst && (
+                                <Button 
+                                    type='tertiary'
+                                    onClick={this.foundation.handlePrev}
+                                    {...prevButtonProps}
+                                >
+                                    {prevButtonProps?.children || locale.prev}
+                                </Button>
+                            )}
+                            <Button 
+                                theme='solid'
+                                onClick={this.foundation.handleNext}
+                                {...nextButtonProps}
+                            >
+                                {isLast ? (finishText || locale.finish) : (nextButtonProps?.children || locale.next)}
+                            </Button>
+                        </div>
+                    </Modal>
+                )}
+            </LocaleConsumer>
+        );
+
+    }
+
+    render() {
+        const { mode, steps, visible } = this.props;
+
+        if (!visible || !steps.length) {
+            return null;
+        }
+
+        return (
+            <>
+                {
+                    mode === 'popup' ? (
+                        <React.Fragment>
+                            {steps?.map((step, index) => this.renderStep(step, index))}
+                            {this.renderSpotlight()}
+                        </React.Fragment>
+                    ) : null
+                }
+                { mode === 'modal' && this.renderModal()}
+            </>
+        );
+    }
+}
+
+export default UserGuide; 

+ 6 - 0
src/images/docIcons/doc-userGuide.svg

@@ -0,0 +1,6 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="2" y="2" width="20" height="20" rx="3" fill="#AAB2BF"/>
+<rect x="4" y="5" width="16" height="14" rx="1" fill="white"/>
+<rect x="6" y="14" width="5" height="3" rx="1" fill="#AAB2BF"/>
+<rect x="13" y="14" width="5" height="3" rx="1" fill="#4CC3FA"/>
+</svg>