@heyramzi/ui 6.1.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/COMPONENTS.md +32 -40
  2. package/README.md +10 -4
  3. package/package.json +31 -29
  4. package/scripts/sync-shadcn.mjs +29 -16
  5. package/scripts/sync-transitions.mjs +11 -11
  6. package/src/ai/ai-button/AiButton.svelte +86 -0
  7. package/src/ai/ai-button/index.ts +4 -0
  8. package/src/ai/confirmation/Confirmation.svelte +8 -8
  9. package/src/ai/confirmation/confirmation-context.svelte.ts +2 -2
  10. package/src/ai/generation-progress/GenerationProgress.svelte +354 -0
  11. package/src/ai/generation-progress/index.ts +7 -0
  12. package/src/ai/index.ts +16 -24
  13. package/src/ai/prompt-input/PromptInput.svelte +19 -1
  14. package/src/ai/prompt-input/PromptInputSubmit.svelte +9 -3
  15. package/src/ai/reasoning/Reasoning.svelte +11 -8
  16. package/src/ambient.d.ts +4 -0
  17. package/src/animations/README.md +17 -17
  18. package/src/animations/index.css +3 -14
  19. package/src/animations/upstream-coverage.test.ts +1 -1
  20. package/src/components/copy/clipboard.test.ts +206 -0
  21. package/src/components/copy/clipboard.ts +49 -0
  22. package/src/components/copy/copied.svelte.ts +39 -0
  23. package/src/components/copy/copy-button.svelte +175 -0
  24. package/src/components/copy/copy-button.test.ts +110 -0
  25. package/src/components/copy/index.ts +3 -0
  26. package/src/components/data-table/data-table.test.ts +134 -0
  27. package/src/components/data-table/data-table.ts +77 -0
  28. package/src/components/data-table/index.ts +20 -0
  29. package/src/components/data-table/infinite-scroll.svelte +69 -0
  30. package/src/components/data-table/pagination.svelte +62 -0
  31. package/src/components/data-table/row-arrow.svelte +28 -0
  32. package/src/components/data-table/row-context-menu.svelte +106 -0
  33. package/src/components/data-table/row-context-menu.test.ts +18 -0
  34. package/src/components/data-table/sortable-head.svelte +56 -0
  35. package/src/components/data-table/table-skeleton.svelte +83 -0
  36. package/src/components/hold-to-confirm/hold-to-confirm.svelte +232 -0
  37. package/src/components/hold-to-confirm/index.ts +1 -0
  38. package/src/components/icon/icon.svelte +23 -0
  39. package/src/components/icon/index.ts +1 -0
  40. package/src/components/language-flag/index.ts +1 -0
  41. package/src/components/language-flag/language-flag.svelte +60 -0
  42. package/src/components/list-page/detail-empty.svelte +50 -0
  43. package/src/components/list-page/detail-header.svelte +107 -0
  44. package/src/components/list-page/detail-panel.svelte +90 -0
  45. package/src/components/list-page/detail-row.svelte +36 -0
  46. package/src/components/list-page/detail-stack.svelte +33 -0
  47. package/src/components/list-page/empty-state.svelte +91 -0
  48. package/src/components/list-page/filter-item.svelte +49 -0
  49. package/src/components/list-page/filter-popover.svelte +88 -0
  50. package/src/components/list-page/filter-section.ts +12 -0
  51. package/src/components/list-page/grouped-list.svelte +116 -0
  52. package/src/components/list-page/index.ts +28 -0
  53. package/src/components/list-page/list-page-shell.svelte +195 -0
  54. package/src/components/list-page/list-table.ts +19 -0
  55. package/src/components/list-page/master-detail.svelte +206 -0
  56. package/src/components/list-page/master-detail.test.ts +37 -0
  57. package/src/components/list-page/notice-bar.svelte +68 -0
  58. package/src/components/list-page/prev-next.svelte +104 -0
  59. package/src/components/list-page/queue-rail.ts +25 -0
  60. package/src/components/list-page/rail-body.svelte +29 -0
  61. package/src/components/list-page/rail-empty.svelte +51 -0
  62. package/src/components/list-page/rail-head.svelte +49 -0
  63. package/src/components/list-page/rail-row.svelte +163 -0
  64. package/src/components/list-page/section-host.ts +24 -0
  65. package/src/components/list-page/section-label.svelte +39 -0
  66. package/src/components/list-page/step.test.ts +21 -0
  67. package/src/components/list-page/step.ts +10 -0
  68. package/src/components/list-page/toolbar-search.svelte +63 -0
  69. package/src/components/list-page/toolbar-trigger.ts +23 -0
  70. package/src/components/list-page/types.ts +1 -0
  71. package/src/components/locale-switch/index.ts +1 -0
  72. package/src/components/locale-switch/locale-switch.svelte +59 -0
  73. package/src/components/mark/index.ts +1 -0
  74. package/src/components/mark/mark.svelte +70 -0
  75. package/src/components/open-in/index.ts +6 -0
  76. package/src/components/open-in/open-in.svelte +106 -0
  77. package/src/components/picker/index.ts +4 -0
  78. package/src/components/picker/mark-picker.svelte +84 -0
  79. package/src/components/picker/picker.svelte +143 -0
  80. package/src/components/picker/trigger.ts +42 -0
  81. package/src/components/picker/types.ts +26 -0
  82. package/src/components/segmented/animated-tabs.svelte +175 -0
  83. package/src/components/segmented/collapse.test.ts +49 -0
  84. package/src/components/segmented/collapse.ts +39 -0
  85. package/src/components/segmented/index.ts +41 -0
  86. package/src/components/segmented/section-runs.svelte +79 -0
  87. package/src/components/segmented/segment.svelte +106 -0
  88. package/src/components/segmented/segmented-filter.svelte +125 -0
  89. package/src/components/segmented/segmented-nav.svelte +82 -0
  90. package/src/components/segmented/segmented-overflow.svelte +85 -0
  91. package/src/components/segmented/segmented-track.svelte +181 -0
  92. package/src/components/segmented/segmented.test.ts +20 -0
  93. package/src/components/segmented/segmented.ts +80 -0
  94. package/src/components/star-rating-input/index.ts +9 -0
  95. package/src/components/star-rating-input/star-rating-input.svelte +256 -0
  96. package/src/components/svg/index.ts +1 -0
  97. package/src/components/svg/svg.svelte +9 -0
  98. package/src/effects/border-beam/BorderBeam.svelte +1 -1
  99. package/src/effects/image-comparison/ImageComparison.css +42 -8
  100. package/src/effects/image-comparison/ImageComparison.svelte +138 -21
  101. package/src/effects/image-comparison/compare.test.ts +17 -0
  102. package/src/effects/image-comparison/compare.ts +5 -0
  103. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +338 -0
  104. package/src/effects/magic-card/MagicCard.css +47 -0
  105. package/src/effects/magic-card/MagicCard.svelte +57 -0
  106. package/src/effects/marquee/Marquee.css +27 -1
  107. package/src/effects/marquee/Marquee.svelte +67 -3
  108. package/src/effects/reveal/index.ts +2 -1
  109. package/src/effects/reveal/observe.test.ts +60 -0
  110. package/src/effects/reveal/observe.ts +33 -1
  111. package/src/effects/reveal/types.ts +7 -0
  112. package/src/effects/theme-toggle/ThemeToggle.css +199 -6
  113. package/src/effects/theme-toggle/ThemeToggle.svelte +51 -26
  114. package/src/gallery.test.ts +52 -0
  115. package/src/index.ts +5 -1
  116. package/src/lib/panel.ts +7 -1
  117. package/src/lib/sliding-pill.ts +68 -0
  118. package/src/lib/theme-transition.ts +41 -13
  119. package/src/lib/tone.ts +48 -0
  120. package/src/shadcn/accordion/accordion-trigger.svelte +15 -6
  121. package/src/shadcn/alert/alert.svelte +3 -1
  122. package/src/shadcn/badge/badge.svelte +8 -7
  123. package/src/shadcn/breadcrumb/breadcrumb-ellipsis.svelte +2 -2
  124. package/src/shadcn/breadcrumb/breadcrumb-separator.svelte +2 -2
  125. package/src/shadcn/calendar/calendar-month-select.svelte +2 -2
  126. package/src/shadcn/calendar/calendar-next-button.svelte +2 -2
  127. package/src/shadcn/calendar/calendar-prev-button.svelte +2 -2
  128. package/src/shadcn/calendar/calendar-year-select.svelte +2 -2
  129. package/src/shadcn/calendar/calendar.svelte +6 -0
  130. package/src/shadcn/code-block/code-block-actions.svelte +23 -0
  131. package/src/shadcn/code-block/code-block-content.svelte +55 -0
  132. package/src/shadcn/code-block/code-block-copy-button.svelte +10 -0
  133. package/src/shadcn/code-block/code-block-footer.svelte +20 -0
  134. package/src/shadcn/code-block/code-block-header.svelte +23 -0
  135. package/src/shadcn/code-block/code-block-line.svelte +71 -0
  136. package/src/shadcn/code-block/code-block-title.svelte +23 -0
  137. package/src/shadcn/code-block/code-block.svelte +47 -0
  138. package/src/shadcn/code-block/index.ts +42 -0
  139. package/src/shadcn/command/index.ts +3 -0
  140. package/src/shadcn/field/index.ts +3 -0
  141. package/src/shadcn/field/simple-field.svelte +71 -0
  142. package/src/shadcn/field/simple-field.test.ts +47 -0
  143. package/src/shadcn/hooks/highlighter.ts +132 -0
  144. package/src/shadcn/hooks/use-highlighted-code.svelte.ts +98 -0
  145. package/src/shadcn/kbd/index.ts +10 -0
  146. package/src/shadcn/kbd/kbd-group.svelte +20 -0
  147. package/src/shadcn/kbd/kbd.svelte +23 -0
  148. package/src/shadcn/message-scroller/index.ts +28 -0
  149. package/src/shadcn/message-scroller/message-scroller-button.svelte +52 -0
  150. package/src/shadcn/message-scroller/message-scroller-content.svelte +17 -0
  151. package/src/shadcn/message-scroller/message-scroller-item.svelte +22 -0
  152. package/src/shadcn/message-scroller/message-scroller-provider.svelte +7 -0
  153. package/src/shadcn/message-scroller/message-scroller-viewport.svelte +20 -0
  154. package/src/shadcn/message-scroller/message-scroller.svelte +20 -0
  155. package/src/shadcn/radio-group/radio-group-item.svelte +3 -2
  156. package/src/shadcn/sheet/sheet-content.svelte +1 -0
  157. package/src/shadcn/sheet/sheet.css +48 -0
  158. package/src/shadcn/sonner/sonner.svelte +7 -10
  159. package/src/shadcn/tabs/tabs-list.svelte +13 -2
  160. package/src/shadcn/tabs/tabs-trigger.svelte +1 -1
  161. package/src/shadcn/toggle-group/toggle-group.svelte +10 -3
  162. package/src/shadcn/trace/index.ts +29 -0
  163. package/src/shadcn/trace/trace-collapsible-content.svelte +29 -0
  164. package/src/shadcn/trace/trace-collapsible-trigger.svelte +29 -0
  165. package/src/shadcn/trace/trace-collapsible.svelte +20 -0
  166. package/src/shadcn/trace/trace-description.svelte +20 -0
  167. package/src/shadcn/trace/trace-icon.svelte +24 -0
  168. package/src/shadcn/trace/trace-line.svelte +31 -0
  169. package/src/shadcn/trace/trace-title.svelte +20 -0
  170. package/src/shadcn/trace/trace.svelte +23 -0
  171. package/src/ai/composer/Composer.css +0 -108
  172. package/src/ai/composer/Composer.svelte +0 -111
  173. package/src/ai/composer/index.ts +0 -10
  174. package/src/ai/conversation/Conversation.svelte +0 -28
  175. package/src/ai/conversation/ConversationContent.svelte +0 -35
  176. package/src/ai/conversation/ConversationEmptyState.svelte +0 -46
  177. package/src/ai/conversation/ConversationScrollButton.svelte +0 -51
  178. package/src/ai/conversation/index.ts +0 -33
  179. package/src/ai/conversation/stick-to-bottom-context.svelte.ts +0 -131
  180. package/src/animations/accordion.css +0 -58
  181. package/src/animations/icon-swap.css +0 -43
  182. package/src/animations/notification-badge.css +0 -81
  183. package/src/animations/panel.css +0 -86
  184. package/src/animations/reasoning-stream.css +0 -49
  185. package/src/animations/skeleton-reveal.css +0 -97
  186. package/src/animations/streaming-text.css +0 -31
  187. package/src/animations/tabs-sliding.css +0 -83
  188. package/src/animations/texts-reveal.css +0 -70
  189. package/src/animations/tooltip.css +0 -72
  190. package/src/animations/view-transition-reveal.ts +0 -74
  191. package/src/charts/animated-value.css +0 -7
  192. package/src/charts/animated-value.svelte +0 -110
  193. package/src/charts/area-chart.css +0 -29
  194. package/src/charts/area-chart.svelte +0 -195
  195. package/src/charts/bar-chart.css +0 -119
  196. package/src/charts/bar-chart.svelte +0 -269
  197. package/src/charts/charts.css +0 -201
  198. package/src/charts/crosshair.svelte +0 -35
  199. package/src/charts/donut-chart.css +0 -56
  200. package/src/charts/donut-chart.svelte +0 -158
  201. package/src/charts/edge-fade.svelte +0 -43
  202. package/src/charts/edge-fade.ts +0 -16
  203. package/src/charts/figure.css +0 -100
  204. package/src/charts/figure.svelte +0 -122
  205. package/src/charts/gauge-chart.css +0 -94
  206. package/src/charts/gauge-chart.svelte +0 -303
  207. package/src/charts/hover-band.test.ts +0 -30
  208. package/src/charts/hover-band.ts +0 -21
  209. package/src/charts/hover-focus.svelte +0 -85
  210. package/src/charts/index.ts +0 -30
  211. package/src/charts/legend.css +0 -131
  212. package/src/charts/legend.svelte +0 -112
  213. package/src/charts/line-chart.css +0 -26
  214. package/src/charts/line-chart.svelte +0 -256
  215. package/src/charts/motion.ts +0 -19
  216. package/src/charts/number-chart.css +0 -168
  217. package/src/charts/number-chart.svelte +0 -177
  218. package/src/charts/primitives/chart-container.svelte +0 -57
  219. package/src/charts/primitives/chart-style.svelte +0 -36
  220. package/src/charts/primitives/chart-tooltip.svelte +0 -161
  221. package/src/charts/primitives/chart-utils.ts +0 -54
  222. package/src/charts/primitives/index.ts +0 -11
  223. package/src/charts/reveal.svelte +0 -61
  224. package/src/charts/ring-chart.css +0 -96
  225. package/src/charts/ring-chart.svelte +0 -158
  226. package/src/charts/sparkline.css +0 -45
  227. package/src/charts/sparkline.svelte +0 -140
  228. package/src/charts/stat-card.css +0 -103
  229. package/src/charts/stat-card.svelte +0 -267
  230. package/src/charts/time-series-chart.css +0 -28
  231. package/src/charts/time-series-chart.svelte +0 -179
  232. package/src/charts/trend-pill.css +0 -38
  233. package/src/charts/trend-pill.svelte +0 -48
  234. package/src/lib/hooks/is-mobile.svelte.ts +0 -11
  235. package/src/lib/utils.ts +0 -45
  236. package/src/showcase/AiSection.svelte +0 -157
  237. package/src/showcase/AnimationsSection.svelte +0 -1658
  238. package/src/showcase/ChartsSection.svelte +0 -458
  239. package/src/showcase/EffectsSection.svelte +0 -152
  240. package/src/showcase/ShadcnSection.svelte +0 -27
  241. package/src/showcase/ShowcaseImport.svelte +0 -126
  242. package/src/showcase/ShowcaseList.svelte +0 -132
  243. package/src/showcase/ShowcaseNav.svelte +0 -247
  244. package/src/showcase/ShowcaseSection.svelte +0 -112
  245. package/src/showcase/ShowcaseShell.svelte +0 -55
  246. package/src/showcase/ShowcaseStage.svelte +0 -342
  247. package/src/showcase/ShowcaseVariant.svelte +0 -109
  248. package/src/showcase/assets/image-reveal/art-1.jpg +0 -0
  249. package/src/showcase/assets/image-reveal/art-2.jpg +0 -0
  250. package/src/showcase/assets/image-reveal/art-3.jpg +0 -0
  251. package/src/showcase/check.svg +0 -1
  252. package/src/showcase/chevron-arms.svg +0 -2
  253. package/src/showcase/chevron-down.svg +0 -1
  254. package/src/showcase/copy.svg +0 -2
  255. package/src/showcase/demos/ai/confirmation.svelte +0 -59
  256. package/src/showcase/demos/ai/conversation.svelte +0 -59
  257. package/src/showcase/demos/ai/loading-state.svelte +0 -19
  258. package/src/showcase/demos/ai/prompt-input.svelte +0 -57
  259. package/src/showcase/demos/ai/reasoning.svelte +0 -19
  260. package/src/showcase/demos/ai/thinking-dots.svelte +0 -15
  261. package/src/showcase/demos/ai/voice-waveform.svelte +0 -24
  262. package/src/showcase/demos/effects/animated-beam.svelte +0 -50
  263. package/src/showcase/demos/effects/animated-group.svelte +0 -47
  264. package/src/showcase/demos/effects/border-beam-default.svelte +0 -20
  265. package/src/showcase/demos/effects/border-beam-line.svelte +0 -20
  266. package/src/showcase/demos/effects/border-beam-mono.svelte +0 -20
  267. package/src/showcase/demos/effects/border-beam-sunset.svelte +0 -20
  268. package/src/showcase/demos/effects/image-comparison.svelte +0 -25
  269. package/src/showcase/demos/effects/image-reveal.svelte +0 -49
  270. package/src/showcase/demos/effects/in-view.svelte +0 -41
  271. package/src/showcase/demos/effects/integrations-hub.svelte +0 -82
  272. package/src/showcase/demos/effects/magnetic.svelte +0 -31
  273. package/src/showcase/demos/effects/marquee.svelte +0 -33
  274. package/src/showcase/demos/effects/progressive-blur.svelte +0 -47
  275. package/src/showcase/demos/effects/spotlight.svelte +0 -40
  276. package/src/showcase/demos/effects/text-effect.svelte +0 -55
  277. package/src/showcase/demos/shadcn/accordion.svelte +0 -45
  278. package/src/showcase/demos/shadcn/alert-dialog.svelte +0 -23
  279. package/src/showcase/demos/shadcn/alert.svelte +0 -30
  280. package/src/showcase/demos/shadcn/attachment.svelte +0 -71
  281. package/src/showcase/demos/shadcn/avatar.svelte +0 -30
  282. package/src/showcase/demos/shadcn/badge.svelte +0 -26
  283. package/src/showcase/demos/shadcn/breadcrumb.svelte +0 -34
  284. package/src/showcase/demos/shadcn/bubble.svelte +0 -36
  285. package/src/showcase/demos/shadcn/button.svelte +0 -11
  286. package/src/showcase/demos/shadcn/calendar.svelte +0 -8
  287. package/src/showcase/demos/shadcn/card.svelte +0 -39
  288. package/src/showcase/demos/shadcn/checkbox.svelte +0 -39
  289. package/src/showcase/demos/shadcn/collapsible.svelte +0 -20
  290. package/src/showcase/demos/shadcn/command.svelte +0 -48
  291. package/src/showcase/demos/shadcn/context-menu.svelte +0 -54
  292. package/src/showcase/demos/shadcn/demo-harness.svelte +0 -12
  293. package/src/showcase/demos/shadcn/demos.test.ts +0 -20
  294. package/src/showcase/demos/shadcn/dialog.svelte +0 -38
  295. package/src/showcase/demos/shadcn/dropdown-menu.svelte +0 -59
  296. package/src/showcase/demos/shadcn/empty.svelte +0 -29
  297. package/src/showcase/demos/shadcn/field.svelte +0 -115
  298. package/src/showcase/demos/shadcn/hover-card.svelte +0 -32
  299. package/src/showcase/demos/shadcn/index.ts +0 -358
  300. package/src/showcase/demos/shadcn/input-otp.svelte +0 -19
  301. package/src/showcase/demos/shadcn/input.svelte +0 -5
  302. package/src/showcase/demos/shadcn/label.svelte +0 -11
  303. package/src/showcase/demos/shadcn/marker.svelte +0 -30
  304. package/src/showcase/demos/shadcn/message.svelte +0 -75
  305. package/src/showcase/demos/shadcn/popover.svelte +0 -36
  306. package/src/showcase/demos/shadcn/progress.svelte +0 -13
  307. package/src/showcase/demos/shadcn/radio-group.svelte +0 -19
  308. package/src/showcase/demos/shadcn/scroll-area.svelte +0 -18
  309. package/src/showcase/demos/shadcn/select.svelte +0 -29
  310. package/src/showcase/demos/shadcn/separator.svelte +0 -12
  311. package/src/showcase/demos/shadcn/sheet.svelte +0 -48
  312. package/src/showcase/demos/shadcn/skeleton.svelte +0 -11
  313. package/src/showcase/demos/shadcn/slider.svelte +0 -7
  314. package/src/showcase/demos/shadcn/sonner.svelte +0 -18
  315. package/src/showcase/demos/shadcn/spinner.svelte +0 -11
  316. package/src/showcase/demos/shadcn/switch.svelte +0 -9
  317. package/src/showcase/demos/shadcn/table.svelte +0 -76
  318. package/src/showcase/demos/shadcn/tabs.svelte +0 -62
  319. package/src/showcase/demos/shadcn/textarea.svelte +0 -5
  320. package/src/showcase/demos/shadcn/toggle-group.svelte +0 -18
  321. package/src/showcase/demos/shadcn/toggle.svelte +0 -14
  322. package/src/showcase/demos/shadcn/tooltip.svelte +0 -13
  323. package/src/showcase/heart.svg +0 -1
  324. package/src/showcase/index.ts +0 -38
  325. package/src/showcase/kit.test.ts +0 -72
  326. package/src/showcase/kit.ts +0 -17
  327. package/src/showcase/plus.svg +0 -1
  328. package/src/showcase/showcase-shell.css +0 -91
  329. package/src/showcase/stacked-section.css +0 -15
  330. package/src/showcase/success-check.svg +0 -1
  331. package/src/showcase/types.ts +0 -29
  332. package/src/svg-raw.d.ts +0 -7
