@heyramzi/ui 2.1.0 → 2.3.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 (317) hide show
  1. package/COMPONENTS.md +1 -1
  2. package/package.json +7 -8
  3. package/scripts/sync-shadcn.mjs +3 -4
  4. package/src/agents/agent-avatar.svelte +604 -575
  5. package/src/agents/agent-cast.svelte +179 -183
  6. package/src/agents/demo.svelte +125 -113
  7. package/src/agents/helpers.ts +23 -17
  8. package/src/agents/index.ts +4 -4
  9. package/src/agents/page-state.ts +6 -6
  10. package/src/agents/types.ts +6 -6
  11. package/src/ai/ai-button/AiButton.svelte +4 -12
  12. package/src/ai/generation-progress/GenerationProgress.svelte +10 -3
  13. package/src/ai/prompt-input/PromptInputTextarea.svelte +1 -2
  14. package/src/ai/response/Response.css +2 -3
  15. package/src/animations/avatar-group-hover.css +2 -16
  16. package/src/animations/card-tilt.css +3 -6
  17. package/src/animations/digit-pop-in.css +2 -15
  18. package/src/animations/dropdown.css +4 -17
  19. package/src/animations/input-clear-dissolve.css +5 -18
  20. package/src/animations/input-shake.css +9 -41
  21. package/src/animations/learn-more-hover.css +17 -9
  22. package/src/animations/like-button.css +2 -4
  23. package/src/animations/page-side-by-side.css +2 -10
  24. package/src/animations/plus-menu-morph.css +2 -5
  25. package/src/animations/shimmer-text.css +5 -18
  26. package/src/animations/spinning-counter.css +2 -6
  27. package/src/animations/success-check.css +2 -15
  28. package/src/animations/thinking-states.css +2 -7
  29. package/src/brand/brand.test.ts +21 -5
  30. package/src/brand/index.ts +4 -10
  31. package/src/canvas/card/card-frame.svelte +43 -39
  32. package/src/canvas/card/resize-handles.svelte +89 -80
  33. package/src/canvas/chrome/chrome-zoom.svelte +101 -100
  34. package/src/canvas/chrome/lock-badge.svelte +8 -8
  35. package/src/canvas/chrome/side-handles.svelte +24 -19
  36. package/src/canvas/chrome/size-chip.svelte +30 -30
  37. package/src/canvas/chrome/snap-guides.svelte +20 -20
  38. package/src/canvas/components/icons/ToolIcon.svelte +8 -8
  39. package/src/canvas/components/shared/svg/PreviewSvg.svelte +19 -19
  40. package/src/canvas/components/shared/svg/ShapeOutline.svelte +71 -48
  41. package/src/canvas/components/shared/svg/StrokeNode.svelte +21 -27
  42. package/src/canvas/components/shared/svg/TextRule.svelte +25 -17
  43. package/src/canvas/edges/bend-handle.svelte +46 -46
  44. package/src/canvas/edges/edge-gradient.svelte +10 -10
  45. package/src/canvas/edges/gradient-edge.svelte +45 -29
  46. package/src/canvas/editor/lock.test.ts +6 -1
  47. package/src/canvas/editor/style.ts +7 -1
  48. package/src/canvas/headless/canvas-cleanup.test.ts +1 -1
  49. package/src/canvas/headless/svg-figure.test.ts +5 -1
  50. package/src/canvas/lod/lod-switch.svelte +111 -106
  51. package/src/canvas/ops/ops.ts +1 -1
  52. package/src/canvas/pen/PenField.svelte +87 -81
  53. package/src/canvas/pen/PenOverlay.svelte +127 -127
  54. package/src/canvas/pen/ShapeNode.svelte +31 -32
  55. package/src/canvas/pen/TextNode.svelte +54 -53
  56. package/src/canvas/pen/host-field.test.svelte +7 -7
  57. package/src/canvas/pen/shapes.ts +7 -1
  58. package/src/canvas/screen/GlyphIcon.svelte +22 -17
  59. package/src/canvas/screen/PartView.svelte +138 -72
  60. package/src/canvas/screen/ScreenBody.svelte +81 -60
  61. package/src/canvas/screen/ScreenCard.svelte +20 -20
  62. package/src/canvas/ui/CanvasToolbar.svelte +173 -85
  63. package/src/canvas/ui/FrameLayer.svelte +59 -59
  64. package/src/canvas/ui/PenLayer.svelte +39 -30
  65. package/src/canvas/ui/StylePanel.svelte +119 -102
  66. package/src/canvas/ui/canvas-grid.svelte +137 -129
  67. package/src/charts/animated-value.svelte +1 -1
  68. package/src/charts/area-chart.css +2 -5
  69. package/src/charts/area-chart.svelte +1 -1
  70. package/src/charts/bar-chart.css +2 -3
  71. package/src/charts/bar-chart.svelte +1 -1
  72. package/src/charts/charts.css +5 -17
  73. package/src/charts/crosshair.svelte +1 -1
  74. package/src/charts/dashboard/dashboard-empty.svelte +25 -25
  75. package/src/charts/dashboard/dashboard-section.svelte +24 -24
  76. package/src/charts/dashboard/data-panel.svelte +50 -50
  77. package/src/charts/dashboard/data-sort-head.svelte +56 -56
  78. package/src/charts/dashboard/data-table.svelte +138 -138
  79. package/src/charts/dashboard/panel-grid.svelte +30 -30
  80. package/src/charts/dashboard/period-filter.svelte +25 -25
  81. package/src/charts/dashboard/rating-chip.svelte +19 -19
  82. package/src/charts/dashboard/stat-band.svelte +32 -32
  83. package/src/charts/dashboard/stat-tile.svelte +106 -99
  84. package/src/charts/donut-chart.svelte +1 -1
  85. package/src/charts/edge-fade.svelte +1 -1
  86. package/src/charts/figure.svelte +1 -1
  87. package/src/charts/gauge-chart.css +4 -7
  88. package/src/charts/gauge-chart.svelte +1 -1
  89. package/src/charts/hover-focus.svelte +1 -1
  90. package/src/charts/legend.css +1 -3
  91. package/src/charts/legend.svelte +1 -1
  92. package/src/charts/line-chart.svelte +1 -1
  93. package/src/charts/number-chart.css +6 -9
  94. package/src/charts/number-chart.svelte +1 -1
  95. package/src/charts/reference-line.svelte +19 -19
  96. package/src/charts/reveal.svelte +1 -1
  97. package/src/charts/ring-chart.css +2 -4
  98. package/src/charts/ring-chart.svelte +1 -1
  99. package/src/charts/sparkline.svelte +5 -2
  100. package/src/charts/stat-card.svelte +1 -1
  101. package/src/charts/time-series-chart.css +2 -5
  102. package/src/charts/trend-pill.css +2 -4
  103. package/src/charts/trend-pill.svelte +1 -1
  104. package/src/components/copy/copy-button.svelte +93 -98
  105. package/src/components/copy/copy-button.test.ts +10 -2
  106. package/src/components/data-table/infinite-scroll.svelte +42 -42
  107. package/src/components/data-table/pagination.svelte +34 -22
  108. package/src/components/data-table/row-arrow.svelte +19 -16
  109. package/src/components/data-table/row-context-menu.svelte +52 -52
  110. package/src/components/data-table/sortable-head.svelte +25 -25
  111. package/src/components/data-table/table-skeleton.svelte +41 -36
  112. package/src/components/docs/doc-page.svelte +18 -9
  113. package/src/components/docs/on-this-page.svelte +16 -3
  114. package/src/components/docs/scroll-spy.svelte.ts +2 -3
  115. package/src/components/field-row/field-row.svelte +10 -8
  116. package/src/components/hold-to-confirm/hold-to-confirm.svelte +5 -1
  117. package/src/components/icon/icon.svelte +14 -14
  118. package/src/components/language-flag/language-flag.svelte +3 -1
  119. package/src/components/list-page/detail-empty.svelte +34 -34
  120. package/src/components/list-page/detail-header.svelte +55 -47
  121. package/src/components/list-page/detail-panel.svelte +51 -59
  122. package/src/components/list-page/detail-row.svelte +19 -19
  123. package/src/components/list-page/detail-stack.svelte +21 -21
  124. package/src/components/list-page/empty-state.svelte +30 -30
  125. package/src/components/list-page/filter-item.svelte +20 -20
  126. package/src/components/list-page/filter-popover.svelte +60 -55
  127. package/src/components/list-page/grouped-list.svelte +4 -2
  128. package/src/components/list-page/index.ts +5 -1
  129. package/src/components/list-page/list-page-shell.svelte +2 -5
  130. package/src/components/list-page/master-detail.svelte +87 -88
  131. package/src/components/list-page/notice-bar.svelte +38 -38
  132. package/src/components/list-page/prev-next.svelte +63 -63
  133. package/src/components/list-page/rail-body.svelte +18 -18
  134. package/src/components/list-page/rail-empty.svelte +23 -30
  135. package/src/components/list-page/rail-head.svelte +24 -24
  136. package/src/components/list-page/rail-row.svelte +77 -73
  137. package/src/components/list-page/section-host.ts +2 -3
  138. package/src/components/locale-switch/locale-switch.svelte +41 -41
  139. package/src/components/mark/mark.svelte +34 -38
  140. package/src/components/open-in/index.ts +1 -5
  141. package/src/components/open-in/open-in.svelte +40 -40
  142. package/src/components/picker/mark-picker.svelte +49 -49
  143. package/src/components/picker/picker.svelte +5 -3
  144. package/src/components/section-header/section-header.test.ts +7 -1
  145. package/src/components/section-header/section-label.svelte +20 -20
  146. package/src/components/segmented/animated-tabs.svelte +31 -2
  147. package/src/components/segmented/collapse.test.ts +2 -1
  148. package/src/components/segmented/index.ts +2 -0
  149. package/src/components/segmented/segment.svelte +52 -49
  150. package/src/components/segmented/segmented-filter.svelte +79 -80
  151. package/src/components/segmented/segmented-nav.svelte +34 -40
  152. package/src/components/segmented/segmented-overflow.svelte +53 -50
  153. package/src/components/segmented/segmented-track.svelte +62 -46
  154. package/src/components/segmented/segmented.ts +2 -3
  155. package/src/components/star-rating-input/star-rating-input.svelte +103 -103
  156. package/src/components/svg/svg.svelte +4 -4
  157. package/src/components/table-view/index.ts +1 -1
  158. package/src/components/table-view/table-view.svelte +352 -13
  159. package/src/components/table-view/table-view.test.ts +108 -1
  160. package/src/components/table-view/types.ts +22 -11
  161. package/src/components/tags-input/index.ts +13 -0
  162. package/src/components/tags-input/tags-input.svelte +305 -0
  163. package/src/components/tags-input/tags.test.ts +64 -0
  164. package/src/components/tags-input/tags.ts +142 -0
  165. package/src/components/theme/index.ts +2 -4
  166. package/src/components/theme/theme-provider.svelte +1 -3
  167. package/src/components/toolbar/toolbar-search.svelte +25 -25
  168. package/src/components/toolbar/toolbar.svelte +11 -7
  169. package/src/components/toolbar/toolbar.test.ts +10 -1
  170. package/src/design/index.css +21 -78
  171. package/src/design/polish.css +26 -35
  172. package/src/editor/editor-toolbar.svelte +104 -99
  173. package/src/editor/editor.svelte +296 -266
  174. package/src/editor/image-toolbar.svelte +20 -22
  175. package/src/editor/index.ts +1 -4
  176. package/src/editor/inline-editor.svelte +280 -238
  177. package/src/editor/markdown.ts +15 -15
  178. package/src/editor/renderer.svelte +71 -71
  179. package/src/editor/slash-menu.svelte +24 -27
  180. package/src/effects/animated-beam/diagrams/BeamSvg.svelte +1 -1
  181. package/src/effects/image-comparison/ImageComparison.svelte +11 -1
  182. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +59 -46
  183. package/src/effects/magic-card/MagicCard.css +14 -7
  184. package/src/effects/marquee/Marquee.svelte +3 -5
  185. package/src/effects/reveal/observe.ts +0 -3
  186. package/src/effects/spotlight/Spotlight.svelte +1 -1
  187. package/src/effects/theme-toggle/ThemeToggle.css +25 -22
  188. package/src/effects/theme-toggle/ThemeToggle.svelte +6 -7
  189. package/src/gallery.test.ts +14 -4
  190. package/src/icons/trades.ts +1 -1
  191. package/src/inbox/README.md +1 -3
  192. package/src/inbox/inbox.test.ts +181 -138
  193. package/src/inbox/index.ts +20 -20
  194. package/src/inbox/logic.ts +114 -111
  195. package/src/inbox/thread-inbox.svelte +227 -151
  196. package/src/inbox/types.ts +130 -108
  197. package/src/lib/theme-transition.ts +2 -4
  198. package/src/lib/tone.ts +2 -4
  199. package/src/lib/utils.ts +2 -2
  200. package/src/modules.test.ts +79 -69
  201. package/src/onboarding/define-tours.test.ts +55 -40
  202. package/src/onboarding/define-tours.ts +23 -23
  203. package/src/onboarding/demo.svelte +24 -5
  204. package/src/onboarding/index.ts +1 -8
  205. package/src/onboarding/onboarding-provider.svelte +19 -7
  206. package/src/onboarding/onboarding.svelte.ts +16 -16
  207. package/src/onboarding/pick.test.ts +61 -52
  208. package/src/onboarding/pick.ts +16 -13
  209. package/src/onboarding/stores.test.ts +80 -80
  210. package/src/onboarding/stores.ts +41 -39
  211. package/src/onboarding/tour-card.svelte +30 -5
  212. package/src/onboarding/tour-runner.svelte +12 -3
  213. package/src/onboarding/types.ts +20 -12
  214. package/src/onboarding/wait.ts +22 -22
  215. package/src/panel/side-panel.svelte +94 -93
  216. package/src/record/demo-data.ts +291 -225
  217. package/src/record/field-section.svelte +38 -34
  218. package/src/record/fields.ts +148 -96
  219. package/src/record/format.ts +63 -56
  220. package/src/record/index.ts +49 -35
  221. package/src/record/info-list.svelte +145 -114
  222. package/src/record/kinds.ts +31 -31
  223. package/src/record/logic.ts +216 -214
  224. package/src/record/next-up.svelte +45 -34
  225. package/src/record/project-list.svelte +56 -50
  226. package/src/record/record-demo.svelte +130 -110
  227. package/src/record/record-header.svelte +37 -28
  228. package/src/record/record-shell.svelte +54 -54
  229. package/src/record/record-stats.svelte +25 -25
  230. package/src/record/record-timeline.svelte +134 -114
  231. package/src/record/record.test.ts +430 -309
  232. package/src/record/steps-rail.svelte +225 -216
  233. package/src/record/types.ts +234 -215
  234. package/src/record/where-we-are.svelte +62 -58
  235. package/src/shadcn/button/button.svelte +1 -1
  236. package/src/shadcn/card/card-title.svelte +1 -5
  237. package/src/shadcn/hooks/use-highlighted-code.svelte.ts +2 -13
  238. package/src/shadcn/sheet/sheet-content.svelte +2 -4
  239. package/src/shadcn/table/table-cell.svelte +2 -3
  240. package/src/shadcn/utilities.css +3 -8
  241. package/src/slides/Controls.svelte +150 -124
  242. package/src/slides/Deck.svelte +243 -238
  243. package/src/slides/Fragment.svelte +64 -57
  244. package/src/slides/Overview.svelte +106 -101
  245. package/src/slides/Presenter.svelte +300 -266
  246. package/src/slides/Slide.svelte +185 -170
  247. package/src/slides/SlideList.svelte +16 -12
  248. package/src/slides/ambient.d.ts +1 -1
  249. package/src/slides/bridges.ts +42 -23
  250. package/src/slides/deck-controller.svelte.ts +47 -35
  251. package/src/slides/demo-slides.svelte +330 -306
  252. package/src/slides/demo.svelte +164 -160
  253. package/src/slides/export/capture.ts +20 -16
  254. package/src/slides/export/ffmpeg.ts +43 -38
  255. package/src/slides/export/index.ts +5 -5
  256. package/src/slides/export/launch.ts +3 -1
  257. package/src/slides/export/pdf.ts +30 -27
  258. package/src/slides/export/types.ts +10 -8
  259. package/src/slides/index.ts +11 -11
  260. package/src/slides/labels.ts +22 -19
  261. package/src/slides/layout.ts +9 -2
  262. package/src/slides/model.ts +4 -4
  263. package/src/slides/navigation.ts +43 -31
  264. package/src/slides/theme.css +5 -7
  265. package/src/slides/types.ts +9 -10
  266. package/src/status/define-statuses.ts +17 -24
  267. package/src/status/index.ts +5 -5
  268. package/src/status/status-badge.svelte +3 -1
  269. package/src/status/status-dot.svelte +1 -3
  270. package/src/status/status-icon.svelte +1 -1
  271. package/src/status/status-types.ts +21 -21
  272. package/src/tasks/board-order.ts +2 -4
  273. package/src/tasks/demo.svelte +189 -31
  274. package/src/tasks/detail/detail-demo.svelte +73 -13
  275. package/src/tasks/detail/detail-types.ts +45 -45
  276. package/src/tasks/detail/task-detail.svelte +40 -10
  277. package/src/tasks/detail/task-property.svelte +8 -2
  278. package/src/tasks/detail/task-thread.svelte +53 -12
  279. package/src/tasks/gantt/gantt-bar.svelte +17 -6
  280. package/src/tasks/gantt/gantt-grid.svelte +40 -9
  281. package/src/tasks/gantt/gantt-group-row.svelte +8 -2
  282. package/src/tasks/gantt/gantt-header.svelte +9 -3
  283. package/src/tasks/gantt/gantt-task-row.svelte +26 -6
  284. package/src/tasks/gantt/gantt.css +2 -1
  285. package/src/tasks/gantt/gantt.test.ts +3 -1
  286. package/src/tasks/gantt/logic.test.ts +110 -20
  287. package/src/tasks/gantt/logic.ts +42 -6
  288. package/src/tasks/inbox/inbox-card.svelte +13 -5
  289. package/src/tasks/inbox/inbox-draft.svelte +1 -1
  290. package/src/tasks/inbox/inbox-logic.ts +6 -40
  291. package/src/tasks/inbox/inbox-statuses.ts +23 -23
  292. package/src/tasks/inbox/inbox-trace.svelte +9 -3
  293. package/src/tasks/inbox/inbox-types.ts +54 -58
  294. package/src/tasks/inbox/index.ts +22 -21
  295. package/src/tasks/index.ts +49 -52
  296. package/src/tasks/kanban-board.svelte +16 -12
  297. package/src/tasks/kanban-types.ts +2 -3
  298. package/src/tasks/presets/sales.ts +6 -6
  299. package/src/tasks/task-gantt.svelte +28 -8
  300. package/src/tasks/task-list.svelte +41 -17
  301. package/src/tasks/time/duration.ts +40 -40
  302. package/src/tasks/time/time-chip.svelte +9 -2
  303. package/src/tasks/time/time-dot.svelte +1 -1
  304. package/src/tasks/time/time-ring.svelte +11 -3
  305. package/src/tasks/time/time-tracker.svelte +57 -15
  306. package/src/tasks/time/time-types.ts +57 -57
  307. package/src/tasks/use-kanban-dnd.svelte.ts +2 -3
  308. package/src/thread/index.ts +7 -7
  309. package/src/thread/logic.ts +36 -33
  310. package/src/thread/thread.svelte +82 -16
  311. package/src/thread/thread.test.ts +58 -47
  312. package/src/thread/types.ts +68 -68
  313. package/src/tokens/craft.css +17 -53
  314. package/src/tokens/index.css +3 -13
  315. package/src/tokens/palette.css +13 -11
  316. package/src/tokens/primitives.css +10 -23
  317. package/src/tasks/inbox/inbox.svelte +0 -203
