@heyramzi/ui 1.54.2 → 2.0.1

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 (363) hide show
  1. package/COMPONENTS.md +3 -2
  2. package/README.md +34 -9
  3. package/package.json +205 -13
  4. package/scripts/sync-shadcn.mjs +2 -0
  5. package/src/ai/generation-progress/GenerationProgress.svelte +12 -12
  6. package/src/ambient.d.ts +8 -0
  7. package/src/brand/brand.test.ts +67 -0
  8. package/src/brand/index.ts +137 -0
  9. package/src/canvas/README.md +236 -0
  10. package/src/canvas/card/card-frame.svelte +61 -0
  11. package/src/canvas/card/card.css +39 -0
  12. package/src/canvas/card/resize-handles.svelte +119 -0
  13. package/src/canvas/card/resize.ts +33 -0
  14. package/src/canvas/chrome/chrome-zoom.svelte +120 -0
  15. package/src/canvas/chrome/chrome.css +231 -0
  16. package/src/canvas/chrome/lock-badge.svelte +18 -0
  17. package/src/canvas/chrome/side-handles.svelte +30 -0
  18. package/src/canvas/chrome/size-chip.svelte +45 -0
  19. package/src/canvas/chrome/snap-guides.svelte +69 -0
  20. package/src/canvas/components/icons/ToolIcon.svelte +30 -0
  21. package/src/canvas/components/shared/svg/PreviewSvg.svelte +67 -0
  22. package/src/canvas/components/shared/svg/ShapeOutline.svelte +76 -0
  23. package/src/canvas/components/shared/svg/StrokeNode.svelte +69 -0
  24. package/src/canvas/components/shared/svg/TextRule.svelte +37 -0
  25. package/src/canvas/edges/bend-handle.svelte +76 -0
  26. package/src/canvas/edges/edge-gradient.svelte +30 -0
  27. package/src/canvas/edges/edge-path.test.ts +58 -0
  28. package/src/canvas/edges/edge-path.ts +293 -0
  29. package/src/canvas/edges/gradient-edge.svelte +43 -0
  30. package/src/canvas/editor/context-menu.test.ts +65 -0
  31. package/src/canvas/editor/context-menu.ts +48 -0
  32. package/src/canvas/editor/dispatch.svelte.test.ts +243 -0
  33. package/src/canvas/editor/dispatch.svelte.ts +205 -0
  34. package/src/canvas/editor/editor-context.ts +62 -0
  35. package/src/canvas/editor/editor-mode.ts +67 -0
  36. package/src/canvas/editor/frame.ts +98 -0
  37. package/src/canvas/editor/hand-context.ts +15 -0
  38. package/src/canvas/editor/history-core.test.ts +239 -0
  39. package/src/canvas/editor/history-core.ts +247 -0
  40. package/src/canvas/editor/keymap.test.ts +161 -0
  41. package/src/canvas/editor/keymap.ts +151 -0
  42. package/src/canvas/editor/layout-actions.test.ts +156 -0
  43. package/src/canvas/editor/layout-actions.ts +219 -0
  44. package/src/canvas/editor/lock.test.ts +68 -0
  45. package/src/canvas/editor/lock.ts +87 -0
  46. package/src/canvas/editor/rebase.test.ts +208 -0
  47. package/src/canvas/editor/rebase.ts +272 -0
  48. package/src/canvas/editor/style.ts +184 -0
  49. package/src/canvas/editor/text-field-context.ts +41 -0
  50. package/src/canvas/editor/z-order.test.ts +75 -0
  51. package/src/canvas/editor/z-order.ts +87 -0
  52. package/src/canvas/headless/alignment.test.ts +59 -0
  53. package/src/canvas/headless/alignment.ts +23 -0
  54. package/src/canvas/headless/break-apart-find.ts +418 -0
  55. package/src/canvas/headless/break-apart-pieces.ts +104 -0
  56. package/src/canvas/headless/break-apart-plan.ts +439 -0
  57. package/src/canvas/headless/break-apart-style.ts +106 -0
  58. package/src/canvas/headless/break-apart.ts +244 -0
  59. package/src/canvas/headless/canvas-cleanup.test.ts +181 -0
  60. package/src/canvas/headless/canvas-cleanup.ts +102 -0
  61. package/src/canvas/headless/edge-anchors.test.ts +70 -0
  62. package/src/canvas/headless/edge-anchors.ts +143 -0
  63. package/src/canvas/headless/edge-memo.test.ts +58 -0
  64. package/src/canvas/headless/edge-memo.ts +32 -0
  65. package/src/canvas/headless/edge-serialization.test.ts +55 -0
  66. package/src/canvas/headless/edge-serialization.ts +32 -0
  67. package/src/canvas/headless/figure-geometry.ts +293 -0
  68. package/src/canvas/headless/fit-viewport.test.ts +85 -0
  69. package/src/canvas/headless/fit-viewport.ts +137 -0
  70. package/src/canvas/headless/grid.test.ts +34 -0
  71. package/src/canvas/headless/grid.ts +94 -0
  72. package/src/canvas/headless/icon-node.test.ts +23 -0
  73. package/src/canvas/headless/icon-node.ts +66 -0
  74. package/src/canvas/headless/index.ts +9 -0
  75. package/src/canvas/headless/linkify.test.ts +23 -0
  76. package/src/canvas/headless/linkify.ts +25 -0
  77. package/src/canvas/headless/pen-marks.ts +133 -0
  78. package/src/canvas/headless/sketch.test.ts +99 -0
  79. package/src/canvas/headless/sketch.ts +302 -0
  80. package/src/canvas/headless/snap-guides.test.ts +61 -0
  81. package/src/canvas/headless/snap-guides.ts +146 -0
  82. package/src/canvas/headless/svg-figure.test.ts +49 -0
  83. package/src/canvas/headless/svg-figure.ts +176 -0
  84. package/src/canvas/headless/table-layout.test.ts +158 -0
  85. package/src/canvas/headless/table-layout.ts +309 -0
  86. package/src/canvas/index.ts +35 -0
  87. package/src/canvas/lod/lod-mode.svelte.ts +30 -0
  88. package/src/canvas/lod/lod-switch.svelte +140 -0
  89. package/src/canvas/lod/render-all.svelte.ts +52 -0
  90. package/src/canvas/ops/arrange.ts +108 -0
  91. package/src/canvas/ops/ops.test.ts +98 -0
  92. package/src/canvas/ops/ops.ts +347 -0
  93. package/src/canvas/pen/PenField.svelte +128 -0
  94. package/src/canvas/pen/PenField.test.ts +87 -0
  95. package/src/canvas/pen/PenOverlay.svelte +172 -0
  96. package/src/canvas/pen/ShapeNode.svelte +64 -0
  97. package/src/canvas/pen/TextNode.svelte +97 -0
  98. package/src/canvas/pen/colors.ts +28 -0
  99. package/src/canvas/pen/eraser.ts +46 -0
  100. package/src/canvas/pen/geometry.ts +148 -0
  101. package/src/canvas/pen/host-field.test.svelte +13 -0
  102. package/src/canvas/pen/index.ts +40 -0
  103. package/src/canvas/pen/ops.ts +16 -0
  104. package/src/canvas/pen/pen-session.ts +177 -0
  105. package/src/canvas/pen/pen.css +149 -0
  106. package/src/canvas/pen/plugin.ts +139 -0
  107. package/src/canvas/pen/shapes.ts +104 -0
  108. package/src/canvas/pen/stroke-path.ts +54 -0
  109. package/src/canvas/pen/types.ts +96 -0
  110. package/src/canvas/pen/ui.ts +26 -0
  111. package/src/canvas/plugin.test.ts +45 -0
  112. package/src/canvas/plugin.ts +159 -0
  113. package/src/canvas/screen/GlyphIcon.svelte +25 -0
  114. package/src/canvas/screen/PartView.svelte +176 -0
  115. package/src/canvas/screen/ScreenBody.svelte +142 -0
  116. package/src/canvas/screen/ScreenCard.svelte +32 -0
  117. package/src/canvas/screen/glyphs/button.svg +1 -0
  118. package/src/canvas/screen/glyphs/close.svg +1 -0
  119. package/src/canvas/screen/glyphs/down.svg +1 -0
  120. package/src/canvas/screen/glyphs/header.svg +1 -0
  121. package/src/canvas/screen/glyphs/hills.svg +1 -0
  122. package/src/canvas/screen/glyphs/image.svg +1 -0
  123. package/src/canvas/screen/glyphs/input.svg +1 -0
  124. package/src/canvas/screen/glyphs/list.svg +1 -0
  125. package/src/canvas/screen/glyphs/modal.svg +1 -0
  126. package/src/canvas/screen/glyphs/plus.svg +1 -0
  127. package/src/canvas/screen/glyphs/up.svg +1 -0
  128. package/src/canvas/screen/glyphs.ts +40 -0
  129. package/src/canvas/screen/labels.ts +41 -0
  130. package/src/canvas/screen/parts.ts +164 -0
  131. package/src/canvas/screen/plugin.ts +67 -0
  132. package/src/canvas/screen/screen.css +593 -0
  133. package/src/canvas/screen/types.ts +47 -0
  134. package/src/canvas/screen/ui.ts +13 -0
  135. package/src/canvas/test-utils.ts +18 -0
  136. package/src/canvas/types.ts +53 -0
  137. package/src/canvas/ui/CanvasToolbar.svelte +169 -0
  138. package/src/canvas/ui/FrameLayer.svelte +116 -0
  139. package/src/canvas/ui/PenLayer.svelte +39 -0
  140. package/src/canvas/ui/StylePanel.svelte +268 -0
  141. package/src/canvas/ui/canvas-grid.svelte +156 -0
  142. package/src/canvas/ui/canvas-ui.css +350 -0
  143. package/src/canvas/ui/forward-wheel.ts +29 -0
  144. package/src/canvas/ui/icons.ts +124 -0
  145. package/src/canvas/ui/place-panel.ts +80 -0
  146. package/src/canvas/ui/shortcut-tip.test.ts +16 -0
  147. package/src/canvas/ui/shortcut-tip.ts +17 -0
  148. package/src/canvas/ui/tools.ts +146 -0
  149. package/src/charts/README.md +104 -0
  150. package/src/charts/ambient.d.ts +4 -0
  151. package/src/charts/animated-value.css +7 -0
  152. package/src/charts/animated-value.svelte +110 -0
  153. package/src/charts/area-chart.css +29 -0
  154. package/src/charts/area-chart.svelte +204 -0
  155. package/src/charts/bar-chart.css +119 -0
  156. package/src/charts/bar-chart.svelte +275 -0
  157. package/src/charts/charts.css +201 -0
  158. package/src/charts/crosshair.svelte +35 -0
  159. package/src/charts/dashboard/chart.ts +5 -0
  160. package/src/charts/dashboard/dashboard-empty.svelte +55 -0
  161. package/src/charts/dashboard/dashboard-section.svelte +58 -0
  162. package/src/charts/dashboard/data-panel.svelte +81 -0
  163. package/src/charts/dashboard/data-sort-head.svelte +102 -0
  164. package/src/charts/dashboard/data-table.svelte +278 -0
  165. package/src/charts/dashboard/data-table.ts +26 -0
  166. package/src/charts/dashboard/index.ts +25 -0
  167. package/src/charts/dashboard/panel-grid.svelte +43 -0
  168. package/src/charts/dashboard/period-filter.svelte +53 -0
  169. package/src/charts/dashboard/rating-chip.svelte +35 -0
  170. package/src/charts/dashboard/stat-band.svelte +47 -0
  171. package/src/charts/dashboard/stat-tile.svelte +176 -0
  172. package/src/charts/dashboard/windows.ts +8 -0
  173. package/src/charts/donut-chart.css +56 -0
  174. package/src/charts/donut-chart.svelte +158 -0
  175. package/src/charts/edge-fade.svelte +43 -0
  176. package/src/charts/edge-fade.ts +16 -0
  177. package/src/charts/figure.css +100 -0
  178. package/src/charts/figure.svelte +122 -0
  179. package/src/charts/gauge-chart.css +94 -0
  180. package/src/charts/gauge-chart.svelte +302 -0
  181. package/src/charts/hover-band.test.ts +30 -0
  182. package/src/charts/hover-band.ts +21 -0
  183. package/src/charts/hover-focus.svelte +85 -0
  184. package/src/charts/index.ts +31 -0
  185. package/src/charts/legend.css +136 -0
  186. package/src/charts/legend.svelte +112 -0
  187. package/src/charts/line-chart.css +26 -0
  188. package/src/charts/line-chart.svelte +256 -0
  189. package/src/charts/motion.ts +19 -0
  190. package/src/charts/number-chart.css +179 -0
  191. package/src/charts/number-chart.svelte +187 -0
  192. package/src/charts/primitives/chart-container.svelte +57 -0
  193. package/src/charts/primitives/chart-style.svelte +36 -0
  194. package/src/charts/primitives/chart-tooltip.svelte +161 -0
  195. package/src/charts/primitives/chart-utils.ts +54 -0
  196. package/src/charts/primitives/index.ts +11 -0
  197. package/src/charts/reference-line.css +18 -0
  198. package/src/charts/reference-line.svelte +31 -0
  199. package/src/charts/reveal.svelte +61 -0
  200. package/src/charts/ring-chart.css +104 -0
  201. package/src/charts/ring-chart.svelte +166 -0
  202. package/src/charts/sparkline.css +45 -0
  203. package/src/charts/sparkline.svelte +141 -0
  204. package/src/charts/stat-card.css +105 -0
  205. package/src/charts/stat-card.svelte +267 -0
  206. package/src/charts/time-series-chart.css +28 -0
  207. package/src/charts/time-series-chart.svelte +179 -0
  208. package/src/charts/trend-pill.css +38 -0
  209. package/src/charts/trend-pill.svelte +48 -0
  210. package/src/components/copy/copy-button.svelte +1 -1
  211. package/src/components/docs/doc-page.svelte +1 -1
  212. package/src/components/docs/on-this-page.svelte +2 -2
  213. package/src/components/list-page/empty-state.svelte +1 -1
  214. package/src/components/list-page/filter-popover.svelte +1 -1
  215. package/src/components/list-page/section-label.svelte +1 -1
  216. package/src/components/segmented/segment.svelte +4 -4
  217. package/src/components/theme/theme-provider.svelte +6 -4
  218. package/src/components/toolbar/index.ts +5 -0
  219. package/src/components/toolbar/toolbar.svelte +168 -0
  220. package/src/components/toolbar/toolbar.test.ts +66 -0
  221. package/src/editor/README.md +58 -0
  222. package/src/editor/ambient.d.ts +1 -0
  223. package/src/editor/block-id.ts +77 -0
  224. package/src/editor/block-ops.test.ts +237 -0
  225. package/src/editor/block-ops.ts +157 -0
  226. package/src/editor/content.css +418 -0
  227. package/src/editor/editor-toolbar.svelte +303 -0
  228. package/src/editor/editor.svelte +469 -0
  229. package/src/editor/extensions.ts +145 -0
  230. package/src/editor/image-toolbar.svelte +123 -0
  231. package/src/editor/index.ts +17 -0
  232. package/src/editor/inline-editor.svelte +261 -0
  233. package/src/editor/markdown-escaping.test.ts +158 -0
  234. package/src/editor/markdown-marks.test.ts +89 -0
  235. package/src/editor/markdown-test-helpers.ts +13 -0
  236. package/src/editor/markdown-youtube.test.ts +93 -0
  237. package/src/editor/markdown.test.ts +250 -0
  238. package/src/editor/markdown.ts +511 -0
  239. package/src/editor/renderer.svelte +92 -0
  240. package/src/editor/serializer.test.ts +133 -0
  241. package/src/editor/serializer.ts +370 -0
  242. package/src/editor/slash-items.ts +115 -0
  243. package/src/editor/slash-menu.svelte +159 -0
  244. package/src/editor/youtube-url.ts +35 -0
  245. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +13 -13
  246. package/src/gallery.test.ts +20 -1
  247. package/src/lib/result.ts +19 -0
  248. package/src/onboarding/README.md +120 -0
  249. package/src/onboarding/define-tours.test.ts +50 -0
  250. package/src/onboarding/define-tours.ts +31 -0
  251. package/src/onboarding/demo.svelte +86 -0
  252. package/src/onboarding/index.ts +12 -0
  253. package/src/onboarding/onboarding-provider.svelte +120 -0
  254. package/src/onboarding/onboarding.svelte.ts +24 -0
  255. package/src/onboarding/pick.test.ts +79 -0
  256. package/src/onboarding/pick.ts +36 -0
  257. package/src/onboarding/stores.test.ts +103 -0
  258. package/src/onboarding/stores.ts +58 -0
  259. package/src/onboarding/tour-card.svelte +70 -0
  260. package/src/onboarding/tour-runner.svelte +197 -0
  261. package/src/onboarding/types.ts +34 -0
  262. package/src/onboarding/wait.ts +29 -0
  263. package/src/shadcn/command/command-input.svelte +1 -1
  264. package/src/slides/Controls.svelte +138 -0
  265. package/src/slides/Deck.svelte +271 -0
  266. package/src/slides/Fragment.svelte +71 -0
  267. package/src/slides/Overview.svelte +113 -0
  268. package/src/slides/Presenter.svelte +292 -0
  269. package/src/slides/README.md +89 -0
  270. package/src/slides/Slide.svelte +179 -0
  271. package/src/slides/SlideList.svelte +19 -0
  272. package/src/slides/ambient.d.ts +1 -0
  273. package/src/slides/bridges.ts +69 -0
  274. package/src/slides/deck-controller.svelte.ts +292 -0
  275. package/src/slides/demo-slides.svelte +358 -0
  276. package/src/slides/demo.svelte +176 -0
  277. package/src/slides/export/capture.ts +113 -0
  278. package/src/slides/export/ffmpeg.ts +120 -0
  279. package/src/slides/export/index.ts +9 -0
  280. package/src/slides/export/launch.ts +10 -0
  281. package/src/slides/export/pdf.ts +152 -0
  282. package/src/slides/export/types.ts +48 -0
  283. package/src/slides/index.ts +19 -0
  284. package/src/slides/labels.ts +48 -0
  285. package/src/slides/layout.ts +51 -0
  286. package/src/slides/model.ts +47 -0
  287. package/src/slides/navigation.ts +100 -0
  288. package/src/slides/theme.css +205 -0
  289. package/src/slides/types.ts +86 -0
  290. package/src/tasks/README.md +312 -0
  291. package/src/tasks/ambient.d.ts +3 -0
  292. package/src/tasks/board-order.ts +137 -0
  293. package/src/tasks/dates.ts +31 -0
  294. package/src/tasks/define-statuses.ts +32 -0
  295. package/src/tasks/demo.svelte +389 -0
  296. package/src/tasks/detail/detail-demo.svelte +186 -0
  297. package/src/tasks/detail/detail-types.ts +54 -0
  298. package/src/tasks/detail/task-chip.svelte +28 -0
  299. package/src/tasks/detail/task-detail.svelte +315 -0
  300. package/src/tasks/detail/task-property.svelte +17 -0
  301. package/src/tasks/detail/task-thread.svelte +258 -0
  302. package/src/tasks/gantt/gantt-bar.svelte +243 -0
  303. package/src/tasks/gantt/gantt-grid.svelte +94 -0
  304. package/src/tasks/gantt/gantt-group-row.svelte +55 -0
  305. package/src/tasks/gantt/gantt-header.svelte +71 -0
  306. package/src/tasks/gantt/gantt-task-row.svelte +186 -0
  307. package/src/tasks/gantt/gantt.css +22 -0
  308. package/src/tasks/gantt/gantt.test.ts +77 -0
  309. package/src/tasks/gantt/logic.test.ts +163 -0
  310. package/src/tasks/gantt/logic.ts +248 -0
  311. package/src/tasks/inbox/inbox-card.svelte +283 -0
  312. package/src/tasks/inbox/inbox-draft.svelte +84 -0
  313. package/src/tasks/inbox/inbox-logic.ts +55 -0
  314. package/src/tasks/inbox/inbox-statuses.ts +48 -0
  315. package/src/tasks/inbox/inbox-trace.svelte +45 -0
  316. package/src/tasks/inbox/inbox-types.ts +108 -0
  317. package/src/tasks/inbox/inbox.svelte +203 -0
  318. package/src/tasks/inbox/index.ts +23 -0
  319. package/src/tasks/index.ts +85 -0
  320. package/src/tasks/kanban-board.svelte +561 -0
  321. package/src/tasks/kanban-column.svelte +117 -0
  322. package/src/tasks/kanban-drag.ts +38 -0
  323. package/src/tasks/kanban-types.ts +65 -0
  324. package/src/tasks/presets/sales.ts +16 -0
  325. package/src/tasks/record/demo-data.ts +286 -0
  326. package/src/tasks/record/field-section.svelte +53 -0
  327. package/src/tasks/record/fields.ts +142 -0
  328. package/src/tasks/record/format.ts +87 -0
  329. package/src/tasks/record/gutter.ts +2 -0
  330. package/src/tasks/record/index.ts +54 -0
  331. package/src/tasks/record/info-list.svelte +316 -0
  332. package/src/tasks/record/kinds.ts +42 -0
  333. package/src/tasks/record/logic.ts +324 -0
  334. package/src/tasks/record/next-up.svelte +70 -0
  335. package/src/tasks/record/project-list.svelte +133 -0
  336. package/src/tasks/record/record-demo.svelte +324 -0
  337. package/src/tasks/record/record-header.svelte +70 -0
  338. package/src/tasks/record/record-shell.svelte +138 -0
  339. package/src/tasks/record/record-stats.svelte +48 -0
  340. package/src/tasks/record/record-timeline.svelte +231 -0
  341. package/src/tasks/record/record.test.ts +350 -0
  342. package/src/tasks/record/steps-rail.svelte +474 -0
  343. package/src/tasks/record/types.ts +280 -0
  344. package/src/tasks/record/where-we-are.svelte +121 -0
  345. package/src/tasks/reorder.ts +19 -0
  346. package/src/tasks/side-panel-types.ts +16 -0
  347. package/src/tasks/side-panel.svelte +193 -0
  348. package/src/tasks/status-badge.svelte +63 -0
  349. package/src/tasks/status-dot.svelte +228 -0
  350. package/src/tasks/status-icon.svelte +30 -0
  351. package/src/tasks/status-types.ts +40 -0
  352. package/src/tasks/task-gantt.svelte +230 -0
  353. package/src/tasks/task-list.svelte +470 -0
  354. package/src/tasks/task-types.ts +33 -0
  355. package/src/tasks/time/duration.ts +69 -0
  356. package/src/tasks/time/time-chip.svelte +62 -0
  357. package/src/tasks/time/time-dot.svelte +58 -0
  358. package/src/tasks/time/time-ring.svelte +74 -0
  359. package/src/tasks/time/time-tracker.svelte +565 -0
  360. package/src/tasks/time/time-types.ts +74 -0
  361. package/src/tasks/use-kanban-dnd.svelte.ts +173 -0
  362. package/src/tokens/base.css +5 -5
  363. package/src/tokens/palette.css +115 -120
