@heyramzi/ui 7.0.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 (253) hide show
  1. package/COMPONENTS.md +28 -17
  2. package/README.md +5 -1
  3. package/package.json +25 -15
  4. package/scripts/sync-shadcn.mjs +17 -13
  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 -6
  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 +14 -14
  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/theme-transition.ts +37 -11
  118. package/src/lib/tone.ts +48 -0
  119. package/src/shadcn/accordion/accordion-trigger.svelte +15 -6
  120. package/src/shadcn/alert/alert.svelte +3 -1
  121. package/src/shadcn/badge/badge.svelte +8 -7
  122. package/src/shadcn/breadcrumb/breadcrumb-ellipsis.svelte +2 -2
  123. package/src/shadcn/breadcrumb/breadcrumb-separator.svelte +2 -2
  124. package/src/shadcn/calendar/calendar-month-select.svelte +2 -2
  125. package/src/shadcn/calendar/calendar-next-button.svelte +2 -2
  126. package/src/shadcn/calendar/calendar-prev-button.svelte +2 -2
  127. package/src/shadcn/calendar/calendar-year-select.svelte +2 -2
  128. package/src/shadcn/calendar/calendar.svelte +6 -0
  129. package/src/shadcn/code-block/code-block-copy-button.svelte +6 -55
  130. package/src/shadcn/command/index.ts +3 -0
  131. package/src/shadcn/field/index.ts +3 -0
  132. package/src/shadcn/field/simple-field.svelte +71 -0
  133. package/src/shadcn/field/simple-field.test.ts +47 -0
  134. package/src/shadcn/message-scroller/message-scroller-button.svelte +2 -2
  135. package/src/shadcn/radio-group/radio-group-item.svelte +3 -2
  136. package/src/shadcn/sheet/sheet-content.svelte +1 -0
  137. package/src/shadcn/sheet/sheet.css +48 -0
  138. package/src/shadcn/sonner/sonner.svelte +7 -10
  139. package/src/shadcn/toggle-group/toggle-group.svelte +10 -3
  140. package/src/shadcn/trace/trace-collapsible-trigger.svelte +2 -2
  141. package/src/ai/composer/Composer.css +0 -108
  142. package/src/ai/composer/Composer.svelte +0 -111
  143. package/src/ai/composer/index.ts +0 -10
  144. package/src/animations/accordion.css +0 -58
  145. package/src/animations/icon-swap.css +0 -43
  146. package/src/animations/notification-badge.css +0 -81
  147. package/src/animations/panel.css +0 -86
  148. package/src/animations/reasoning-stream.css +0 -49
  149. package/src/animations/skeleton-reveal.css +0 -97
  150. package/src/animations/streaming-text.css +0 -31
  151. package/src/animations/tabs-sliding.css +0 -83
  152. package/src/animations/texts-reveal.css +0 -70
  153. package/src/animations/tooltip.css +0 -72
  154. package/src/animations/view-transition-reveal.ts +0 -74
  155. package/src/lib/utils.ts +0 -45
  156. package/src/showcase/AiSection.svelte +0 -150
  157. package/src/showcase/AnimationsSection.svelte +0 -1658
  158. package/src/showcase/EffectsSection.svelte +0 -152
  159. package/src/showcase/ShadcnSection.svelte +0 -27
  160. package/src/showcase/ShowcaseImport.svelte +0 -126
  161. package/src/showcase/ShowcaseList.svelte +0 -132
  162. package/src/showcase/ShowcaseNav.svelte +0 -247
  163. package/src/showcase/ShowcaseSection.svelte +0 -112
  164. package/src/showcase/ShowcaseShell.svelte +0 -55
  165. package/src/showcase/ShowcaseStage.svelte +0 -342
  166. package/src/showcase/ShowcaseVariant.svelte +0 -109
  167. package/src/showcase/assets/image-reveal/art-1.jpg +0 -0
  168. package/src/showcase/assets/image-reveal/art-2.jpg +0 -0
  169. package/src/showcase/assets/image-reveal/art-3.jpg +0 -0
  170. package/src/showcase/check.svg +0 -1
  171. package/src/showcase/chevron-arms.svg +0 -2
  172. package/src/showcase/chevron-down.svg +0 -1
  173. package/src/showcase/copy.svg +0 -2
  174. package/src/showcase/demos/ai/confirmation.svelte +0 -59
  175. package/src/showcase/demos/ai/loading-state.svelte +0 -19
  176. package/src/showcase/demos/ai/prompt-input.svelte +0 -57
  177. package/src/showcase/demos/ai/reasoning.svelte +0 -19
  178. package/src/showcase/demos/ai/thinking-dots.svelte +0 -15
  179. package/src/showcase/demos/ai/voice-waveform.svelte +0 -24
  180. package/src/showcase/demos/effects/animated-beam.svelte +0 -50
  181. package/src/showcase/demos/effects/animated-group.svelte +0 -47
  182. package/src/showcase/demos/effects/border-beam-default.svelte +0 -20
  183. package/src/showcase/demos/effects/border-beam-line.svelte +0 -20
  184. package/src/showcase/demos/effects/border-beam-mono.svelte +0 -20
  185. package/src/showcase/demos/effects/border-beam-sunset.svelte +0 -20
  186. package/src/showcase/demos/effects/image-comparison.svelte +0 -25
  187. package/src/showcase/demos/effects/image-reveal.svelte +0 -49
  188. package/src/showcase/demos/effects/in-view.svelte +0 -41
  189. package/src/showcase/demos/effects/integrations-hub.svelte +0 -82
  190. package/src/showcase/demos/effects/magnetic.svelte +0 -31
  191. package/src/showcase/demos/effects/marquee.svelte +0 -33
  192. package/src/showcase/demos/effects/progressive-blur.svelte +0 -47
  193. package/src/showcase/demos/effects/spotlight.svelte +0 -40
  194. package/src/showcase/demos/effects/text-effect.svelte +0 -55
  195. package/src/showcase/demos/shadcn/accordion.svelte +0 -45
  196. package/src/showcase/demos/shadcn/alert-dialog.svelte +0 -23
  197. package/src/showcase/demos/shadcn/alert.svelte +0 -30
  198. package/src/showcase/demos/shadcn/attachment.svelte +0 -71
  199. package/src/showcase/demos/shadcn/avatar.svelte +0 -30
  200. package/src/showcase/demos/shadcn/badge.svelte +0 -26
  201. package/src/showcase/demos/shadcn/breadcrumb.svelte +0 -34
  202. package/src/showcase/demos/shadcn/bubble.svelte +0 -36
  203. package/src/showcase/demos/shadcn/button.svelte +0 -11
  204. package/src/showcase/demos/shadcn/calendar.svelte +0 -8
  205. package/src/showcase/demos/shadcn/card.svelte +0 -39
  206. package/src/showcase/demos/shadcn/checkbox.svelte +0 -39
  207. package/src/showcase/demos/shadcn/code-block.svelte +0 -80
  208. package/src/showcase/demos/shadcn/collapsible.svelte +0 -20
  209. package/src/showcase/demos/shadcn/command.svelte +0 -48
  210. package/src/showcase/demos/shadcn/context-menu.svelte +0 -54
  211. package/src/showcase/demos/shadcn/demo-harness.svelte +0 -12
  212. package/src/showcase/demos/shadcn/demos.test.ts +0 -20
  213. package/src/showcase/demos/shadcn/dialog.svelte +0 -38
  214. package/src/showcase/demos/shadcn/dropdown-menu.svelte +0 -59
  215. package/src/showcase/demos/shadcn/empty.svelte +0 -29
  216. package/src/showcase/demos/shadcn/field.svelte +0 -115
  217. package/src/showcase/demos/shadcn/hover-card.svelte +0 -32
  218. package/src/showcase/demos/shadcn/index.ts +0 -382
  219. package/src/showcase/demos/shadcn/input-otp.svelte +0 -19
  220. package/src/showcase/demos/shadcn/input.svelte +0 -5
  221. package/src/showcase/demos/shadcn/label.svelte +0 -11
  222. package/src/showcase/demos/shadcn/marker.svelte +0 -30
  223. package/src/showcase/demos/shadcn/message-scroller.svelte +0 -44
  224. package/src/showcase/demos/shadcn/message.svelte +0 -75
  225. package/src/showcase/demos/shadcn/popover.svelte +0 -36
  226. package/src/showcase/demos/shadcn/progress.svelte +0 -13
  227. package/src/showcase/demos/shadcn/radio-group.svelte +0 -19
  228. package/src/showcase/demos/shadcn/scroll-area.svelte +0 -18
  229. package/src/showcase/demos/shadcn/select.svelte +0 -29
  230. package/src/showcase/demos/shadcn/separator.svelte +0 -12
  231. package/src/showcase/demos/shadcn/sheet.svelte +0 -48
  232. package/src/showcase/demos/shadcn/skeleton.svelte +0 -11
  233. package/src/showcase/demos/shadcn/slider.svelte +0 -7
  234. package/src/showcase/demos/shadcn/sonner.svelte +0 -18
  235. package/src/showcase/demos/shadcn/spinner.svelte +0 -11
  236. package/src/showcase/demos/shadcn/switch.svelte +0 -9
  237. package/src/showcase/demos/shadcn/table.svelte +0 -76
  238. package/src/showcase/demos/shadcn/tabs.svelte +0 -62
  239. package/src/showcase/demos/shadcn/textarea.svelte +0 -5
  240. package/src/showcase/demos/shadcn/toggle-group.svelte +0 -18
  241. package/src/showcase/demos/shadcn/toggle.svelte +0 -14
  242. package/src/showcase/demos/shadcn/tooltip.svelte +0 -13
  243. package/src/showcase/demos/shadcn/trace.svelte +0 -51
  244. package/src/showcase/heart.svg +0 -1
  245. package/src/showcase/index.ts +0 -38
  246. package/src/showcase/kit.test.ts +0 -72
  247. package/src/showcase/kit.ts +0 -16
  248. package/src/showcase/plus.svg +0 -1
  249. package/src/showcase/showcase-shell.css +0 -91
  250. package/src/showcase/stacked-section.css +0 -15
  251. package/src/showcase/success-check.svg +0 -1
  252. package/src/showcase/types.ts +0 -29
  253. package/src/svg-raw.d.ts +0 -7
