@rainbow-oh/yee-c 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/dist/cjs/Button/button.js +2 -2
  2. package/dist/cjs/Button/style/index.less +23 -8
  3. package/dist/cjs/Card/demo/collapsible.js +2 -1
  4. package/dist/cjs/Carousel/demo/arrows.js +1 -1
  5. package/dist/cjs/Carousel/demo/autoplay.js +1 -1
  6. package/dist/cjs/Carousel/demo/effect.js +1 -1
  7. package/dist/cjs/Carousel/demo/position.js +1 -1
  8. package/dist/cjs/Config-Provider/interface.d.ts +4 -0
  9. package/dist/cjs/Form/Field.js +32 -10
  10. package/dist/cjs/Form/demo/basic.js +56 -81
  11. package/dist/cjs/Form/demo/form-table.js +68 -67
  12. package/dist/cjs/Form/interface.d.ts +17 -1
  13. package/dist/cjs/Menu/style/index.less +4 -2
  14. package/dist/cjs/Radio/demo/button.js +1 -0
  15. package/dist/cjs/Radio/interface.d.ts +4 -0
  16. package/dist/cjs/Radio/radio-group.js +28 -14
  17. package/dist/cjs/Radio/radio.js +3 -1
  18. package/dist/cjs/Radio/style/index.less +19 -5
  19. package/dist/cjs/Segmented/demo/basic.d.ts +3 -0
  20. package/dist/cjs/Segmented/demo/basic.js +51 -0
  21. package/dist/cjs/Segmented/demo/block.d.ts +3 -0
  22. package/dist/cjs/Segmented/demo/block.js +49 -0
  23. package/dist/cjs/Segmented/demo/disabled.d.ts +3 -0
  24. package/dist/cjs/Segmented/demo/disabled.js +47 -0
  25. package/dist/cjs/Segmented/demo/size.d.ts +3 -0
  26. package/dist/cjs/Segmented/demo/size.js +37 -0
  27. package/dist/cjs/Segmented/demo/variant.d.ts +3 -0
  28. package/dist/cjs/Segmented/demo/variant.js +50 -0
  29. package/dist/cjs/Segmented/index.d.ts +3 -0
  30. package/dist/cjs/Segmented/index.js +36 -0
  31. package/dist/cjs/Segmented/interface.d.ts +87 -0
  32. package/dist/cjs/Segmented/interface.js +17 -0
  33. package/dist/cjs/Segmented/segmented.d.ts +5 -0
  34. package/dist/cjs/Segmented/segmented.js +180 -0
  35. package/dist/cjs/Segmented/style/index.less +128 -0
  36. package/dist/cjs/Spin/spin.js +2 -1
  37. package/dist/cjs/Steps/step.js +1 -1
  38. package/dist/cjs/Steps/style/index.less +8 -2
  39. package/dist/cjs/Switch/demo/content.js +22 -1
  40. package/dist/cjs/Switch/style/index.less +16 -0
  41. package/dist/cjs/Switch/switch.js +9 -2
  42. package/dist/cjs/Table/interface.d.ts +5 -1
  43. package/dist/cjs/Table/theader/cell.js +4 -5
  44. package/dist/cjs/Tag/demo/size.js +1 -1
  45. package/dist/cjs/Tag/demo/variant.d.ts +3 -0
  46. package/dist/cjs/Tag/demo/variant.js +39 -0
  47. package/dist/cjs/Tag/interface.d.ts +5 -0
  48. package/dist/cjs/Tag/style/index.less +109 -25
  49. package/dist/cjs/Tag/tag.js +3 -0
  50. package/dist/cjs/Tour/demo/basic.d.ts +3 -0
  51. package/dist/cjs/Tour/demo/basic.js +69 -0
  52. package/dist/cjs/Tour/demo/indicators.d.ts +3 -0
  53. package/dist/cjs/Tour/demo/indicators.js +76 -0
  54. package/dist/cjs/Tour/demo/placement.d.ts +3 -0
  55. package/dist/cjs/Tour/demo/placement.js +80 -0
  56. package/dist/cjs/Tour/demo/scroll.d.ts +3 -0
  57. package/dist/cjs/Tour/demo/scroll.js +72 -0
  58. package/dist/cjs/Tour/index.d.ts +3 -0
  59. package/dist/cjs/Tour/index.js +36 -0
  60. package/dist/cjs/Tour/interface.d.ts +118 -0
  61. package/dist/cjs/Tour/interface.js +17 -0
  62. package/dist/cjs/Tour/style/index.less +103 -0
  63. package/dist/cjs/Tour/tour.d.ts +5 -0
  64. package/dist/cjs/Tour/tour.js +403 -0
  65. package/dist/cjs/Tree/demo/checkable.js +6 -6
  66. package/dist/cjs/index.d.ts +4 -0
  67. package/dist/cjs/index.js +6 -0
  68. package/dist/cjs/locale/interface.d.ts +7 -0
  69. package/dist/cjs/locale/lang/en_US.js +6 -0
  70. package/dist/cjs/locale/lang/ja_JP.js +6 -0
  71. package/dist/cjs/locale/lang/zh_CN.js +6 -0
  72. package/dist/cjs/locale/lang/zh_TW.js +6 -0
  73. package/dist/cjs/style/color.blue.css +1 -1
  74. package/dist/cjs/style/color.crimson.css +1 -1
  75. package/dist/cjs/style/color.css +21 -4
  76. package/dist/cjs/style/size.css +38 -0
  77. package/dist/cjs/style/themes.css +3 -3
  78. package/dist/cjs/style/variables.css +60 -5
  79. package/dist/cjs/version.d.ts +1 -1
  80. package/dist/cjs/version.js +1 -1
  81. package/dist/esm/Button/button.js +2 -2
  82. package/dist/esm/Button/style/index.less +23 -8
  83. package/dist/esm/Card/demo/collapsible.js +2 -1
  84. package/dist/esm/Carousel/demo/arrows.js +1 -1
  85. package/dist/esm/Carousel/demo/autoplay.js +1 -1
  86. package/dist/esm/Carousel/demo/effect.js +1 -1
  87. package/dist/esm/Carousel/demo/position.js +1 -1
  88. package/dist/esm/Config-Provider/interface.d.ts +4 -0
  89. package/dist/esm/Form/Field.js +11 -4
  90. package/dist/esm/Form/demo/basic.js +2 -18
  91. package/dist/esm/Form/demo/form-table.js +4 -10
  92. package/dist/esm/Form/interface.d.ts +17 -1
  93. package/dist/esm/Menu/style/index.less +4 -2
  94. package/dist/esm/Radio/demo/button.js +1 -0
  95. package/dist/esm/Radio/interface.d.ts +4 -0
  96. package/dist/esm/Radio/radio-group.js +9 -2
  97. package/dist/esm/Radio/radio.js +3 -1
  98. package/dist/esm/Radio/style/index.less +19 -5
  99. package/dist/esm/Segmented/demo/basic.d.ts +3 -0
  100. package/dist/esm/Segmented/demo/basic.js +33 -0
  101. package/dist/esm/Segmented/demo/block.d.ts +3 -0
  102. package/dist/esm/Segmented/demo/block.js +29 -0
  103. package/dist/esm/Segmented/demo/disabled.d.ts +3 -0
  104. package/dist/esm/Segmented/demo/disabled.js +22 -0
  105. package/dist/esm/Segmented/demo/size.d.ts +3 -0
  106. package/dist/esm/Segmented/demo/size.js +15 -0
  107. package/dist/esm/Segmented/demo/variant.d.ts +3 -0
  108. package/dist/esm/Segmented/demo/variant.js +49 -0
  109. package/dist/esm/Segmented/index.d.ts +3 -0
  110. package/dist/esm/Segmented/index.js +2 -0
  111. package/dist/esm/Segmented/interface.d.ts +87 -0
  112. package/dist/esm/Segmented/interface.js +1 -0
  113. package/dist/esm/Segmented/segmented.d.ts +5 -0
  114. package/dist/esm/Segmented/segmented.js +112 -0
  115. package/dist/esm/Segmented/style/index.less +128 -0
  116. package/dist/esm/Spin/spin.js +2 -1
  117. package/dist/esm/Steps/step.js +4 -1
  118. package/dist/esm/Steps/style/index.less +8 -2
  119. package/dist/esm/Switch/demo/content.js +12 -3
  120. package/dist/esm/Switch/style/index.less +16 -0
  121. package/dist/esm/Switch/switch.js +7 -5
  122. package/dist/esm/Table/interface.d.ts +5 -1
  123. package/dist/esm/Table/theader/cell.js +2 -3
  124. package/dist/esm/Tag/demo/size.js +6 -2
  125. package/dist/esm/Tag/demo/variant.d.ts +3 -0
  126. package/dist/esm/Tag/demo/variant.js +48 -0
  127. package/dist/esm/Tag/interface.d.ts +5 -0
  128. package/dist/esm/Tag/style/index.less +109 -25
  129. package/dist/esm/Tag/tag.js +4 -2
  130. package/dist/esm/Tour/demo/basic.d.ts +3 -0
  131. package/dist/esm/Tour/demo/basic.js +69 -0
  132. package/dist/esm/Tour/demo/indicators.d.ts +3 -0
  133. package/dist/esm/Tour/demo/indicators.js +73 -0
  134. package/dist/esm/Tour/demo/placement.d.ts +3 -0
  135. package/dist/esm/Tour/demo/placement.js +53 -0
  136. package/dist/esm/Tour/demo/scroll.d.ts +3 -0
  137. package/dist/esm/Tour/demo/scroll.js +58 -0
  138. package/dist/esm/Tour/index.d.ts +3 -0
  139. package/dist/esm/Tour/index.js +2 -0
  140. package/dist/esm/Tour/interface.d.ts +118 -0
  141. package/dist/esm/Tour/interface.js +1 -0
  142. package/dist/esm/Tour/style/index.less +103 -0
  143. package/dist/esm/Tour/tour.d.ts +5 -0
  144. package/dist/esm/Tour/tour.js +383 -0
  145. package/dist/esm/Tree/demo/checkable.js +6 -6
  146. package/dist/esm/index.d.ts +4 -0
  147. package/dist/esm/index.js +4 -0
  148. package/dist/esm/locale/interface.d.ts +7 -0
  149. package/dist/esm/locale/lang/en_US.js +6 -0
  150. package/dist/esm/locale/lang/ja_JP.js +6 -0
  151. package/dist/esm/locale/lang/zh_CN.js +6 -0
  152. package/dist/esm/locale/lang/zh_TW.js +6 -0
  153. package/dist/esm/style/color.blue.css +1 -1
  154. package/dist/esm/style/color.crimson.css +1 -1
  155. package/dist/esm/style/color.css +21 -4
  156. package/dist/esm/style/size.css +38 -0
  157. package/dist/esm/style/themes.css +3 -3
  158. package/dist/esm/style/variables.css +60 -5
  159. package/dist/esm/version.d.ts +1 -1
  160. package/dist/esm/version.js +1 -1
  161. package/dist/umd/yee-c.min.css +4 -4
  162. package/dist/umd/yee-c.min.js +1 -1
  163. package/package.json +1 -1
  164. package/src/Button/button.tsx +2 -2
  165. package/src/Button/style/index.less +23 -8
  166. package/src/Card/demo/collapsible.tsx +1 -0
  167. package/src/Carousel/demo/arrows.tsx +1 -1
  168. package/src/Carousel/demo/autoplay.tsx +1 -1
  169. package/src/Carousel/demo/effect.tsx +1 -1
  170. package/src/Carousel/demo/position.tsx +1 -1
  171. package/src/Config-Provider/index.en-US.md +2 -0
  172. package/src/Config-Provider/index.zh-CN.md +2 -0
  173. package/src/Config-Provider/interface.ts +4 -0
  174. package/src/Form/Field.tsx +30 -11
  175. package/src/Form/demo/basic.tsx +2 -24
  176. package/src/Form/demo/form-table.tsx +16 -13
  177. package/src/Form/index.en-US.md +4 -0
  178. package/src/Form/index.zh-CN.md +4 -0
  179. package/src/Form/interface.ts +17 -1
  180. package/src/Menu/style/index.less +4 -2
  181. package/src/Radio/demo/button.tsx +1 -0
  182. package/src/Radio/index.en-US.md +2 -0
  183. package/src/Radio/index.zh-CN.md +2 -0
  184. package/src/Radio/interface.ts +4 -0
  185. package/src/Radio/radio-group.tsx +9 -1
  186. package/src/Radio/radio.tsx +2 -1
  187. package/src/Radio/style/index.less +19 -5
  188. package/src/Segmented/demo/basic.tsx +24 -0
  189. package/src/Segmented/demo/block.tsx +17 -0
  190. package/src/Segmented/demo/disabled.tsx +18 -0
  191. package/src/Segmented/demo/size.tsx +10 -0
  192. package/src/Segmented/demo/variant.tsx +28 -0
  193. package/src/Segmented/index.en-US.md +49 -0
  194. package/src/Segmented/index.ts +12 -0
  195. package/src/Segmented/index.zh-CN.md +49 -0
  196. package/src/Segmented/interface.ts +92 -0
  197. package/src/Segmented/segmented.tsx +136 -0
  198. package/src/Segmented/style/index.less +128 -0
  199. package/src/Spin/spin.tsx +1 -0
  200. package/src/Steps/step.tsx +1 -1
  201. package/src/Steps/style/index.less +8 -2
  202. package/src/Switch/demo/content.tsx +15 -5
  203. package/src/Switch/style/index.less +16 -0
  204. package/src/Switch/switch.tsx +14 -6
  205. package/src/Table/index.en-US.md +1 -0
  206. package/src/Table/index.zh-CN.md +1 -0
  207. package/src/Table/interface.ts +5 -1
  208. package/src/Table/theader/cell.tsx +2 -9
  209. package/src/Tag/demo/size.tsx +2 -2
  210. package/src/Tag/demo/variant.tsx +47 -0
  211. package/src/Tag/index.en-US.md +2 -0
  212. package/src/Tag/index.zh-CN.md +2 -0
  213. package/src/Tag/interface.ts +5 -0
  214. package/src/Tag/style/index.less +109 -25
  215. package/src/Tag/tag.tsx +2 -0
  216. package/src/Tour/demo/basic.tsx +52 -0
  217. package/src/Tour/demo/indicators.tsx +63 -0
  218. package/src/Tour/demo/placement.tsx +55 -0
  219. package/src/Tour/demo/scroll.tsx +49 -0
  220. package/src/Tour/index.en-US.md +61 -0
  221. package/src/Tour/index.ts +10 -0
  222. package/src/Tour/index.zh-CN.md +61 -0
  223. package/src/Tour/interface.ts +126 -0
  224. package/src/Tour/style/index.less +103 -0
  225. package/src/Tour/tour.tsx +399 -0
  226. package/src/Tree/demo/checkable.tsx +6 -6
  227. package/src/index.ts +6 -0
  228. package/src/locale/interface.ts +9 -0
  229. package/src/locale/lang/en_US.ts +7 -0
  230. package/src/locale/lang/ja_JP.ts +7 -0
  231. package/src/locale/lang/zh_CN.ts +7 -0
  232. package/src/locale/lang/zh_TW.ts +7 -0
  233. package/src/style/color.blue.css +1 -1
  234. package/src/style/color.crimson.css +1 -1
  235. package/src/style/color.css +21 -4
  236. package/src/style/size.css +38 -0
  237. package/src/version.ts +1 -1
  238. package/dist/cjs/utils/x.d.ts +0 -11
  239. package/dist/cjs/utils/x.js +0 -51
  240. package/dist/esm/utils/x.d.ts +0 -11
  241. package/dist/esm/utils/x.js +0 -49
  242. package/src/utils/x.ts +0 -33