@@ -0,0 +1,120 @@
1
+ <script lang="ts">
2
+ import { tryCatchAsync } from "../lib/result.js";
3
+ import { untrack } from "svelte";
4
+ import { onboarding } from "./onboarding.svelte.js";
5
+ import { keepFoundSteps, pickForReplay, pickTour, toursForRoute, type SeenMap } from "./pick.js";
6
+ import TourRunner from "./tour-runner.svelte";
7
+ import type { Outcome, SeenStore, Step, Strings, Tour } from "./types.js";
8
+ import { waitForElement } from "./wait.js";
9
+
10
+ let {
11
+ tours,
12
+ store,
13
+ strings,
14
+ routeId,
15
+ enabled = true,
16
+ replayKey = true,
17
+ waitMs = 2000,
18
+ }: {
19
+ tours: readonly Tour[];
20
+ store: SeenStore;
21
+ strings: Strings;
22
+ /** `page.route.id`. A tour runs on the route it names. */
23
+ routeId: string | null | undefined;
24
+ /** False while signed out, or anywhere a tour has no business starting. */
25
+ enabled?: boolean;
26
+ /** Press `?` to replay this page's tour. */
27
+ replayKey?: boolean;
28
+ /** How long to wait for the first target before giving the tour up. */
29
+ waitMs?: number;
30
+ } = $props();
31
+
32
+ type Active = { tour: Tour; steps: Step[]; replay: boolean; run: number };
33
+
34
+ let active = $state<Active | null>(null);
35
+ let seen: SeenMap | null = null;
36
+ let loaded: Promise<SeenMap | null> | null = null;
37
+ let run = 0;
38
+
39
+ // One read per session. A failed read shows nothing: a tour repeating on every page is worse than none.
40
+ function loadSeen(): Promise<SeenMap | null> {
41
+ loaded ??= store.load().then(
42
+ (map) => (seen = map),
43
+ () => null,
44
+ );
45
+ return loaded;
46
+ }
47
+
48
+ async function launch(tour: Tour, replay: boolean, signal: AbortSignal): Promise<boolean> {
49
+ let steps: Step[] = [];
50
+ if (tour.kind === "steps") {
51
+ await waitForElement(tour.steps[0].target, waitMs, signal);
52
+ if (signal.aborted) return false;
53
+ steps = keepFoundSteps(tour.steps, (selector) => document.querySelector(selector) !== null);
54
+ if (steps.length === 0) return false;
55
+ }
56
+ if (active) return false;
57
+ active = { tour, steps, replay, run: ++run };
58
+ return true;
59
+ }
60
+
61
+ let controller: AbortController | null = null;
62
+
63
+ $effect(() => {
64
+ const id = routeId;
65
+ const list = tours;
66
+ const on = enabled;
67
+ onboarding.hasTour = on && toursForRoute(list, id).length > 0;
68
+ active = null;
69
+ if (!on || !id) return;
70
+ const mine = new AbortController();
71
+ controller = mine;
72
+ void loadSeen().then((map) => {
73
+ if (!map || mine.signal.aborted) return;
74
+ const tour = pickTour(untrack(() => tours), id, map);
75
+ if (tour) void launch(tour, false, mine.signal);
76
+ });
77
+ return () => mine.abort();
78
+ });
79
+
80
+ $effect(() => onboarding.bind(async (tourId) => {
81
+ const tour = pickForReplay(tours, routeId, tourId);
82
+ if (!tour || !controller || controller.signal.aborted) return false;
83
+ return launch(tour, true, controller.signal);
84
+ }));
85
+
86
+ $effect(() => {
87
+ onboarding.running = active !== null;
88
+ return () => {
89
+ onboarding.running = false;
90
+ };
91
+ });
92
+
93
+ async function finish(outcome: Outcome | null) {
94
+ const done = active;
95
+ active = null;
96
+ if (!done || done.replay || !outcome) return;
97
+ const version = done.tour.version ?? 1;
98
+ if (seen) seen = { ...seen, [done.tour.id]: version };
99
+ // A failed write shows the tour again next session, which beats an error toast.
100
+ await tryCatchAsync(() => store.markSeen(done.tour.id, version, outcome));
101
+ }
102
+
103
+ function onkeydown(event: KeyboardEvent) {
104
+ if (!replayKey || event.key !== "?" || event.ctrlKey || event.metaKey || event.altKey) return;
105
+ const el = event.target instanceof HTMLElement ? event.target : null;
106
+ if (el?.closest("input, textarea, select, [contenteditable]")) return;
107
+ if (el?.isContentEditable) return;
108
+ if (!onboarding.hasTour || active) return;
109
+ event.preventDefault();
110
+ void onboarding.replay();
111
+ }
112
+ </script>
113
+
114
+ <svelte:window {onkeydown} />
115
+
116
+ {#if active}
117
+ {#key active.run}
118
+ <TourRunner tour={active.tour} steps={active.steps} {strings} onfinish={finish} />
119
+ {/key}
120
+ {/if}
@@ -0,0 +1,24 @@
1
+ type Replay = (tourId?: string) => Promise<boolean>;
2
+
3
+ class Onboarding {
4
+ /** True when the current page has a tour, so a menu item can hide itself. */
5
+ hasTour = $state(false);
6
+ /** True while a tour is on screen. */
7
+ running = $state(false);
8
+ #replay: Replay | null = null;
9
+
10
+ /** The provider registers itself here. Returns the undo for cleanup. */
11
+ bind(replay: Replay): () => void {
12
+ this.#replay = replay;
13
+ return () => {
14
+ if (this.#replay === replay) this.#replay = null;
15
+ };
16
+ }
17
+
18
+ /** Restarts this page's tour, or the one named. Writes no seen-state. False when nothing started. */
19
+ replay(tourId?: string): Promise<boolean> {
20
+ return this.#replay ? this.#replay(tourId) : Promise.resolve(false);
21
+ }
22
+ }
23
+
24
+ export const onboarding = new Onboarding();
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from "vite-plus/test";
2
+ import { isSeen, keepFoundSteps, pickForReplay, pickTour, toursForRoute } from "./pick.js";
3
+ import type { Tour } from "./types.js";
4
+
5
+ const step = { target: "#a", title: "A", body: "a" };
6
+ const tours: Tour[] = [
7
+ { id: "home", route: "/(app)", kind: "intro", title: "Hi", body: "Welcome." },
8
+ { id: "board", route: "/(app)/projects", kind: "steps", steps: [step] },
9
+ { id: "board-extra", route: "/(app)/projects", kind: "steps", steps: [step], version: 2 },
10
+ { id: "record", route: "/(app)/projects/[id]", kind: "steps", steps: [step] },
11
+ ];
12
+
13
+ describe("isSeen", () => {
14
+ it("counts a tour as seen once the stored version reaches its own", () => {
15
+ expect(isSeen(tours[1], {})).toBe(false);
16
+ expect(isSeen(tours[1], { board: 1 })).toBe(true);
17
+ expect(isSeen(tours[2], { "board-extra": 1 })).toBe(false);
18
+ expect(isSeen(tours[2], { "board-extra": 2 })).toBe(true);
19
+ });
20
+
21
+ it("shows a tour again after its version is bumped", () => {
22
+ const v1: Tour = { id: "t", route: "/", kind: "steps", steps: [step] };
23
+ const v2: Tour = { ...v1, version: 2 };
24
+ expect(isSeen(v1, { t: 1 })).toBe(true);
25
+ expect(isSeen(v2, { t: 1 })).toBe(false);
26
+ });
27
+ });
28
+
29
+ describe("pickTour", () => {
30
+ it("matches the route id exactly, so a params route never borrows its parent's tour", () => {
31
+ expect(pickTour(tours, "/(app)", {})?.id).toBe("home");
32
+ expect(pickTour(tours, "/(app)/projects/[id]", {})?.id).toBe("record");
33
+ expect(pickTour(tours, "/(app)/settings", {})).toBeUndefined();
34
+ });
35
+
36
+ it("takes the first unseen tour on a route, one at a time", () => {
37
+ expect(pickTour(tours, "/(app)/projects", {})?.id).toBe("board");
38
+ expect(pickTour(tours, "/(app)/projects", { board: 1 })?.id).toBe("board-extra");
39
+ expect(pickTour(tours, "/(app)/projects", { board: 1, "board-extra": 2 })).toBeUndefined();
40
+ });
41
+
42
+ it("picks nothing without a route", () => {
43
+ expect(pickTour(tours, null, {})).toBeUndefined();
44
+ expect(pickTour(tours, undefined, {})).toBeUndefined();
45
+ });
46
+ });
47
+
48
+ describe("toursForRoute", () => {
49
+ it("lists every tour on the route in definition order", () => {
50
+ expect(toursForRoute(tours, "/(app)/projects").map((t) => t.id)).toEqual(["board", "board-extra"]);
51
+ });
52
+ });
53
+
54
+ describe("pickForReplay", () => {
55
+ it("restarts the page's first tour even when it was seen", () => {
56
+ expect(pickForReplay(tours, "/(app)/projects")?.id).toBe("board");
57
+ });
58
+
59
+ it("finds a tour by id on any route", () => {
60
+ expect(pickForReplay(tours, "/(app)", "record")?.id).toBe("record");
61
+ expect(pickForReplay(tours, "/(app)", "missing")).toBeUndefined();
62
+ });
63
+ });
64
+
65
+ describe("keepFoundSteps", () => {
66
+ const steps = [
67
+ { ...step, target: "#a" },
68
+ { ...step, target: "#b" },
69
+ { ...step, target: "#c" },
70
+ ];
71
+
72
+ it("drops the steps whose target isn't on the page", () => {
73
+ expect(keepFoundSteps(steps, (s) => s !== "#b").map((s) => s.target)).toEqual(["#a", "#c"]);
74
+ });
75
+
76
+ it("returns nothing when no target is found, so no empty spotlight starts", () => {
77
+ expect(keepFoundSteps(steps, () => false)).toEqual([]);
78
+ });
79
+ });
@@ -0,0 +1,36 @@
1
+ import type { Step, Tour } from "./types.js";
2
+
3
+ export type SeenMap = Record<string, number>;
4
+
5
+ /** The seen version has to reach the tour's own, so bumping `version` shows it again. */
6
+ export function isSeen(tour: Tour, seen: SeenMap): boolean {
7
+ return (seen[tour.id] ?? 0) >= (tour.version ?? 1);
8
+ }
9
+
10
+ export function toursForRoute(tours: readonly Tour[], routeId: string | null | undefined): Tour[] {
11
+ return routeId ? tours.filter((tour) => tour.route === routeId) : [];
12
+ }
13
+
14
+ /** The first unseen tour on a route, in the order they were defined. One per page view. */
15
+ export function pickTour(
16
+ tours: readonly Tour[],
17
+ routeId: string | null | undefined,
18
+ seen: SeenMap,
19
+ ): Tour | undefined {
20
+ return toursForRoute(tours, routeId).find((tour) => !isSeen(tour, seen));
21
+ }
22
+
23
+ /** What replay starts: a named tour, or the page's first one whether or not it was seen. */
24
+ export function pickForReplay(
25
+ tours: readonly Tour[],
26
+ routeId: string | null | undefined,
27
+ tourId?: string,
28
+ ): Tour | undefined {
29
+ if (tourId) return tours.find((tour) => tour.id === tourId);
30
+ return toursForRoute(tours, routeId)[0];
31
+ }
32
+
33
+ /** Keeps the steps whose target is on the page. A tour with none left starts nothing. */
34
+ export function keepFoundSteps(steps: readonly Step[], exists: (selector: string) => boolean): Step[] {
35
+ return steps.filter((step) => exists(step.target));
36
+ }
@@ -0,0 +1,103 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
2
+ import { localStorageStore, memoryStore } from "./stores.js";
3
+ import type { SeenStore } from "./types.js";
4
+
5
+ function fakeStorage(): Storage {
6
+ const data = new Map<string, string>();
7
+ return {
8
+ get length() {
9
+ return data.size;
10
+ },
11
+ clear: () => data.clear(),
12
+ getItem: (key) => data.get(key) ?? null,
13
+ key: (i) => [...data.keys()][i] ?? null,
14
+ removeItem: (key) => void data.delete(key),
15
+ setItem: (key, value) => void data.set(key, String(value)),
16
+ };
17
+ }
18
+
19
+ function behavesLikeAStore(make: () => SeenStore) {
20
+ it("starts empty", async () => {
21
+ expect(await make().load()).toEqual({});
22
+ });
23
+
24
+ it("remembers the version it saw", async () => {
25
+ const store = make();
26
+ await store.markSeen("home", 1, "completed");
27
+ await store.markSeen("board", 2, "dismissed");
28
+ expect(await store.load()).toEqual({ home: 1, board: 2 });
29
+ });
30
+
31
+ it("never goes back to an older version", async () => {
32
+ const store = make();
33
+ await store.markSeen("home", 3, "completed");
34
+ await store.markSeen("home", 2, "completed");
35
+ expect((await store.load()).home).toBe(3);
36
+ });
37
+
38
+ it("resets one tour or all of them", async () => {
39
+ const store = make();
40
+ await store.markSeen("home", 1, "completed");
41
+ await store.markSeen("board", 1, "completed");
42
+ await store.reset("home");
43
+ expect(await store.load()).toEqual({ board: 1 });
44
+ await store.reset();
45
+ expect(await store.load()).toEqual({});
46
+ });
47
+ }
48
+
49
+ describe("memoryStore", () => {
50
+ behavesLikeAStore(() => memoryStore());
51
+
52
+ it("takes a starting state and keeps its own copy", async () => {
53
+ const initial = { home: 1 };
54
+ const store = memoryStore(initial);
55
+ await store.markSeen("board", 1, "completed");
56
+ expect(initial).toEqual({ home: 1 });
57
+ expect(await store.load()).toEqual({ home: 1, board: 1 });
58
+ });
59
+ });
60
+
61
+ describe("localStorageStore", () => {
62
+ beforeEach(() => {
63
+ vi.stubGlobal("localStorage", fakeStorage());
64
+ });
65
+ afterEach(() => {
66
+ vi.unstubAllGlobals();
67
+ });
68
+
69
+ behavesLikeAStore(() => localStorageStore());
70
+
71
+ it("shares state between two stores on one key and keeps keys apart", async () => {
72
+ await localStorageStore("a").markSeen("home", 1, "completed");
73
+ expect(await localStorageStore("a").load()).toEqual({ home: 1 });
74
+ expect(await localStorageStore("b").load()).toEqual({});
75
+ });
76
+
77
+ it("reads corrupt or foreign data as empty", async () => {
78
+ localStorage.setItem("heyramzi:onboarding", "{nope");
79
+ expect(await localStorageStore().load()).toEqual({});
80
+ localStorage.setItem("heyramzi:onboarding", JSON.stringify(["x"]));
81
+ expect(await localStorageStore().load()).toEqual({});
82
+ localStorage.setItem("heyramzi:onboarding", JSON.stringify({ home: 1, bad: "yes" }));
83
+ expect(await localStorageStore().load()).toEqual({ home: 1 });
84
+ });
85
+
86
+ it("doesn't throw when storage is blocked", async () => {
87
+ vi.stubGlobal("localStorage", {
88
+ getItem: () => {
89
+ throw new Error("blocked");
90
+ },
91
+ setItem: () => {
92
+ throw new Error("blocked");
93
+ },
94
+ removeItem: () => {
95
+ throw new Error("blocked");
96
+ },
97
+ });
98
+ const store = localStorageStore();
99
+ expect(await store.load()).toEqual({});
100
+ await expect(store.markSeen("home", 1, "completed")).resolves.toBeUndefined();
101
+ await expect(store.reset()).resolves.toBeUndefined();
102
+ });
103
+ });
@@ -0,0 +1,58 @@
1
+ import { tryCatch } from "../lib/result.js";
2
+ import type { Outcome, SeenStore } from "./types.js";
3
+
4
+ type Seen = Record<string, number>;
5
+
6
+ /** Holds seen-state in memory. Tests and previews. */
7
+ export function memoryStore(initial: Seen = {}): SeenStore {
8
+ let seen: Seen = { ...initial };
9
+ return {
10
+ async load() {
11
+ return { ...seen };
12
+ },
13
+ async markSeen(tourId, version, _outcome: Outcome) {
14
+ seen[tourId] = Math.max(seen[tourId] ?? 0, version);
15
+ },
16
+ async reset(tourId) {
17
+ if (tourId === undefined) seen = {};
18
+ else delete seen[tourId];
19
+ },
20
+ };
21
+ }
22
+
23
+ function readSeen(key: string): Seen {
24
+ const read = tryCatch(() => JSON.parse(localStorage.getItem(key) ?? "{}") as unknown);
25
+ const parsed = read.ok ? read.value : null;
26
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
27
+ return Object.fromEntries(
28
+ Object.entries(parsed).filter((entry): entry is [string, number] => typeof entry[1] === "number"),
29
+ );
30
+ }
31
+
32
+ // Blocked storage (private mode, a full disk) means the tour shows again next visit, which beats an error.
33
+ function writeSeen(key: string, seen: Seen): void {
34
+ tryCatch(() => localStorage.setItem(key, JSON.stringify(seen)));
35
+ }
36
+
37
+ /** Keeps seen-state in this browser. Demos and signed-out pages. */
38
+ export function localStorageStore(key = "heyramzi:onboarding"): SeenStore {
39
+ return {
40
+ async load() {
41
+ return readSeen(key);
42
+ },
43
+ async markSeen(tourId, version, _outcome: Outcome) {
44
+ const seen = readSeen(key);
45
+ seen[tourId] = Math.max(seen[tourId] ?? 0, version);
46
+ writeSeen(key, seen);
47
+ },
48
+ async reset(tourId) {
49
+ if (tourId === undefined) {
50
+ tryCatch(() => localStorage.removeItem(key));
51
+ return;
52
+ }
53
+ const seen = readSeen(key);
54
+ delete seen[tourId];
55
+ writeSeen(key, seen);
56
+ },
57
+ };
58
+ }
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ import { Button } from "../shadcn/button/index.js";
3
+ import { X } from "phosphor-svelte";
4
+ import type { HTMLAttributes } from "svelte/elements";
5
+ import type { Strings } from "./types.js";
6
+
7
+ type Attrs = HTMLAttributes<HTMLElement>;
8
+
9
+ // The card is plain markup. The engine hands it the attribute bags that wire it to the machine.
10
+ let {
11
+ title,
12
+ body,
13
+ image,
14
+ intro = false,
15
+ index,
16
+ total,
17
+ strings,
18
+ titleAttrs,
19
+ bodyAttrs,
20
+ onclose,
21
+ onprev,
22
+ onnext,
23
+ }: {
24
+ title: string;
25
+ body: string;
26
+ image?: string;
27
+ intro?: boolean;
28
+ index: number;
29
+ total: number;
30
+ strings: Strings;
31
+ titleAttrs: Attrs;
32
+ bodyAttrs: Attrs;
33
+ onclose: () => void;
34
+ onprev: () => void;
35
+ /** The last step's button reads "done", and this closes the tour. */
36
+ onnext: () => void;
37
+ } = $props();
38
+
39
+ const last = $derived(index === total - 1);
40
+ </script>
41
+
42
+ {#if image}
43
+ <img src={image} alt="" class="aspect-video w-full rounded-t-[inherit] object-cover outline -outline-offset-1 outline-black/10 dark:outline-white/10" />
44
+ {/if}
45
+ <div class="flex flex-col gap-1.5 p-4 pb-3">
46
+ <div class="flex items-start justify-between gap-3">
47
+ <h2 {...titleAttrs} class={["font-semibold leading-snug text-balance", intro ? "text-base" : "text-sm"]}>{title}</h2>
48
+ <Button onclick={onclose} variant="ghost" size="sm" icon class="-mt-1 -mr-1.5 text-muted-foreground" aria-label={strings.skip}>
49
+ <X weight="bold" />
50
+ </Button>
51
+ </div>
52
+ <p {...bodyAttrs} class="text-muted-foreground text-sm text-pretty">{body}</p>
53
+ </div>
54
+ <div class="flex items-center justify-between gap-3 px-4 pb-4">
55
+ {#if total > 1}
56
+ <div role="img" aria-label={strings.progress(index + 1, total)} class="flex items-center gap-1.5">
57
+ {#each { length: total } as _, i (i)}
58
+ <span class={["h-1.5 rounded-full transition-[width,background-color] duration-(--duration-fast) ease-(--ease-smooth-out) motion-reduce:transition-none", i === index ? "bg-foreground w-4" : "bg-border w-1.5"]}></span>
59
+ {/each}
60
+ </div>
61
+ {:else}
62
+ <span></span>
63
+ {/if}
64
+ <div class="flex items-center gap-2">
65
+ {#if index > 0}
66
+ <Button onclick={onprev} variant="ghost" size="sm">{strings.back}</Button>
67
+ {/if}
68
+ <Button onclick={onnext} size="sm">{last ? strings.done : strings.next}</Button>
69
+ </div>
70
+ </div>
@@ -0,0 +1,197 @@
1
+ <script lang="ts">
2
+ // The only file that knows Zag. Swap the machine and nothing else in the package changes.
3
+ import * as zag from "@zag-js/tour";
4
+ import { normalizeProps, portal, useMachine } from "@zag-js/svelte";
5
+ import { onMount, tick, untrack } from "svelte";
6
+ import TourCard from "./tour-card.svelte";
7
+ import type { Outcome, Step, Strings, Tour } from "./types.js";
8
+
9
+ let {
10
+ tour,
11
+ steps,
12
+ strings,
13
+ onfinish,
14
+ }: {
15
+ tour: Tour;
16
+ steps: Step[];
17
+ strings: Strings;
18
+ /** `null` means the tour never showed, so nothing is worth recording. */
19
+ onfinish: (outcome: Outcome | null) => void;
20
+ } = $props();
21
+
22
+ // The provider keys this component per run, so the tour never changes under it.
23
+ const { tour: fixed, steps: fixedSteps } = untrack(() => ({ tour, steps }));
24
+ const intro = fixed.kind === "intro" ? fixed : null;
25
+
26
+ const zagSteps: zag.StepDetails[] = intro
27
+ ? [{ id: "intro", type: "dialog", title: intro.title, description: intro.body, backdrop: true }]
28
+ : fixedSteps.map((step, i) => ({
29
+ id: `step-${i}`,
30
+ type: "tooltip",
31
+ target: () => document.querySelector<HTMLElement>(step.target),
32
+ title: step.title,
33
+ description: step.body,
34
+ placement: step.placement ?? "bottom",
35
+ offset: { mainAxis: 12 },
36
+ backdrop: true,
37
+ }));
38
+
39
+ let finished = false;
40
+ function finish(outcome: Outcome | null) {
41
+ if (finished) return;
42
+ finished = true;
43
+ onfinish(outcome);
44
+ }
45
+
46
+ // Dismissing on the last step reports "dismissed" and then "completed" in one go, so the
47
+ // dismissal waits a microtask and the completion wins.
48
+ function onStatusChange({ status }: zag.StatusChangeDetails) {
49
+ if (status === "completed") finish("completed");
50
+ else if (status === "dismissed" || status === "skipped") queueMicrotask(() => finish("dismissed"));
51
+ else if (status === "not-found") finish(null);
52
+ }
53
+
54
+ const id = $props.id();
55
+ const service = useMachine(zag.machine, () => ({
56
+ id,
57
+ steps: zagSteps,
58
+ onStatusChange,
59
+ closeOnInteractOutside: false,
60
+ spotlightOffset: { x: 8, y: 8 },
61
+ spotlightRadius: 10,
62
+ translations: {
63
+ progressText: ({ current, total }) => strings.progress(current + 1, total),
64
+ nextStep: strings.next,
65
+ prevStep: strings.back,
66
+ close: strings.skip,
67
+ skip: strings.skip,
68
+ },
69
+ }));
70
+ const api = $derived(zag.connect(service, normalizeProps));
71
+
72
+ const dismiss = () => service.send({ type: "DISMISS" });
73
+
74
+ onMount(() => api.start());
75
+
76
+ // Zag marks a repositioning (scroll, resize) with a zero-length spotlight transition. The
77
+ // backdrop hole has to snap with it, or it trails the ring.
78
+ const instant = $derived(String(api.getSpotlightProps().style ?? "").includes("0ms"));
79
+
80
+ let card = $state<HTMLElement | null>(null);
81
+ let cleanupMargin: (() => void) | null = null;
82
+
83
+ // Under 640px the card is a sheet over the bottom of the screen. Scroll the target above it.
84
+ $effect(() => {
85
+ const step = api.open ? api.step : null;
86
+ cleanupMargin?.();
87
+ cleanupMargin = null;
88
+ if (!step?.target || !matchMedia("(max-width: 639px)").matches) return;
89
+ let cancelled = false;
90
+ void tick().then(() => {
91
+ const target = step.target?.();
92
+ if (cancelled || !target || !card) return;
93
+ const previous = target.style.scrollMarginBottom;
94
+ target.style.scrollMarginBottom = `${card.offsetHeight + 16}px`;
95
+ target.scrollIntoView({ block: "nearest", behavior: "instant" });
96
+ cleanupMargin = () => {
97
+ target.style.scrollMarginBottom = previous;
98
+ };
99
+ });
100
+ return () => {
101
+ cancelled = true;
102
+ };
103
+ });
104
+
105
+ const index = $derived(Math.max(api.stepIndex, 0));
106
+ const step = $derived(api.step);
107
+ </script>
108
+
109
+ {#if step}
110
+ <div use:portal {...api.getBackdropProps()} class="tour-backdrop" data-instant={instant || undefined}></div>
111
+ <div use:portal {...api.getSpotlightProps()} class="tour-spotlight"></div>
112
+ <div use:portal {...api.getPositionerProps()} class="tour-positioner">
113
+ <div
114
+ {...api.getContentProps()}
115
+ bind:this={card}
116
+ class={["tour-card bg-popover text-popover-foreground ring-foreground/10 shadow-overlay pointer-events-auto max-w-full text-sm ring-1 outline-none", intro ? "w-96 rounded-2xl" : "w-80 rounded-xl"]}
117
+ >
118
+ <TourCard
119
+ title={String(step.title)}
120
+ body={String(step.description)}
121
+ image={intro?.image}
122
+ intro={intro !== null}
123
+ {index}
124
+ total={api.totalSteps}
125
+ {strings}
126
+ titleAttrs={api.getTitleProps()}
127
+ bodyAttrs={api.getDescriptionProps()}
128
+ onclose={dismiss}
129
+ onprev={() => api.prev()}
130
+ onnext={() => (api.lastStep ? dismiss() : api.next())}
131
+ />
132
+ </div>
133
+ </div>
134
+ {/if}
135
+
136
+ <style>
137
+ .tour-backdrop,
138
+ .tour-spotlight,
139
+ .tour-positioner {
140
+ --tour-z-index: 60;
141
+ z-index: calc(var(--tour-layer) + var(--tour-z-index));
142
+ }
143
+ .tour-backdrop {
144
+ background: oklch(0 0 0 / 0.5);
145
+ transition: clip-path var(--duration-fast) var(--ease-smooth-out);
146
+ }
147
+ .tour-backdrop[data-instant] {
148
+ transition-duration: 0ms;
149
+ }
150
+ .tour-spotlight {
151
+ outline: 2px solid oklch(1 0 0 / 0.7);
152
+ transition-property: left, top, width, height;
153
+ transition-duration: var(--duration-fast);
154
+ transition-timing-function: var(--ease-smooth-out);
155
+ }
156
+ .tour-positioner[data-type="dialog"] {
157
+ position: fixed;
158
+ inset: 0;
159
+ display: grid;
160
+ place-items: center;
161
+ padding: 1rem;
162
+ pointer-events: none;
163
+ }
164
+ .tour-card[data-state="open"] {
165
+ animation: tour-in var(--duration-fast) var(--ease-smooth-out);
166
+ }
167
+ @keyframes tour-in {
168
+ from {
169
+ opacity: 0;
170
+ scale: 0.97;
171
+ }
172
+ }
173
+ @media (max-width: 639px) {
174
+ .tour-positioner {
175
+ position: fixed !important;
176
+ inset: auto 0 0 0 !important;
177
+ transform: none !important;
178
+ translate: none !important;
179
+ min-width: 0 !important;
180
+ width: auto !important;
181
+ display: block !important;
182
+ padding: 0.75rem 0.75rem calc(0.75rem + env(safe-area-inset-bottom));
183
+ }
184
+ .tour-card {
185
+ width: 100%;
186
+ }
187
+ }
188
+ @media (prefers-reduced-motion: reduce) {
189
+ .tour-backdrop,
190
+ .tour-spotlight {
191
+ transition: none;
192
+ }
193
+ .tour-card[data-state="open"] {
194
+ animation: none;
195
+ }
196
+ }
197
+ </style>