@@ -1,4 +1,5 @@
1
- import type { RevealViewOptions } from "./types.js";
1
+ import type { Action } from "svelte/action";
2
+ import type { InViewActionOptions, RevealViewOptions } from "./types.js";
2
3
 
3
4
  /**
4
5
  * Runs `onChange` when `el` enters or leaves the viewport. Returns the teardown.
@@ -31,3 +32,34 @@ export function observeInView(
31
32
  observer.observe(el);
32
33
  return () => observer.disconnect();
33
34
  }
35
+
36
+ /**
37
+ * Svelte action that calls back when the node enters or leaves the viewport.
38
+ * Options are read at call time, so a `use:onInView={...}` update swaps the callbacks.
39
+ *
40
+ * @example
41
+ * <section use:onInView={{ amount: 0.2, onEnter: () => (visible = true) }}>
42
+ */
43
+ export const onInView: Action<HTMLElement, InViewActionOptions | undefined> = (node, initial) => {
44
+ let options = initial ?? {};
45
+ let entered = false;
46
+
47
+ const { amount = 0.1, margin, once } = options;
48
+
49
+ const stop = observeInView(node, { amount, margin, once }, (inView) => {
50
+ if (inView && !entered) {
51
+ entered = true;
52
+ options.onEnter?.();
53
+ } else if (!inView && entered) {
54
+ entered = false;
55
+ options.onLeave?.();
56
+ }
57
+ });
58
+
59
+ return {
60
+ update(next) {
61
+ options = next ?? {};
62
+ },
63
+ destroy: stop,
64
+ };
65
+ };
@@ -23,3 +23,10 @@ export type RevealViewOptions = {
23
23
  /** `rootMargin` for the observer, e.g. `"0px 0px -15% 0px"`. */
24
24
  margin?: string;
25
25
  };