@@ -20,9 +20,24 @@
20
20
  route: "/demo/board",
21
21
  kind: "steps",
22
22
  steps: [
23
- { target: '[data-tour="demo-new"]', title: "Start here", body: "A new project takes a name and a client. That's all.", placement: "bottom-end" },
24
- { target: '[data-tour="demo-filter"]', title: "Narrow it down", body: "Late shows what slipped. Mine shows what's yours.", placement: "bottom-start" },
25
- { target: '[data-tour="demo-list"]', title: "Open a row", body: "Click a project to see its tasks, files and notes.", placement: "top" },
23
+ {
24
+ target: '[data-tour="demo-new"]',
25
+ title: "Start here",
26
+ body: "A new project takes a name and a client. That's all.",
27
+ placement: "bottom-end",
28
+ },
29
+ {
30
+ target: '[data-tour="demo-filter"]',
31
+ title: "Narrow it down",
32
+ body: "Late shows what slipped. Mine shows what's yours.",
33
+ placement: "bottom-start",
34
+ },
35
+ {
36
+ target: '[data-tour="demo-list"]',
37
+ title: "Open a row",
38
+ body: "Click a project to see its tasks, files and notes.",
39
+ placement: "top",
40
+ },
26
41
  ],
27
42
  },
28
43
  ]);
