@robertlinde/react-tour-kit 0.8.8 → 1.0.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 (368) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/MIGRATION.md +205 -0
  3. package/README.md +401 -850
  4. package/dist/react-native-platform/hooks/use-tour-scroll-view.hook.d.ts +14 -0
  5. package/dist/react-native-platform/hooks/use-tour-scroll-view.hook.d.ts.map +1 -0
  6. package/dist/react-native-platform/hooks/use-tour-scroll-view.hook.js +21 -0
  7. package/dist/react-native-platform/hooks/use-tour-scroll-view.hook.js.map +1 -0
  8. package/dist/react-native-platform/hooks/use-tour-target.hook.d.ts +15 -0
  9. package/dist/react-native-platform/hooks/use-tour-target.hook.d.ts.map +1 -0
  10. package/dist/react-native-platform/hooks/use-tour-target.hook.js +28 -0
  11. package/dist/react-native-platform/hooks/use-tour-target.hook.js.map +1 -0
  12. package/dist/react-native-platform/host-store.d.ts +24 -0
  13. package/dist/react-native-platform/host-store.d.ts.map +1 -0
  14. package/dist/react-native-platform/host-store.js +63 -0
  15. package/dist/react-native-platform/host-store.js.map +1 -0
  16. package/dist/react-native-platform/insets.util.d.ts +11 -0
  17. package/dist/react-native-platform/insets.util.d.ts.map +1 -0
  18. package/dist/react-native-platform/insets.util.js +31 -0
  19. package/dist/react-native-platform/insets.util.js.map +1 -0
  20. package/dist/react-native-platform/layer/native-renderer.d.ts +7 -0
  21. package/dist/react-native-platform/layer/native-renderer.d.ts.map +1 -0
  22. package/dist/react-native-platform/layer/native-renderer.js +496 -0
  23. package/dist/react-native-platform/layer/native-renderer.js.map +1 -0
  24. package/dist/react-native-platform/layer/tour-host.d.ts +13 -0
  25. package/dist/react-native-platform/layer/tour-host.d.ts.map +1 -0
  26. package/dist/react-native-platform/layer/tour-host.js +70 -0
  27. package/dist/react-native-platform/layer/tour-host.js.map +1 -0
  28. package/dist/react-native-platform/measure.util.d.ts +20 -0
  29. package/dist/react-native-platform/measure.util.d.ts.map +1 -0
  30. package/dist/react-native-platform/measure.util.js +100 -0
  31. package/dist/react-native-platform/measure.util.js.map +1 -0
  32. package/dist/react-native-platform/native-context.d.ts +12 -0
  33. package/dist/react-native-platform/native-context.d.ts.map +1 -0
  34. package/dist/react-native-platform/native-context.js +4 -0
  35. package/dist/react-native-platform/native-context.js.map +1 -0
  36. package/dist/react-native-platform/native-environment.d.ts +14 -0
  37. package/dist/react-native-platform/native-environment.d.ts.map +1 -0
  38. package/dist/react-native-platform/native-environment.js +20 -0
  39. package/dist/react-native-platform/native-environment.js.map +1 -0
  40. package/dist/react-native-platform/native-platform.d.ts +19 -0
  41. package/dist/react-native-platform/native-platform.d.ts.map +1 -0
  42. package/dist/react-native-platform/native-platform.js +265 -0
  43. package/dist/react-native-platform/native-platform.js.map +1 -0
  44. package/dist/react-native-platform/reduced-motion.d.ts +4 -0
  45. package/dist/react-native-platform/reduced-motion.d.ts.map +1 -0
  46. package/dist/react-native-platform/reduced-motion.js +44 -0
  47. package/dist/react-native-platform/reduced-motion.js.map +1 -0
  48. package/dist/react-native-platform/registry.d.ts +18 -0
  49. package/dist/react-native-platform/registry.d.ts.map +1 -0
  50. package/dist/react-native-platform/registry.js +51 -0
  51. package/dist/react-native-platform/registry.js.map +1 -0
  52. package/dist/react-native-platform/tour-provider.d.ts +16 -3
  53. package/dist/react-native-platform/tour-provider.d.ts.map +1 -0
  54. package/dist/react-native-platform/tour-provider.js +92 -211
  55. package/dist/react-native-platform/tour-provider.js.map +1 -1
  56. package/dist/react-native.d.ts +14 -8
  57. package/dist/react-native.d.ts.map +1 -0
  58. package/dist/react-native.js +7 -6
  59. package/dist/react-native.js.map +1 -1
  60. package/dist/react-platform/dom/elements.util.d.ts +35 -0
  61. package/dist/react-platform/dom/elements.util.d.ts.map +1 -0
  62. package/dist/react-platform/dom/elements.util.js +155 -0
  63. package/dist/react-platform/dom/elements.util.js.map +1 -0
  64. package/dist/react-platform/dom/occluders.util.d.ts +13 -0
  65. package/dist/react-platform/dom/occluders.util.d.ts.map +1 -0
  66. package/dist/react-platform/dom/occluders.util.js +75 -0
  67. package/dist/react-platform/dom/occluders.util.js.map +1 -0
  68. package/dist/react-platform/dom/scroll-lock.util.d.ts +13 -0
  69. package/dist/react-platform/dom/scroll-lock.util.d.ts.map +1 -0
  70. package/dist/react-platform/dom/scroll-lock.util.js +219 -0
  71. package/dist/react-platform/dom/scroll-lock.util.js.map +1 -0
  72. package/dist/react-platform/dom/scroll.util.d.ts +44 -0
  73. package/dist/react-platform/dom/scroll.util.d.ts.map +1 -0
  74. package/dist/react-platform/dom/scroll.util.js +250 -0
  75. package/dist/react-platform/dom/scroll.util.js.map +1 -0
  76. package/dist/react-platform/dom/viewport.util.d.ts +16 -0
  77. package/dist/react-platform/dom/viewport.util.d.ts.map +1 -0
  78. package/dist/react-platform/dom/viewport.util.js +65 -0
  79. package/dist/react-platform/dom/viewport.util.js.map +1 -0
  80. package/dist/react-platform/dom/web-platform.d.ts +15 -0
  81. package/dist/react-platform/dom/web-platform.d.ts.map +1 -0
  82. package/dist/react-platform/dom/web-platform.js +169 -0
  83. package/dist/react-platform/dom/web-platform.js.map +1 -0
  84. package/dist/react-platform/layer/styles.d.ts +12 -0
  85. package/dist/react-platform/layer/styles.d.ts.map +1 -0
  86. package/dist/react-platform/layer/styles.js +285 -0
  87. package/dist/react-platform/layer/styles.js.map +1 -0
  88. package/dist/react-platform/layer/web-renderer.d.ts +4 -0
  89. package/dist/react-platform/layer/web-renderer.d.ts.map +1 -0
  90. package/dist/react-platform/layer/web-renderer.js +390 -0
  91. package/dist/react-platform/layer/web-renderer.js.map +1 -0
  92. package/dist/react-platform/tour-provider.d.ts +9 -3
  93. package/dist/react-platform/tour-provider.d.ts.map +1 -0
  94. package/dist/react-platform/tour-provider.js +28 -218
  95. package/dist/react-platform/tour-provider.js.map +1 -1
  96. package/dist/react.d.ts +10 -8
  97. package/dist/react.d.ts.map +1 -0
  98. package/dist/react.js +5 -6
  99. package/dist/react.js.map +1 -1
  100. package/dist/shared/engine/abort.util.d.ts +17 -0
  101. package/dist/shared/engine/abort.util.d.ts.map +1 -0
  102. package/dist/shared/engine/abort.util.js +72 -0
  103. package/dist/shared/engine/abort.util.js.map +1 -0
  104. package/dist/shared/engine/report.util.d.ts +12 -0
  105. package/dist/shared/engine/report.util.d.ts.map +1 -0
  106. package/dist/shared/engine/report.util.js +51 -0
  107. package/dist/shared/engine/report.util.js.map +1 -0
  108. package/dist/shared/engine/tour-adapter.type.d.ts +60 -0
  109. package/dist/shared/engine/tour-adapter.type.d.ts.map +1 -0
  110. package/dist/shared/engine/tour-adapter.type.js +2 -0
  111. package/dist/shared/engine/tour-adapter.type.js.map +1 -0
  112. package/dist/shared/engine/tour-engine.d.ts +92 -0
  113. package/dist/shared/engine/tour-engine.d.ts.map +1 -0
  114. package/dist/shared/engine/tour-engine.js +787 -0
  115. package/dist/shared/engine/tour-engine.js.map +1 -0
  116. package/dist/shared/geometry/box.util.d.ts +39 -0
  117. package/dist/shared/geometry/box.util.d.ts.map +1 -0
  118. package/dist/shared/geometry/box.util.js +146 -0
  119. package/dist/shared/geometry/box.util.js.map +1 -0
  120. package/dist/shared/geometry/compute-tooltip-layout.util.d.ts +60 -0
  121. package/dist/shared/geometry/compute-tooltip-layout.util.d.ts.map +1 -0
  122. package/dist/shared/geometry/compute-tooltip-layout.util.js +148 -0
  123. package/dist/shared/geometry/compute-tooltip-layout.util.js.map +1 -0
  124. package/dist/shared/geometry/geometry.type.d.ts +34 -0
  125. package/dist/shared/geometry/geometry.type.d.ts.map +1 -0
  126. package/dist/shared/geometry/geometry.type.js +2 -0
  127. package/dist/shared/geometry/geometry.type.js.map +1 -0
  128. package/dist/shared/geometry/reveal.util.d.ts +38 -0
  129. package/dist/shared/geometry/reveal.util.d.ts.map +1 -0
  130. package/dist/shared/geometry/reveal.util.js +135 -0
  131. package/dist/shared/geometry/reveal.util.js.map +1 -0
  132. package/dist/shared/geometry/target-visibility.util.d.ts +10 -0
  133. package/dist/shared/geometry/target-visibility.util.d.ts.map +1 -0
  134. package/dist/shared/geometry/target-visibility.util.js +29 -0
  135. package/dist/shared/geometry/target-visibility.util.js.map +1 -0
  136. package/dist/shared/platform/create-tour-adapter.d.ts +9 -0
  137. package/dist/shared/platform/create-tour-adapter.d.ts.map +1 -0
  138. package/dist/shared/platform/create-tour-adapter.js +130 -0
  139. package/dist/shared/platform/create-tour-adapter.js.map +1 -0
  140. package/dist/shared/platform/platform.type.d.ts +77 -0
  141. package/dist/shared/platform/platform.type.d.ts.map +1 -0
  142. package/dist/shared/platform/platform.type.js +2 -0
  143. package/dist/shared/platform/platform.type.js.map +1 -0
  144. package/dist/shared/platform/reveal-in-scroll-containers.d.ts +27 -0
  145. package/dist/shared/platform/reveal-in-scroll-containers.d.ts.map +1 -0
  146. package/dist/shared/platform/reveal-in-scroll-containers.js +49 -0
  147. package/dist/shared/platform/reveal-in-scroll-containers.js.map +1 -0
  148. package/dist/shared/react/tour-context.d.ts +7 -0
  149. package/dist/shared/react/tour-context.d.ts.map +1 -0
  150. package/dist/shared/react/tour-context.js +23 -0
  151. package/dist/shared/react/tour-context.js.map +1 -0
  152. package/dist/shared/react/tour-provider-core.d.ts +41 -0
  153. package/dist/shared/react/tour-provider-core.d.ts.map +1 -0
  154. package/dist/shared/react/tour-provider-core.js +24 -0
  155. package/dist/shared/react/tour-provider-core.js.map +1 -0
  156. package/dist/shared/react/tour-root.d.ts +14 -0
  157. package/dist/shared/react/tour-root.d.ts.map +1 -0
  158. package/dist/shared/react/tour-root.js +22 -0
  159. package/dist/shared/react/tour-root.js.map +1 -0
  160. package/dist/shared/react/use-isomorphic-layout-effect.hook.d.ts +4 -0
  161. package/dist/shared/react/use-isomorphic-layout-effect.hook.d.ts.map +1 -0
  162. package/dist/shared/react/use-isomorphic-layout-effect.hook.js +5 -0
  163. package/dist/shared/react/use-isomorphic-layout-effect.hook.js.map +1 -0
  164. package/dist/shared/react/use-tour-view.hook.d.ts +5 -0
  165. package/dist/shared/react/use-tour-view.hook.d.ts.map +1 -0
  166. package/dist/shared/react/use-tour-view.hook.js +14 -0
  167. package/dist/shared/react/use-tour-view.hook.js.map +1 -0
  168. package/dist/shared/react/use-tour.hook.d.ts +7 -0
  169. package/dist/shared/react/use-tour.hook.d.ts.map +1 -0
  170. package/dist/shared/react/use-tour.hook.js +10 -0
  171. package/dist/shared/react/use-tour.hook.js.map +1 -0
  172. package/dist/shared/renderer/default-tooltip.d.ts +5 -0
  173. package/dist/shared/renderer/default-tooltip.d.ts.map +1 -0
  174. package/dist/shared/renderer/default-tooltip.js +13 -0
  175. package/dist/shared/renderer/default-tooltip.js.map +1 -0
  176. package/dist/shared/renderer/motion.d.ts +29 -0
  177. package/dist/shared/renderer/motion.d.ts.map +1 -0
  178. package/dist/shared/renderer/motion.js +49 -0
  179. package/dist/shared/renderer/motion.js.map +1 -0
  180. package/dist/shared/renderer/renderer-context.d.ts +17 -0
  181. package/dist/shared/renderer/renderer-context.d.ts.map +1 -0
  182. package/dist/shared/renderer/renderer-context.js +16 -0
  183. package/dist/shared/renderer/renderer-context.js.map +1 -0
  184. package/dist/shared/renderer/renderer.type.d.ts +164 -0
  185. package/dist/shared/renderer/renderer.type.d.ts.map +1 -0
  186. package/dist/shared/renderer/renderer.type.js +2 -0
  187. package/dist/shared/renderer/renderer.type.js.map +1 -0
  188. package/dist/shared/renderer/tooltip-model.d.ts +32 -0
  189. package/dist/shared/renderer/tooltip-model.d.ts.map +1 -0
  190. package/dist/shared/renderer/tooltip-model.js +24 -0
  191. package/dist/shared/renderer/tooltip-model.js.map +1 -0
  192. package/dist/shared/renderer/tour-overlay.d.ts +24 -0
  193. package/dist/shared/renderer/tour-overlay.d.ts.map +1 -0
  194. package/dist/shared/renderer/tour-overlay.js +228 -0
  195. package/dist/shared/renderer/tour-overlay.js.map +1 -0
  196. package/dist/shared/types/tour-context.type.d.ts +21 -11
  197. package/dist/shared/types/tour-context.type.d.ts.map +1 -0
  198. package/dist/shared/types/tour-end-info.type.d.ts +19 -2
  199. package/dist/shared/types/tour-end-info.type.d.ts.map +1 -0
  200. package/dist/shared/types/tour-i18n.type.d.ts +8 -0
  201. package/dist/shared/types/tour-i18n.type.d.ts.map +1 -0
  202. package/dist/shared/types/tour-step.type.d.ts +35 -6
  203. package/dist/shared/types/tour-step.type.d.ts.map +1 -0
  204. package/dist/shared/types/tour-theme.type.d.ts +22 -2
  205. package/dist/shared/types/tour-theme.type.d.ts.map +1 -0
  206. package/dist/shared/types/tour-theme.type.js +1 -16
  207. package/dist/shared/types/tour-theme.type.js.map +1 -1
  208. package/dist/shared/types/tour-tooltip-props.type.d.ts +21 -17
  209. package/dist/shared/types/tour-tooltip-props.type.d.ts.map +1 -0
  210. package/dist/shared/utils/frame.util.d.ts +6 -0
  211. package/dist/shared/utils/frame.util.d.ts.map +1 -0
  212. package/dist/shared/utils/frame.util.js +20 -0
  213. package/dist/shared/utils/frame.util.js.map +1 -0
  214. package/dist/shared/utils/get-truncated-dots.util.d.ts +13 -7
  215. package/dist/shared/utils/get-truncated-dots.util.d.ts.map +1 -0
  216. package/dist/shared/utils/get-truncated-dots.util.js +30 -30
  217. package/dist/shared/utils/get-truncated-dots.util.js.map +1 -1
  218. package/dist/shared/utils/i18n.util.d.ts +7 -0
  219. package/dist/shared/utils/i18n.util.d.ts.map +1 -0
  220. package/dist/shared/utils/i18n.util.js +34 -0
  221. package/dist/shared/utils/i18n.util.js.map +1 -0
  222. package/dist/shared/utils/theme.util.d.ts +6 -0
  223. package/dist/shared/utils/theme.util.d.ts.map +1 -0
  224. package/dist/shared/utils/theme.util.js +31 -0
  225. package/dist/shared/utils/theme.util.js.map +1 -0
  226. package/package.json +72 -55
  227. package/src/react-native-platform/hooks/use-tour-scroll-view.hook.ts +24 -0
  228. package/src/react-native-platform/hooks/use-tour-target.hook.ts +30 -0
  229. package/src/react-native-platform/host-store.ts +92 -0
  230. package/src/react-native-platform/insets.util.ts +39 -0
  231. package/src/react-native-platform/layer/native-renderer.tsx +776 -0
  232. package/src/react-native-platform/layer/tour-host.tsx +105 -0
  233. package/src/react-native-platform/measure.util.ts +140 -0
  234. package/src/react-native-platform/native-context.ts +15 -0
  235. package/src/react-native-platform/native-environment.ts +33 -0
  236. package/src/react-native-platform/native-platform.ts +368 -0
  237. package/src/react-native-platform/reduced-motion.ts +47 -0
  238. package/src/react-native-platform/registry.ts +70 -0
  239. package/src/react-native-platform/tour-provider.tsx +148 -0
  240. package/src/react-native.ts +15 -0
  241. package/src/react-platform/dom/elements.util.ts +191 -0
  242. package/src/react-platform/dom/occluders.util.ts +88 -0
  243. package/src/react-platform/dom/scroll-lock.util.ts +261 -0
  244. package/src/react-platform/dom/scroll.util.ts +310 -0
  245. package/src/react-platform/dom/viewport.util.ts +74 -0
  246. package/src/react-platform/dom/web-platform.ts +208 -0
  247. package/src/react-platform/layer/styles.ts +286 -0
  248. package/src/react-platform/layer/web-renderer.tsx +594 -0
  249. package/src/react-platform/tour-provider.tsx +44 -0
  250. package/src/react.ts +13 -0
  251. package/src/shared/engine/abort.util.ts +84 -0
  252. package/src/shared/engine/report.util.ts +58 -0
  253. package/src/shared/engine/tour-adapter.type.ts +62 -0
  254. package/src/shared/engine/tour-engine.ts +1068 -0
  255. package/src/shared/geometry/box.util.ts +185 -0
  256. package/src/shared/geometry/compute-tooltip-layout.util.ts +238 -0
  257. package/src/shared/geometry/geometry.type.ts +38 -0
  258. package/src/shared/geometry/reveal.util.ts +190 -0
  259. package/src/shared/geometry/target-visibility.util.ts +44 -0
  260. package/src/shared/platform/create-tour-adapter.ts +151 -0
  261. package/src/shared/platform/platform.type.ts +81 -0
  262. package/src/shared/platform/reveal-in-scroll-containers.ts +78 -0
  263. package/src/shared/react/tour-context.ts +28 -0
  264. package/src/shared/react/tour-provider-core.ts +67 -0
  265. package/src/shared/react/tour-root.tsx +34 -0
  266. package/src/shared/react/use-isomorphic-layout-effect.hook.ts +7 -0
  267. package/src/shared/react/use-tour-view.hook.ts +17 -0
  268. package/src/shared/react/use-tour.hook.ts +11 -0
  269. package/src/shared/renderer/default-tooltip.tsx +52 -0
  270. package/src/shared/renderer/motion.ts +59 -0
  271. package/src/shared/renderer/renderer-context.ts +33 -0
  272. package/src/shared/renderer/renderer.type.ts +183 -0
  273. package/src/shared/renderer/tooltip-model.ts +44 -0
  274. package/src/shared/renderer/tour-overlay.tsx +453 -0
  275. package/src/shared/types/tour-context.type.ts +23 -0
  276. package/src/shared/types/tour-end-info.type.ts +32 -0
  277. package/src/shared/types/tour-i18n.type.ts +15 -0
  278. package/src/shared/types/tour-step.type.ts +40 -0
  279. package/src/shared/types/tour-theme.type.ts +28 -0
  280. package/src/shared/types/tour-tooltip-props.type.ts +27 -0
  281. package/src/shared/utils/frame.util.ts +21 -0
  282. package/src/shared/utils/get-truncated-dots.util.ts +46 -0
  283. package/src/shared/utils/i18n.util.ts +44 -0
  284. package/src/shared/utils/theme.util.ts +38 -0
  285. package/dist/react-native-platform/components/index.d.ts +0 -2
  286. package/dist/react-native-platform/components/index.js +0 -3
  287. package/dist/react-native-platform/components/index.js.map +0 -1
  288. package/dist/react-native-platform/components/tour-overlay.d.ts +0 -3
  289. package/dist/react-native-platform/components/tour-overlay.js +0 -26
  290. package/dist/react-native-platform/components/tour-overlay.js.map +0 -1
  291. package/dist/react-native-platform/components/tour-tooltip.d.ts +0 -3
  292. package/dist/react-native-platform/components/tour-tooltip.js +0 -167
  293. package/dist/react-native-platform/components/tour-tooltip.js.map +0 -1
  294. package/dist/react-native-platform/hooks/index.d.ts +0 -1
  295. package/dist/react-native-platform/hooks/index.js +0 -2
  296. package/dist/react-native-platform/hooks/index.js.map +0 -1
  297. package/dist/react-native-platform/hooks/use-tour-target.d.ts +0 -2
  298. package/dist/react-native-platform/hooks/use-tour-target.js +0 -13
  299. package/dist/react-native-platform/hooks/use-tour-target.js.map +0 -1
  300. package/dist/react-native-platform/index.d.ts +0 -7
  301. package/dist/react-native-platform/index.js +0 -7
  302. package/dist/react-native-platform/index.js.map +0 -1
  303. package/dist/react-native-platform/platform-adapter.d.ts +0 -2
  304. package/dist/react-native-platform/platform-adapter.js +0 -84
  305. package/dist/react-native-platform/platform-adapter.js.map +0 -1
  306. package/dist/react-native-platform/target-registry.d.ts +0 -10
  307. package/dist/react-native-platform/target-registry.js +0 -17
  308. package/dist/react-native-platform/target-registry.js.map +0 -1
  309. package/dist/react-native-platform/types/tour-provider.props.type.d.ts +0 -12
  310. package/dist/react-native-platform/types/tour-provider.props.type.js +0 -2
  311. package/dist/react-native-platform/types/tour-provider.props.type.js.map +0 -1
  312. package/dist/react-platform/components/index.d.ts +0 -2
  313. package/dist/react-platform/components/index.js +0 -3
  314. package/dist/react-platform/components/index.js.map +0 -1
  315. package/dist/react-platform/components/tour-overlay.d.ts +0 -3
  316. package/dist/react-platform/components/tour-overlay.js +0 -29
  317. package/dist/react-platform/components/tour-overlay.js.map +0 -1
  318. package/dist/react-platform/components/tour-tooltip.d.ts +0 -2
  319. package/dist/react-platform/components/tour-tooltip.js +0 -155
  320. package/dist/react-platform/components/tour-tooltip.js.map +0 -1
  321. package/dist/react-platform/index.d.ts +0 -6
  322. package/dist/react-platform/index.js +0 -6
  323. package/dist/react-platform/index.js.map +0 -1
  324. package/dist/react-platform/platform-adapter.d.ts +0 -2
  325. package/dist/react-platform/platform-adapter.js +0 -101
  326. package/dist/react-platform/platform-adapter.js.map +0 -1
  327. package/dist/react-platform/types/tour-provider.props.type.d.ts +0 -12
  328. package/dist/react-platform/types/tour-provider.props.type.js +0 -2
  329. package/dist/react-platform/types/tour-provider.props.type.js.map +0 -1
  330. package/dist/react-platform/utils/find-visible-element.util.d.ts +0 -1
  331. package/dist/react-platform/utils/find-visible-element.util.js +0 -11
  332. package/dist/react-platform/utils/find-visible-element.util.js.map +0 -1
  333. package/dist/react-platform/utils/index.d.ts +0 -1
  334. package/dist/react-platform/utils/index.js +0 -2
  335. package/dist/react-platform/utils/index.js.map +0 -1
  336. package/dist/shared/context/index.d.ts +0 -1
  337. package/dist/shared/context/index.js +0 -2
  338. package/dist/shared/context/index.js.map +0 -1
  339. package/dist/shared/context/tour-context.d.ts +0 -3
  340. package/dist/shared/context/tour-context.js +0 -14
  341. package/dist/shared/context/tour-context.js.map +0 -1
  342. package/dist/shared/hooks/index.d.ts +0 -1
  343. package/dist/shared/hooks/index.js +0 -2
  344. package/dist/shared/hooks/index.js.map +0 -1
  345. package/dist/shared/hooks/use-tour.hook.d.ts +0 -2
  346. package/dist/shared/hooks/use-tour.hook.js +0 -7
  347. package/dist/shared/hooks/use-tour.hook.js.map +0 -1
  348. package/dist/shared/index.d.ts +0 -5
  349. package/dist/shared/index.js +0 -5
  350. package/dist/shared/index.js.map +0 -1
  351. package/dist/shared/types/index.d.ts +0 -10
  352. package/dist/shared/types/index.js +0 -2
  353. package/dist/shared/types/index.js.map +0 -1
  354. package/dist/shared/types/platform-adapter.type.d.ts +0 -24
  355. package/dist/shared/types/platform-adapter.type.js +0 -2
  356. package/dist/shared/types/platform-adapter.type.js.map +0 -1
  357. package/dist/shared/types/rect.type.d.ts +0 -11
  358. package/dist/shared/types/rect.type.js +0 -2
  359. package/dist/shared/types/rect.type.js.map +0 -1
  360. package/dist/shared/types/tour-overlay-props.type.d.ts +0 -8
  361. package/dist/shared/types/tour-overlay-props.type.js +0 -2
  362. package/dist/shared/types/tour-overlay-props.type.js.map +0 -1
  363. package/dist/shared/utils/calculate-tooltip-position.util.d.ts +0 -14
  364. package/dist/shared/utils/calculate-tooltip-position.util.js +0 -49
  365. package/dist/shared/utils/calculate-tooltip-position.util.js.map +0 -1
  366. package/dist/shared/utils/index.d.ts +0 -3
  367. package/dist/shared/utils/index.js +0 -3
  368. package/dist/shared/utils/index.js.map +0 -1
