@heyramzi/ui 2.0.1 → 2.2.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 (341) hide show
  1. package/package.json +48 -24
  2. package/scripts/sync-shadcn.mjs +3 -4
  3. package/src/agents/agent-avatar.svelte +604 -575
  4. package/src/agents/agent-cast.svelte +179 -183
  5. package/src/agents/demo.svelte +125 -113
  6. package/src/agents/helpers.ts +23 -17
  7. package/src/agents/index.ts +4 -4
  8. package/src/agents/page-state.ts +6 -6
  9. package/src/agents/types.ts +6 -6
  10. package/src/ai/ai-button/AiButton.svelte +4 -12
  11. package/src/ai/generation-progress/GenerationProgress.svelte +10 -3
  12. package/src/ai/prompt-input/PromptInputTextarea.svelte +1 -2
  13. package/src/ai/response/Response.css +2 -3
  14. package/src/animations/avatar-group-hover.css +2 -16
  15. package/src/animations/card-tilt.css +3 -6
  16. package/src/animations/digit-pop-in.css +2 -15
  17. package/src/animations/dropdown.css +4 -17
  18. package/src/animations/input-clear-dissolve.css +5 -18
  19. package/src/animations/input-shake.css +9 -41
  20. package/src/animations/learn-more-hover.css +17 -9
  21. package/src/animations/like-button.css +2 -4
  22. package/src/animations/page-side-by-side.css +2 -10
  23. package/src/animations/plus-menu-morph.css +2 -5
  24. package/src/animations/shimmer-text.css +5 -18
  25. package/src/animations/spinning-counter.css +2 -6
  26. package/src/animations/success-check.css +2 -15
  27. package/src/animations/thinking-states.css +2 -7
  28. package/src/brand/brand.test.ts +21 -5
  29. package/src/brand/index.ts +4 -10
  30. package/src/canvas/README.md +9 -12
  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/svg-figure.test.ts +5 -1
  49. package/src/canvas/lod/lod-switch.svelte +111 -106
  50. package/src/canvas/pen/PenField.svelte +87 -81
  51. package/src/canvas/pen/PenOverlay.svelte +127 -127
  52. package/src/canvas/pen/ShapeNode.svelte +31 -32
  53. package/src/canvas/pen/TextNode.svelte +54 -53
  54. package/src/canvas/pen/host-field.test.svelte +7 -7
  55. package/src/canvas/pen/shapes.ts +7 -1
  56. package/src/canvas/screen/GlyphIcon.svelte +22 -17
  57. package/src/canvas/screen/PartView.svelte +138 -72
  58. package/src/canvas/screen/ScreenBody.svelte +81 -60
  59. package/src/canvas/screen/ScreenCard.svelte +20 -20
  60. package/src/canvas/ui/CanvasToolbar.svelte +173 -85
  61. package/src/canvas/ui/FrameLayer.svelte +59 -59
  62. package/src/canvas/ui/PenLayer.svelte +39 -30
  63. package/src/canvas/ui/StylePanel.svelte +119 -102
  64. package/src/canvas/ui/canvas-grid.svelte +137 -129
  65. package/src/charts/animated-value.svelte +1 -1
  66. package/src/charts/area-chart.css +2 -5
  67. package/src/charts/area-chart.svelte +1 -1
  68. package/src/charts/bar-chart.css +2 -3
  69. package/src/charts/bar-chart.svelte +1 -1
  70. package/src/charts/charts.css +5 -17
  71. package/src/charts/crosshair.svelte +1 -1
  72. package/src/charts/dashboard/dashboard-empty.svelte +25 -25
  73. package/src/charts/dashboard/dashboard-section.svelte +24 -24
  74. package/src/charts/dashboard/data-panel.svelte +50 -50
  75. package/src/charts/dashboard/data-sort-head.svelte +56 -56
  76. package/src/charts/dashboard/data-table.svelte +138 -138
  77. package/src/charts/dashboard/panel-grid.svelte +30 -30
  78. package/src/charts/dashboard/period-filter.svelte +25 -25
  79. package/src/charts/dashboard/rating-chip.svelte +19 -19
  80. package/src/charts/dashboard/stat-band.svelte +32 -32
  81. package/src/charts/dashboard/stat-tile.svelte +106 -99
  82. package/src/charts/donut-chart.svelte +1 -1
  83. package/src/charts/edge-fade.svelte +1 -1
  84. package/src/charts/figure.svelte +1 -1
  85. package/src/charts/gauge-chart.css +4 -7
  86. package/src/charts/gauge-chart.svelte +1 -1
  87. package/src/charts/hover-focus.svelte +1 -1
  88. package/src/charts/legend.css +1 -3
  89. package/src/charts/legend.svelte +1 -1
  90. package/src/charts/line-chart.svelte +1 -1
  91. package/src/charts/number-chart.css +6 -9
  92. package/src/charts/number-chart.svelte +1 -1
  93. package/src/charts/reference-line.svelte +19 -19
  94. package/src/charts/reveal.svelte +1 -1
  95. package/src/charts/ring-chart.css +2 -4
  96. package/src/charts/ring-chart.svelte +1 -1
  97. package/src/charts/sparkline.svelte +5 -2
  98. package/src/charts/stat-card.svelte +1 -1
  99. package/src/charts/time-series-chart.css +2 -5
  100. package/src/charts/trend-pill.css +2 -4
  101. package/src/charts/trend-pill.svelte +1 -1
  102. package/src/components/copy/copy-button.svelte +93 -98
  103. package/src/components/copy/copy-button.test.ts +10 -2
  104. package/src/components/data-table/infinite-scroll.svelte +42 -42
  105. package/src/components/data-table/pagination.svelte +34 -22
  106. package/src/components/data-table/row-arrow.svelte +19 -16
  107. package/src/components/data-table/row-context-menu.svelte +52 -52
  108. package/src/components/data-table/sortable-head.svelte +25 -25
  109. package/src/components/data-table/table-skeleton.svelte +41 -36
  110. package/src/components/docs/doc-page.svelte +18 -9
  111. package/src/components/docs/on-this-page.svelte +16 -3
  112. package/src/components/docs/scroll-spy.svelte.ts +2 -3
  113. package/src/components/field-row/field-row.svelte +10 -8
  114. package/src/components/hold-to-confirm/hold-to-confirm.svelte +5 -1
  115. package/src/components/icon/icon.svelte +14 -14
  116. package/src/components/language-flag/language-flag.svelte +3 -1
  117. package/src/components/list-page/detail-empty.svelte +34 -34
  118. package/src/components/list-page/detail-header.svelte +55 -47
  119. package/src/components/list-page/detail-panel.svelte +51 -59
  120. package/src/components/list-page/detail-row.svelte +19 -19
  121. package/src/components/list-page/detail-stack.svelte +21 -21
  122. package/src/components/list-page/empty-state.svelte +30 -30
  123. package/src/components/list-page/filter-item.svelte +20 -20
  124. package/src/components/list-page/filter-popover.svelte +60 -55
  125. package/src/components/list-page/grouped-list.svelte +4 -2
  126. package/src/components/list-page/index.ts +8 -4
  127. package/src/components/list-page/list-page-shell.svelte +2 -5
  128. package/src/components/list-page/master-detail.svelte +87 -88
  129. package/src/components/list-page/notice-bar.svelte +38 -38
  130. package/src/components/list-page/prev-next.svelte +63 -63
  131. package/src/components/list-page/rail-body.svelte +18 -18
  132. package/src/components/list-page/rail-empty.svelte +23 -30
  133. package/src/components/list-page/rail-head.svelte +24 -24
  134. package/src/components/list-page/rail-row.svelte +77 -73
  135. package/src/components/list-page/section-host.ts +2 -3
  136. package/src/components/locale-switch/locale-switch.svelte +41 -41
  137. package/src/components/mark/mark.svelte +34 -38
  138. package/src/components/open-in/index.ts +1 -5
  139. package/src/components/open-in/open-in.svelte +40 -40
  140. package/src/components/picker/mark-picker.svelte +49 -49
  141. package/src/components/picker/picker.svelte +5 -3
  142. package/src/components/section-header/index.ts +1 -0
  143. package/src/components/section-header/section-header.svelte +1 -1
  144. package/src/components/section-header/section-header.test.ts +7 -1
  145. package/src/components/section-header/section-label.svelte +39 -0
  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/table-view.svelte +17 -2
  158. package/src/components/table-view/table-view.test.ts +7 -1
  159. package/src/components/table-view/types.ts +11 -11
  160. package/src/components/tags-input/index.ts +13 -0
  161. package/src/components/tags-input/tags-input.svelte +305 -0
  162. package/src/components/tags-input/tags.test.ts +64 -0
  163. package/src/components/tags-input/tags.ts +142 -0
  164. package/src/components/theme/index.ts +2 -4
  165. package/src/components/theme/theme-provider.svelte +1 -3
  166. package/src/components/toolbar/index.ts +2 -0
  167. package/src/components/{list-page → toolbar}/toolbar-search.svelte +25 -25
  168. package/src/components/toolbar/toolbar.svelte +13 -9
  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/inbox/README.md +27 -0
  191. package/src/inbox/inbox.test.ts +214 -0
  192. package/src/inbox/index.ts +31 -0
  193. package/src/inbox/logic.ts +169 -0
  194. package/src/inbox/thread-inbox.svelte +439 -0
  195. package/src/inbox/types.ts +161 -0
  196. package/src/lib/theme-transition.ts +2 -4
  197. package/src/lib/tone.ts +2 -4
  198. package/src/lib/utils.ts +2 -2
  199. package/src/modules.test.ts +105 -0
  200. package/src/onboarding/define-tours.test.ts +55 -40
  201. package/src/onboarding/define-tours.ts +23 -23
  202. package/src/onboarding/demo.svelte +24 -5
  203. package/src/onboarding/index.ts +1 -8
  204. package/src/onboarding/onboarding-provider.svelte +19 -7
  205. package/src/onboarding/onboarding.svelte.ts +16 -16
  206. package/src/onboarding/pick.test.ts +61 -52
  207. package/src/onboarding/pick.ts +16 -13
  208. package/src/onboarding/stores.test.ts +80 -80
  209. package/src/onboarding/stores.ts +41 -39
  210. package/src/onboarding/tour-card.svelte +30 -5
  211. package/src/onboarding/tour-runner.svelte +12 -3
  212. package/src/onboarding/types.ts +20 -12
  213. package/src/onboarding/wait.ts +22 -22
  214. package/src/panel/README.md +5 -0
  215. package/src/panel/index.ts +2 -0
  216. package/src/panel/side-panel.svelte +194 -0
  217. package/src/record/README.md +3 -0
  218. package/src/record/demo-data.ts +352 -0
  219. package/src/record/field-section.svelte +57 -0
  220. package/src/record/fields.ts +194 -0
  221. package/src/record/format.ts +94 -0
  222. package/src/{tasks/record → record}/index.ts +49 -35
  223. package/src/{tasks/record → record}/info-list.svelte +145 -114
  224. package/src/record/kinds.ts +42 -0
  225. package/src/record/logic.ts +326 -0
  226. package/src/record/next-up.svelte +81 -0
  227. package/src/{tasks/record → record}/project-list.svelte +56 -50
  228. package/src/{tasks/record → record}/record-demo.svelte +130 -110
  229. package/src/{tasks/record → record}/record-header.svelte +37 -28
  230. package/src/{tasks/record → record}/record-shell.svelte +54 -54
  231. package/src/record/record-stats.svelte +48 -0
  232. package/src/record/record-timeline.svelte +251 -0
  233. package/src/record/record.test.ts +471 -0
  234. package/src/{tasks/record → record}/steps-rail.svelte +225 -216
  235. package/src/record/types.ts +299 -0
  236. package/src/record/where-we-are.svelte +125 -0
  237. package/src/shadcn/button/button.svelte +1 -1
  238. package/src/shadcn/card/card-title.svelte +1 -5
  239. package/src/shadcn/hooks/use-highlighted-code.svelte.ts +2 -13
  240. package/src/shadcn/sheet/sheet-content.svelte +2 -4
  241. package/src/shadcn/table/table-cell.svelte +2 -3
  242. package/src/shadcn/utilities.css +3 -8
  243. package/src/slides/Controls.svelte +150 -124
  244. package/src/slides/Deck.svelte +243 -238
  245. package/src/slides/Fragment.svelte +64 -57
  246. package/src/slides/Overview.svelte +106 -101
  247. package/src/slides/Presenter.svelte +300 -266
  248. package/src/slides/Slide.svelte +185 -170
  249. package/src/slides/SlideList.svelte +16 -12
  250. package/src/slides/ambient.d.ts +1 -1
  251. package/src/slides/bridges.ts +42 -23
  252. package/src/slides/deck-controller.svelte.ts +47 -35
  253. package/src/slides/demo-slides.svelte +330 -306
  254. package/src/slides/demo.svelte +164 -160
  255. package/src/slides/export/capture.ts +20 -16
  256. package/src/slides/export/ffmpeg.ts +43 -38
  257. package/src/slides/export/index.ts +5 -5
  258. package/src/slides/export/launch.ts +3 -1
  259. package/src/slides/export/pdf.ts +30 -27
  260. package/src/slides/export/types.ts +10 -8
  261. package/src/slides/index.ts +11 -11
  262. package/src/slides/labels.ts +22 -19
  263. package/src/slides/layout.ts +9 -2
  264. package/src/slides/model.ts +4 -4
  265. package/src/slides/navigation.ts +43 -31
  266. package/src/slides/theme.css +5 -7
  267. package/src/slides/types.ts +9 -10
  268. package/src/status/README.md +18 -0
  269. package/src/status/define-statuses.ts +25 -0
  270. package/src/status/index.ts +11 -0
  271. package/src/{tasks → status}/status-badge.svelte +3 -1
  272. package/src/{tasks → status}/status-dot.svelte +1 -3
  273. package/src/{tasks → status}/status-icon.svelte +1 -1
  274. package/src/status/status-types.ts +40 -0
  275. package/src/tasks/README.md +1 -1
  276. package/src/tasks/board-order.ts +2 -4
  277. package/src/tasks/demo.svelte +192 -34
  278. package/src/tasks/detail/detail-demo.svelte +75 -15
  279. package/src/tasks/detail/detail-types.ts +45 -45
  280. package/src/tasks/detail/task-detail.svelte +40 -10
  281. package/src/tasks/detail/task-property.svelte +8 -2
  282. package/src/tasks/detail/task-thread.svelte +53 -12
  283. package/src/tasks/gantt/gantt-bar.svelte +17 -6
  284. package/src/tasks/gantt/gantt-grid.svelte +40 -9
  285. package/src/tasks/gantt/gantt-group-row.svelte +8 -2
  286. package/src/tasks/gantt/gantt-header.svelte +9 -3
  287. package/src/tasks/gantt/gantt-task-row.svelte +26 -6
  288. package/src/tasks/gantt/gantt.css +2 -1
  289. package/src/tasks/gantt/gantt.test.ts +4 -2
  290. package/src/tasks/gantt/logic.test.ts +110 -20
  291. package/src/tasks/gantt/logic.ts +42 -6
  292. package/src/tasks/inbox/inbox-card.svelte +15 -7
  293. package/src/tasks/inbox/inbox-draft.svelte +1 -1
  294. package/src/tasks/inbox/inbox-logic.ts +6 -40
  295. package/src/tasks/inbox/inbox-statuses.ts +24 -24
  296. package/src/tasks/inbox/inbox-trace.svelte +9 -3
  297. package/src/tasks/inbox/inbox-types.ts +54 -58
  298. package/src/tasks/inbox/index.ts +22 -21
  299. package/src/tasks/index.ts +52 -53
  300. package/src/tasks/kanban-board.svelte +16 -12
  301. package/src/tasks/kanban-types.ts +2 -3
  302. package/src/tasks/presets/sales.ts +7 -7
  303. package/src/tasks/task-gantt.svelte +28 -8
  304. package/src/tasks/task-list.svelte +41 -17
  305. package/src/tasks/time/duration.ts +40 -40
  306. package/src/tasks/time/time-chip.svelte +9 -2
  307. package/src/tasks/time/time-dot.svelte +1 -1
  308. package/src/tasks/time/time-ring.svelte +11 -3
  309. package/src/tasks/time/time-tracker.svelte +57 -15
  310. package/src/tasks/time/time-types.ts +57 -57
  311. package/src/tasks/use-kanban-dnd.svelte.ts +2 -3
  312. package/src/thread/README.md +31 -0
  313. package/src/thread/index.ts +12 -0
  314. package/src/thread/logic.ts +61 -0
  315. package/src/thread/thread.svelte +432 -0
  316. package/src/thread/thread.test.ts +73 -0
  317. package/src/thread/types.ts +93 -0
  318. package/src/tokens/craft.css +17 -53
  319. package/src/tokens/index.css +3 -13
  320. package/src/tokens/palette.css +13 -11
  321. package/src/tokens/primitives.css +10 -23
  322. package/src/components/list-page/section-label.svelte +0 -39
  323. package/src/tasks/define-statuses.ts +0 -32
  324. package/src/tasks/inbox/inbox.svelte +0 -203
  325. package/src/tasks/record/demo-data.ts +0 -286
  326. package/src/tasks/record/field-section.svelte +0 -53
  327. package/src/tasks/record/fields.ts +0 -142
  328. package/src/tasks/record/format.ts +0 -87
  329. package/src/tasks/record/kinds.ts +0 -42
  330. package/src/tasks/record/logic.ts +0 -324
  331. package/src/tasks/record/next-up.svelte +0 -70
  332. package/src/tasks/record/record-stats.svelte +0 -48
  333. package/src/tasks/record/record-timeline.svelte +0 -231
  334. package/src/tasks/record/record.test.ts +0 -350
  335. package/src/tasks/record/types.ts +0 -280
  336. package/src/tasks/record/where-we-are.svelte +0 -121
  337. package/src/tasks/side-panel.svelte +0 -193
  338. package/src/tasks/status-types.ts +0 -40
  339. /package/src/components/{list-page → toolbar}/toolbar-trigger.ts +0 -0
  340. /package/src/{tasks → panel}/side-panel-types.ts +0 -0
  341. /package/src/{tasks/record → record}/gutter.ts +0 -0