@@ -0,0 +1,338 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import { ArrowRight } from "phosphor-svelte";
6
+
7
+ // =========================
8
+ // TYPES
9
+ // =========================
10
+ type Props = {
11
+ href?: string;
12
+ /** `submit` posts the form the button sits in. */
13
+ type?: "button" | "submit";
14
+ disabled?: boolean;
15
+ size?: "default" | "md" | "sm";
16
+ tone?: "ink" | "coral";
17
+ beam?: boolean;
18
+ dot?: boolean;
19
+ class?: string;
20
+ onclick?: (event: MouseEvent) => void;
21
+ children: import("svelte").Snippet;
22
+ };
23
+
24
+ // =========================
25
+ // PROPS
26
+ // =========================
27
+ let { href, type = "button", disabled = false, size = "default", tone = "ink", beam = true, dot = true, class: className, onclick, children }: Props = $props();
28
+
29
+ // =========================
30
+ // DERIVED
31
+ // =========================
32
+ let classes = $derived(
33
+ [
34
+ "cta-hover",
35
+ size === "md" && "cta-hover--md",
36
+ size === "sm" && "cta-hover--sm",
37
+ tone === "coral" && "cta-hover--coral",
38
+ !beam && "cta-hover--no-beam",
39
+ !dot && "cta-hover--no-dot",
40
+ className,
41
+ ]
42
+ .filter(Boolean)
43
+ .join(" "),
44
+ );
45
+ </script>
46
+
47
+ <!-- =========================== -->
48
+ <!-- MARKUP -->
49
+ <!-- =========================== -->
50
+ {#if href}
51
+ <a {href} {onclick} class={classes}>
52
+ <span class="cta-hover__resting">
53
+ {#if dot}<span class="cta-hover__dot"></span>{/if}
54
+ <span class="cta-hover__label">{@render children()}</span>
55
+ {#if !dot}<ArrowRight class="cta-hover__arrow cta-hover__arrow--inline" weight="bold" />{/if}
56
+ </span>
57
+ {#if dot}
58
+ <span class="cta-hover__reveal" aria-hidden="true">
59
+ <span class="cta-hover__label">{@render children()}</span>
60
+ <ArrowRight class="cta-hover__arrow" weight="bold" />
61
+ </span>
62
+ {/if}
63
+ </a>
64
+ {:else}
65
+ <button {type} {disabled} {onclick} class={classes}>
66
+ <span class="cta-hover__resting">
67
+ {#if dot}<span class="cta-hover__dot"></span>{/if}
68
+ <span class="cta-hover__label">{@render children()}</span>
69
+ {#if !dot}<ArrowRight class="cta-hover__arrow cta-hover__arrow--inline" weight="bold" />{/if}
70
+ </span>
71
+ {#if dot}
72
+ <span class="cta-hover__reveal" aria-hidden="true">
73
+ <span class="cta-hover__label">{@render children()}</span>
74
+ <ArrowRight class="cta-hover__arrow" weight="bold" />
75
+ </span>
76
+ {/if}
77
+ </button>
78
+ {/if}
79
+
80
+ <!-- =========================== -->
81
+ <!-- STYLE -->
82
+ <!-- =========================== -->
83
+ <style>
84
+ @keyframes rotate-chime {
85
+ from {
86
+ transform: translate(-50%, -50%) rotate(0deg);
87
+ }
88
+ to {
89
+ transform: translate(-50%, -50%) rotate(360deg);
90
+ }
91
+ }
92
+
93
+ .cta-hover {
94
+ position: relative;
95
+ display: inline-flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ height: 44px;
99
+ padding: 0 24px;
100
+ border-radius: var(--radius-lg, 8px);
101
+ font-family: var(--font-sans);
102
+ font-size: 14px;
103
+ font-weight: 550;
104
+ letter-spacing: -0.01em;
105
+ cursor: pointer;
106
+ overflow: hidden;
107
+ background: transparent;
108
+ color: var(--color-background);
109
+ text-decoration: none;
110
+ will-change: transform;
111
+ transition: transform var(--duration-quick) var(--ease-smooth-out);
112
+ }
113
+
114
+ /* Spinning gradient circle, clipped by overflow:hidden to button shape */
115
+ .cta-hover::before {
116
+ content: "";
117
+ position: absolute;
118
+ top: 50%;
119
+ left: 50%;
120
+ width: max(200%, 150px);
121
+ aspect-ratio: 1;
122
+ border-radius: 50%;
123
+ background: conic-gradient(
124
+ transparent 0deg,
125
+ transparent 220deg,
126
+ oklch(0.65 0.20 30) 280deg,
127
+ oklch(0.82 0.10 30) 320deg,
128
+ transparent 360deg
129
+ );
130
+ transform: translate(-50%, -50%);
131
+ animation: rotate-chime 5s linear infinite;
132
+ pointer-events: none;
133
+ z-index: 0;
134
+ transition: opacity var(--duration-fast) var(--ease-smooth-out);
135
+ }
136
+
137
+ :global(.dark) .cta-hover::before {
138
+ background: conic-gradient(
139
+ transparent 0deg,
140
+ transparent 220deg,
141
+ oklch(0.78 0.08 40) 280deg,
142
+ oklch(0.88 0.04 40) 320deg,
143
+ transparent 360deg
144
+ );
145
+ }
146
+
147
+ /* Inner fill: leaves 1.5px border gap showing the spinning gradient */
148
+ .cta-hover::after {
149
+ content: "";
150
+ position: absolute;
151
+ inset: 1.5px;
152
+ border-radius: calc(var(--radius-lg, 8px) - 1.5px);
153
+ background: var(--color-foreground);
154
+ z-index: 1;
155
+ pointer-events: none;
156
+ }
157
+
158
+ /* 40px, between 44px and 32px, for a CTA in a condensed bar. 32px is too
159
+ small to read as the main action, so only the label shrinks, to 13px. */
160
+ .cta-hover--md {
161
+ height: 40px;
162
+ padding: 0 24px;
163
+ font-size: 13px;
164
+ }
165
+
166
+ .cta-hover--md .cta-hover__dot {
167
+ width: 7px;
168
+ height: 7px;
169
+ }
170
+
171
+ .cta-hover--sm {
172
+ height: 32px;
173
+ padding: 0 20px;
174
+ font-size: 13px;
175
+ }
176
+
177
+ .cta-hover--sm .cta-hover__dot {
178
+ width: 6px;
179
+ height: 6px;
180
+ }
181
+
182
+ .cta-hover--no-beam::before {
183
+ display: none;
184
+ }
185
+
186
+ .cta-hover--no-beam {
187
+ background: var(--color-foreground);
188
+ }
189
+
190
+ /* No-dot variant: arrow inline, shifts right on hover */
191
+ :global(.cta-hover__arrow--inline) {
192
+ width: 14px;
193
+ height: 14px;
194
+ flex-shrink: 0;
195
+ transition: transform var(--duration-quick) var(--ease-smooth-out);
196
+ }
197
+
198
+ @media (hover: hover) and (pointer: fine) {
199
+ .cta-hover--no-dot:hover :global(.cta-hover__arrow--inline) {
200
+ transform: translateX(3px);
201
+ }
202
+ }
203
+
204
+ .cta-hover:active {
205
+ transform: scale(0.97);
206
+ }
207
+
208
+ /* In flight: the label still reads, the button stops answering. */
209
+ .cta-hover:disabled {
210
+ opacity: 0.7;
211
+ cursor: default;
212
+ }
213
+
214
+ .cta-hover:disabled:active {
215
+ transform: none;
216
+ }
217
+
218
+ /* The CTA shipped with no focus ring. This is shadcn's ring-3 ring-ring/50,
219
+ written as a box-shadow because this component is plain CSS. */
220
+ .cta-hover:focus-visible {
221
+ outline: none;
222
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-ring) 50%, transparent);
223
+ }
224
+
225
+ .cta-hover__resting {
226
+ position: relative;
227
+ z-index: 2;
228
+ display: flex;
229
+ align-items: center;
230
+ justify-content: center;
231
+ gap: 8px;
232
+ width: 100%;
233
+ transition: opacity var(--duration-fast) var(--ease-smooth-out), transform var(--duration-fast) var(--ease-smooth-out);
234
+ }
235
+
236
+ /* terra-600, not terra-400: the dot becomes the hover fill under a cream
237
+ label, and cream on terra-400 is 3.24:1. It works in both themes. */
238
+ .cta-hover__dot {
239
+ width: 8px;
240
+ height: 8px;
241
+ border-radius: 50%;
242
+ background: var(--color-terra-600);
243
+ flex-shrink: 0;
244
+ transition: transform var(--duration-fast) var(--ease-smooth-out);
245
+ }
246
+
247
+ .cta-hover__label {
248
+ white-space: nowrap;
249
+ transition:
250
+ transform var(--duration-fast) var(--ease-smooth-out),
251
+ opacity var(--duration-fast) var(--ease-smooth-out);
252
+ }
253
+
254
+ .cta-hover__reveal {
255
+ position: absolute;
256
+ inset: 0;
257
+ z-index: 3;
258
+ display: flex;
259
+ align-items: center;
260
+ justify-content: center;
261
+ gap: 8px;
262
+ opacity: 0;
263
+ transform: translateX(48px);
264
+ transition:
265
+ transform var(--duration-fast) var(--ease-smooth-out),
266
+ opacity var(--duration-fast) var(--ease-smooth-out);
267
+ /* The reveal label always lands on the terra-600 dot fill, where cream
268
+ clears AA at button-label sizes. */
269
+ color: var(--color-brand-cream);
270
+ pointer-events: none;
271
+ }
272
+
273
+ :global(.cta-hover__arrow) {
274
+ width: 16px;
275
+ height: 16px;
276
+ flex-shrink: 0;
277
+ }
278
+
279
+ /* Coral fill, heavier than the ink default. It comes after the .dark rules
280
+ so one set of colors covers both themes. */
281
+ .cta-hover--coral {
282
+ /* Cream label on terra-600: on terra-400 it's 3.24:1 and fails AA. */
283
+ color: var(--color-brand-cream);
284
+ }
285
+
286
+ .cta-hover--coral::after {
287
+ background: var(--color-terra-600);
288
+ }
289
+
290
+ .cta-hover--coral.cta-hover--no-beam {
291
+ background: var(--color-terra-600);
292
+ }
293
+
294
+ .cta-hover--coral::before,
295
+ :global(.dark) .cta-hover--coral::before {
296
+ background: conic-gradient(
297
+ transparent 0deg,
298
+ transparent 220deg,
299
+ var(--color-brand-cream) 280deg,
300
+ var(--color-terra-200) 320deg,
301
+ transparent 360deg
302
+ );
303
+ }
304
+
305
+ /* Ink dot on coral, so hover turns the button black under a cream label
306
+ instead of washing it out to cream. */
307
+ .cta-hover--coral .cta-hover__dot,
308
+ :global(.dark) .cta-hover--coral .cta-hover__dot {
309
+ background: var(--color-brand-ink);
310
+ }
311
+
312
+ @media (hover: hover) and (pointer: fine) {
313
+ .cta-hover:hover::before {
314
+ opacity: 0;
315
+ }
316
+
317
+ .cta-hover:hover .cta-hover__dot {
318
+ transform: scale(100);
319
+ }
320
+
321
+ .cta-hover:not(.cta-hover--no-dot):hover .cta-hover__resting .cta-hover__label {
322
+ transform: translateX(48px);
323
+ opacity: 0;
324
+ }
325
+
326
+ .cta-hover:not(.cta-hover--no-dot):hover .cta-hover__reveal {
327
+ opacity: 1;
328
+ transform: translateX(-4px);
329
+ }
330
+ }
331
+
332
+ @media (prefers-reduced-motion: reduce) {
333
+ .cta-hover::before {
334
+ animation: none;
335
+ opacity: 0;
336
+ }
337
+ }
338
+ </style>
@@ -0,0 +1,47 @@
1
+ /* The glow and the hover edge read `--magic-card-glow`, so a caller picks the
2
+ brand colour with a class: `[--magic-card-glow:var(--brand-accent)]`. */
3
+ .magic-card {
4
+ transition:
5
+ border-color 300ms ease,
6
+ box-shadow 300ms ease;
7
+ }
8
+
9
+ .magic-card:hover {
10
+ border-color: color-mix(in oklch, var(--magic-card-glow, var(--color-primary)) 30%, var(--color-border));
11
+ box-shadow: 0 0 0 1px color-mix(in oklch, var(--magic-card-glow, var(--color-primary)) 8%, transparent);
12
+ }
13
+
14
+ .magic-card:focus-within {
15
+ outline: 2px solid var(--magic-card-glow, var(--color-primary));
16
+ outline-offset: 2px;
17
+ }
18
+
19
+ .magic-card__glow {
20
+ position: absolute;
21
+ inset: 0;
22
+ border-radius: inherit;
23
+ pointer-events: none;
24
+ opacity: 0;
25
+ transition: opacity 300ms ease;
26
+ background: radial-gradient(
27
+ var(--magic-card-radius, 500px) circle at var(--magic-card-x, 50%) var(--magic-card-y, 50%),
28
+ color-mix(
29
+ in oklch,
30
+ var(--magic-card-glow, var(--color-primary)) var(--magic-card-strength, 6%),
31
+ transparent
32
+ )
33
+ 0%,
34
+ transparent 40%
35
+ );
36
+ }
37
+
38
+ .magic-card:hover .magic-card__glow {
39
+ opacity: 1;
40
+ }
41
+
42
+ @media (prefers-reduced-motion: reduce) {
43
+ .magic-card,
44
+ .magic-card__glow {
45
+ transition: none;
46
+ }
47
+ }
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import "./MagicCard.css";
6
+ import { cn } from "@heyramzi/utils";
7
+ import type { Snippet } from "svelte";
8
+
9
+ // =========================
10
+ // TYPES
11
+ // =========================
12
+ interface Props {
13
+ children: Snippet;
14
+ /** Draw the pointer-following glow. Off leaves the frosted card and the hover edge. */
15
+ glow?: boolean;
16
+ /** Glow radius in px. */
17
+ radius?: number;
18
+ class?: string;
19
+ /** Classes for the padded box around the children. */
20
+ contentClass?: string;
21
+ }
22
+
23
+ // =========================
24
+ // PROPS
25
+ // =========================
26
+ let { children, glow = true, radius = 500, class: className, contentClass }: Props = $props();
27
+
28
+ // =========================
29
+ // FUNCTIONS
30
+ // =========================
31
+ function handlePointerMove(event: PointerEvent) {
32
+ const card = event.currentTarget as HTMLElement;
33
+ const rect = card.getBoundingClientRect();
34
+ card.style.setProperty("--magic-card-x", `${event.clientX - rect.left}px`);
35
+ card.style.setProperty("--magic-card-y", `${event.clientY - rect.top}px`);
36
+ }
37
+ </script>
38
+
39
+ <!-- =========================== -->
40
+ <!-- MARKUP -->
41
+ <!-- =========================== -->
42
+ <div
43
+ role="group"
44
+ onpointermove={glow ? handlePointerMove : undefined}
45
+ style:--magic-card-radius="{radius}px"
46
+ class={cn(
47
+ "magic-card relative overflow-hidden rounded-lg border border-border/50 bg-card/85 shadow-xs backdrop-blur-md [--magic-card-strength:6%] dark:border-border/40 dark:bg-card/70 dark:[--magic-card-strength:10%]",
48
+ className,
49
+ )}
50
+ >
51
+ {#if glow}
52
+ <div class="magic-card__glow" aria-hidden="true"></div>
53
+ {/if}
54
+ <div class={cn("relative z-[1] p-6", contentClass)}>
55
+ {@render children()}
56
+ </div>
57
+ </div>
@@ -14,7 +14,7 @@
14
14
  flex-shrink: 0;
15
15
  justify-content: space-around;
16
16
  gap: var(--gap, 1rem);
17
- animation: marquee-x var(--duration, 40s) linear infinite;
17
+ animation: marquee-x var(--duration, 40s) linear var(--marquee-delay, 0s) infinite;
18
18
  }
19
19
  .marquee--vertical .marquee__track {
20
20
  flex-direction: column;
@@ -23,6 +23,32 @@
23
23
  .marquee__track--reverse {
24
24
  animation-direction: reverse;
25
25
  }
26
+ .marquee[data-overflow-only] {
27
+ width: fit-content;
28
+ max-width: 100%;
29
+ padding: 0;
30
+ }
31
+ .marquee[data-still] .marquee__track {
32
+ animation: none;
33
+ }
34
+ .marquee[data-fade] {
35
+ mask-image: linear-gradient(
36
+ to right,
37
+ transparent,
38
+ black var(--marquee-fade),
39
+ black calc(100% - var(--marquee-fade)),
40
+ transparent
41
+ );
42
+ }
43
+ .marquee--vertical[data-fade] {
44
+ mask-image: linear-gradient(
45
+ to bottom,
46
+ transparent,
47
+ black var(--marquee-fade),
48
+ black calc(100% - var(--marquee-fade)),
49
+ transparent
50
+ );
51
+ }
26
52
  .marquee[data-pause-hover]:hover .marquee__track {
27
53
  animation-play-state: paused;
28
54
  }
@@ -18,6 +18,18 @@
18
18
  repeat?: number;
19
19
  duration?: string;
20
20
  gap?: string;
21
+ /** Scroll speed in px/s. Sets the loop time from the measured track, and wins over `duration`. */
22
+ speed?: number;
23
+ /** Fades both ends along the scroll axis. Any CSS length, such as `16px` or `13%`. */
24
+ fade?: string;
25
+ /** Waits this many ms before the first scroll starts. */
26
+ delay?: number;
27
+ /**
28
+ * Scrolls only while the content is longer than the box, for a line of text with a max width.
29
+ * The box shrinks to its content, drops its padding, and shows one still copy when it fits.
30
+ */
31
+ overflowOnly?: boolean;
32
+ style?: string;
21
33
  }
22
34
 
23
35
  // =========================
@@ -32,6 +44,11 @@
32
44
  repeat = 4,
33
45
  duration,
34
46
  gap,
47
+ speed,
48
+ fade,
49
+ delay,
50
+ overflowOnly = false,
51
+ style,
35
52
  }: MarqueeProps = $props();
36
53
 
37
54
  // =========================
@@ -40,6 +57,9 @@
40
57
  let rootEl = $state<HTMLDivElement | null>(null);
41
58
  let isVisible = $state(true);
42
59
  let tabActive = $state(true);
60
+ let boxSize = $state(0);
61
+ let trackSize = $state(0);
62
+ let gapSize = $state(0);
43
63
 
44
64
  // =========================
45
65
  // DERIVED
@@ -48,10 +68,27 @@
48
68
  // background-tab marquee stops burning paint/compositor budget.
49
69
  let paused = $derived(!isVisible || !tabActive);
50
70
 
71
+ // WHY: the 0.5px slack absorbs the rounding between a fractional text width and its box.
72
+ let overflowing = $derived(trackSize > boxSize + 0.5);
73
+ let still = $derived(overflowOnly && !overflowing);
74
+ // WHY: two copies loop seamlessly once one copy is wider than the box.
75
+ let copies = $derived(overflowOnly ? (overflowing ? 2 : 1) : repeat);
76
+ let loopSeconds = $derived(
77
+ speed && trackSize > 0 ? (trackSize + gapSize) / speed : undefined,
78
+ );
79
+
51
80
  // WHY: only emit the CSS vars that were passed as props, so a consumer can
52
81
  // still override timing or spacing with a `[--duration:24s]` utility class.
53
82
  let styleVars = $derived(
54
- [duration && `--duration:${duration}`, gap && `--gap:${gap}`].filter(Boolean).join(";"),
83
+ [
84
+ loopSeconds ? `--duration:${loopSeconds}s` : duration && `--duration:${duration}`,
85
+ gap && `--gap:${gap}`,
86
+ fade && `--marquee-fade:${fade}`,
87
+ delay && `--marquee-delay:${delay}ms`,
88
+ style,
89
+ ]
90
+ .filter(Boolean)
91
+ .join(";"),
55
92
  );
56
93
 
57
94
  // =========================
@@ -69,6 +106,27 @@
69
106
  return () => observer.disconnect();
70
107
  });
71
108
 
109
+ $effect(() => {
110
+ const firstTrack = rootEl?.firstElementChild;
111
+ if (!rootEl || !firstTrack || !(speed || overflowOnly)) return;
112
+ const root = rootEl;
113
+ const measure = () => {
114
+ const computed = getComputedStyle(root);
115
+ const box = root.getBoundingClientRect();
116
+ const track = firstTrack.getBoundingClientRect();
117
+ const padStart = parseFloat(vertical ? computed.paddingTop : computed.paddingLeft) || 0;
118
+ const padEnd = parseFloat(vertical ? computed.paddingBottom : computed.paddingRight) || 0;
119
+ boxSize = (vertical ? box.height : box.width) - padStart - padEnd;
120
+ trackSize = vertical ? track.height : track.width;
121
+ gapSize = parseFloat(vertical ? computed.rowGap : computed.columnGap) || 0;
122
+ };
123
+ measure();
124
+ const observer = new ResizeObserver(measure);
125
+ observer.observe(root);
126
+ observer.observe(firstTrack);
127
+ return () => observer.disconnect();
128
+ });
129
+
72
130
  $effect(() => {
73
131
  const onVisibility = () => {
74
132
  tabActive = document.visibilityState === "visible";
@@ -87,9 +145,15 @@
87
145
  style={styleVars}
88
146
  data-pause-hover={pauseOnHover ? "" : undefined}
89
147
  data-paused={paused ? "" : undefined}
148
+ data-overflow-only={overflowOnly ? "" : undefined}
149
+ data-still={still ? "" : undefined}
150
+ data-fade={fade && !still ? "" : undefined}
90
151
  >
91
- {#each Array(repeat).fill(0) as _, i (i)}
92
- <div class={cn("marquee__track", reverse && "marquee__track--reverse")}>
152
+ {#each Array(copies).fill(0) as _, i (i)}
153
+ <div
154
+ class={cn("marquee__track", reverse && "marquee__track--reverse")}
155
+ aria-hidden={i > 0 ? "true" : undefined}
156
+ >
93
157
  {@render children?.()}
94
158
  </div>
95
159
  {/each}
@@ -1,10 +1,11 @@
1
1
  export { default as InView } from "./InView.svelte";
2
2
  export { default as AnimatedGroup } from "./AnimatedGroup.svelte";
3
3
  export { default as TextEffect } from "./TextEffect.svelte";
4
- export { observeInView } from "./observe.js";
4
+ export { observeInView, onInView } from "./observe.js";
5
5
  export type {
6
6
  RevealPreset,
7
7
  RevealPer,
8
8
  RevealTiming,
9
9
  RevealViewOptions,
10
+ InViewActionOptions,
10
11
  } from "./types.js";
@@ -0,0 +1,60 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
2
+ import { onInView } from "./observe.js";
3
+
4
+ type Callback = (entries: { isIntersecting: boolean }[]) => void;
5
+
6
+ let callbacks: Callback[] = [];
7
+
8
+ class FakeObserver {
9
+ constructor(callback: Callback) {
10
+ callbacks.push(callback);
11
+ }
12
+ observe() {}
13
+ disconnect() {}
14
+ }
15
+
16
+ function fire(isIntersecting: boolean) {
17
+ for (const callback of callbacks) callback([{ isIntersecting }]);
18
+ }
19
+
20
+ describe("onInView", () => {
21
+ beforeEach(() => {
22
+ callbacks = [];
23
+ vi.stubGlobal("IntersectionObserver", FakeObserver);
24
+ });
25
+
26
+ afterEach(() => {
27
+ vi.unstubAllGlobals();
28
+ });
29
+
30
+ it("calls onEnter once when the node first shows", () => {
31
+ const onEnter = vi.fn();
32
+ onInView(document.createElement("div"), { onEnter });
33
+ fire(true);
34
+ fire(true);
35
+ expect(onEnter).toHaveBeenCalledTimes(1);
36
+ });
37
+
38
+ it("calls onLeave only after an enter, and only when once is off", () => {
39
+ const onEnter = vi.fn();
40
+ const onLeave = vi.fn();
41
+ onInView(document.createElement("div"), { once: false, onEnter, onLeave });
42
+ fire(false);
43
+ expect(onLeave).not.toHaveBeenCalled();
44
+ fire(true);
45
+ fire(false);
46
+ fire(true);
47
+ expect(onEnter).toHaveBeenCalledTimes(2);
48
+ expect(onLeave).toHaveBeenCalledTimes(1);
49
+ });
50
+
51
+ it("reads the callbacks from the latest update", () => {
52
+ const first = vi.fn();
53
+ const second = vi.fn();
54
+ const action = onInView(document.createElement("div"), { onEnter: first });
55
+ action?.update?.({ onEnter: second });
56
+ fire(true);
57
+ expect(first).not.toHaveBeenCalled();
58
+ expect(second).toHaveBeenCalledTimes(1);
59
+ });
60
+ });