package/README.md CHANGED
@@ -1,18 +1,45 @@
1
1
  # react-tour-kit
2
2
 
3
- Cross-platform guided tour library for React and React Native. Build interactive onboarding experiences with customizable tooltips, smart positioning, cross-page navigation, and async step actions. Fully themeable with TypeScript support.
4
-
5
- ## Features
6
-
7
- - **Cross-Platform**: Works with React (web) and React Native
8
- - **Cross-Page Tours**: Navigate between pages/screens during a tour
9
- - **Themeable**: Customize colors without building custom components
10
- - **Fully Customizable**: Replace the default tooltip and overlay with your own components
11
- - **Smart Positioning**: Automatic tooltip positioning with viewport boundary detection
12
- - **Keyboard Navigation**: Arrow keys and Escape (web) or hardware back button (Android)
13
- - **Async Step Actions**: Run async code before each step (e.g., navigate, switch tabs, open dialogs)
14
- - **Rich End Callbacks**: Know _why_ a tour ended (`completed`, `closed`, `blur`, `escape`, `target-missing`) with global and per-tour handlers
15
- - **TypeScript First**: Full TypeScript support
3
+ Guided tours for React and React Native that never leave the screen.
4
+
5
+ Highlight any element, explain it in a tooltip, and walk users through your app — across pages and screens, into scroll containers and modals. The tour takes care of everything around it: it waits for targets to appear, scrolls them into view (clear of sticky headers and floating buttons), keeps the tooltip on screen on every device, animates smoothly, and is accessible out of the box.
6
+
7
+ ```tsx
8
+ const {startTour} = useTour();
9
+
10
+ startTour([
11
+ {target: '[data-tour="search"]', title: 'Search', content: 'Find anything in your workspace.'},
12
+ {target: '[data-tour="new"]', title: 'Create', content: 'Start a new project here.'},
13
+ ]);
14
+ ```
15
+
16
+ - **Simple**: one provider, one hook. Steps are plain objects.
17
+ - **Always in sight**: the target is scrolled into view — through nested scroll containers, horizontal lists, and past sticky headers, cookie banners and floating buttons — and the tooltip stays inside the visible screen and its safe areas, at any size, orientation and zoom.
18
+ - **Across pages and screens**: a step's `onBeforeStep` navigates; the tour waits for the target by itself.
19
+ - **Smooth**: the highlight glides from step to step, with the scroll. Nothing runs while a step is on screen.
20
+ - **Accessible**: a real modal dialog with focus management, keyboard navigation and screen-reader announcements on the web; screen-reader focus, roles and the back button on React Native.
21
+ - **Themeable and customisable**: colours, texts, a content-only custom tooltip, and CSS hooks on the web.
22
+ - **Small and typed**: no dependencies besides React (and React Native), written in TypeScript.
23
+
24
+ ## Contents
25
+
26
+ - [Installation](#installation)
27
+ - [API at a glance](#api-at-a-glance)
28
+ - [Web](#web)
29
+ - [React Native](#react-native)
30
+ - [Steps](#steps)
31
+ - [Tours across pages and screens](#tours-across-pages-and-screens)
32
+ - [`useTour()`](#usetour)
33
+ - [When a tour ends](#when-a-tour-ends)
34
+ - [Theme](#theme)
35
+ - [Texts and languages](#texts-and-languages)
36
+ - [Custom tooltip](#custom-tooltip)
37
+ - [Styling with CSS (web)](#styling-with-css-web)
38
+ - [What the tour does for you](#what-the-tour-does-for-you)
39
+ - [Testing your app](#testing-your-app)
40
+ - [Requirements](#requirements)
41
+ - [Migrating from 0.x](#migrating-from-0x)
42
+ - [Development](#development)
16
43
 
17
44
  ## Installation
18
45
 
@@ -20,1056 +47,580 @@ Cross-platform guided tour library for React and React Native. Build interactive
20
47
  npm install @robertlinde/react-tour-kit
21
48
  ```
22
49
 
23
- For React Native, the default overlay also requires `react-native-svg`:
50
+ There is nothing else to install: no `react-native-svg`, no stylesheet to import.
24
51
 
25
- ```bash
26
- npm install react-native-svg
27
- # or, with Expo:
28
- npx expo install react-native-svg
29
- ```
52
+ | Platform | Import from | Targets |
53
+ | ------------ | ------------------------------------------ | ----------------------------------------------- |
54
+ | Web | `@robertlinde/react-tour-kit/react` | CSS selectors such as `'[data-tour="welcome"]'` |
55
+ | React Native | `@robertlinde/react-tour-kit/react-native` | ids registered with `useTourTarget` |
30
56
 
31
- (Not needed if you're using web only, or if you provide a custom `OverlayComponent`.)
57
+ ## API at a glance
32
58
 
33
- ## Live Demos
59
+ Everything you pass to the tour, with links to the details.
34
60
 
35
- Try the demos locally:
61
+ **`<TourProvider>`**: all props are optional.
36
62
 
37
- ```bash
38
- # Web demo (React + React Router)
39
- cd demos/react
40
- npm install
41
- npm run dev
63
+ | Prop | Type | Default | Details |
64
+ | ----------------------- | --------------------------------------------------- | ------------- | ------------------------------------------- |
65
+ | `theme` | `TourTheme`: colours | blue on white | [Theme](#theme) |
66
+ | `i18n` | `TourI18n`: texts | English | [Texts and languages](#texts-and-languages) |
67
+ | `onTourEnd` | `(tourId, info) => void` | – | [When a tour ends](#when-a-tour-ends) |
68
+ | `closeOnOverlayClick` | `boolean`: a press on the dimmed area ends the tour | `true` | [When a tour ends](#when-a-tour-ends) |
69
+ | `TooltipComponent` | `ComponentType<TourTooltipProps>` | built-in | [Custom tooltip](#custom-tooltip) |
70
+ | `insets` (React Native) | `{top, right, bottom, left}` | safe defaults | [React Native](#react-native) |
42
71
 
43
- # React Native demo (Expo + React Navigation)
44
- cd demos/react-native
45
- npm install
46
- npm run ios # or npm run android
47
- ```
72
+ **`startTour(steps, tourId?, options?)`**, from [`useTour()`](#usetour):
48
73
 
49
- ## Platform Guide
74
+ | Argument | Type | Details |
75
+ | ------------------- | ------------------------ | --------------------------------------------------------------------------- |
76
+ | `steps` | `TourStep[]` | below |
77
+ | `tourId` | `string` | names the tour in `onTourEnd`; starting the running tour again does nothing |
78
+ | `options.onTourEnd` | `(tourId, info) => void` | for this tour only, before the provider's |
50
79
 
51
- | Platform | Import from | Target elements with |
52
- | ---------------- | ----------------------------- | --------------------------------------------- |
53
- | **React (Web)** | `react-tour-kit/react` | CSS selectors (e.g., `[data-tour="welcome"]`) |
54
- | **React Native** | `react-tour-kit/react-native` | `useTourTarget` hook or refs |
80
+ **`TourStep`**, see [Steps](#steps):
55
81
 
56
- ---
82
+ | Field | Type | Default |
83
+ | -------------- | ------------------------------------------------------------------------------ | ------------------ |
84
+ | `target` | web: CSS selector or element ref; React Native: `useTourTarget` id or view ref | none: centred step |
85
+ | `title` | `ReactNode` | required |
86
+ | `content` | `ReactNode` | required |
87
+ | `placement` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'bottom'` |
88
+ | `onBeforeStep` | `(target, {signal}) => void \| Promise<void>` | – |
57
89
 
58
- ## React (Web)
90
+ **React Native only**: `useTourTarget(id)` registers a view as a target, `useTourScrollView(ref?)` lets the tour scroll a list, and `<TourHost />` draws the tour in modal screens. See [React Native](#react-native).
59
91
 
60
- ### Quick Start
92
+ ## Web
61
93
 
62
- #### 1. Wrap your app with TourProvider
94
+ **1. Wrap your app in `TourProvider`** — inside your router if steps navigate between pages.
63
95
 
64
96
  ```tsx
65
97
  import {TourProvider} from '@robertlinde/react-tour-kit/react';
66
98
 
67
- function App() {
99
+ export function App() {
68
100
  return (
69
- <TourProvider>
70
- <YourApp />
71
- </TourProvider>
101
+ <BrowserRouter>
102
+ <TourProvider>
103
+ <Routes>{/* … */}</Routes>
104
+ </TourProvider>
105
+ </BrowserRouter>
72
106
  );
73
107
  }
74
108
  ```
75
109
 
76
- #### 2. Add data attributes to target elements
110
+ **2. Mark the elements** you want to highlight. Any CSS selector works; data attributes keep the tour independent of your styling.
77
111
 
78
112
  ```tsx
79
- function Dashboard() {
80
- return (
81
- <div>
82
- <header data-tour="welcome">
83
- <h1>Dashboard</h1>
84
- </header>
85
-
86
- <nav data-tour="sidebar">{/* Navigation items */}</nav>
87
-
88
- <button data-tour="settings">Settings</button>
89
- </div>
90
- );
91
- }
113
+ <nav data-tour="sidebar">…</nav>
114
+ <button data-tour="settings">Settings</button>
92
115
  ```
93
116
 
94
- #### 3. Start a tour
117
+ **3. Start a tour** from anywhere inside the provider.
95
118
 
96
119
  ```tsx
97
- import {useTour, type TourStep} from '@robertlinde/react-tour-kit/react';
120
+ import {type TourStep, useTour} from '@robertlinde/react-tour-kit/react';
98
121
 
99
- const tourSteps: TourStep[] = [
122
+ const onboarding: TourStep[] = [
100
123
  {
101
- target: '[data-tour="welcome"]', // CSS selector
102
124
  title: 'Welcome!',
103
- content: 'This is your dashboard. Let me show you around.',
104
- placement: 'bottom',
125
+ content: 'Let me show you around.', // no target: shown in the centre of the screen
105
126
  },
106
127
  {
107
128
  target: '[data-tour="sidebar"]',
108
129
  title: 'Navigation',
109
- content: 'Use the sidebar to navigate between different sections.',
130
+ content: 'Everything you need is one click away.',
110
131
  placement: 'right',
111
132
  },
112
133
  {
113
134
  target: '[data-tour="settings"]',
114
135
  title: 'Settings',
115
- content: 'Click here to customize your preferences.',
116
- placement: 'left',
136
+ content: 'Make it yours.',
117
137
  },
118
138
  ];
119
139
 
120
- function WelcomeButton() {
140
+ function StartButton() {
121
141
  const {startTour} = useTour();
122
-
123
- return <button onClick={() => startTour(tourSteps, 'onboarding')}>Start Tour</button>;
142
+ return <button onClick={() => startTour(onboarding, 'onboarding')}>Take the tour</button>;
124
143
  }
125
144
  ```
126
145
 
127
- ### Keyboard Navigation (Web)
146
+ While a tour is open, the page cannot be scrolled away from it with the wheel, touch or keyboard (a classic, always-visible scrollbar stays usable, as browsers do not let pages block it; the tour then keeps the target in view), and the tour is announced as a modal dialog. Keyboard: <kbd>→</kbd> next, <kbd>←</kbd> back (swapped in right-to-left pages), <kbd>Esc</kbd> closes. Presses, focus and keys inside the tour do not reach your page's own listeners, so a menu that closes on "click outside" stays open while a step points at it. Opening the tour (a modal dialog) does close elements shown with `popover="auto"`: for a popover a step points at, use `popover="manual"`.
128
147
 
129
- | Key | Action |
130
- | --------------- | ------------- |
131
- | `→` Arrow Right | Next step |
132
- | `←` Arrow Left | Previous step |
133
- | `Escape` | Close tour |
148
+ ### Next.js
134
149
 
135
- ---
136
-
137
- ## React Native
138
-
139
- ### Quick Start
140
-
141
- #### 1. Wrap your app with TourProvider
150
+ The web entry is marked `'use client'`, so `TourProvider` can be rendered directly in a server component such as the root layout. It renders nothing extra on the server.
142
151
 
143
152
  ```tsx
144
- import {TourProvider} from '@robertlinde/react-tour-kit/react-native';
153
+ // app/layout.tsx
154
+ import {TourProvider} from '@robertlinde/react-tour-kit/react';
145
155
 
146
- function App() {
156
+ export default function RootLayout({children}: {children: React.ReactNode}) {
147
157
  return (
148
- <TourProvider>
149
- <YourApp />
150
- </TourProvider>
158
+ <html lang="en">
159
+ <body>
160
+ <TourProvider>{children}</TourProvider>
161
+ </body>
162
+ </html>
151
163
  );
152
164
  }
153
165
  ```
154
166
 
155
- #### 2. Register target elements with useTourTarget
167
+ Components that call `useTour()` must be client components (`'use client'`).
168
+
169
+ ## React Native
170
+
171
+ **1. Wrap your app in `TourProvider`** at the root, around your navigation container. It has to fill the window, so place it **outside** any `SafeAreaView` or padded container and pass the safe-area insets instead:
156
172
 
157
173
  ```tsx
158
- import {View} from 'react-native';
159
- import {useTourTarget} from '@robertlinde/react-tour-kit/react-native';
174
+ import {NavigationContainer} from '@react-navigation/native';
175
+ import {SafeAreaProvider, useSafeAreaInsets} from 'react-native-safe-area-context';
176
+ import {TourProvider} from '@robertlinde/react-tour-kit/react-native';
160
177
 
161
- function MyComponent() {
162
- const welcomeRef = useTourTarget<View>('welcome-button');
163
- const settingsRef = useTourTarget<View>('settings-button');
178
+ function Root() {
179
+ const insets = useSafeAreaInsets();
180
+ return (
181
+ <TourProvider insets={insets}>
182
+ <NavigationContainer>{/* … */}</NavigationContainer>
183
+ </TourProvider>
184
+ );
185
+ }
164
186
 
187
+ export default function App() {
165
188
  return (
166
- <View>
167
- <TouchableOpacity ref={welcomeRef}>
168
- <Text>Welcome</Text>
169
- </TouchableOpacity>
170
-
171
- <TouchableOpacity ref={settingsRef}>
172
- <Text>Settings</Text>
173
- </TouchableOpacity>
174
- </View>
189
+ <SafeAreaProvider>
190
+ <Root />
191
+ </SafeAreaProvider>
175
192
  );
176
193
  }
177
194
  ```
178
195
 
179
- #### 3. Start a tour
180
-
181
- ```tsx
182
- import {useTour, type TourStep} from '@robertlinde/react-tour-kit/react-native';
196
+ Without `insets`, conservative defaults keep the tooltip clear of the notch, Dynamic Island, status bar and home indicator.
183
197
 
184
- const tourSteps: TourStep[] = [
185
- {
186
- target: 'welcome-button', // String ID from useTourTarget
187
- title: 'Welcome!',
188
- content: 'This is your dashboard. Let me show you around.',
189
- placement: 'bottom',
190
- },
191
- {
192
- target: 'settings-button',
193
- title: 'Settings',
194
- content: 'Tap here to customize your preferences.',
195
- placement: 'left',
196
- },
197
- ];
198
+ **2. Register the views** you want to highlight with `useTourTarget`, and use the id as the step's `target`:
198
199
 
199
- function StartTourButton() {
200
- const {startTour} = useTour();
200
+ ```tsx
201
+ import {Pressable, Text} from 'react-native';
202
+ import {useTourTarget} from '@robertlinde/react-tour-kit/react-native';
201
203
 
204
+ function SettingsButton() {
205
+ const ref = useTourTarget('settings');
202
206
  return (
203
- <TouchableOpacity onPress={() => startTour(tourSteps, 'onboarding')}>
204
- <Text>Start Tour</Text>
205
- </TouchableOpacity>
207
+ <Pressable ref={ref} onPress={openSettings}>
208
+ <Text>Settings</Text>
209
+ </Pressable>
206
210
  );
207
211
  }
208
212
  ```
209
213
 
210
- ### Using Refs Directly (React Native)
214
+ The ref is typed for `View` and components that forward their ref to one, such as `Pressable`. For other components, pass a type argument: `useTourTarget<MyComponentRef>('id')`. On the Old Architecture, Android leaves out views that only lay out their children; if a plain `View` target is never found there, give it `collapsable={false}`.
211
215
 
212
- You can also pass refs directly instead of string IDs:
216
+ **3. Let the tour scroll your lists.** For targets that can be off screen, register the scroll views around them with `useTourScrollView` — it works with `ScrollView`, `FlatList`, `SectionList` and their animated versions, vertical and horizontal, nested too:
213
217
 
214
218
  ```tsx
215
- import {useRef} from 'react';
216
- import {useTour, type TourStep} from '@robertlinde/react-tour-kit/react-native';
217
-
218
- function MyComponent() {
219
- const buttonRef = useRef(null);
219
+ import {FlatList, ScrollView} from 'react-native';
220
+ import {useTourScrollView} from '@robertlinde/react-tour-kit/react-native';
220
221
 
221
- const steps: TourStep[] = [
222
- {
223
- target: buttonRef, // Pass ref directly
224
- title: 'Welcome!',
225
- content: 'Tap here to get started.',
226
- },
227
- ];
222
+ function Home() {
223
+ const scrollRef = useTourScrollView();
224
+ return <ScrollView ref={scrollRef}>…</ScrollView>;
225
+ }
228
226
 
229
- return <TouchableOpacity ref={buttonRef}>...</TouchableOpacity>;
227
+ function Carousel() {
228
+ const listRef = useTourScrollView<FlatList<Item>>();
229
+ return <FlatList ref={listRef} horizontal data={items} renderItem={renderItem} />;
230
230
  }
231
231
  ```
232
232
 
233
- ### Navigation (React Native)
234
-
235
- | Action | Trigger |
236
- | ------------- | ------------------------------------------------- |
237
- | Next step | Tap "Next" button |
238
- | Previous step | Tap "Back" button |
239
- | Close tour | Tap close button, overlay, or Android back button |
240
-
241
- ---
242
-
243
- ## Cross-Page/Cross-Screen Tours
244
-
245
- Tours can span multiple pages (web) or screens (React Native). The key is to:
233
+ If the list already has a ref, pass it in: `useTourScrollView(existingRef)`. Items of a virtualised list that are not rendered yet cannot be found; scroll to them in the step's `onBeforeStep` (for example with `scrollToIndex`).
246
234
 
247
- 1. Wrap your **router/navigator** with `TourProvider`
248
- 2. Use `onBeforeStep` to navigate before showing a step
249
-
250
- ### Web (React Router)
235
+ **4. Start a tour** exactly as on the web:
251
236
 
252
237
  ```tsx
253
- import {BrowserRouter, useNavigate} from 'react-router-dom';
254
- import {TourProvider, useTour, type TourStep} from '@robertlinde/react-tour-kit/react';
238
+ import {type TourStep, useTour} from '@robertlinde/react-tour-kit/react-native';
255
239
 
256
- // TourProvider wraps the router
257
- function App() {
258
- return (
259
- <TourProvider>
260
- <BrowserRouter>
261
- <Routes>
262
- <Route path="/" element={<Home />} />
263
- <Route path="/settings" element={<Settings />} />
264
- </Routes>
265
- </BrowserRouter>
266
- </TourProvider>
267
- );
268
- }
269
-
270
- // Define steps with navigation
271
- function Home() {
272
- const navigate = useNavigate();
273
- const {startTour} = useTour();
274
-
275
- const steps: TourStep[] = [
276
- {
277
- target: '[data-tour="welcome"]',
278
- title: 'Welcome',
279
- content: 'Let me show you around.',
280
- placement: 'bottom',
281
- },
282
- {
283
- target: '[data-tour="nav-settings"]',
284
- title: 'Settings Link',
285
- content: "Now let's visit the settings page.",
286
- placement: 'bottom',
287
- // Navigate before the NEXT step shows, then poll until its target mounts
288
- onBeforeStep: async (target) => {
289
- navigate('/settings');
290
- while (!document.querySelector(target as string)) {
291
- await new Promise((r) => setTimeout(r, 50));
292
- }
293
- },
294
- },
295
- {
296
- target: '[data-tour="theme-toggle"]',
297
- title: 'Theme Settings',
298
- content: 'This element is on the Settings page!',
299
- placement: 'right',
300
- },
301
- {
302
- target: '[data-tour="welcome"]',
303
- title: 'Back Home',
304
- content: 'And we can navigate back.',
305
- placement: 'bottom',
306
- onBeforeStep: async (target) => {
307
- navigate('/');
308
- while (!document.querySelector(target as string)) {
309
- await new Promise((r) => setTimeout(r, 50));
310
- }
311
- },
312
- },
313
- ];
240
+ const steps: TourStep[] = [
241
+ {target: 'profile', title: 'Your profile', content: 'Tap here to edit it.'},
242
+ {target: 'settings', title: 'Settings', content: 'Make it yours.', placement: 'top'},
243
+ ];
314
244
 
315
- return <button onClick={() => startTour(steps, 'cross-page')}>Start Tour</button>;
316
- }
245
+ const {startTour} = useTour();
246
+ startTour(steps, 'onboarding');
317
247
  ```
318
248
 
319
- ### React Native (React Navigation)
249
+ The Android back button closes the tour. Screen readers move to the tooltip and announce each step.
320
250
 
321
- ```tsx
322
- import {NavigationContainer, useNavigation} from '@react-navigation/native';
323
- import {createNativeStackNavigator} from '@react-navigation/native-stack';
324
- import {TourProvider, useTour, useTourTarget, type TourStep} from '@robertlinde/react-tour-kit/react-native';
251
+ ### Modal screens
325
252
 
326
- const Stack = createNativeStackNavigator();
253
+ The tour is drawn inside your app's root view. Screens presented as native modals (react-navigation's `presentation: 'modal'`, or a React Native `<Modal>`) are drawn above that view, so to highlight something in them, add a `<TourHost />` as the last child of the modal screen:
327
254
 
328
- // TourProvider wraps the navigator
329
- function App() {
330
- return (
331
- <TourProvider>
332
- <NavigationContainer>
333
- <Stack.Navigator>
334
- <Stack.Screen name="Home" component={HomeScreen} />
335
- <Stack.Screen name="Settings" component={SettingsScreen} />
336
- </Stack.Navigator>
337
- </NavigationContainer>
338
- </TourProvider>
339
- );
340
- }
341
-
342
- // Define steps with navigation
343
- function HomeScreen() {
344
- const navigation = useNavigation();
345
- const {startTour} = useTour();
346
- const headerRef = useTourTarget<View>('header');
347
-
348
- const steps: TourStep[] = [
349
- {
350
- target: 'header',
351
- title: 'Welcome',
352
- content: 'Let me show you around.',
353
- placement: 'bottom',
354
- },
355
- {
356
- target: 'settings-header',
357
- title: 'Settings Screen',
358
- content: 'This element is on the Settings screen!',
359
- placement: 'bottom',
360
- onBeforeStep: async (target) => {
361
- navigation.navigate('Settings');
362
- await new Promise((r) => setTimeout(r, 300)); // Wait for animation
363
- },
364
- },
365
- {
366
- target: 'header',
367
- title: 'Back Home',
368
- content: 'And we can navigate back.',
369
- placement: 'bottom',
370
- onBeforeStep: async (target) => {
371
- navigation.navigate('Home');
372
- await new Promise((r) => setTimeout(r, 300));
373
- },
374
- },
375
- ];
255
+ ```tsx
256
+ import {TourHost} from '@robertlinde/react-tour-kit/react-native';
376
257
 
258
+ function HelpScreen() {
377
259
  return (
378
- <View>
379
- <View ref={headerRef}>{/* ... */}</View>
380
- <Button onPress={() => startTour(steps, 'cross-screen')} title="Start Tour" />
260
+ <View style={{flex: 1}}>
261
+ {/* … */}
262
+ <TourHost />
381
263
  </View>
382
264
  );
383
265
  }
384
-
385
- function SettingsScreen() {
386
- const headerRef = useTourTarget<View>('settings-header');
387
-
388
- return <View ref={headerRef}>{/* ... */}</View>;
389
- }
390
266
  ```
391
267
 
392
- ### Important Notes for Cross-Page Tours
393
-
394
- 1. **TourProvider placement**: Must wrap your router/navigator so tour state persists across navigation
395
- 2. **Target availability**: Targets must be mounted when their step is active. Use `onBeforeStep` to navigate first
396
- 3. **Wait for navigation**: On web, poll on the `target` argument in `onBeforeStep` (e.g. `while (!document.querySelector(target))`) instead of a fixed delay. On React Native, a short delay is usually fine because navigation animations have a known duration.
397
- 4. **Persistent elements**: Elements like navigation bars that exist on all pages don't need `onBeforeStep`
398
-
399
- ---
268
+ While the screen is open, the tour is drawn there; when it closes, the tour returns to the root. VoiceOver then ignores the rest of the modal screen while a step is shown (TalkBack can still reach it).
400
269
 
401
- ## API Reference
402
-
403
- ### TourProvider
404
-
405
- The provider component that enables tour functionality throughout your app.
270
+ Inside a React Native `<Modal>`, the Android back button goes to the modal's `onRequestClose`, not to the tour. End the tour there first:
406
271
 
407
272
  ```tsx
408
- import {TourProvider} from '@robertlinde/react-tour-kit/react';
273
+ const {isActive, endTour} = useTour();
409
274
 
410
- <TourProvider
411
- TooltipComponent={CustomTooltip} // Optional: custom tooltip component
412
- OverlayComponent={CustomOverlay} // Optional: custom overlay component
413
- theme={theme} // Optional: theme configuration
414
- onTourEnd={(tourId, info) => {
415
- // Optional: callback when tour ends
416
- // info.reason is one of: 'completed' | 'closed' | 'blur' | 'escape' | 'target-missing'
417
- console.log(`Tour ${tourId} ended: ${info.reason} at step ${info.stepIndex + 1}/${info.totalSteps}`);
418
- }}
419
- >
420
- {children}
421
- </TourProvider>;
275
+ <Modal visible={isOpen} onRequestClose={() => (isActive ? endTour() : setIsOpen(false))}>
276
+ {/* … */}
277
+ <TourHost />
278
+ </Modal>;
422
279
  ```
423
280
 
424
- #### Props
281
+ ### Android navigation bar
425
282
 
426
- | Prop | Type | Description |
427
- | --------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
428
- | `children` | `ReactNode` | Your application content |
429
- | `theme` | `TourTheme` | Theme configuration for colors (see Theming section) |
430
- | `i18n` | `TourI18n` | Internationalization options for labels (see i18n section) |
431
- | `TooltipComponent` | `ForwardRefExoticComponent<TourTooltipProps>` | Custom tooltip component (must use forwardRef) |
432
- | `OverlayComponent` | `ComponentType<TourOverlayProps>` | Custom overlay component |
433
- | `onTourEnd` | `TourEndCallback` | Callback fired when any tour ends. Receives `(tourId, info)` with the reason and step context (see [Tour End Callbacks](#tour-end-callbacks)) |
434
- | `closeOnOverlayClick` | `boolean` | Whether clicking the overlay closes the tour. When `false`, users must click the ✕ button. Defaults to `true` |
283
+ The tour draws in your app's own window, so the system navigation bar looks exactly as it does in your app. Since Android 15 apps draw edge to edge, and with 3-button navigation Android puts a translucent contrast scrim behind the buttons. To keep the bar transparent (so the tour's dimmed overlay shows behind the buttons), turn the scrim off:
435
284
 
436
- ### useTour Hook
285
+ - **Expo**: add the [`expo-navigation-bar`](https://docs.expo.dev/versions/latest/sdk/navigation-bar/) config plugin to your app config, then rebuild the native app:
437
286
 
438
- Access the tour context from any component.
287
+ ```json
288
+ {
289
+ "expo": {
290
+ "plugins": [["expo-navigation-bar", {"enforceContrast": false}]]
291
+ }
292
+ }
293
+ ```
439
294
 
440
- ```tsx
441
- import {useTour} from '@robertlinde/react-tour-kit/react';
442
-
443
- function MyComponent() {
444
- const {
445
- isActive, // boolean: is a tour currently running?
446
- currentStep, // number: current step index (0-based)
447
- steps, // TourStep[]: all steps in current tour
448
- currentTourId, // string | null: ID of current tour
449
- startTour, // (steps: TourStep[], tourId?: string, options?: StartTourOptions) => void
450
- endTour, // () => void
451
- nextStep, // () => void
452
- prevStep, // () => void
453
- goToStep, // (step: number) => void
454
- } = useTour();
455
- }
456
- ```
295
+ - **Bare React Native**: in `MainActivity`, after `super.onCreate(…)`:
457
296
 
458
- ### TourStep
297
+ ```kotlin
298
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
299
+ window.isNavigationBarContrastEnforced = false
300
+ }
301
+ ```
459
302
 
460
- Define the steps in your tour.
303
+ (0.x drew the tour in a React Native `<Modal>`, which opens its own window. Under edge to edge, React Native forces the contrast scrim on in that window, which showed as a white strip. 1.0 no longer uses a modal.)
461
304
 
462
- ```tsx
305
+ ## Steps
306
+
307
+ ```ts
463
308
  type TourStep = {
464
- target: TourTarget; // Target element (see below)
465
- title: string; // Step title
466
- content: string; // Step description/content
467
- placement?: 'top' | 'bottom' | 'left' | 'right'; // Tooltip position (default: 'bottom')
468
- onBeforeStep?: (target: TourTarget) => void | Promise<void>; // Async action before showing step; receives the step's target so you can poll until it mounts
309
+ target?: TourTarget; // what to highlight; leave it out for a step in the centre of the screen
310
+ title: ReactNode;
311
+ content: ReactNode;
312
+ placement?: 'top' | 'bottom' | 'left' | 'right'; // preferred side, default 'bottom'
313
+ onBeforeStep?: (target, {signal}) => void | Promise<void>;
469
314
  };
470
-
471
- // Target types differ by platform:
472
- // Web: CSS selector string (e.g., '[data-tour="welcome"]', '#my-button')
473
- // React Native: String ID (registered via useTourTarget) or RefObject
474
- type TourTarget = string | RefObject<unknown>;
475
315
  ```
476
316
 
477
- ### Tour End Callbacks
317
+ - **`target`**: on the web a CSS selector (the first _visible_ match is used) or a ref to an element; on React Native an id registered with `useTourTarget`, or a ref to a view.
318
+ - **`title` and `content`** can be any React content (text, links, images).
319
+ - **`placement`** is a preference. If there is not enough room on that side, the tooltip moves to the opposite side, then to the roomier of the remaining sides; it is never pushed off screen.
320
+ - **`onBeforeStep`** runs before the step is shown — to navigate, open a menu, switch a tab, or load data. The tour waits for its promise, then for the target to appear (up to 5 seconds), so there is no need to poll or add delays. It receives the step's target and an `AbortSignal` that aborts when the step is abandoned (the user moves on or closes the tour). Make it idempotent: it runs again when the user comes back to the step. If it throws or rejects, the tour ends with the reason `'error'`.
478
321
 
479
- Tours can end in several ways, and callbacks receive context about **why** and **where**. You can register an end handler at two levels: globally on the provider, or per-tour via `startTour`.
322
+ **Missing targets**: if a step's target does not appear within 5 seconds, or cannot be scrolled into view, the step is skipped (with a warning in development) and the tour continues in the direction the user was going. Going back past the first step returns to the step the user came from. If no step can be shown at all, the tour ends with `'target-missing'`.
480
323
 
481
- #### Callback signature
324
+ **Carousels and sliders**: the tour scrolls everything users can scroll, but never an element with `overflow: hidden` — what such an element shows is up to your app (carousels, sliders, off-canvas menus), and scrolling it would leave it out of step with your app's state. Bring the target into view in `onBeforeStep`, for example by moving the carousel to its slide.
482
325
 
483
- ```tsx
484
- type TourEndReason =
485
- | 'completed' // User clicked Finish on the last step
486
- | 'closed' // User clicked the close (✕) button, or `endTour()` was called programmatically
487
- | 'blur' // User clicked/tapped the overlay outside the highlighted element
488
- | 'escape' // User pressed Escape (web only)
489
- | 'target-missing'; // The step's target element could not be found and the tour had to end
490
-
491
- type TourEndInfo = {
492
- reason: TourEndReason;
493
- stepIndex: number; // Zero-based index of the step the user was on
494
- step: TourStep; // The step object the user was on
495
- totalSteps: number; // Total number of steps in the tour
496
- };
497
-
498
- type TourEndCallback = (tourId: string | undefined, info: TourEndInfo) => void;
499
- ```
500
-
501
- > **Note (React Native):** the `'escape'` reason only fires on web. On Android, the hardware back button is reported as `'closed'`.
326
+ ## Tours across pages and screens
502
327
 
503
- #### Provider-level callback
328
+ Put `TourProvider` around your router or navigator, and let each step open its page in `onBeforeStep`. Making every step open its own page keeps going back working too.
504
329
 
505
- Use the provider-level `onTourEnd` for cross-cutting concerns that should run for every tour — analytics, logging, etc.
330
+ ### Web (React Router)
506
331
 
507
332
  ```tsx
508
- <TourProvider
509
- onTourEnd={(tourId, info) => {
510
- analytics.track('tour_ended', {
511
- tourId,
512
- reason: info.reason,
513
- lastStep: info.stepIndex + 1,
514
- totalSteps: info.totalSteps,
515
- });
516
- }}
517
- >
518
- <App />
519
- </TourProvider>
520
- ```
521
-
522
- #### Per-tour callback
523
-
524
- Pass an `onTourEnd` in the options argument to `startTour` when you need tour-specific end logic. This keeps the logic colocated with the tour that owns it, instead of dispatching on `tourId` in the provider callback.
333
+ import {useNavigate} from 'react-router-dom';
334
+ import {type TourStep, useTour} from '@robertlinde/react-tour-kit/react';
525
335
 
526
- ```tsx
527
- function WelcomeButton() {
336
+ function useOnboarding() {
337
+ const navigate = useNavigate();
528
338
  const {startTour} = useTour();
529
339
 
530
- const handleStart = () => {
531
- startTour(onboardingSteps, 'onboarding', {
532
- onTourEnd: (tourId, info) => {
533
- if (info.reason === 'completed') {
534
- markOnboardingComplete();
535
- }
536
- },
537
- });
538
- };
539
-
540
- return <button onClick={handleStart}>Start Onboarding</button>;
340
+ // Opens a page unless it is already open (no duplicate history entries).
341
+ const on = (path: string, step: TourStep): TourStep => ({
342
+ ...step,
343
+ onBeforeStep() {
344
+ if (location.pathname !== path) {
345
+ navigate(path);
346
+ }
347
+ },
348
+ });
349
+
350
+ return () =>
351
+ startTour(
352
+ [
353
+ on('/', {target: '[data-tour="projects"]', title: 'Projects', content: 'All your work.'}),
354
+ on('/settings', {target: '[data-tour="theme"]', title: 'Theme', content: 'Light or dark?'}),
355
+ on('/', {target: '[data-tour="help"]', title: 'Help', content: 'We are here for you.'}),
356
+ ],
357
+ 'onboarding',
358
+ );
541
359
  }
542
360
  ```
543
361
 
544
- #### Precedence
545
-
546
- When both a provider-level and a per-tour callback are defined, **the per-tour callback fires first**, then the provider-level callback. Both always run on every end — the per-tour callback does not suppress the provider-level one.
362
+ ### React Native (React Navigation)
547
363
 
548
364
  ```tsx
549
- // Provider:
550
- onTourEnd = (id, info) => console.log('provider', id, info.reason);
365
+ import {CommonActions, createNavigationContainerRef, StackActions} from '@react-navigation/native';
551
366
 
552
- // Tour:
553
- startTour(steps, 'onboarding', {
554
- onTourEnd: (id, info) => console.log('tour', id, info.reason),
555
- });
556
-
557
- // On end, logs in order:
558
- // 'tour' 'onboarding' 'completed'
559
- // 'provider' 'onboarding' 'completed'
560
- ```
367
+ export const navigation = createNavigationContainerRef<Screens>(); // <NavigationContainer ref={navigation}>
561
368
 
562
- #### Reacting to specific reasons
563
-
564
- Branch on `info.reason` to distinguish outcomes:
565
-
566
- ```tsx
567
- onTourEnd={(tourId, info) => {
568
- switch (info.reason) {
569
- case 'completed':
570
- markComplete(tourId);
571
- break;
572
- case 'blur':
573
- case 'closed':
574
- case 'escape':
575
- trackDismissal(tourId, info.reason, info.stepIndex);
576
- break;
577
- case 'target-missing':
578
- reportBug(`Tour ${tourId} broke at step ${info.stepIndex}: ${info.step.target}`);
579
- break;
369
+ /** Shows a screen: goes back to it when it is already open, otherwise opens it. */
370
+ function show(screen: keyof Screens) {
371
+ if (!navigation.isReady() || navigation.getCurrentRoute()?.name === screen) {
372
+ return;
580
373
  }
581
- }}
582
- ```
583
374
 
584
- ## Theming
585
-
586
- Customize the look of the default tooltip and overlay without building custom components.
587
-
588
- ### Theme Options
589
-
590
- ```tsx
591
- import {TourProvider, type TourTheme} from '@robertlinde/react-tour-kit/react';
592
-
593
- const theme: TourTheme = {
594
- primaryColor: '#10b981', // Buttons, step badge, highlight border (default: '#3b82f6')
595
- tooltipBackground: '#ffffff', // Tooltip background color (default: '#ffffff')
596
- titleColor: '#1f2937', // Title text color (default: '#1f2937')
597
- contentColor: '#4b5563', // Body text color (default: '#4b5563')
598
- overlayColor: 'rgba(0, 0, 0, 0.5)', // Semi-transparent overlay (default: 'rgba(0, 0, 0, 0.5)')
599
- };
600
-
601
- function App() {
602
- return (
603
- <TourProvider theme={theme}>
604
- <YourApp />
605
- </TourProvider>
606
- );
375
+ const isOpen = navigation.getRootState()?.routes.some((route) => route.name === screen) ?? false;
376
+ navigation.dispatch(isOpen ? StackActions.popTo(screen) : CommonActions.navigate(screen));
607
377
  }
608
- ```
609
-
610
- ### Theme Examples
611
378
 
612
- **Dark theme:**
613
-
614
- ```tsx
615
- const darkTheme: TourTheme = {
616
- primaryColor: '#8b5cf6',
617
- tooltipBackground: '#1f2937',
618
- titleColor: '#f9fafb',
619
- contentColor: '#d1d5db',
620
- overlayColor: 'rgba(0, 0, 0, 0.7)',
621
- };
622
- ```
623
-
624
- **Brand colors:**
379
+ const on = (screen: keyof Screens, step: TourStep): TourStep => ({
380
+ ...step,
381
+ onBeforeStep() {
382
+ show(screen);
383
+ },
384
+ });
625
385
 
626
- ```tsx
627
- const brandTheme: TourTheme = {
628
- primaryColor: '#your-brand-color',
629
- };
630
- // Other values use defaults
386
+ const steps = [
387
+ on('Home', {target: 'projects', title: 'Projects', content: 'All your work.'}),
388
+ on('Settings', {target: 'theme', title: 'Theme', content: 'Light or dark?'}),
389
+ ];
631
390
  ```
632
391
 
633
- ### React Native
392
+ The [demos](#development) contain complete, working examples of both.
634
393
 
635
- Theming works the same way in React Native:
394
+ ## `useTour()`
636
395
 
637
- ```tsx
638
- import {TourProvider, type TourTheme} from '@robertlinde/react-tour-kit/react-native';
639
-
640
- const theme: TourTheme = {
641
- primaryColor: '#10b981',
642
- overlayColor: 'rgba(0, 0, 0, 0.7)',
643
- };
644
-
645
- function App() {
646
- return (
647
- <TourProvider theme={theme}>
648
- <YourApp />
649
- </TourProvider>
650
- );
651
- }
396
+ ```ts
397
+ const {
398
+ isActive, // boolean: a tour is running
399
+ currentStep, // number: index of the step on screen
400
+ steps, // readonly TourStep[]: the steps of the running tour
401
+ currentTourId, // string | undefined
402
+ startTour, // (steps, tourId?, options?) => void
403
+ endTour, // () => void — ends with the reason 'closed'
404
+ nextStep, // () => void — completes the tour on the last step
405
+ prevStep, // () => void
406
+ goToStep, // (index: number) => void
407
+ } = useTour();
652
408
  ```
653
409
 
654
- ---
655
-
656
- ## Internationalization (i18n)
657
-
658
- Customize all text labels and templates to support different languages or match your app's terminology.
659
-
660
- ### i18n Options
410
+ The functions never change identity, so they are safe in effect dependencies. Starting the tour that is already running (same `tourId`, or the same steps array without an id) does nothing, which makes "start on mount" effects safe in Strict Mode. Starting a different tour ends the current one with `'closed'`.
661
411
 
662
412
  ```tsx
663
- import {TourProvider, type TourI18n} from '@robertlinde/react-tour-kit/react';
664
-
665
- const i18n: TourI18n = {
666
- nextLabel: 'Next', // "Next" button text
667
- prevLabel: 'Back', // "Back" button text
668
- finishLabel: 'Finish', // Button text on last step (default: 'Finish' on web, 'Done' on native)
669
- stepCounterTemplate: '{current} / {total}', // Step counter format (use {current} and {total} placeholders)
670
- closeAriaLabel: 'Close tour', // Accessibility label for close button (web only)
671
- dotAriaLabelTemplate: 'Go to step {step}', // Accessibility label template for step dots (web only)
672
- };
673
-
674
- function App() {
675
- return (
676
- <TourProvider i18n={i18n}>
677
- <YourApp />
678
- </TourProvider>
679
- );
680
- }
413
+ // Show the onboarding once.
414
+ useEffect(() => {
415
+ if (!user.hasSeenOnboarding) {
416
+ startTour(onboarding, 'onboarding');
417
+ }
418
+ }, [user.hasSeenOnboarding, startTour]);
681
419
  ```
682
420
 
683
- ### i18n Examples
421
+ ## When a tour ends
684
422
 
685
- **Spanish:**
423
+ Pass `onTourEnd` to the provider (for every tour, for example analytics) and/or to `startTour` (for one tour). Both run exactly once per tour: the tour's own callback first, then the provider's.
686
424
 
687
425
  ```tsx
688
- const spanishI18n: TourI18n = {
689
- nextLabel: 'Siguiente',
690
- prevLabel: 'Anterior',
691
- finishLabel: 'Finalizar',
692
- stepCounterTemplate: 'Paso {current} de {total}',
693
- closeAriaLabel: 'Cerrar tour',
694
- dotAriaLabelTemplate: 'Ir al paso {step}',
695
- };
426
+ <TourProvider
427
+ onTourEnd={(tourId, info) => {
428
+ analytics.track('tour_ended', {tourId, reason: info.reason, step: info.stepIndex + 1, of: info.totalSteps});
429
+ }}
430
+ >
696
431
  ```
697
432
 
698
- **German:**
699
-
700
433
  ```tsx
701
- const germanI18n: TourI18n = {
702
- nextLabel: 'Weiter',
703
- prevLabel: 'Zurück',
704
- finishLabel: 'Fertig',
705
- stepCounterTemplate: 'Schritt {current} von {total}',
706
- closeAriaLabel: 'Tour schließen',
707
- dotAriaLabelTemplate: 'Zu Schritt {step} gehen',
708
- };
434
+ startTour(onboarding, 'onboarding', {
435
+ onTourEnd(tourId, info) {
436
+ if (info.reason === 'completed') {
437
+ markOnboardingDone();
438
+ }
439
+ },
440
+ });
709
441
  ```
710
442
 
711
- **Custom terminology:**
443
+ | `info.reason` | When |
444
+ | ------------------ | ----------------------------------------------------------------------------------- |
445
+ | `'completed'` | Finish on the last step |
446
+ | `'closed'` | The close button, `endTour()`, the Android back button, or another tour was started |
447
+ | `'blur'` | A click or tap on the dimmed area (only with `closeOnOverlayClick`, the default) |
448
+ | `'escape'` | <kbd>Esc</kbd> on the web, the VoiceOver escape gesture on iOS |
449
+ | `'target-missing'` | No remaining step could be shown |
450
+ | `'error'` | A step's `onBeforeStep` threw or rejected; the error is in `info.error` |
712
451
 
713
- ```tsx
714
- const customI18n: TourI18n = {
715
- nextLabel: 'Continue',
716
- prevLabel: 'Go Back',
717
- finishLabel: 'Got it!',
718
- stepCounterTemplate: 'Step {current} of {total}',
719
- };
720
- // Other values use defaults
721
- ```
452
+ `info` also has `stepIndex`, `step` and `totalSteps`. If a callback throws, the error is reported (`reportError` in browsers, `console.error` on React Native) and the tour still closes.
722
453
 
723
- ### React Native
454
+ Set `closeOnOverlayClick={false}` on the provider to close only with the close button, <kbd>Esc</kbd> and the back button.
724
455
 
725
- i18n works the same way in React Native:
456
+ ## Theme
726
457
 
727
458
  ```tsx
728
- import {TourProvider, type TourI18n} from '@robertlinde/react-tour-kit/react-native';
729
-
730
- const i18n: TourI18n = {
731
- nextLabel: 'Siguiente',
732
- prevLabel: 'Anterior',
733
- finishLabel: 'Listo',
734
- stepCounterTemplate: 'Paso {current} de {total}',
735
- };
736
-
737
- function App() {
738
- return (
739
- <TourProvider i18n={i18n}>
740
- <YourApp />
741
- </TourProvider>
742
- );
743
- }
459
+ <TourProvider
460
+ theme={{
461
+ primaryColor: '#8b5cf6',
462
+ tooltipBackground: '#1f2937',
463
+ titleColor: '#f9fafb',
464
+ contentColor: '#d1d5db',
465
+ overlayColor: 'rgba(0, 0, 0, 0.7)',
466
+ }}
467
+ >
744
468
  ```
745
469
 
746
- ### Default Values
747
-
748
- | Option | Web Default | React Native Default |
749
- | ---------------------- | ----------------------- | ----------------------------- |
750
- | `nextLabel` | `'Next'` | `'Next'` |
751
- | `prevLabel` | `'Back'` | `'Back'` |
752
- | `finishLabel` | `'Finish'` | `'Done'` |
753
- | `stepCounterTemplate` | `'{current} / {total}'` | `'Step {current} of {total}'` |
754
- | `closeAriaLabel` | `'Close tour'` | N/A |
755
- | `dotAriaLabelTemplate` | `'Go to step {step}'` | N/A |
756
-
757
- ---
470
+ | Key | Used for | Default |
471
+ | ------------------- | ----------------------------------------------------------- | ---------------------- |
472
+ | `primaryColor` | Buttons, step badge, active dot, highlight ring | `'#2563eb'` |
473
+ | `primaryTextColor` | Text on `primaryColor` | `'#ffffff'` |
474
+ | `tooltipBackground` | The tooltip | `'#ffffff'` |
475
+ | `titleColor` | The title | `'#111827'` |
476
+ | `contentColor` | The content; muted parts (dots, Back button) derive from it | `'#4b5563'` |
477
+ | `overlayColor` | The dimmed area (include transparency) | `'rgba(0, 0, 0, 0.5)'` |
758
478
 
759
- ## Advanced Usage
479
+ The defaults meet WCAG AA contrast. A dark theme only needs a dark background and light text colours.
760
480
 
761
- ### Async Step Actions
762
-
763
- Run code before a step is shown. Useful for opening dialogs, switching tabs, navigating, etc.
481
+ ## Texts and languages
764
482
 
765
483
  ```tsx
766
- const steps: TourStep[] = [
767
- {
768
- target: '[data-tour="settings-tab"]',
769
- title: 'Settings Tab',
770
- content: 'Click here to access settings.',
771
- },
772
- {
773
- target: '[data-tour="notifications-panel"]',
774
- title: 'Notifications',
775
- content: 'Configure your notification preferences here.',
776
- onBeforeStep: async (target) => {
777
- // Open the settings tab before showing this step
778
- document.querySelector('[data-tour="settings-tab"]')?.click();
779
- // Poll until the panel actually mounts instead of guessing a delay
780
- while (!document.querySelector(target as string)) {
781
- await new Promise((resolve) => setTimeout(resolve, 50));
782
- }
783
- },
784
- },
785
- ];
484
+ <TourProvider
485
+ i18n={{
486
+ nextLabel: 'Weiter',
487
+ prevLabel: 'Zurück',
488
+ finishLabel: 'Fertig',
489
+ stepCounterTemplate: '{current} von {total}',
490
+ closeAriaLabel: 'Tour schließen',
491
+ dotAriaLabelTemplate: 'Zu Schritt {step}',
492
+ }}
493
+ >
786
494
  ```
787
495
 
788
- ### Custom Tooltip Component
496
+ | Key | Default |
497
+ | ---------------------- | ------------------------ |
498
+ | `nextLabel` | `'Next'` |
499
+ | `prevLabel` | `'Back'` |
500
+ | `finishLabel` | `'Finish'` |
501
+ | `stepCounterTemplate` | `'{current} of {total}'` |
502
+ | `closeAriaLabel` | `'Close tour'` |
503
+ | `dotAriaLabelTemplate` | `'Go to step {step}'` |
789
504
 
790
- Create your own tooltip with your design system.
505
+ ## Custom tooltip
791
506
 
792
- **Web:**
507
+ `TooltipComponent` replaces what is _inside_ the tooltip. The tour still measures it, places it, keeps it on screen, animates it, and moves focus to it — your component only renders content and calls the callbacks:
793
508
 
794
509
  ```tsx
795
- import {forwardRef} from 'react';
796
- import {TourProvider, type TourTooltipProps} from '@robertlinde/react-tour-kit/react';
797
-
798
- const CustomTooltip = forwardRef<HTMLDivElement, TourTooltipProps>(
799
- ({title, content, currentStep, totalSteps, position, isPositioned, onClose, onNext, onPrev}, ref) => (
800
- <div
801
- ref={ref}
802
- className="custom-tooltip"
803
- style={{
804
- position: 'fixed',
805
- top: position.top,
806
- left: position.left,
807
- opacity: isPositioned ? 1 : 0,
808
- }}
809
- >
810
- <h3>{title}</h3>
811
- <p>{content}</p>
812
- <div className="tooltip-footer">
510
+ import {type TourTooltipProps} from '@robertlinde/react-tour-kit/react';
511
+
512
+ function MyTooltip({step, currentStep, totalSteps, onNext, onPrev, onClose, titleId, descriptionId}: TourTooltipProps) {
513
+ const isLast = currentStep === totalSteps - 1;
514
+ return (
515
+ <div className="my-tooltip">
516
+ <h3 id={titleId}>{step.title}</h3>
517
+ <div id={descriptionId}>{step.content}</div>
518
+ <footer>
519
+ <button onClick={onClose}>Skip</button>
813
520
  <span>
814
- {currentStep + 1} of {totalSteps}
521
+ {currentStep + 1}/{totalSteps}
815
522
  </span>
816
- <button onClick={onPrev} disabled={currentStep === 0}>
523
+ <button disabled={currentStep === 0} onClick={onPrev}>
817
524
  Back
818
525
  </button>
819
- <button onClick={onNext}>{currentStep === totalSteps - 1 ? 'Finish' : 'Next'}</button>
820
- <button onClick={onClose}>×</button>
821
- </div>
526
+ <button onClick={onNext}>{isLast ? 'Done' : 'Next'}</button>
527
+ </footer>
822
528
  </div>
823
- ),
824
- );
825
-
826
- function App() {
827
- return (
828
- <TourProvider TooltipComponent={CustomTooltip}>
829
- <YourApp />
830
- </TourProvider>
831
529
  );
832
530
  }
833
- ```
834
-
835
- **React Native:**
836
531
 
837
- ```tsx
838
- import {forwardRef} from 'react';
839
- import {View, Text, TouchableOpacity} from 'react-native';
840
- import {TourProvider, type TourTooltipProps} from '@robertlinde/react-tour-kit/react-native';
841
-
842
- const CustomTooltip = forwardRef<View, TourTooltipProps>(
843
- ({title, content, position, isPositioned, onNext, onClose}, ref) => (
844
- <View
845
- ref={ref}
846
- style={{
847
- position: 'absolute',
848
- top: position.top,
849
- left: position.left,
850
- opacity: isPositioned ? 1 : 0,
851
- backgroundColor: '#fff',
852
- padding: 16,
853
- borderRadius: 8,
854
- }}
855
- >
856
- <Text style={{fontWeight: 'bold'}}>{title}</Text>
857
- <Text>{content}</Text>
858
- <TouchableOpacity onPress={onNext}>
859
- <Text>Next</Text>
860
- </TouchableOpacity>
861
- </View>
862
- ),
863
- );
864
-
865
- function App() {
866
- return (
867
- <TourProvider TooltipComponent={CustomTooltip}>
868
- <YourApp />
869
- </TourProvider>
870
- );
871
- }
532
+ <TourProvider TooltipComponent={MyTooltip}>…</TourProvider>;
872
533
  ```
873
534
 
874
- ### Custom Overlay Component
535
+ On React Native, render views instead; everything else is the same.
875
536
 
876
- Customize the backdrop overlay.
537
+ | Prop | Description |
538
+ | -------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
539
+ | `step` | The step being shown |
540
+ | `currentStep`, `totalSteps` | Zero-based index and count |
541
+ | `onNext`, `onPrev`, `onClose` | Navigation; `onNext` completes the tour on the last step |
542
+ | `onGoToStep(index)` | Jump to a step (for clickable dots) |
543
+ | `placement` | The side the tooltip ended up on, or `'center'` |
544
+ | `arrowOffset` | Where an arrow should point, in pixels from the tooltip's left (top/bottom) or top (left/right) edge; or `undefined` |
545
+ | `titleId`, `descriptionId` (web) | Put them on the title and content elements so screen readers announce them |
877
546
 
878
- **Web:**
547
+ The card around your content has the theme's background, rounded corners and a shadow; style the inside as you like. `getTruncatedDots(current, total)` is exported for building a compact dot pager like the built-in one.
879
548
 
880
- ```tsx
881
- import {TourProvider, type TourOverlayProps} from '@robertlinde/react-tour-kit/react';
549
+ ## Styling with CSS (web)
882
550
 
883
- function CustomOverlay({highlightRect, onClose}: TourOverlayProps) {
884
- return (
885
- <div className="custom-overlay" onClick={onClose}>
886
- <div
887
- className="highlight-border"
888
- style={{
889
- position: 'absolute',
890
- top: highlightRect.top - 4,
891
- left: highlightRect.left - 4,
892
- width: highlightRect.width + 8,
893
- height: highlightRect.height + 8,
894
- border: '2px solid #your-brand-color',
895
- borderRadius: '8px',
896
- }}
897
- />
898
- </div>
899
- );
900
- }
551
+ The built-in tooltip and overlay use stable class names and CSS variables, so you can restyle them without a custom component. All built-in rules live in a CSS cascade layer, so your own CSS wins over them — whatever its specificity, without `!important`. (If your CSS is itself inside a layer, such as Tailwind's, write these overrides outside of it.)
901
552
 
902
- function App() {
903
- return (
904
- <TourProvider OverlayComponent={CustomOverlay}>
905
- <YourApp />
906
- </TourProvider>
907
- );
908
- }
909
- ```
910
-
911
- **React Native:**
553
+ The exceptions are the few properties that keep the tour in place, clickable and readable, which global styles would otherwise change (Tailwind's preflight, normalize.css, Bootstrap, or rules for every `dialog` or `button`): the position, size and spacing of the dialog, the highlight and the tooltip box, and the box, spacing, colours and font size of the buttons. They are `!important` in the built-in styles; their colours still come from the variables below. To change one of them anyway, use `!important` in your rule.
912
554
 
913
- ```tsx
914
- import {View, TouchableWithoutFeedback, StyleSheet} from 'react-native';
915
- import {TourProvider, type TourOverlayProps} from '@robertlinde/react-tour-kit/react-native';
916
-
917
- function CustomOverlay({highlightRect, onClose}: TourOverlayProps) {
918
- return (
919
- <TouchableWithoutFeedback onPress={onClose}>
920
- <View style={StyleSheet.absoluteFill}>
921
- {/* Your custom overlay with cutout */}
922
- <View
923
- style={{
924
- position: 'absolute',
925
- top: highlightRect.top - 4,
926
- left: highlightRect.left - 4,
927
- width: highlightRect.width + 8,
928
- height: highlightRect.height + 8,
929
- borderWidth: 2,
930
- borderColor: '#your-brand-color',
931
- borderRadius: 8,
932
- }}
933
- />
934
- </View>
935
- </TouchableWithoutFeedback>
936
- );
555
+ ```css
556
+ .rtk-root {
557
+ --rtk-radius: 4px; /* tooltip corners */
558
+ --rtk-spotlight-radius: 2px; /* highlight corners */
559
+ --rtk-z-index: 10000; /* only used where the browser has no top layer */
937
560
  }
938
561
 
939
- function App() {
940
- return (
941
- <TourProvider OverlayComponent={CustomOverlay}>
942
- <YourApp />
943
- </TourProvider>
944
- );
562
+ .rtk-title {
563
+ font-family: 'Inter', sans-serif;
945
564
  }
946
565
  ```
947
566
 
948
- ### Persisting Tour Completion
567
+ - **Variables**: `--rtk-radius`, `--rtk-spotlight-radius`, `--rtk-ease`, `--rtk-z-index`. The colours `--rtk-primary`, `--rtk-on-primary`, `--rtk-bg`, `--rtk-title`, `--rtk-text` and `--rtk-overlay` come from the `theme` prop (set on `.rtk-root` directly): change them with `theme`, or override them on an element inside the tour, such as `.rtk-card`.
568
+ - **Classes**: `.rtk-root` (the dialog), `.rtk-backdrop`, `.rtk-spotlight` (the highlight), `.rtk-tooltip` (positioned box), `.rtk-card` (the tooltip itself), `.rtk-header`, `.rtk-counter`, `.rtk-close`, `.rtk-body`, `.rtk-title`, `.rtk-content`, `.rtk-footer`, `.rtk-button`, `.rtk-button--prev`, `.rtk-button--next`, `.rtk-dots`, `.rtk-dot`, `.rtk-arrow`.
569
+ - **State**: `.rtk-tooltip[data-placement="top|bottom|left|right|center"]`, `.rtk-root[data-state="closing"]`.
949
570
 
950
- Track which tours users have fully completed. Only persist on `'completed'` so dismissed or interrupted tours aren't marked done.
571
+ The tour is rendered as a modal `<dialog>` in the browser's top layer, so it is always above your app, whatever its `z-index` values and stacking contexts.
951
572
 
952
- ```tsx
953
- import {TourProvider, type TourEndCallback} from '@robertlinde/react-tour-kit/react';
954
-
955
- function App() {
956
- const handleTourEnd: TourEndCallback = async (tourId, info) => {
957
- if (tourId && info.reason === 'completed') {
958
- // Save to your backend
959
- await fetch('/api/users/me/completed-tours/' + tourId, {
960
- method: 'POST',
961
- });
962
- }
963
- };
573
+ ## What the tour does for you
964
574
 
965
- return (
966
- <TourProvider onTourEnd={handleTourEnd}>
967
- <YourApp />
968
- </TourProvider>
969
- );
970
- }
971
- ```
972
-
973
- See [Tour End Callbacks](#tour-end-callbacks) for the full list of end reasons and how to register per-tour end handlers.
974
-
975
- ### Conditional Tour Start
976
-
977
- Start tours based on user state:
978
-
979
- ```tsx
980
- function TourTrigger({tourId, steps, autoStart = true}) {
981
- const {startTour} = useTour();
982
- const {user} = useUser(); // Your auth hook
983
-
984
- useEffect(() => {
985
- if (autoStart && user && !user.completedTours?.[tourId]) {
986
- // Auto-start if user hasn't completed this tour
987
- setTimeout(() => startTour(steps, tourId), 500);
988
- }
989
- }, [user, tourId, autoStart]);
990
-
991
- return <button onClick={() => startTour(steps, tourId)}>Start Tour</button>;
992
- }
993
- ```
994
-
995
- ## Styling
996
-
997
- The default components use inline styles for zero-dependency styling. You have three options for customization:
998
-
999
- 1. **Theming**: Use the `theme` prop to customize colors (see [Theming](#theming) section)
1000
- 2. **Custom Components**: Provide your own `TooltipComponent` and `OverlayComponent` for full control
1001
- 3. **CSS Overrides** (Web only): Override styles via CSS selectors
1002
-
1003
- ## Platform Support
575
+ - **Keeps everything in sight.** Before a step is shown, its target is scrolled into view — through every scroll container around it, including horizontal ones — with room for the tooltip. Elements that stay put while the page scrolls (sticky headers, cookie banners, floating buttons; any number, any shape) are detected and the target is scrolled clear of them, in both directions. Targets larger than the screen are aligned to their start. The tooltip is placed inside the visible screen minus safe areas and the on-screen keyboard, flips to another side when there is no room, and scrolls its own content if it is taller than the screen. When the window is resized or rotated, or your app moves the target, the tour follows.
576
+ - **Waits instead of guessing.** Targets that are not rendered yet (lazy pages, animations, data loading) are awaited, event-driven, for up to 5 seconds.
577
+ - **Animates.** The tour fades in, the highlight glides from target to target together with the scroll, and the tooltip enters from the side it is on. On React Native every animation runs on the UI thread, so it stays smooth while JavaScript is busy. With reduced motion turned on in the system, movement is replaced by short fades.
578
+ - **Is accessible.** On the web the tour is a modal dialog (`aria-modal`, labelled by the step title, described by its content): focus moves into the tooltip and back to where it was when the tour ends, the rest of the page is inert, arrow keys and <kbd>Esc</kbd> work (but leave text fields, sliders, radio groups and other controls in a custom tooltip alone), a long tooltip scrolls with the keyboard, and step changes are announced. Buttons have accessible names and large enough touch targets. In Windows high-contrast mode the highlight is drawn as an outline. On React Native, screen-reader focus moves to the tooltip, the app behind it is hidden from screen readers (in a `TourHost`, see [Modal screens](#modal-screens)), and the back button closes the tour.
579
+ - **Costs nothing while idle.** While a step is on screen and nothing moves, the tour does no work at all: no timers, no measuring, no re-rendering. Scrolling and resizing are followed once per frame, without re-rendering your app. `useTour()` only re-renders components when the tour's state changes.
580
+ - **Is safe.** Callbacks run exactly once, also in Strict Mode, with double clicks and when tours replace each other. Errors in your callbacks are reported and never leave the tour stuck.
1004
581
 
1005
- **Web:**
582
+ ## Testing your app
1006
583
 
1007
- - Chrome, Firefox, Safari, Edge (latest versions)
584
+ - **jsdom (Jest, Vitest)**: tours run in jsdom, with or without a `ResizeObserver` polyfill. jsdom has no layout, so every target counts as visible at the top of the screen; test that your steps and callbacks work, and leave the visual part to end-to-end tests.
585
+ - **Jest**: the package is ESM-only, and Jest runs CommonJS by default, so Jest has to transform it. Exempt it from `transformIgnorePatterns`: on the web, for example `'/node_modules/(?!@robertlinde/react-tour-kit/)'` (with a Babel or SWC transform that turns ES modules into CommonJS, which `babel-jest` with `@babel/preset-env` does); with React Native's preset, `'node_modules/(?!((jest-)?react-native|@react-native(-community)?|@robertlinde/react-tour-kit)/)'`; with Next.js's `next/jest`, add the package to `transpilePackages` in `next.config`. Vitest needs nothing.
586
+ - **End-to-end (web)**: the tooltip is a `dialog` named after the step title, so `page.getByRole('dialog', {name: 'Settings'})` finds it. The buttons are named by your `i18n` labels.
587
+ - **End-to-end (React Native)**: the built-in tooltip has the testIDs `rtk-tooltip`, `rtk-next`, `rtk-prev` and `rtk-close`. Prefer them over texts: on Android, "Back" and "Home" also match the system navigation bar. While a tour is open, the app behind it is hidden from accessibility (except while a `TourHost` draws it), so UI-test tools mostly see only the tour.
1008
588
 
1009
- **React Native:**
589
+ ## Requirements
1010
590
 
1011
- - iOS 13+
1012
- - Android 5.0+ (API 21+)
1013
- - React Native 0.70+
591
+ - React 18 or 19.
592
+ - Web: current versions of Chrome, Edge, Firefox and Safari.
593
+ - React Native 0.79 or newer (Expo SDK 53 or newer).
594
+ - ESM-only: works with every bundler (Vite, webpack, Next.js, Metro, …) and with Node's `import`.
595
+ - TypeScript: `moduleResolution` `bundler`, `node16` or `nodenext` (the defaults of current React Native, Expo, Next.js and Vite projects).
1014
596
 
1015
- Requires React 18+.
597
+ ## Migrating from 0.x
1016
598
 
1017
- ## TypeScript
1018
-
1019
- Full TypeScript support is included. Import types as needed:
1020
-
1021
- ```tsx
1022
- import type {
1023
- TourStep,
1024
- TourContextType,
1025
- TourProviderProps,
1026
- TourTooltipProps,
1027
- TourOverlayProps,
1028
- TourTheme,
1029
- TourI18n,
1030
- TourEndCallback,
1031
- TourEndInfo,
1032
- TourEndReason,
1033
- StartTourOptions,
1034
- } from '@robertlinde/react-tour-kit/react';
1035
- ```
1036
-
1037
- ## Utilities
1038
-
1039
- The package exports utility functions for custom implementations:
1040
-
1041
- ```tsx
1042
- import {
1043
- calculateTooltipPosition, // Calculate optimal tooltip position
1044
- findVisibleElement, // Find first visible element matching selector
1045
- } from '@robertlinde/react-tour-kit/react';
1046
- ```
599
+ 1.0 is a rewrite with a smaller, safer API. Most apps only need to change a custom tooltip, if they have one. See [MIGRATION.md](./MIGRATION.md) for every change with before-and-after examples, and [CHANGELOG.md](./CHANGELOG.md) for what is new.
1047
600
 
1048
601
  ## Development
1049
602
 
1050
603
  ```bash
1051
- # Install dependencies
1052
604
  npm install
1053
-
1054
- # Build
1055
- npm run build
1056
-
1057
- # Watch mode
1058
- npm run start:dev
1059
-
1060
- # Lint
605
+ npm test # unit and component tests (Vitest, real browser)
606
+ npm run test:e2e # end-to-end tests on desktop and mobile browsers (Playwright)
1061
607
  npm run lint
608
+ npm run typecheck
609
+ npm run build
610
+ ```
1062
611
 
1063
- # Format
1064
- npm run format
612
+ How the code is organised, and how to add a feature so that both platforms get it, is described in [ARCHITECTURE.md](./ARCHITECTURE.md).
1065
613
 
1066
- # Run web demo
1067
- cd demos/react && npm install && npm run dev
614
+ The demos use the library's source directly, so every change is live there:
1068
615
 
1069
- # Run native demo
1070
- cd demos/react-native && npm install && npm run ios
616
+ ```bash
617
+ npm run demo:web # web demo: http://localhost:5173 — includes a /lab page with edge cases
618
+ npm run demo:ios # React Native demo in Expo Go (run `npm install` in demos/react-native first)
619
+ npm run demo:android
1071
620
  ```
1072
621
 
622
+ All tests are in `test/` (see [ARCHITECTURE.md](./ARCHITECTURE.md#tests)). The React Native demo is tested with [Maestro](https://maestro.dev) flows: with the app open, run `maestro test test/maestro -e APP_ID=host.exp.Exponent`. To see the Android navigation bar setting in effect, build the app natively with `npx expo run:android`.
623
+
1073
624
  ## License
1074
625
 
1075
626
  MIT