@@ -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
  }
@@ -40,12 +40,28 @@
40
40
  </script>
41
41
 
42
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" />
43
+ <img
44
+ src={image}
45
+ alt=""
46
+ class="aspect-video w-full rounded-t-[inherit] object-cover outline -outline-offset-1 outline-black/10 dark:outline-white/10"
47
+ />
44
48
  {/if}
45
49
  <div class="flex flex-col gap-1.5 p-4 pb-3">
46
50
  <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}>
51
+ <h2
52
+ {...titleAttrs}
53
+ class={["font-semibold leading-snug text-balance", intro ? "text-base" : "text-sm"]}
54
+ >
55
+ {title}
56
+ </h2>
57
+ <Button
58
+ onclick={onclose}
59
+ variant="ghost"
60
+ size="sm"
61
+ icon
62
+ class="-mt-1 -mr-1.5 text-muted-foreground"
63
+ aria-label={strings.skip}
64
+ >
49
65
  <X weight="bold" />
50
66
  </Button>
51
67
  </div>
@@ -53,9 +69,18 @@
53
69
  </div>
54
70
  <div class="flex items-center justify-between gap-3 px-4 pb-4">
55
71
  {#if total > 1}
56
- <div role="img" aria-label={strings.progress(index + 1, total)} class="flex items-center gap-1.5">
72
+ <div
73
+ role="img"
74
+ aria-label={strings.progress(index + 1, total)}
75
+ class="flex items-center gap-1.5"
76
+ >
57
77
  {#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>
78
+ <span
79
+ class={[
80
+ "h-1.5 rounded-full transition-[width,background-color] duration-(--duration-fast) ease-(--ease-smooth-out) motion-reduce:transition-none",
81
+ i === index ? "bg-foreground w-4" : "bg-border w-1.5",
82
+ ]}
83
+ ></span>
59
84
  {/each}
60
85
  </div>
61
86
  {:else}
@@ -47,7 +47,8 @@
47
47
  // dismissal waits a microtask and the completion wins.
48
48
  function onStatusChange({ status }: zag.StatusChangeDetails) {
49
49
  if (status === "completed") finish("completed");
50
- else if (status === "dismissed" || status === "skipped") queueMicrotask(() => finish("dismissed"));
50
+ else if (status === "dismissed" || status === "skipped")
51
+ queueMicrotask(() => finish("dismissed"));
51
52
  else if (status === "not-found") finish(null);
52
53
  }
53
54
 
@@ -107,13 +108,21 @@
107
108
  </script>
108
109
 
109
110
  {#if step}
110
- <div use:portal {...api.getBackdropProps()} class="tour-backdrop" data-instant={instant || undefined}></div>
111
+ <div
112
+ use:portal
113
+ {...api.getBackdropProps()}
114
+ class="tour-backdrop"
115
+ data-instant={instant || undefined}
116
+ ></div>
111
117
  <div use:portal {...api.getSpotlightProps()} class="tour-spotlight"></div>
112
118
  <div use:portal {...api.getPositionerProps()} class="tour-positioner">
113
119
  <div
114
120
  {...api.getContentProps()}
115
121
  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"]}
122
+ class={[
123
+ "tour-card bg-popover text-popover-foreground ring-foreground/10 shadow-overlay pointer-events-auto max-w-full text-sm ring-1 outline-none",
124
+ intro ? "w-96 rounded-2xl" : "w-80 rounded-xl",
125
+ ]}
117
126
  >
118
127
  <TourCard
119
128
  title={String(step.title)}
@@ -9,26 +9,34 @@ export type Step = { target: string; title: string; body: string; placement?: Pl
9
9
  * `version` starts at 1. Raise it and everyone who saw the old one sees the tour again.
10
10
  */
11
11
  export type Tour =
12
- | { id: string; version?: number; route: string; kind: "steps"; steps: Step[] }
13
- | { id: string; version?: number; route: string; kind: "intro"; title: string; body: string; image?: string };
12
+ | { id: string; version?: number; route: string; kind: "steps"; steps: Step[] }
13
+ | {
14
+ id: string;
15
+ version?: number;
16
+ route: string;
17
+ kind: "intro";
18
+ title: string;
19
+ body: string;
20
+ image?: string;
21
+ };
14
22
 
15
23
  export type Outcome = "completed" | "dismissed";
16
24
 
17
25
  /** Where seen-state lives. `load` runs once per session. */
18
26
  export interface SeenStore {
19
- /** Tour id to the version seen. */
20
- load(): Promise<Record<string, number>>;
21
- markSeen(tourId: string, version: number, outcome: Outcome): Promise<void>;
22
- /** One tour, or all of them when no id is given. */
23
- reset(tourId?: string): Promise<void>;
27
+ /** Tour id to the version seen. */
28
+ load(): Promise<Record<string, number>>;
29
+ markSeen(tourId: string, version: number, outcome: Outcome): Promise<void>;
30
+ /** One tour, or all of them when no id is given. */
31
+ reset(tourId?: string): Promise<void>;
24
32
  }
25
33
 
26
34
  export type Strings = {
27
- next: string;
28
- back: string;
29
- done: string;
30
- skip: string;
31
- progress: (n: number, total: number) => string;
35
+ next: string;
36
+ back: string;
37
+ done: string;
38
+ skip: string;
39
+ progress: (n: number, total: number) => string;
32
40
  };
33
41
 
34
42
  export const MAX_STEPS = 6;
@@ -2,28 +2,28 @@ const find = (selector: string) => document.querySelector(selector);
2
2
 
3
3
  /** Resolves with the element once the selector matches, or null after `timeoutMs` or an abort. */
4
4
  export function waitForElement(
5
- selector: string,
6
- timeoutMs: number,
7
- signal?: AbortSignal,
5
+ selector: string,
6
+ timeoutMs: number,
7
+ signal?: AbortSignal,
8
8
  ): Promise<Element | null> {
9
- return new Promise((resolve) => {
10
- const now = find(selector);
11
- if (now) return resolve(now);
12
- if (signal?.aborted) return resolve(null);
9
+ return new Promise((resolve) => {
10
+ const now = find(selector);
11
+ if (now) return resolve(now);
12
+ if (signal?.aborted) return resolve(null);
13
13
 
14
- const finish = (el: Element | null) => {
15
- observer.disconnect();
16
- clearTimeout(timer);
17
- signal?.removeEventListener("abort", onAbort);
18
- resolve(el);
19
- };
20
- const onAbort = () => finish(null);
21
- const observer = new MutationObserver(() => {
22
- const el = find(selector);
23
- if (el) finish(el);
24
- });
25
- const timer = setTimeout(() => finish(find(selector)), timeoutMs);
26
- observer.observe(document.documentElement, { childList: true, subtree: true });
27
- signal?.addEventListener("abort", onAbort);
28
- });
14
+ const finish = (el: Element | null) => {
15
+ observer.disconnect();
16
+ clearTimeout(timer);
17
+ signal?.removeEventListener("abort", onAbort);
18
+ resolve(el);
19
+ };
20
+ const onAbort = () => finish(null);
21
+ const observer = new MutationObserver(() => {
22
+ const el = find(selector);
23
+ if (el) finish(el);
24
+ });
25
+ const timer = setTimeout(() => finish(find(selector)), timeoutMs);
26
+ observer.observe(document.documentElement, { childList: true, subtree: true });
27
+ signal?.addEventListener("abort", onAbort);
28
+ });
29
29
  }
@@ -0,0 +1,5 @@
1
+ # @heyramzi/ui/panel
2
+
3
+ `SidePanel`, a sheet for one record: badges, links out to other tools, a copy-a-prompt handoff and a stats strip. `PanelStat` types one stat. It used to live in `@heyramzi/ui/tasks`, which still re-exports both, and `tasks/side-panel` still resolves.
4
+
5
+ Exports: `SidePanel` and the types `PanelStat`, `SidePanelWidth`.
@@ -0,0 +1,2 @@
1
+ export { default as SidePanel } from "./side-panel.svelte";
2
+ export type { PanelStat, SidePanelWidth } from "./side-panel-types.js";
@@ -0,0 +1,194 @@
1
+ <script lang="ts" module>
2
+ import type { PanelStat, SidePanelWidth } from "./side-panel-types.js";
3
+ export type { PanelStat, SidePanelWidth };
4
+ </script>
5
+
6
+ <script lang="ts">
7
+ // =========================
8
+ // IMPORTS
9
+ // =========================
10
+ import type { Snippet } from "svelte";
11
+ import * as Sheet from "../shadcn/sheet/index.js";
12
+ import { OpenIn, type OpenInTool } from "../components/open-in/index.js";
13
+ import { CopyButton } from "../components/copy/index.js";
14
+ import type { Tone } from "../lib/tone.js";
15
+ import { cn } from "../lib/utils.js";
16
+ import StatusBadge from "../status/status-badge.svelte";
17
+
18
+ // =========================
19
+ // PROPS
20
+ // =========================
21
+ interface Props {
22
+ open: boolean;
23
+ /** Names the dialog for screen readers and heads the panel. */
24
+ title: string;
25
+ /** Merged onto the title. A short path is set `font-mono`, a subject is not. */
26
+ titleClass?: string;
27
+ /** The facts the record is filed under, read out under the title. */
28
+ description?: string;
29
+ /** Status chips, left of the action row. */
30
+ badges?: { label: string; tone: Tone }[];
31
+ /** Doors out to another tool. `text` replaces the tool's name on the button. */
32
+ openIn?: { tool: OpenInTool; href: string; label: string; text?: string }[];
33
+ /** A prompt for an AI about this record, copied on click. */
34
+ handoff?: { value: () => string | Promise<string>; label: string; copiedLabel: string };
35
+ /** The record's numbers, as a strip under the badges. Same place in every panel. */
36
+ stats?: PanelStat[];
37
+ /** Anything else the record can do, drawn last in the action row. */
38
+ actions?: Snippet;
39
+ /** Pinned actions. The body scrolls above it; the caller aligns the row. */
40
+ footer?: Snippet;
41
+ children: Snippet;
42
+ width?: SidePanelWidth;
43
+ /** The body brings its own header: the title is read out but not drawn, and
44
+ the body is neither padded nor scrolled by the panel. */
45
+ bare?: boolean;
46
+ /** Fires once when the panel closes, from the close button, Escape or the overlay. */
47
+ onClose?: () => void;
48
+ }
49
+ let {
50
+ open = $bindable(),
51
+ title,
52
+ titleClass,
53
+ description,
54
+ badges = [],
55
+ openIn = [],
56
+ handoff,
57
+ stats = [],
58
+ actions,
59
+ footer,
60
+ children,
61
+ width = "sm",
62
+ bare = false,
63
+ onClose,
64
+ }: Props = $props();
65
+
66
+ // =========================
67
+ // STATE
68
+ // =========================
69
+ let panel = $state<HTMLElement | null>(null);
70
+
71
+ // =========================
72
+ // DERIVED
73
+ // =========================
74
+ let hasRow = $derived(badges.length > 0 || openIn.length > 0 || Boolean(handoff || actions));
75
+ const STAT_TONE = {
76
+ default: "",
77
+ muted: "text-muted-foreground",
78
+ good: "text-success",
79
+ bad: "text-destructive",
80
+ } as const;
81
+ const WIDTH: Record<SidePanelWidth, string> = {
82
+ sm: "sm:max-w-sm",
83
+ md: "sm:max-w-md",
84
+ lg: "sm:max-w-xl",
85
+ xl: "sm:max-w-2xl",
86
+ "2xl": "sm:max-w-3xl",
87
+ };
88
+
89
+ // =========================
90
+ // HANDLERS
91
+ // =========================
92
+ // Focus goes to the panel unless a text field comes first, or the first control draws
93
+ // a ring on a record nobody touched.
94
+ const FIRST_CONTROL =
95
+ 'a[href], button:not([disabled]), input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled])';
96
+ function onOpenAutoFocus(event: Event) {
97
+ if (!panel || panel.querySelector(FIRST_CONTROL)?.matches("input, textarea, select")) return;
98
+ event.preventDefault();
99
+ panel.focus();
100
+ }
101
+ </script>
102
+
103
+ <!-- =========================== -->
104
+ <!-- MARKUP -->
105
+ <!-- =========================== -->
106
+ <Sheet.Root
107
+ bind:open
108
+ onOpenChange={(next) => {
109
+ if (!next) onClose?.();
110
+ }}
111
+ >
112
+ <Sheet.Content
113
+ side="right"
114
+ bind:ref={panel}
115
+ tabindex={-1}
116
+ class={cn("flex w-full flex-col gap-0 p-0", WIDTH[width], bare && "overflow-hidden")}
117
+ {onOpenAutoFocus}
118
+ >
119
+ {#if bare}
120
+ <Sheet.Header class="sr-only">
121
+ <Sheet.Title>{title}</Sheet.Title>
122
+ </Sheet.Header>
123
+ {@render children()}
124
+ {:else}
125
+ <Sheet.Header class="gap-1 border-b border-border">
126
+ <!-- WHY the right padding: the close button sits over this corner and
127
+ a long title runs straight under it. -->
128
+ <Sheet.Title class={cn("pr-9 text-base leading-snug", titleClass)}>{title}</Sheet.Title>
129
+ {#if description}
130
+ <Sheet.Description>{description}</Sheet.Description>
131
+ {/if}
132
+ {#if hasRow}
133
+ <div class="flex items-center justify-between gap-3 pt-1.5">
134
+ <div class="flex min-w-0 flex-wrap items-center gap-2">
135
+ {#each badges as badge (`${badge.tone}:${badge.label}`)}
136
+ <StatusBadge tone={badge.tone}>{badge.label}</StatusBadge>
137
+ {/each}
138
+ </div>
139
+ <div class="flex shrink-0 items-center gap-1">
140
+ {#each openIn as door (door.href)}
141
+ <OpenIn
142
+ tool={door.tool}
143
+ href={door.href}
144
+ title={door.label}
145
+ label={door.text}
146
+ variant="button"
147
+ size="sm"
148
+ />
149
+ {/each}
150
+ {#if handoff}
151
+ <CopyButton
152
+ agent
153
+ value={handoff.value}
154
+ label={handoff.label}
155
+ copiedLabel={handoff.copiedLabel}
156
+ />
157
+ {/if}
158
+ {@render actions?.()}
159
+ </div>
160
+ </div>
161
+ {/if}
162
+ {#if stats.length > 0}
163
+ <dl class="flex flex-wrap gap-x-6 gap-y-3 pt-3">
164
+ {#each stats as stat (stat.label)}
165
+ <div class="min-w-0">
166
+ <dt class="text-xs text-muted-foreground">{stat.label}</dt>
167
+ <dd
168
+ class={cn("text-sm font-medium tabular-nums", STAT_TONE[stat.tone ?? "default"])}
169
+ >
170
+ {stat.value}
171
+ {#if stat.sub}
172
+ <span class="text-xs font-normal text-muted-foreground">{stat.sub}</span>
173
+ {/if}
174
+ </dd>
175
+ </div>
176
+ {/each}
177
+ </dl>
178
+ {/if}
179
+ </Sheet.Header>
180
+
181
+ <!-- WHY the body pads and spaces its sections itself: a panel that lets the
182
+ caller choose drifts, one with a grey band and one with a white page. -->
183
+ <div class="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4">
184
+ {@render children()}
185
+ </div>
186
+
187
+ {#if footer}
188
+ <Sheet.Footer class="mt-0 flex-row items-center justify-end border-t border-border">
189
+ {@render footer()}
190
+ </Sheet.Footer>
191
+ {/if}
192
+ {/if}
193
+ </Sheet.Content>
194
+ </Sheet.Root>