@@ -55,8 +70,12 @@
55
70
 
56
71
  <div class="flex flex-col gap-4">
57
72
  <div class="flex flex-wrap gap-2">
58
- <Button variant="outline" size="sm" onclick={() => start("/demo/board")}>Show the page tour</Button>
59
- <Button variant="outline" size="sm" onclick={() => start("/demo/intro")}>Show the intro card</Button>
73
+ <Button variant="outline" size="sm" onclick={() => start("/demo/board")}
74
+ >Show the page tour</Button
75
+ >
76
+ <Button variant="outline" size="sm" onclick={() => start("/demo/intro")}
77
+ >Show the intro card</Button
78
+ >
60
79
  </div>
61
80
 
62
81
  <div class="bg-card ring-foreground/10 flex flex-col gap-4 rounded-xl p-4 ring-1">
@@ -2,11 +2,4 @@ export { default as OnboardingProvider } from "./onboarding-provider.svelte";
2
2
  export { onboarding } from "./onboarding.svelte.js";
3
3
  export { defineTours } from "./define-tours.js";
4
4
  export { localStorageStore, memoryStore } from "./stores.js";
5
- export type {
6
- Outcome,
7
- Placement,
8
- SeenStore,
9
- Step,
10
- Strings,
11
- Tour,
12
- } from "./types.js";
5
+ export type { Outcome, Placement, SeenStore, Step, Strings, Tour } from "./types.js";
@@ -2,7 +2,13 @@
2
2
  import { tryCatchAsync } from "../lib/result.js";