26
+
27
+ export type InViewActionOptions = RevealViewOptions & {
28
+ /** Called when the node enters the viewport. */
29
+ onEnter?: () => void;
30
+ /** Called when the node leaves the viewport. Only fires when `once` is false. */
31
+ onLeave?: () => void;
32
+ };
@@ -1,9 +1,3 @@
1
- ::view-transition-old(root),
2
- ::view-transition-new(root) {
3
- animation: none;
4
- mix-blend-mode: normal;
5
- }
6
-
7
1
  .theme-toggle:active {
8
2
  transform: scale(0.97);
9
3
  }
@@ -19,3 +13,202 @@
19
13
  background: color-mix(in oklch, var(--color-background) 12%, transparent);
20
14
  color: var(--color-background);
21
15
  }
16
+
17
+ /* The slide variant: a pill track with a thumb that moves to the active side.
18
+ A caller sets brand colours with custom properties on the class:
19
+ --theme-toggle-thumb-light, --theme-toggle-thumb-dark, --theme-toggle-track,
20
+ --theme-toggle-track-hover, --theme-toggle-sun, --theme-toggle-moon,
21
+ --theme-toggle-thumb-icon and --theme-toggle-glow. */
22
+ .theme-toggle-slide {
23
+ --tt-thumb: 1.5rem;
24
+ --tt-pad: 0.25rem;
25
+ position: relative;
26
+ display: flex;
27
+ align-items: center;
28
+ box-sizing: content-box;
29
+ width: calc(var(--tt-thumb) * 2);
30
+ height: var(--tt-thumb);
31
+ padding: var(--tt-pad);
32
+ border: 0;
33
+ border-radius: 9999px;
34
+ cursor: pointer;
35
+ background: var(
36
+ --theme-toggle-track,
37
+ color-mix(in oklch, var(--color-foreground) 10%, transparent)
38
+ );
39
+ transition:
40
+ background-color 300ms,
41
+ box-shadow 300ms,
42
+ transform 300ms;
43
+ }
44
+
45
+ .theme-toggle-slide--lg {
46
+ --tt-thumb: 2rem;
47
+ }
48
+
49
+ .theme-toggle-slide:hover {
50
+ background: var(
51
+ --theme-toggle-track-hover,
52
+ color-mix(in oklch, var(--color-foreground) 20%, transparent)
53
+ );
54
+ box-shadow: 0 0 15px color-mix(in oklch, var(--theme-toggle-glow, transparent) 30%, transparent);
55
+ }
56
+
57
+ .theme-toggle-slide:active {
58
+ transform: scale(0.97);
59
+ }
60
+
61
+ .theme-toggle-slide:focus-visible {
62
+ outline: 2px solid var(--color-ring, var(--color-primary));
63
+ outline-offset: 2px;
64
+ }
65
+
66
+ .theme-toggle-slide__glow {
67
+ position: absolute;
68
+ inset: 0;
69
+ border-radius: inherit;
70
+ pointer-events: none;
71
+ opacity: 0;
72
+ background: radial-gradient(
73
+ circle at var(--tt-glow-x, 25%) 50%,
74
+ color-mix(in oklch, var(--theme-toggle-glow, transparent) 20%, transparent),
75
+ transparent 70%
76
+ );
77
+ transition: opacity 500ms;
78
+ }
79
+
80
+ .theme-toggle-slide:hover .theme-toggle-slide__glow {
81
+ opacity: 1;
82
+ }
83
+
84
+ .theme-toggle-slide[data-state="dark"] {
85
+ --tt-glow-x: 75%;
86
+ }
87
+
88
+ .theme-toggle-slide__icon {
89
+ position: absolute;
90
+ display: flex;
91
+ opacity: 0.3;
92
+ transform: scale(0.75);
93
+ transition:
94
+ opacity 300ms,
95
+ transform 300ms;
96
+ }
97
+
98
+ .theme-toggle-slide__icon--sun {
99
+ left: calc(var(--tt-pad) + (var(--tt-thumb) - 14px) / 2);
100
+ color: var(--theme-toggle-sun, var(--color-muted-foreground));
101
+ }
102
+
103
+ .theme-toggle-slide__icon--moon {
104
+ right: calc(var(--tt-pad) + (var(--tt-thumb) - 14px) / 2);
105
+ color: var(--theme-toggle-moon, var(--color-muted-foreground));
106
+ }
107
+
108
+ .theme-toggle-slide[data-state="light"] .theme-toggle-slide__icon--sun,
109
+ .theme-toggle-slide[data-state="dark"] .theme-toggle-slide__icon--moon {
110
+ opacity: 0.7;
111
+ transform: scale(1);
112
+ }
113
+
114
+ .theme-toggle-slide[data-state="light"] .theme-toggle-slide__icon--sun {
115
+ animation: theme-toggle-spin 8s linear infinite;
116
+ }
117
+
118
+ .theme-toggle-slide[data-state="dark"] .theme-toggle-slide__icon--moon {
119
+ animation: theme-toggle-wiggle 2s ease-in-out infinite;
120
+ }
121
+
122
+ .theme-toggle-slide__thumb {
123
+ --tt-thumb-color: var(--theme-toggle-thumb-light, var(--color-primary));
124
+ position: relative;
125
+ display: flex;
126
+ align-items: center;
127
+ justify-content: center;
128
+ width: var(--tt-thumb);
129
+ height: var(--tt-thumb);
130
+ border-radius: 9999px;
131
+ background: var(--tt-thumb-color);
132
+ color: var(--theme-toggle-thumb-icon, var(--color-primary-foreground));
133
+ box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
134
+ transition:
135
+ transform 500ms cubic-bezier(0.165, 0.84, 0.44, 1),
136
+ background-color 500ms;
137
+ }
138
+
139
+ .theme-toggle-slide[data-state="dark"] .theme-toggle-slide__thumb {
140
+ --tt-thumb-color: var(--theme-toggle-thumb-dark, var(--color-primary));
141
+ transform: translateX(var(--tt-thumb));
142
+ }
143
+
144
+ .theme-toggle-slide__ring {
145
+ position: absolute;
146
+ inset: 0;
147
+ border-radius: inherit;
148
+ background: color-mix(in oklch, var(--tt-thumb-color) 50%, transparent);
149
+ animation: theme-toggle-pulse 2s ease-in-out infinite;
150
+ pointer-events: none;
151
+ }
152
+
153
+ .theme-toggle-slide__mark {
154
+ position: absolute;
155
+ transition:
156
+ opacity 300ms,
157
+ transform 300ms;
158
+ }
159
+
160
+ .theme-toggle-slide__mark--sun {
161
+ opacity: 1;
162
+ transform: scale(1) rotate(0);
163
+ }
164
+
165
+ .theme-toggle-slide__mark--moon {
166
+ opacity: 0;
167
+ transform: scale(0) rotate(-90deg);
168
+ }
169
+
170
+ .theme-toggle-slide[data-state="dark"] .theme-toggle-slide__mark--sun {
171
+ opacity: 0;
172
+ transform: scale(0) rotate(90deg);
173
+ }
174
+
175
+ .theme-toggle-slide[data-state="dark"] .theme-toggle-slide__mark--moon {
176
+ opacity: 1;
177
+ transform: scale(1) rotate(0);
178
+ }
179
+
180
+ @keyframes theme-toggle-spin {
181
+ to {
182
+ transform: rotate(360deg);
183
+ }
184
+ }
185
+
186
+ @keyframes theme-toggle-wiggle {
187
+ 0%,
188
+ 100% {
189
+ transform: rotate(-5deg);
190
+ }
191
+ 50% {
192
+ transform: rotate(5deg);
193
+ }
194
+ }
195
+
196
+ @keyframes theme-toggle-pulse {
197
+ 0%,
198
+ 100% {
199
+ transform: scale(1);
200
+ opacity: 0;
201
+ }
202
+ 50% {
203
+ transform: scale(1.5);
204
+ opacity: 0.4;
205
+ }
206
+ }
207
+
208
+ @media (prefers-reduced-motion: reduce) {
209
+ .theme-toggle-slide,
210
+ .theme-toggle-slide * {
211
+ animation: none !important;
212
+ transition: none !important;
213
+ }
214
+ }
@@ -4,9 +4,7 @@
4
4
  // =========================