@@ -0,0 +1,61 @@
1
+ ---
2
+ category: Components
3
+ title: Tour
4
+ subtitle: 新手引导
5
+ group:
6
+ title: 反馈
7
+ order: 29
8
+ toc: 'content'
9
+ ---
10
+
11
+ # Tour 新手引导
12
+
13
+ 步骤式新手引导:全屏遮罩 + 高亮目标元素 + 贴附说明卡片,支持多步推进、进度指示、自动滚动定位。常用于产品功能向导、新用户 onboarding。
14
+
15
+ ## 代码演示
16
+
17
+ <code src="./demo/basic.tsx" title="基础用法" description="多步引导,受控 open,支持上一步/下一步/完成/跳过"></code>
18
+ <code src="./demo/placement.tsx" title="气泡位置" description="卡片相对目标的 placement,靠近视口边缘时自动翻转"></code>
19
+ <code src="./demo/indicators.tsx" title="自定义进度指示" description="通过 indicatorsRender 自定义步骤进度点"></code>
20
+ <code src="./demo/scroll.tsx" title="滚动定位与容错" description="切换步骤时自动滚动到目标;目标不存在时卡片居中"></code>
21
+
22
+ ## API
23
+
24
+ ### TourProps
25
+
26
+ | 属性名 | 类型 | 描述 | 默认值 |
27
+ | --- | --- | --- | --- |
28
+ | open | `boolean` | 是否显示引导(受控) | - |
29
+ | steps | `TourStep[]` | 引导步骤 | - |
30
+ | current | `number` | 受控当前步骤索引 | - |
31
+ | defaultCurrent | `number` | 默认当前步骤索引 | `0` |
32
+ | onClose | `() => void` | 关闭/跳过时的回调 | - |
33
+ | onFinish | `() => void` | 完成全部步骤时的回调 | - |
34
+ | onChange | `(current: number) => void` | 步骤变化时的回调 | - |
35
+ | placement | `Placement` | 卡片默认位置 | `'bottom'` |
36
+ | arrow | `boolean` | 是否显示卡片箭头 | - |
37
+ | mask | `boolean` | 是否显示遮罩(含高亮挖洞与点击锁定) | `true` |
38
+ | closable | `boolean` | 是否显示右上关闭按钮 | `true` |
39
+ | closeIcon | `React.ReactNode` | 自定义关闭图标 | - |
40
+ | maskClosable | `boolean` | 点击遮罩是否关闭 | `false` |
41
+ | indicatorsRender | `(current: number, total: number) => React.ReactNode` | 自定义进度指示 | 默认 `current+1 / total` |
42
+ | scrollIntoViewOptions | `boolean \| ScrollIntoViewOptions` | 切换步骤时目标 scrollIntoView 的参数 | `true` |
43
+ | className / style | | 自定义根类名/样式 | - |
44
+ | classNames / styles | `Partial<Record<'highlight' \| 'content' \| 'header' \| 'body' \| 'footer', …>>` | 语义化结构类名/样式 | - |
45
+
46
+ > `Placement` = `'top' \| 'right' \| 'bottom' \| 'left' \| 'topLeft' \| 'topRight' \| 'bottomLeft' \| 'bottomRight' \| 'leftTop' \| 'leftBottom' \| 'rightTop' \| 'rightBottom'`,与 `Trigger` 一致。
47
+
48
+ ### TourStep
49
+
50
+ | 属性名 | 类型 | 描述 | 默认值 |
51
+ | --- | --- | --- | --- |
52
+ | target | `(() => HTMLElement \| null) \| string \| HTMLElement` | 高亮目标(查找函数 / CSS 选择器 / 元素) | - |
53
+ | title | `React.ReactNode` | 卡片标题 | - |
54
+ | description | `React.ReactNode` | 卡片描述 | - |
55
+ | placement | `Placement` | 该步卡片位置,覆盖 TourProps.placement | - |
56
+ | arrow | `boolean` | 该步是否显示箭头 | - |
57
+ | className | `string` | 该步卡片自定义类名 | - |
58
+
59
+ ### 内置文案
60
+
61
+ 按钮(上一步/下一步/完成/跳过)与进度文案取自 `locale.tour`(`previous`/`next`/`finish`/`skip`),随 `ConfigProvider` 的 locale 切换;可通过语言包或 `ConfigProvider.value.tour` 覆盖。
@@ -0,0 +1,126 @@
1
+ import type { Placement } from '../Trigger/interface';
2
+ import type { DataAttributeProps } from '../utils/types';
3
+
4
+ export type TourTarget = (() => HTMLElement | null) | string | HTMLElement;
5
+
6
+ export type TourSemanticDOM =
7
+ | 'highlight'
8
+ | 'content'
9
+ | 'header'
10
+ | 'body'
11
+ | 'footer';
12
+
13
+ export interface TourStep {
14
+ /**
15
+ * The element to highlight, supports a finder function, a CSS selector string, or an HTMLElement
16
+ */
17
+ target: TourTarget;
18
+ /**
19
+ * Card title
20
+ */
21
+ title?: React.ReactNode;
22
+ /**
23
+ * Card description
24
+ */
25
+ description?: React.ReactNode;
26
+ /**
27
+ * Card placement relative to the highlighted target
28
+ * @default 'bottom'
29
+ */
30
+ placement?: Placement;
31
+ /**
32
+ * Whether to show the card arrow
33
+ */
34
+ arrow?: boolean;
35
+ /**
36
+ * Custom step class name
37
+ */
38
+ className?: string;
39
+ }
40
+
41
+ export interface TourProps extends DataAttributeProps {
42
+ /**
43
+ * Custom prefix class name
44
+ */
45
+ prefixCls?: string;
46
+ /**
47
+ * Custom root class name
48
+ */
49
+ className?: string;
50
+ /**
51
+ * Custom root style
52
+ */
53
+ style?: React.CSSProperties;
54
+ /**
55
+ * Semantic structure class names
56
+ */
57
+ classNames?: Partial<Record<TourSemanticDOM, string>>;
58
+ /**
59
+ * Semantic structure styles
60
+ */
61
+ styles?: Partial<Record<TourSemanticDOM, React.CSSProperties>>;
62
+ /**
63
+ * Whether the tour is visible (controlled)
64
+ */
65
+ open: boolean;
66
+ /**
67
+ * Tour steps
68
+ */
69
+ steps: TourStep[];
70
+ /**
71
+ * Controlled current step index
72
+ */
73
+ current?: number;
74
+ /**
75
+ * Default current step index
76
+ */
77
+ defaultCurrent?: number;
78
+ /**
79
+ * Callback when the tour is closed / skipped
80
+ */
81
+ onClose?: () => void;
82
+ /**
83
+ * Callback when all steps are finished
84
+ */
85
+ onFinish?: () => void;
86
+ /**
87
+ * Callback when current step changes
88
+ */
89
+ onChange?: (current: number) => void;
90
+ /**
91
+ * Default card placement relative to the highlighted target
92
+ * @default 'bottom'
93
+ */
94
+ placement?: Placement;
95
+ /**
96
+ * Whether to show the card arrow by default
97
+ */
98
+ arrow?: boolean;
99
+ /**
100
+ * Whether to show the mask layer (highlight cutout + click lock)
101
+ * @default true
102
+ */
103
+ mask?: boolean;
104
+ /**
105
+ * Whether the close button is shown at the top-right of the card
106
+ * @default true
107
+ */
108
+ closable?: boolean;
109
+ /**
110
+ * Custom close icon
111
+ */
112
+ closeIcon?: React.ReactNode;
113
+ /**
114
+ * Whether clicking the mask closes the tour
115
+ * @default false
116
+ */
117
+ maskClosable?: boolean;
118
+ /**
119
+ * Custom step indicators renderer, receives (current, total)
120
+ */
121
+ indicatorsRender?: (current: number, total: number) => React.ReactNode;
122
+ /**
123
+ * Options passed to target.scrollIntoView on each step
124
+ */
125
+ scrollIntoViewOptions?: boolean | ScrollIntoViewOptions;
126
+ }
@@ -0,0 +1,103 @@
1
+ @pls: .yee-tour;
2
+
3
+ @{pls} {
4
+ position: fixed;
5
+ inset: 0;
6
+ z-index: var(--yee-tour-mask-z-index);
7
+ pointer-events: none;
8
+
9
+ // ① click-intercept layer (box-shadow receives no pointer events, so a
10
+ // transparent full-screen layer is needed to lock interaction + carry maskClosable)
11
+ &-mask {
12
+ position: absolute;
13
+ inset: 0;
14
+ pointer-events: auto;
15
+ background-color: transparent;
16
+
17
+ // No target to cut out → the mask itself becomes the full-screen dim layer.
18
+ &-full {
19
+ background-color: var(--yee-tour-mask-color);
20
+ }
21
+ }
22
+
23
+ // ② highlight cutout via a huge box-shadow; top/left/width/height come from
24
+ // motion inline styles, position/shape/visuals from here
25
+ &-highlight {
26
+ position: fixed;
27
+ box-sizing: border-box;
28
+ border-radius: var(--yee-tour-highlight-border-radius);
29
+ box-shadow: 0 0 0 9999px var(--yee-tour-mask-color);
30
+ z-index: 1;
31
+ pointer-events: none;
32
+ }
33
+ }
34
+
35
+ // card
36
+ .yee-tour-content {
37
+ position: fixed;
38
+ z-index: 2;
39
+ pointer-events: auto;
40
+ box-sizing: border-box;
41
+ width: var(--yee-tour-content-width);
42
+ max-width: calc(100vw - 48px);
43
+ padding: var(--yee-tour-content-padding);
44
+ background-color: var(--yee-tour-content-bg-color);
45
+ border-radius: var(--yee-tour-content-border-radius);
46
+ box-shadow: var(--yee-tour-content-box-shadow);
47
+
48
+ .yee-tour-close {
49
+ position: absolute;
50
+ top: var(--yee-tour-close-top);
51
+ inset-inline-end: var(--yee-tour-close-right);
52
+ display: inline-flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ padding: 0;
56
+ border: none;
57
+ background: transparent;
58
+ color: var(--yee-tour-close-color);
59
+ cursor: pointer;
60
+ opacity: 0.55;
61
+ transition: opacity var(--yee-tour-transition-duration) linear;
62
+
63
+ &:hover {
64
+ opacity: 1;
65
+ }
66
+ }
67
+
68
+ .yee-tour-header {
69
+ margin-bottom: var(--yee-tour-header-margin-bottom);
70
+ padding-inline-end: var(--yee-tour-close-size);
71
+ color: var(--yee-tour-title-color);
72
+ font-size: var(--yee-tour-title-font-size);
73
+ font-weight: var(--yee-tour-title-font-weight);
74
+ line-height: var(--yee-tour-title-line-height);
75
+ }
76
+
77
+ .yee-tour-body {
78
+ color: var(--yee-tour-description-color);
79
+ font-size: var(--yee-tour-description-font-size);
80
+ line-height: var(--yee-tour-description-line-height);
81
+ }
82
+
83
+ .yee-tour-footer {
84
+ display: flex;
85
+ align-items: center;
86
+ justify-content: space-between;
87
+ gap: var(--yee-tour-button-gap);
88
+ margin-top: var(--yee-tour-footer-margin-top);
89
+ }
90
+
91
+ .yee-tour-indicators {
92
+ display: inline-flex;
93
+ align-items: center;
94
+ gap: var(--yee-tour-indicator-gap);
95
+ }
96
+
97
+ .yee-tour-buttons {
98
+ display: inline-flex;
99
+ align-items: center;
100
+ gap: var(--yee-tour-button-gap);
101
+ margin-left: auto;
102
+ }
103
+ }
@@ -0,0 +1,399 @@
1
+ import clsx from 'clsx';
2
+ import { X } from 'lucide-react';
3
+ import { motion } from 'motion/react';
4
+ import React, {
5
+ useCallback,
6
+ useContext,
7
+ useLayoutEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from 'react';
12
+ import { createPortal } from 'react-dom';
13
+ import Button from '../Button';
14
+ import { GlobalContext } from '../Config-Provider';
15
+ import type { Placement } from '../Trigger/interface';
16
+ import useEsc from '../hooks/useEsc';
17
+ import useLatest from '../hooks/useLatest';
18
+ import useMergedState from '../hooks/useMergedState';
19
+ import { useLocale } from '../locale';
20
+ import mergeContextToProps from '../utils/mergeContextToProps';
21
+ import type { TourProps, TourTarget } from './interface';
22
+
23
+ import './style/index.less';
24
+
25
+ const GAP = 12;
26
+
27
+ const resolveTarget = (target: TourTarget | undefined): HTMLElement | null => {
28
+ if (!target) return null;
29
+ if (typeof target === 'function') {
30
+ try {
31
+ return target();
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+ if (typeof target === 'string') {
37
+ if (typeof document === 'undefined') return null;
38
+ return document.querySelector<HTMLElement>(target);
39
+ }
40
+ return target;
41
+ };
42
+
43
+ type TourSide = 'top' | 'bottom' | 'left' | 'right';
44
+ type TourAlign = 'start' | 'center' | 'end';
45
+
46
+ const SIDE_PREFIXES: readonly TourSide[] = ['top', 'bottom', 'left', 'right'];
47
+
48
+ // Split a 12-way Placement into a base side + cross-axis alignment.
49
+ // 'bottom' -> { side: 'bottom', align: 'center' }
50
+ // 'topLeft' -> { side: 'top', align: 'start' }
51
+ // 'leftBottom' -> { side: 'left', align: 'end' }
52
+ const parsePlacement = (
53
+ p: Placement | undefined,
54
+ ): { side: TourSide; align: TourAlign } => {
55
+ if (!p) return { side: 'bottom', align: 'center' };
56
+ const side = SIDE_PREFIXES.find((s) => p.startsWith(s)) ?? 'bottom';
57
+ const rest = p.slice(side.length);
58
+ let align: TourAlign = 'center';
59
+ if (side === 'top' || side === 'bottom') {
60
+ if (rest === 'Left') align = 'start';
61
+ else if (rest === 'Right') align = 'end';
62
+ } else if (rest === 'Top') {
63
+ align = 'start';
64
+ } else if (rest === 'Bottom') {
65
+ align = 'end';
66
+ }
67
+ return { side, align };
68
+ };
69
+
70
+ const OPPOSITE_SIDE: Record<TourSide, TourSide> = {
71
+ top: 'bottom',
72
+ bottom: 'top',
73
+ left: 'right',
74
+ right: 'left',
75
+ };
76
+
77
+ // Compute the card's fixed position from the highlight rect + placement, with
78
+ // viewport flip + clamp. Returning rect's origin while size is unknown lets the
79
+ // caller keep the card hidden until measured.
80
+ const computeCardPos = (
81
+ rect: DOMRect,
82
+ placement: Placement | undefined,
83
+ size: { width: number; height: number },
84
+ gap: number,
85
+ ): { top: number; left: number } => {
86
+ const { width: cw, height: ch } = size;
87
+ const { top, left, width, height } = rect;
88
+
89
+ if (cw === 0 || ch === 0) {
90
+ return { top, left };
91
+ }
92
+
93
+ const vw = window.innerWidth;
94
+ const vh = window.innerHeight;
95
+
96
+ const { side: preferredSide, align } = parsePlacement(placement);
97
+
98
+ // Lay the card out for a given side; align shifts it along the cross axis.
99
+ const layoutFor = (side: TourSide): { top: number; left: number } => {
100
+ if (side === 'top' || side === 'bottom') {
101
+ const main = side === 'top' ? top - ch - gap : top + height + gap;
102
+ let cross: number;
103
+ if (align === 'start') cross = left;
104
+ else if (align === 'end') cross = left + width - cw;
105
+ else cross = left + width / 2 - cw / 2;
106
+ return { top: main, left: cross };
107
+ }
108
+ const main = side === 'left' ? left - cw - gap : left + width + gap;
109
+ let cross: number;
110
+ if (align === 'start') cross = top;
111
+ else if (align === 'end') cross = top + height - ch;
112
+ else cross = top + height / 2 - ch / 2;
113
+ return { top: cross, left: main };
114
+ };
115
+
116
+ // True when the card fits on the main axis for this side.
117
+ const fits = (
118
+ side: TourSide,
119
+ pos: { top: number; left: number },
120
+ ): boolean => {
121
+ if (side === 'top') return pos.top >= gap;
122
+ if (side === 'bottom') return pos.top + ch <= vh - gap;
123
+ if (side === 'left') return pos.left >= gap;
124
+ return pos.left + cw <= vw - gap;
125
+ };
126
+
127
+ let side = preferredSide;
128
+ let pos = layoutFor(side);
129
+
130
+ // Flip to the opposite side when the preferred one overflows and the
131
+ // opposite fits. Alignment is preserved across the flip.
132
+ const flippedSide = OPPOSITE_SIDE[side];
133
+ const flipped = layoutFor(flippedSide);
134
+ if (!fits(side, pos) && fits(flippedSide, flipped)) {
135
+ side = flippedSide;
136
+ pos = flipped;
137
+ }
138
+
139
+ // Clamp into the viewport (mostly fixes cross-axis overflow).
140
+ return {
141
+ top: Math.max(gap, Math.min(pos.top, vh - ch - gap)),
142
+ left: Math.max(gap, Math.min(pos.left, vw - cw - gap)),
143
+ };
144
+ };
145
+
146
+ const Tour: React.FC<TourProps> = (baseprops) => {
147
+ const { tour: tourCtx } = useContext(GlobalContext);
148
+ const props = mergeContextToProps<TourProps>(baseprops, tourCtx);
149
+ const { locale } = useLocale();
150
+ const tourLocale = locale?.tour ?? {
151
+ previous: 'Previous',
152
+ next: 'Next',
153
+ finish: 'Finish',
154
+ skip: 'Skip',
155
+ };
156
+
157
+ const {
158
+ prefixCls = 'yee-tour',
159
+ className,
160
+ style,
161
+ classNames,
162
+ styles,
163
+ open,
164
+ steps = [],
165
+ current,
166
+ defaultCurrent = 0,
167
+ onClose,
168
+ onFinish,
169
+ onChange,
170
+ placement = 'bottom',
171
+ mask = true,
172
+ closable = true,
173
+ closeIcon,
174
+ maskClosable = false,
175
+ indicatorsRender,
176
+ scrollIntoViewOptions = true,
177
+ ...rest
178
+ } = props;
179
+
180
+ const [mergedCurrent, setMergedCurrent] = useMergedState<number>(
181
+ defaultCurrent,
182
+ {
183
+ value: current,
184
+ defaultValue: defaultCurrent,
185
+ },
186
+ );
187
+
188
+ const step = steps[mergedCurrent];
189
+ const total = steps.length;
190
+ const isLast = mergedCurrent >= total - 1;
191
+ const stepPlacement = step?.placement ?? placement;
192
+
193
+ const targetEl = useMemo<HTMLElement | null>(
194
+ () => resolveTarget(step?.target),
195
+ [step, open],
196
+ );
197
+
198
+ const [rect, setRect] = useState<DOMRect | null>(null);
199
+ const rafRef = useRef<number | null>(null);
200
+
201
+ // Keep the latest scrollIntoViewOptions in a ref so the tracking effect below
202
+ // doesn't re-run (and re-scroll) when the caller passes an inline object on
203
+ // every render — that would loop with rAF → setRect → re-render forever.
204
+ const scrollIntoViewOptionsRef = useLatest(scrollIntoViewOptions);
205
+
206
+ // Track target rect: recompute on open / step / scroll / resize (rAF throttled).
207
+ useLayoutEffect(() => {
208
+ if (!open || !targetEl) {
209
+ setRect(null);
210
+ return;
211
+ }
212
+ const update = () => {
213
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
214
+ rafRef.current = requestAnimationFrame(() => {
215
+ setRect(targetEl.getBoundingClientRect() as DOMRect);
216
+ });
217
+ };
218
+ update();
219
+ // Bring the target into view, but only when it isn't already — the default
220
+ // (`true` ≈ `{ block: 'start' }`) would otherwise yank an already-visible
221
+ // element to the top of the viewport on every step change. A custom options
222
+ // object is honored as-is (its own `nearest` alignment handles the visible
223
+ // case), and `false` disables auto-scroll entirely.
224
+ const scrollTargetIfNeeded = () => {
225
+ const opts = scrollIntoViewOptionsRef.current;
226
+ if (opts === false) return;
227
+ if (opts === true) {
228
+ const vr = targetEl.getBoundingClientRect();
229
+ const inViewport =
230
+ vr.top >= 0 &&
231
+ vr.left >= 0 &&
232
+ vr.bottom <= window.innerHeight &&
233
+ vr.right <= window.innerWidth;
234
+ if (inViewport) return;
235
+ }
236
+ try {
237
+ targetEl.scrollIntoView?.(opts as ScrollIntoViewOptions);
238
+ } catch {
239
+ // noop
240
+ }
241
+ };
242
+ scrollTargetIfNeeded();
243
+ window.addEventListener('resize', update);
244
+ window.addEventListener('scroll', update, true);
245
+ return () => {
246
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
247
+ window.removeEventListener('resize', update);
248
+ window.removeEventListener('scroll', update, true);
249
+ };
250
+ }, [open, targetEl, mergedCurrent]);
251
+
252
+ useEsc({ enabled: open, onEsc: () => onClose?.() });
253
+
254
+ const handleNext = useCallback(() => {
255
+ if (isLast) {
256
+ onFinish?.();
257
+ return;
258
+ }
259
+ const nextCur = mergedCurrent + 1;
260
+ setMergedCurrent(nextCur);
261
+ onChange?.(nextCur);
262
+ }, [isLast, mergedCurrent, onChange, onFinish, setMergedCurrent]);
263
+
264
+ const handlePrev = useCallback(() => {
265
+ if (mergedCurrent <= 0) return;
266
+ const prevCur = mergedCurrent - 1;
267
+ setMergedCurrent(prevCur);
268
+ onChange?.(prevCur);
269
+ }, [mergedCurrent, onChange, setMergedCurrent]);
270
+
271
+ // Measure the card so we can position it against the highlight rect.
272
+ const cardRef = useRef<HTMLDivElement>(null);
273
+ const [cardSize, setCardSize] = useState({ width: 0, height: 0 });
274
+ useLayoutEffect(() => {
275
+ if (!open) return;
276
+ const node = cardRef.current;
277
+ if (!node) return;
278
+ const r = node.getBoundingClientRect();
279
+ setCardSize({ width: r.width, height: r.height });
280
+ }, [open, step]);
281
+
282
+ const cardPos = useMemo(() => {
283
+ if (!cardSize.width || !cardSize.height) return null;
284
+ if (rect) return computeCardPos(rect, stepPlacement, cardSize, GAP);
285
+ // No target: center the card in the viewport.
286
+ return {
287
+ top: window.innerHeight / 2 - cardSize.height / 2,
288
+ left: window.innerWidth / 2 - cardSize.width / 2,
289
+ };
290
+ }, [rect, stepPlacement, cardSize]);
291
+
292
+ if (!open || typeof document === 'undefined' || !step) {
293
+ return null;
294
+ }
295
+
296
+ return createPortal(
297
+ <div className={clsx(prefixCls, className)} style={style} {...rest}>
298
+ {/* 1 click-intercept mask (box-shadow itself receives no pointer events).
299
+ When there's no target to cut out, the mask dims the full screen. */}
300
+ {mask ? (
301
+ <div
302
+ className={clsx(
303
+ `${prefixCls}-mask`,
304
+ !rect && `${prefixCls}-mask-full`,
305
+ )}
306
+ onClick={maskClosable ? onClose : undefined}
307
+ />
308
+ ) : null}
309
+
310
+ {/* 2 highlight cutout via box-shadow, animated between steps */}
311
+ {mask && rect ? (
312
+ <motion.div
313
+ className={clsx(`${prefixCls}-highlight`, classNames?.highlight)}
314
+ initial={false}
315
+ animate={{
316
+ top: rect.top,
317
+ left: rect.left,
318
+ width: rect.width,
319
+ height: rect.height,
320
+ }}
321
+ transition={{ type: 'spring', visualDuration: 0.3, bounce: 0.2 }}
322
+ style={styles?.highlight}
323
+ />
324
+ ) : null}
325
+
326
+ {/* 3 card: position computed from the highlight rect + placement */}
327
+ <motion.div
328
+ ref={cardRef}
329
+ className={clsx(
330
+ `${prefixCls}-content`,
331
+ classNames?.content,
332
+ step?.className,
333
+ )}
334
+ initial={false}
335
+ animate={
336
+ cardPos
337
+ ? { top: cardPos.top, left: cardPos.left }
338
+ : { top: 0, left: 0 }
339
+ }
340
+ transition={{ type: 'spring', visualDuration: 0.3, bounce: 0.2 }}
341
+ style={{
342
+ position: 'fixed',
343
+ visibility: cardPos ? 'visible' : 'hidden',
344
+ ...(styles?.content as React.CSSProperties),
345
+ }}
346
+ >
347
+ {closable ? (
348
+ <Button
349
+ type="text"
350
+ className={`${prefixCls}-close`}
351
+ onClick={onClose}
352
+ size="small"
353
+ icon={closeIcon ?? <X size={16} />}
354
+ aria-label={tourLocale.skip}
355
+ />
356
+ ) : null}
357
+ {step?.title ? (
358
+ <div
359
+ className={clsx(`${prefixCls}-header`, classNames?.header)}
360
+ style={styles?.header}
361
+ >
362
+ {step.title}
363
+ </div>
364
+ ) : null}
365
+ <div
366
+ className={clsx(`${prefixCls}-body`, classNames?.body)}
367
+ style={styles?.body}
368
+ >
369
+ {step?.description}
370
+ </div>
371
+ <div
372
+ className={clsx(`${prefixCls}-footer`, classNames?.footer)}
373
+ style={styles?.footer}
374
+ >
375
+ <span className={`${prefixCls}-indicators`}>
376
+ {indicatorsRender
377
+ ? indicatorsRender(mergedCurrent, total)
378
+ : `${mergedCurrent + 1} / ${total}`}
379
+ </span>
380
+ <span className={`${prefixCls}-buttons`}>
381
+ {mergedCurrent > 0 ? (
382
+ <Button size="small" onClick={handlePrev}>
383
+ {tourLocale.previous}
384
+ </Button>
385
+ ) : null}
386
+ <Button type="primary" size="small" onClick={handleNext}>
387
+ {isLast ? tourLocale.finish : tourLocale.next}
388
+ </Button>
389
+ </span>
390
+ </div>
391
+ </motion.div>
392
+ </div>,
393
+ document.body,
394
+ );
395
+ };
396
+
397
+ Tour.displayName = 'Tour';
398
+
399
+ export default Tour;