3
3
  import { untrack } from "svelte";
4
4
  import { onboarding } from "./onboarding.svelte.js";
5
- import { keepFoundSteps, pickForReplay, pickTour, toursForRoute, type SeenMap } from "./pick.js";
5
+ import {
6
+ keepFoundSteps,
7
+ pickForReplay,
8
+ pickTour,
9
+ toursForRoute,
10
+ type SeenMap,
11
+ } from "./pick.js";
6
12
  import TourRunner from "./tour-runner.svelte";
7
13
  import type { Outcome, SeenStore, Step, Strings, Tour } from "./types.js";
8
14
  import { waitForElement } from "./wait.js";
@@ -71,17 +77,23 @@
71
77
  controller = mine;
72
78
  void loadSeen().then((map) => {
73
79
  if (!map || mine.signal.aborted) return;
74
- const tour = pickTour(untrack(() => tours), id, map);
80
+ const tour = pickTour(
81
+ untrack(() => tours),
82
+ id,
83
+ map,
84
+ );
75
85
  if (tour) void launch(tour, false, mine.signal);
76
86
  });
77
87
  return () => mine.abort();
78
88
  });
79
89
 
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
- }));
90
+ $effect(() =>
91
+ onboarding.bind(async (tourId) => {
92
+ const tour = pickForReplay(tours, routeId, tourId);
93
+ if (!tour || !controller || controller.signal.aborted) return false;
94
+ return launch(tour, true, controller.signal);
95
+ }),
96
+ );
85
97
 