5
5
  import "./ThemeToggle.css";
6
6
  import { cn } from "@heyramzi/utils";
7
- import MonitorIcon from "@lucide/svelte/icons/monitor";
8
- import MoonIcon from "@lucide/svelte/icons/moon";
9
- import SunIcon from "@lucide/svelte/icons/sun";
7
+ import { Monitor, Moon, Sun } from "phosphor-svelte";
10
8
  import { revealTheme } from "../../lib/theme-transition.js";
11
9
 
12
10
  // =========================
@@ -15,13 +13,15 @@
15
13
  interface Props {
16
14
  /** Used when `mode` is absent. */
17
15
  isDark?: boolean;
18
- /** The icon follows the mode: Sun, Moon or Monitor. */
16
+ /** The icon follows the mode: Sun, Moon or Monitor. The slide variant has no third state, so `system` reads as light. */
19
17
  mode?: "light" | "dark" | "system";
20
18
  /** Fires after the wipe has started. The caller flips the theme here. */
21
19
  onToggle: (event: MouseEvent) => void;
22
- variant?: "outline" | "ghost" | "inverted";
20
+ /** `slide` is a pill track with a thumb that moves between a sun and a moon. */
21
+ variant?: "outline" | "ghost" | "inverted" | "slide";
23
22
  /** Alias for `variant="inverted"`. */
24
23
  inverted?: boolean;
24
+ /** `sm` is 32px tall, `default` is 40px, in every variant. */
25
25
  size?: "default" | "sm";
26
26
  /** Overrides the aria-label and the screen-reader text. */
27
27
  label?: string;
@@ -78,24 +78,49 @@
78
78
  <!-- =========================== -->
79
79
  <!-- MARKUP -->
80
80
  <!-- =========================== -->
81
- <button
82
- onclick={handleClick}
83
- aria-label={text}
84
- {title}
85
- class={cn(
86
- "theme-toggle relative flex cursor-pointer items-center justify-center rounded-lg transition-all duration-300",
87
- size === "sm" ? "size-8" : "size-10",
88
- style === "inverted" && "theme-toggle--inverted",
89
- style === "ghost" && "text-muted-foreground hover:bg-muted/40 hover:text-foreground",
90
- style === "outline" &&
91
- "border border-border/60 bg-muted/30 text-muted-foreground hover:border-border hover:text-foreground",
92
- className,
93
- )}
94
- >
95
- <SunIcon class={iconClass(active === "light", "rotate-90")} />
96
- <MoonIcon class={iconClass(active === "dark", "-rotate-90")} />
97
- {#if mode}
98
- <MonitorIcon class={iconClass(active === "system", "rotate-90")} />
99
- {/if}
100
- <span class="sr-only">{text}</span>
101
- </button>
81
+ {#if style === "slide"}
82
+ <button
83
+ type="button"
84
+ onclick={handleClick}
85
+ aria-label={text}
86
+ {title}
87
+ data-state={active === "dark" ? "dark" : "light"}
88
+ class={cn("theme-toggle-slide", size === "default" && "theme-toggle-slide--lg", className)}
89
+ >
90
+ <span class="theme-toggle-slide__glow" aria-hidden="true"></span>
91
+ <span class="theme-toggle-slide__icon theme-toggle-slide__icon--sun" aria-hidden="true">
92
+ <Sun size={14} weight="fill" />
93
+ </span>
94
+ <span class="theme-toggle-slide__icon theme-toggle-slide__icon--moon" aria-hidden="true">
95
+ <Moon size={14} weight="fill" />
96
+ </span>
97
+ <span class="theme-toggle-slide__thumb" aria-hidden="true">
98
+ <span class="theme-toggle-slide__ring"></span>
99
+ <Sun size={14} weight="fill" class="theme-toggle-slide__mark theme-toggle-slide__mark--sun" />
100
+ <Moon size={14} weight="fill" class="theme-toggle-slide__mark theme-toggle-slide__mark--moon" />
101
+ </span>
102
+ <span class="sr-only">{text}</span>
103
+ </button>
104
+ {:else}
105
+ <button
106
+ onclick={handleClick}
107
+ aria-label={text}
108
+ {title}
109
+ class={cn(
110
+ "theme-toggle relative flex cursor-pointer items-center justify-center rounded-lg transition-all duration-300",
111
+ size === "sm" ? "size-8" : "size-10",
112
+ style === "inverted" && "theme-toggle--inverted",
113
+ style === "ghost" && "text-muted-foreground hover:bg-muted/40 hover:text-foreground",
114
+ style === "outline" &&
115
+ "border border-border/60 bg-muted/30 text-muted-foreground hover:border-border hover:text-foreground",
116
+ className,
117
+ )}
118
+ >
119
+ <Sun class={iconClass(active === "light", "rotate-90")} />
120
+ <Moon class={iconClass(active === "dark", "-rotate-90")} />
121
+ {#if mode}
122
+ <Monitor class={iconClass(active === "system", "rotate-90")} />
123
+ {/if}
124
+ <span class="sr-only">{text}</span>
125
+ </button>
126
+ {/if}
@@ -0,0 +1,52 @@
1
+ /*
2
+ * The gallery lives on the site, one copy of every demo, and it has to hold everything this package ships.
3
+ */
4
+
5
+ /*
6
+ * These read the site's files off disk, so an animation without a stage or a synced primitive without a demo fails here and not in front of a reader.
7
+ */
8
+
9
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { describe, expect, it } from "vite-plus/test";
12
+
13
+ /** The runner's cwd is the workspace root or the package; find the package. */
14
+ function packageRoot(): string {
15
+ let dir = process.cwd();
16
+ for (;;) {
17
+ if (existsSync(join(dir, ".upstream", "shadcn.lock.json"))) return dir;
18
+ const candidate = join(dir, "packages", "ui");
19
+ if (existsSync(join(candidate, ".upstream", "shadcn.lock.json"))) return candidate;
20
+ const parent = dirname(dir);
21
+ if (parent === dir) throw new Error("packages/ui not found");
22
+ dir = parent;
23
+ }
24
+ }
25
+
26
+ const PKG = packageRoot();
27
+ const SITE = join(PKG, "..", "..", "site", "src", "lib");
28
+
29
+ describe("site gallery coverage", () => {
30
+ it("gives every animation a stage", () => {
31
+ const section = readFileSync(join(SITE, "demos", "motion.svelte"), "utf8");
32
+ const missing = readdirSync(join(PKG, "src", "animations"))
33
+ .filter((file) => file.endsWith(".css") && file !== "index.css")
34
+ .filter((file) => !section.includes(`animations/${file.replace(".css", "")}`));
35
+ expect(missing).toEqual([]);
36
+ });
37
+
38
+ it("lists every synced shadcn demo", () => {
39
+ const manifest = readFileSync(join(SITE, "shadcn-demos", "index.ts"), "utf8");
40
+ const vendored = readdirSync(join(PKG, ".upstream", "shadcn-demos"))
41
+ .filter((file) => file.endsWith(".svelte"))
42
+ .map((file) => file.replace(".svelte", ""));
43
+ expect(vendored.filter((name) => !manifest.includes(`name: "${name}"`))).toEqual([]);
44
+ });
45
+
46
+ it("keeps a demo file behind every listed primitive", () => {
47
+ const manifest = readFileSync(join(SITE, "shadcn-demos", "index.ts"), "utf8");
48
+ const listed = [...manifest.matchAll(/name: "([a-z-]+)"/g)].map((m) => m[1]);
49
+ expect(listed.length).toBeGreaterThan(0);
50
+ expect(listed.filter((name) => !existsSync(join(SITE, "shadcn-demos", `${name}.svelte`)))).toEqual([]);
51
+ });
52
+ });
package/src/index.ts CHANGED
@@ -16,6 +16,8 @@ export type { AnimatedBeamProps, AnchorPosition } from "./effects/animated-beam/
16
16
 
17
17
  export { default as Spotlight } from "./effects/spotlight/Spotlight.svelte";
18
18
 
19
+ export { default as MagicCard } from "./effects/magic-card/MagicCard.svelte";
20
+
19
21
  export { default as Marquee } from "./effects/marquee/Marquee.svelte";
20
22
 
21
23
  export { default as ImageReveal } from "./effects/image-reveal/ImageReveal.svelte";
@@ -40,10 +42,12 @@ export type {
40
42
 
41
43
  export { default as ProgressiveBlur } from "./effects/progressive-blur/ProgressiveBlur.svelte";
42
44
 
45
+ export { default as InteractiveHoverButton } from "./effects/interactive-hover-button/InteractiveHoverButton.svelte";
46
+
43
47
  export { default as ImageComparison } from "./effects/image-comparison/ImageComparison.svelte";
44
48
 
45
49
  export { magnetic } from "./effects/magnetic/magnetic.js";
46
50
  export type { MagneticOptions } from "./effects/magnetic/magnetic.js";
47
51
 
48
52
  export { revealTheme } from "./lib/theme-transition.js";
49
- export type { RevealThemeOptions } from "./lib/theme-transition.js";
53
+ export type { RevealOrigin, RevealThemeOptions } from "./lib/theme-transition.js";
package/src/lib/panel.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // different props, and their edges drifted because the classes were typed twice.
5
5
 
6
6
  // Dashboards used `Card`, so /ads had a ring and a shadow where /social had a
7
- // hairline. Flat, one hairline, `rounded-xl`, per DESIGN.md.
7
+ // hairline. Flat, one hairline, `rounded-xl`.
8
8
  export const panelShellClass = "overflow-hidden rounded-xl border border-border bg-card";
9
9
 
10
10
  /**
@@ -13,3 +13,9 @@ export const panelShellClass = "overflow-hidden rounded-xl border border-border
13
13
  */
14
14
  export const panelHeaderClass =
15
15
  "flex min-h-11 flex-wrap items-center justify-between gap-x-3 gap-y-1 border-b border-border px-4 py-2";
16
+
17
+ /**
18
+ * The inset well for a control block inside a panel. A visible step at the
19
+ * same depth, for filter rows and grouped controls.
20
+ */
21
+ export const insetWellClass = "rounded-lg border border-border bg-muted/40 p-2";
@@ -7,39 +7,41 @@ export interface RevealThemeOptions {
7
7
  easing?: string;
8
8
  }
9
9
 
10
+ /** Where the wipe grows from: a click, an element, or a point in the viewport. */
11
+ export type RevealOrigin = MouseEvent | Element | { x: number; y: number };
12
+
10
13
  const DEFAULT_DURATION = 400;
14
+ const STYLE_ID = "heyramzi-reveal-theme";
11
15
 
12
16
  /**
13
17
  * Run `apply` inside a circular wipe that grows from the centre of the clicked
14
- * element. Falls back to a plain `apply()` when the browser has no view
15
- * transitions, the user prefers reduced motion, or `skip` is set.
16
- *
17
- * The page also needs `::view-transition-old(root)` and `::view-transition-new(root)`
18
- * set to `animation: none`. `ThemeToggle` ships that CSS; a bare caller adds its own.
18
+ * element, the element, or a point. Falls back to a plain `apply()` when the
19
+ * browser has no view transitions, the user prefers reduced motion, or `skip`
20
+ * is set. It adds the `::view-transition-*(root)` reset itself.
19
21
  */
20
22
  export function revealTheme(
21
- event: MouseEvent,
23
+ origin: RevealOrigin,
22
24
  apply: () => void,
23
25
  options: RevealThemeOptions = {},
24
26
  ): void {
25
27
  const { skip = false, duration = DEFAULT_DURATION, easing = "ease-in-out" } = options;
26
- const origin = event.currentTarget;
28
+ const point = resolveOrigin(origin);
27
29
 
28
30
  const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
29
31
 
30
- if (skip || prefersReduced || !document.startViewTransition || !(origin instanceof Element)) {
32
+ if (skip || prefersReduced || !document.startViewTransition || !point) {
31
33
  apply();
32
34
  return;
33
35
  }
34
36
 
35
- const rect = origin.getBoundingClientRect();
36
- const x = rect.left + rect.width / 2;
37
- const y = rect.top + rect.height / 2;
37
+ const { x, y } = point;
38
38
  const endRadius = Math.hypot(
39
39
  Math.max(x, window.innerWidth - x),
40
40
  Math.max(y, window.innerHeight - y),
41
41
  );
42
42
 
43
+ ensureStyles();
44
+
43
45
  const transition = document.startViewTransition(() => {
44
46
  apply();
45
47
  });
@@ -51,3 +53,27 @@ export function revealTheme(
51
53
  );
52
54
  });
53
55
  }
56
+
57
+ function centreOf(element: Element): { x: number; y: number } {
58
+ const rect = element.getBoundingClientRect();
59
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
60
+ }
61
+
62
+ // A click on a child still wipes from the control that owns the handler.
63
+ function resolveOrigin(origin: RevealOrigin): { x: number; y: number } | null {
64
+ if (origin instanceof Element) return centreOf(origin);
65
+ if (origin instanceof MouseEvent) {
66
+ return origin.currentTarget instanceof Element
67
+ ? centreOf(origin.currentTarget)
68
+ : { x: origin.clientX, y: origin.clientY };
69
+ }
70
+ return origin;
71
+ }
72
+
73
+ function ensureStyles(): void {
74
+ if (document.getElementById(STYLE_ID)) return;
75
+ const style = document.createElement("style");
76
+ style.id = STYLE_ID;
77
+ style.textContent = `::view-transition-old(root),::view-transition-new(root){animation:none;mix-blend-mode:normal;}`;
78
+ document.head.appendChild(style);
79
+ }
@@ -0,0 +1,48 @@
1
+ export type Tone = "neutral" | "accent" | "info" | "success" | "warning" | "destructive";
2
+
3
+ /**
4
+ * The severity scale, as Tailwind classes for a chip and as CSS colors for the
5
+ * dot. Tokens are plain CSS variables, so a host app can retheme them. The
6
+ * accent tone reads `--tone-accent` and falls back to `--color-primary`, for a
7
+ * host whose primary is a neutral and whose accent is a brand color.
8
+ */
9
+
10
+ export const TONE_CLASS: Record<Tone, string> = {
11
+ neutral: "border-border bg-muted/40 text-muted-foreground",
12
+ accent:
13
+ "border-[color-mix(in_oklab,var(--tone-accent,var(--color-primary))_30%,transparent)] bg-[color-mix(in_oklab,var(--tone-accent,var(--color-primary))_10%,transparent)] text-[color:var(--tone-accent,var(--color-primary))]",
14
+ info: "border-info/30 bg-info/10 text-info-800 dark:text-info-400",
15
+ success: "border-success/30 bg-success/10 text-success-800 dark:text-success-400",
16
+ warning: "border-warning/30 bg-warning/10 text-warning-800 dark:text-warning-400",
17
+ destructive: "border-destructive/30 bg-destructive/10 text-destructive",
18
+ };
19
+
20
+ /** The same scale as text, for a verdict inside a line of muted facts. */
21
+ export const TONE_TEXT: Record<Tone, string> = {
22
+ neutral: "text-muted-foreground",
23
+ accent: "text-[color:var(--tone-accent,var(--color-primary))]",
24
+ info: "text-info-800 dark:text-info-400",
25
+ success: "text-success-800 dark:text-success-400",
26
+ warning: "text-warning-800 dark:text-warning-400",
27
+ destructive: "text-destructive",
28
+ };
29
+
30
+ /** The same scale as icon color. */
31
+ export const TONE_ICON: Record<Tone, string> = {
32
+ neutral: "text-muted-foreground",
33
+ accent: "text-[color:var(--tone-accent,var(--color-primary))]",
34
+ info: "text-info",
35
+ success: "text-success",
36
+ warning: "text-warning",
37
+ destructive: "text-destructive",
38
+ };
39
+
40
+ /** The scale as CSS colors, for the SVG dot. */
41
+ export const TONE_COLOR: Record<Tone, string> = {
42
+ neutral: "var(--color-muted-foreground)",
43
+ accent: "var(--tone-accent,var(--color-primary))",
44
+ info: "var(--color-info)",
45
+ success: "var(--color-success)",
46
+ warning: "var(--color-warning)",
47
+ destructive: "var(--color-destructive)",
48
+ };
@@ -2,15 +2,20 @@
2
2
  import { CaretDown } from "phosphor-svelte";
3
3
  import { Accordion as AccordionPrimitive } from "bits-ui";
4
4
  import { cn, type WithoutChild } from "@heyramzi/utils";
5
+ import type { Snippet } from "svelte";
5
6
 
6
7
  let {
7
8
  ref = $bindable(null),
8
9
  class: className,
9
10
  level = 3,
11
+ indicator,
10
12
  children,
11
13
  ...restProps
12
14
  }: WithoutChild<AccordionPrimitive.TriggerProps> & {
13
15
  level?: AccordionPrimitive.HeaderProps["level"];
16
+ /** Replaces the caret, e.g. a plus that turns into a minus. Style it off
17
+ * `group-aria-expanded/accordion-trigger:`. */
18
+ indicator?: Snippet;
14
19
  } = $props();
15
20
  </script>
16
21
 
@@ -25,11 +30,15 @@
25
30
  {...restProps}
26
31
  >
27
32
  {@render children?.()}
28
- <!-- One caret that turns over, not two that swap. A transform and not a
29
- `d:` path morph, which is Chromium-only. -->
30
- <CaretDown
31
- data-slot="accordion-trigger-icon"
32
- class="cn-accordion-trigger-icon pointer-events-none shrink-0 transition-transform duration-[var(--duration-fast)] ease-[var(--ease-smooth-out)] group-aria-expanded/accordion-trigger:rotate-180 motion-reduce:transition-none"
33
- />
33
+ {#if indicator}
34
+ {@render indicator()}
35
+ {:else}
36
+ <!-- One caret that turns over, not two that swap. A transform and not a
37
+ `d:` path morph, which is Chromium-only. -->
38
+ <CaretDown
39
+ data-slot="accordion-trigger-icon"
40
+ class="cn-accordion-trigger-icon pointer-events-none shrink-0 transition-transform duration-[var(--duration-fast)] ease-[var(--ease-smooth-out)] group-aria-expanded/accordion-trigger:rotate-180 motion-reduce:transition-none"
41
+ />
42
+ {/if}
34
43
  </AccordionPrimitive.Trigger>
35
44
  </AccordionPrimitive.Header>
@@ -26,6 +26,8 @@
26
26
  ref = $bindable(null),
27
27
  class: className,
28
28
  variant = "default",
29
+ // A caller whose message isn't urgent passes `status`, so a screen reader doesn't interrupt.
30
+ role = "alert",
29
31
  children,
30
32
  ...restProps
31
33
  }: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
@@ -38,7 +40,7 @@
38
40
  data-slot="alert"
39
41
  class={cn(alertVariants({ variant }), className)}
40
42
  {...restProps}
41
- role="alert"
43
+ {role}
42
44
  >
43
45
  {@render children?.()}
44
46
  </div>