86
98
  $effect(() => {
87
99
  onboarding.running = active !== null;
@@ -1,24 +1,24 @@
1
1
  type Replay = (tourId?: string) => Promise<boolean>;
2
2
 
3
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;
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
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
- }
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
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
- }
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
22
  }
23
23
 
24
24
  export const onboarding = new Onboarding();
@@ -1,79 +1,88 @@
1
1
  import { describe, expect, it } from "vite-plus/test";
2
- import { isSeen, keepFoundSteps, pickForReplay, pickTour, toursForRoute } from "./pick.js";
2
+ import {
3
+ isSeen,
4
+ keepFoundSteps,
5
+ pickForReplay,
6
+ pickTour,
7
+ toursForRoute,
8
+ } from "./pick.js";
3
9
  import type { Tour } from "./types.js";
4
10
 
5
11
  const step = { target: "#a", title: "A", body: "a" };
6
12
  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] },
13
+ { id: "home", route: "/(app)", kind: "intro", title: "Hi", body: "Welcome." },
14
+ { id: "board", route: "/(app)/projects", kind: "steps", steps: [step] },
15
+ { id: "board-extra", route: "/(app)/projects", kind: "steps", steps: [step], version: 2 },
16
+ { id: "record", route: "/(app)/projects/[id]", kind: "steps", steps: [step] },
11
17
  ];
12
18
 
13
19
  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
+ it("counts a tour as seen once the stored version reaches its own", () => {
21
+ expect(isSeen(tours[1], {})).toBe(false);
22
+ expect(isSeen(tours[1], { board: 1 })).toBe(true);
23
+ expect(isSeen(tours[2], { "board-extra": 1 })).toBe(false);
24
+ expect(isSeen(tours[2], { "board-extra": 2 })).toBe(true);
25
+ });
20
26
 
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
+ it("shows a tour again after its version is bumped", () => {
28
+ const v1: Tour = { id: "t", route: "/", kind: "steps", steps: [step] };
29
+ const v2: Tour = { ...v1, version: 2 };
30
+ expect(isSeen(v1, { t: 1 })).toBe(true);
31
+ expect(isSeen(v2, { t: 1 })).toBe(false);
32
+ });
27
33
  });
28
34
 
29
35
  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
- });
36
+ it("matches the route id exactly, so a params route never borrows its parent's tour", () => {
37
+ expect(pickTour(tours, "/(app)", {})?.id).toBe("home");
38
+ expect(pickTour(tours, "/(app)/projects/[id]", {})?.id).toBe("record");
39
+ expect(pickTour(tours, "/(app)/settings", {})).toBeUndefined();
40
+ });
35
41
 
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
- });
42
+ it("takes the first unseen tour on a route, one at a time", () => {
43
+ expect(pickTour(tours, "/(app)/projects", {})?.id).toBe("board");
44
+ expect(pickTour(tours, "/(app)/projects", { board: 1 })?.id).toBe("board-extra");
45
+ expect(pickTour(tours, "/(app)/projects", { board: 1, "board-extra": 2 })).toBeUndefined();
46
+ });
41
47
 
42
- it("picks nothing without a route", () => {
43
- expect(pickTour(tours, null, {})).toBeUndefined();
44
- expect(pickTour(tours, undefined, {})).toBeUndefined();
45
- });
48
+ it("picks nothing without a route", () => {
49
+ expect(pickTour(tours, null, {})).toBeUndefined();
50
+ expect(pickTour(tours, undefined, {})).toBeUndefined();
51
+ });
46
52
  });
47
53
 
48
54
  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
- });
55
+ it("lists every tour on the route in definition order", () => {
56
+ expect(toursForRoute(tours, "/(app)/projects").map((t) => t.id)).toEqual([
57
+ "board",
58
+ "board-extra",
59
+ ]);
60
+ });
52
61
  });
53
62
 
54
63
  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
- });
64
+ it("restarts the page's first tour even when it was seen", () => {
65
+ expect(pickForReplay(tours, "/(app)/projects")?.id).toBe("board");
66
+ });
58
67
 
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
- });
68
+ it("finds a tour by id on any route", () => {
69
+ expect(pickForReplay(tours, "/(app)", "record")?.id).toBe("record");
70
+ expect(pickForReplay(tours, "/(app)", "missing")).toBeUndefined();
71
+ });
63
72
  });
64
73
 
65
74
  describe("keepFoundSteps", () => {
66
- const steps = [
67
- { ...step, target: "#a" },
68
- { ...step, target: "#b" },
69
- { ...step, target: "#c" },
70
- ];
75
+ const steps = [
76
+ { ...step, target: "#a" },
77
+ { ...step, target: "#b" },
78
+ { ...step, target: "#c" },
79
+ ];
71
80
 
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
- });
81
+ it("drops the steps whose target isn't on the page", () => {
82
+ expect(keepFoundSteps(steps, (s) => s !== "#b").map((s) => s.target)).toEqual(["#a", "#c"]);
83
+ });
75
84
 
76
- it("returns nothing when no target is found, so no empty spotlight starts", () => {
77
- expect(keepFoundSteps(steps, () => false)).toEqual([]);
78
- });
85
+ it("returns nothing when no target is found, so no empty spotlight starts", () => {
86
+ expect(keepFoundSteps(steps, () => false)).toEqual([]);
87
+ });
79
88
  });
@@ -4,33 +4,36 @@ export type SeenMap = Record<string, number>;
4
4
 
5
5
  /** The seen version has to reach the tour's own, so bumping `version` shows it again. */
6
6
  export function isSeen(tour: Tour, seen: SeenMap): boolean {
7
- return (seen[tour.id] ?? 0) >= (tour.version ?? 1);
7
+ return (seen[tour.id] ?? 0) >= (tour.version ?? 1);
8
8
  }
9
9
 
10
10
  export function toursForRoute(tours: readonly Tour[], routeId: string | null | undefined): Tour[] {
11
- return routeId ? tours.filter((tour) => tour.route === routeId) : [];
11
+ return routeId ? tours.filter((tour) => tour.route === routeId) : [];
12
12
  }
13
13
 
14
14
  /** The first unseen tour on a route, in the order they were defined. One per page view. */
15
15
  export function pickTour(
16
- tours: readonly Tour[],
17
- routeId: string | null | undefined,
18
- seen: SeenMap,
16
+ tours: readonly Tour[],
17
+ routeId: string | null | undefined,
18
+ seen: SeenMap,
19
19
  ): Tour | undefined {
20
- return toursForRoute(tours, routeId).find((tour) => !isSeen(tour, seen));
20
+ return toursForRoute(tours, routeId).find((tour) => !isSeen(tour, seen));
21
21
  }
22
22
 
23
23
  /** What replay starts: a named tour, or the page's first one whether or not it was seen. */
24
24
  export function pickForReplay(
25
- tours: readonly Tour[],
26
- routeId: string | null | undefined,
27
- tourId?: string,
25
+ tours: readonly Tour[],
26
+ routeId: string | null | undefined,
27
+ tourId?: string,
28
28
  ): Tour | undefined {
29
- if (tourId) return tours.find((tour) => tour.id === tourId);
30
- return toursForRoute(tours, routeId)[0];
29
+ if (tourId) return tours.find((tour) => tour.id === tourId);
30
+ return toursForRoute(tours, routeId)[0];
31
31
  }
32
32
 
33
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));
34
+ export function keepFoundSteps(
35
+ steps: readonly Step[],
36
+ exists: (selector: string) => boolean,
37
+ ): Step[] {
38
+ return steps.filter((step) => exists(step.target));
36
39
  }
@@ -3,101 +3,101 @@ import { localStorageStore, memoryStore } from "./stores.js";
3
3
  import type { SeenStore } from "./types.js";
4
4
 
5
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
- };
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
17
  }
18
18
 
19
19
  function behavesLikeAStore(make: () => SeenStore) {
20
- it("starts empty", async () => {
21
- expect(await make().load()).toEqual({});
22
- });
20
+ it("starts empty", async () => {
21
+ expect(await make().load()).toEqual({});
22
+ });
23
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
- });
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
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
- });
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
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
- });
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
47
  }
48
48
 
49
49
  describe("memoryStore", () => {
50
- behavesLikeAStore(() => memoryStore());
50
+ behavesLikeAStore(() => memoryStore());
51
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
- });
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
59
  });
60
60
 
61
61
  describe("localStorageStore", () => {
62
- beforeEach(() => {
63
- vi.stubGlobal("localStorage", fakeStorage());
64
- });
65
- afterEach(() => {
66
- vi.unstubAllGlobals();
67
- });
62
+ beforeEach(() => {
63
+ vi.stubGlobal("localStorage", fakeStorage());
64
+ });
65
+ afterEach(() => {
66
+ vi.unstubAllGlobals();
67
+ });
68
68
 
69
- behavesLikeAStore(() => localStorageStore());
69
+ behavesLikeAStore(() => localStorageStore());
70
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
- });
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
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
- });
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
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
- });
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
103
  });
@@ -5,54 +5,56 @@ type Seen = Record<string, number>;
5
5
 
6
6
  /** Holds seen-state in memory. Tests and previews. */
7
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
- };
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
21
  }
22
22
 
23
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
- );
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(
29
+ (entry): entry is [string, number] => typeof entry[1] === "number",
30
+ ),
31
+ );
30
32
  }
31
33
 
32
34
  // Blocked storage (private mode, a full disk) means the tour shows again next visit, which beats an error.
33
35
  function writeSeen(key: string, seen: Seen): void {
34
- tryCatch(() => localStorage.setItem(key, JSON.stringify(seen)));
36
+ tryCatch(() => localStorage.setItem(key, JSON.stringify(seen)));
35
37
  }
36
38
 
37
39
  /** Keeps seen-state in this browser. Demos and signed-out pages. */
38
40
  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
- };
41
+ return {
42
+ async load() {
43
+ return readSeen(key);
44
+ },
45
+ async markSeen(tourId, version, _outcome: Outcome) {
46
+ const seen = readSeen(key);
47
+ seen[tourId] = Math.max(seen[tourId] ?? 0, version);
48
+ writeSeen(key, seen);
49
+ },
50
+ async reset(tourId) {
51
+ if (tourId === undefined) {
52
+ tryCatch(() => localStorage.removeItem(key));
53
+ return;
54
+ }
55
+ const seen = readSeen(key);
56
+ delete seen[tourId];
57
+ writeSeen(key, seen);
58
+ },
59
+ };
58
60
  }