fancy-ui-svelte 0.7.0 → 0.9.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 (58) hide show
  1. package/README.md +107 -81
  2. package/dist/fancy-ui/animated-testimonials/README.md +44 -0
  3. package/dist/fancy-ui/animated-tooltip/README.md +40 -0
  4. package/dist/fancy-ui/apple-card-carousel/README.md +46 -0
  5. package/dist/fancy-ui/bento-grid/BentoGrid.test.d.ts +1 -0
  6. package/dist/fancy-ui/bento-grid/BentoGrid.test.js +109 -0
  7. package/dist/fancy-ui/border-beam/README.md +37 -0
  8. package/dist/fancy-ui/card-3d/CardContainer.test.d.ts +1 -0
  9. package/dist/fancy-ui/card-3d/CardContainer.test.js +118 -0
  10. package/dist/fancy-ui/card-spotlight/CardSpotlight.test.d.ts +1 -0
  11. package/dist/fancy-ui/card-spotlight/CardSpotlight.test.js +58 -0
  12. package/dist/fancy-ui/displacement-text/README.md +39 -0
  13. package/dist/fancy-ui/dock/README.md +1 -1
  14. package/dist/fancy-ui/editorial-engine/EditorialEngine.test.d.ts +1 -0
  15. package/dist/fancy-ui/editorial-engine/EditorialEngine.test.js +54 -0
  16. package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte +1250 -1108
  17. package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte.d.ts +11 -5
  18. package/dist/fancy-ui/fluid-cursor/FluidCursor.test.js +48 -0
  19. package/dist/fancy-ui/fluid-cursor/README.md +57 -6
  20. package/dist/fancy-ui/fluid-cursor/fluid-shared.d.ts +72 -0
  21. package/dist/fancy-ui/fluid-cursor/fluid-shared.js +125 -0
  22. package/dist/fancy-ui/fluid-cursor/fluid-shared.test.d.ts +1 -0
  23. package/dist/fancy-ui/fluid-cursor/fluid-shared.test.js +39 -0
  24. package/dist/fancy-ui/fluid-cursor/index.d.ts +1 -0
  25. package/dist/fancy-ui/fluid-cursor/webgpu-engine.d.ts +36 -0
  26. package/dist/fancy-ui/fluid-cursor/webgpu-engine.js +926 -0
  27. package/dist/fancy-ui/frosted-glass/FrostedGlass.svelte +5 -3
  28. package/dist/fancy-ui/frosted-glass/README.md +4 -2
  29. package/dist/fancy-ui/index.d.ts +1 -0
  30. package/dist/fancy-ui/index.js +1 -0
  31. package/dist/fancy-ui/line-hover-link/README.md +37 -0
  32. package/dist/fancy-ui/line-reveal/LineReveal.test.d.ts +1 -0
  33. package/dist/fancy-ui/line-reveal/LineReveal.test.js +77 -0
  34. package/dist/fancy-ui/liquid-glass/LiquidGlass.svelte +19 -2
  35. package/dist/fancy-ui/liquid-glass/LiquidGlass.svelte.d.ts +4 -0
  36. package/dist/fancy-ui/liquid-glass/LiquidGlass.test.js +14 -0
  37. package/dist/fancy-ui/liquid-glass/README.md +7 -1
  38. package/dist/fancy-ui/liquid-text/LiquidText.svelte +1151 -0
  39. package/dist/fancy-ui/liquid-text/LiquidText.svelte.d.ts +37 -0
  40. package/dist/fancy-ui/liquid-text/LiquidText.test.d.ts +1 -0
  41. package/dist/fancy-ui/liquid-text/LiquidText.test.js +219 -0
  42. package/dist/fancy-ui/liquid-text/README.md +139 -0
  43. package/dist/fancy-ui/liquid-text/index.d.ts +2 -0
  44. package/dist/fancy-ui/liquid-text/index.js +1 -0
  45. package/dist/fancy-ui/marquee/README.md +57 -0
  46. package/dist/fancy-ui/matrix-rain/MatrixRain.test.d.ts +1 -0
  47. package/dist/fancy-ui/matrix-rain/MatrixRain.test.js +30 -0
  48. package/dist/fancy-ui/matrix-rain/README.md +42 -0
  49. package/dist/fancy-ui/noise-reveal/README.md +52 -0
  50. package/dist/fancy-ui/rainbow-button/README.md +36 -0
  51. package/dist/fancy-ui/registry.js +144 -20
  52. package/dist/fancy-ui/ripple-button/README.md +36 -0
  53. package/dist/fancy-ui/smooth-cursor/SmoothCursor.test.d.ts +1 -0
  54. package/dist/fancy-ui/smooth-cursor/SmoothCursor.test.js +45 -0
  55. package/dist/fancy-ui/terminal-text/README.md +48 -0
  56. package/dist/fancy-ui/terminal-text/TerminalText.test.d.ts +1 -0
  57. package/dist/fancy-ui/terminal-text/TerminalText.test.js +88 -0
  58. package/package.json +18 -4
@@ -68,7 +68,7 @@ export const registry = {
68
68
  "apple-card-carousel": {
69
69
  name: "AppleCardCarousel",
70
70
  slug: "apple-card-carousel",
71
- description: "Horizontal card carousel with spring-animated full-screen expansion, inspired by Apple's App Store UI",
71
+ description: "Horizontal card carousel where the focused card expands into a spring-animated full-screen view",
72
72
  category: "cards",
73
73
  status: "done",
74
74
  credits: [
@@ -246,7 +246,7 @@ export const registry = {
246
246
  "blur-reveal": {
247
247
  name: "BlurReveal",
248
248
  slug: "blur-reveal",
249
- description: "Scroll-triggered blur-to-clear reveal animation with staggered children",
249
+ description: "Scroll-triggered reveal that unblurs and slides each direct child into place as the container enters the viewport, staggering up to 10 children via CSS nth-child delays and respecting reduced motion",
250
250
  category: "text",
251
251
  status: "done",
252
252
  credits: [{ source: "Magic UI", url: "https://magicui.design/docs/components/blur-fade" }],
@@ -284,7 +284,7 @@ export const registry = {
284
284
  "border-beam": {
285
285
  name: "BorderBeam",
286
286
  slug: "border-beam",
287
- description: "Animated beam effect that travels around borders",
287
+ description: "Gradient beam that races around a container's border using a CSS offset-path animation, masked so only the border ring is painted, with configurable size, speed, anchor position, and gradient colors",
288
288
  category: "effects",
289
289
  status: "done",
290
290
  credits: [{ source: "Magic UI", url: "https://magicui.design/docs/components/border-beam" }],
@@ -405,7 +405,7 @@ export const registry = {
405
405
  "image-trail-cursor": {
406
406
  name: "ImageTrailCursor",
407
407
  slug: "image-trail-cursor",
408
- description: "Cursor-following image trail with 8 animation variants",
408
+ description: "Trail of images that spawn and animate along the cursor's path, powered by GSAP timelines, with 8 selectable variants spanning simple fades, momentum drift, rotation flings, and 3D perspective tilt",
409
409
  category: "effects",
410
410
  status: "done",
411
411
  tags: ["cursor", "animation", "images", "trail", "interactive"],
@@ -643,7 +643,7 @@ export const registry = {
643
643
  "ripple-button": {
644
644
  name: "RippleButton",
645
645
  slug: "ripple-button",
646
- description: "Button with ripple click effect",
646
+ description: "Button that spawns an expanding, fading ripple circle centered on the click point, with configurable color and duration",
647
647
  category: "buttons",
648
648
  status: "done",
649
649
  credits: [{ source: "Magic UI", url: "https://magicui.design/docs/components/ripple-button" }],
@@ -734,7 +734,7 @@ export const registry = {
734
734
  "bg-stars": {
735
735
  name: "StarsBackground",
736
736
  slug: "bg-stars",
737
- description: "Animated starfield background with parallax mouse tracking",
737
+ description: "Three-layer starfield background that scrolls continuously while parallax-shifting in response to cursor movement via spring physics, with stars rendered as box-shadow dots across near, mid, and far depth layers",
738
738
  category: "backgrounds",
739
739
  status: "done",
740
740
  credits: [
@@ -773,11 +773,11 @@ export const registry = {
773
773
  dock: {
774
774
  name: "Dock",
775
775
  slug: "dock",
776
- description: "macOS-style dock with icon magnification on hover",
776
+ description: "Icon dock where each item magnifies smoothly as the cursor approaches, sharing pointer position via Svelte context so every DockIcon scales by proximity within a configurable magnification and distance range",
777
777
  category: "navigation",
778
778
  status: "done",
779
779
  credits: [{ source: "Magic UI", url: "https://magicui.design/docs/components/dock" }],
780
- tags: ["dock", "navigation", "macos", "magnification", "hover"],
780
+ tags: ["dock", "navigation", "launcher", "magnification", "hover"],
781
781
  props: [
782
782
  {
783
783
  name: "magnification",
@@ -809,7 +809,7 @@ export const registry = {
809
809
  "fluid-cursor": {
810
810
  name: "FluidCursor",
811
811
  slug: "fluid-cursor",
812
- description: "WebGL fluid simulation that follows cursor movement",
812
+ description: "Full Navier-Stokes fluid simulation that swirls color in response to cursor and touch movement, running on WebGL2 with a WebGL1 fallback and an optional WebGPU HDR path for wide-gamut, brighter-than-white glow",
813
813
  category: "effects",
814
814
  status: "done",
815
815
  credits: [
@@ -935,12 +935,24 @@ export const registry = {
935
935
  default: "true",
936
936
  description: "Confine simulation to parent container",
937
937
  },
938
+ {
939
+ name: "hdr",
940
+ type: "boolean",
941
+ default: "false",
942
+ description: "Render via the WebGPU HDR engine — wide gamut + brighter-than-white glow on HDR displays. Needs Chrome/Edge 129+ or Safari 26+; falls back to wide-gamut WebGL (Chrome 104+, Safari 16.4+, Firefox 132+), then to standard rendering",
943
+ },
944
+ {
945
+ name: "hdrBoost",
946
+ type: "number",
947
+ default: "1.5",
948
+ description: "Display exposure multiplier in HDR mode, clamped to [1, 4]",
949
+ },
938
950
  ],
939
951
  },
940
952
  "glow-border": {
941
953
  name: "GlowBorder",
942
954
  slug: "glow-border",
943
- description: "Animated glowing border effect with gradient support",
955
+ description: "Glowing ring that animates around a container's edge by sweeping a radial gradient's position, masked with CSS mask-composite so only the border ring is painted, supporting single or multi-color gradients",
944
956
  category: "effects",
945
957
  status: "done",
946
958
  credits: [
@@ -1088,7 +1100,7 @@ export const registry = {
1088
1100
  "flickering-grid": {
1089
1101
  name: "FlickeringGrid",
1090
1102
  slug: "flickering-grid",
1091
- description: "Canvas-based grid of squares with flickering opacity",
1103
+ description: "Canvas grid of squares that flicker in and out of opacity at random each frame, pausing automatically via IntersectionObserver when scrolled off-screen and resizing to fit its container via ResizeObserver",
1092
1104
  category: "backgrounds",
1093
1105
  status: "done",
1094
1106
  credits: [
@@ -1171,7 +1183,7 @@ export const registry = {
1171
1183
  "colourful-text": {
1172
1184
  name: "ColourfulText",
1173
1185
  slug: "colourful-text",
1174
- description: "Per-character color animation with shuffling colors",
1186
+ description: "Per-character text animation where each glyph transitions to a new color from a shuffled palette, staggered across characters and reshuffling automatically every 5 seconds using pure CSS transitions",
1175
1187
  category: "text",
1176
1188
  status: "done",
1177
1189
  credits: [
@@ -1226,7 +1238,7 @@ export const registry = {
1226
1238
  "hyper-text": {
1227
1239
  name: "HyperText",
1228
1240
  slug: "hyper-text",
1229
- description: "Character scramble effect that activates on hover",
1241
+ description: "Text that scrambles into random uppercase letters and resolves character-by-character back to the original on hover, with a staggered per-letter reveal and an optional animate-on-load mode",
1230
1242
  category: "text",
1231
1243
  status: "done",
1232
1244
  credits: [{ source: "Magic UI", url: "https://magicui.design/docs/components/hyper-text" }],
@@ -1309,7 +1321,7 @@ export const registry = {
1309
1321
  "sparkles-text": {
1310
1322
  name: "SparklesText",
1311
1323
  slug: "sparkles-text",
1312
- description: "Text with animated SVG sparkle stars overlay",
1324
+ description: "Text overlaid with small SVG star sparkles that scale, rotate, and fade in a continuous loop, each one regenerating at a random position once its lifespan expires, cycling between two configurable colors",
1313
1325
  category: "text",
1314
1326
  status: "done",
1315
1327
  credits: [{ source: "Aceternity UI", url: "https://ui.aceternity.com/components/sparkles" }],
@@ -1333,7 +1345,7 @@ export const registry = {
1333
1345
  "box-reveal": {
1334
1346
  name: "BoxReveal",
1335
1347
  slug: "box-reveal",
1336
- description: "Content reveal with sliding colored box animation",
1348
+ description: "Reveal animation where a solid color box slides left to right across content on viewport entry, unveiling it as it fades up into place underneath, triggered once via IntersectionObserver",
1337
1349
  category: "text",
1338
1350
  status: "done",
1339
1351
  credits: [{ source: "Magic UI", url: "https://magicui.design/docs/components/box-reveal" }],
@@ -1383,7 +1395,7 @@ export const registry = {
1383
1395
  "card-spotlight": {
1384
1396
  name: "CardSpotlight",
1385
1397
  slug: "card-spotlight",
1386
- description: "Card with mouse-following radial gradient spotlight overlay",
1398
+ description: "Card whose radial-gradient spotlight tracks the cursor as it moves inside, fading in on hover and parking itself off-canvas when the pointer leaves, with configurable gradient size, color, and opacity",
1387
1399
  category: "cards",
1388
1400
  status: "done",
1389
1401
  credits: [
@@ -1779,6 +1791,18 @@ export const registry = {
1779
1791
  },
1780
1792
  { name: "scale", type: "number", default: "-180", description: "Displacement scale" },
1781
1793
  { name: "frost", type: "number", default: "0.05", description: "Frosted glass opacity" },
1794
+ {
1795
+ name: "fallbackBlur",
1796
+ type: "number",
1797
+ default: "20",
1798
+ description: "Backdrop blur in pixels for the Safari fallback",
1799
+ },
1800
+ {
1801
+ name: "fallbackSaturation",
1802
+ type: "number",
1803
+ default: "180",
1804
+ description: "Backdrop saturation percentage for the Safari fallback",
1805
+ },
1782
1806
  {
1783
1807
  name: "containerClass",
1784
1808
  type: "string",
@@ -1943,7 +1967,7 @@ export const registry = {
1943
1967
  ripple: {
1944
1968
  name: "Ripple",
1945
1969
  slug: "ripple",
1946
- description: "Concentric pulsing circles with ripple wave animation",
1970
+ description: "Set of concentric circles that pulse in a staggered wave, each ring larger, more transparent, and more delayed than the last, with the outermost ring rendered dashed to suggest a fading signal",
1947
1971
  category: "effects",
1948
1972
  status: "done",
1949
1973
  credits: [{ source: "Magic UI", url: "https://magicui.design/docs/components/ripple" }],
@@ -2101,7 +2125,7 @@ export const registry = {
2101
2125
  "matrix-rain": {
2102
2126
  name: "MatrixRain",
2103
2127
  slug: "matrix-rain",
2104
- description: "Canvas-based Matrix-style falling glyph rain with configurable color, speed, and density",
2128
+ description: "Canvas-based falling glyph rain with configurable color, speed, and density",
2105
2129
  category: "backgrounds",
2106
2130
  status: "done",
2107
2131
  tags: ["background", "matrix", "canvas", "animation", "glyphs", "cyberpunk"],
@@ -2110,7 +2134,7 @@ export const registry = {
2110
2134
  name: "color",
2111
2135
  type: "string",
2112
2136
  default: '"#00ff41"',
2113
- description: "Glyph color (the classic Matrix green)",
2137
+ description: "Glyph color (classic terminal green)",
2114
2138
  },
2115
2139
  {
2116
2140
  name: "speed",
@@ -2182,7 +2206,7 @@ export const registry = {
2182
2206
  "noise-reveal": {
2183
2207
  name: "NoiseReveal",
2184
2208
  slug: "noise-reveal",
2185
- description: "WebGL image reveal with a Perlin-noise dissolve mask, contracting radial gradient, and wave displacement, inspired by a Codrops shader effect",
2209
+ description: "WebGL image reveal with a Perlin-noise dissolve mask, contracting radial gradient, and wave displacement",
2186
2210
  category: "media",
2187
2211
  status: "done",
2188
2212
  credits: [
@@ -2225,6 +2249,106 @@ export const registry = {
2225
2249
  },
2226
2250
  ],
2227
2251
  },
2252
+ "liquid-text": {
2253
+ name: "LiquidText",
2254
+ slug: "liquid-text",
2255
+ description: "Big text that liquefies along the cursor's path via a raw-WebGL fluid solver, with chromatic-aberration fringing that relaxes back over time",
2256
+ category: "text",
2257
+ status: "done",
2258
+ credits: [
2259
+ { source: "Inspired by ryanritzenthaler.com (behavioral study)" },
2260
+ { source: "Fluid solver after Pavel Dobryakov's WebGL fluid simulation" },
2261
+ ],
2262
+ tags: ["text", "webgl", "fluid", "displacement", "cursor", "shader", "chromatic-aberration"],
2263
+ props: [
2264
+ { name: "text", type: "string", default: '"Liquid"', description: "Text to display" },
2265
+ {
2266
+ name: "font",
2267
+ type: "string",
2268
+ default: '""',
2269
+ description: "Font family; empty string resolves via getComputedStyle(host).fontFamily",
2270
+ },
2271
+ {
2272
+ name: "fontSize",
2273
+ type: "number",
2274
+ default: "0",
2275
+ description: "Font size in pixels; 0 auto-fits to the container width",
2276
+ },
2277
+ {
2278
+ name: "fontWeight",
2279
+ type: "number | string",
2280
+ default: "700",
2281
+ description: "Font weight",
2282
+ },
2283
+ {
2284
+ name: "lightColor",
2285
+ type: "string",
2286
+ default: '"#000000"',
2287
+ description: "Text color in light mode",
2288
+ },
2289
+ {
2290
+ name: "darkColor",
2291
+ type: "string",
2292
+ default: '"#ffffff"',
2293
+ description: "Text color in dark mode",
2294
+ },
2295
+ { name: "class", type: "string", description: "Additional CSS classes" },
2296
+ {
2297
+ name: "strength",
2298
+ type: "number",
2299
+ default: "0.5",
2300
+ description: "Geometric UV warp gain",
2301
+ },
2302
+ {
2303
+ name: "radius",
2304
+ type: "number",
2305
+ default: "160",
2306
+ description: "Splat radius in screen pixels",
2307
+ },
2308
+ {
2309
+ name: "forceGain",
2310
+ type: "number",
2311
+ default: "17",
2312
+ description: "Mouse-delta to force multiplier",
2313
+ },
2314
+ {
2315
+ name: "dissipation",
2316
+ type: "number",
2317
+ default: "0.98",
2318
+ description: "Per-frame velocity decay factor",
2319
+ },
2320
+ {
2321
+ name: "viscosity",
2322
+ type: "number",
2323
+ default: "4",
2324
+ description: "Jacobi viscous diffusion strength (8 iterations)",
2325
+ },
2326
+ {
2327
+ name: "chromaticRatio",
2328
+ type: "number",
2329
+ default: "0.2",
2330
+ description: "Chromatic offset as a ratio of the warp",
2331
+ },
2332
+ {
2333
+ name: "staticBelow",
2334
+ type: "number",
2335
+ default: "1024",
2336
+ description: "innerWidth at/below which the component renders as static DOM text (also used at mount)",
2337
+ },
2338
+ {
2339
+ name: "interactive",
2340
+ type: "boolean",
2341
+ default: "true",
2342
+ description: "Whether pointer movement drives the fluid simulation",
2343
+ },
2344
+ {
2345
+ name: "pauseWhenHidden",
2346
+ type: "boolean",
2347
+ default: "true",
2348
+ description: "Pause the render loop while the document is hidden",
2349
+ },
2350
+ ],
2351
+ },
2228
2352
  };
2229
2353
  // =============================================================================
2230
2354
  // Registry Helpers
@@ -0,0 +1,36 @@
1
+ # RippleButton
2
+
3
+ A button that spawns a Material-style expanding ripple circle from the exact click position on every click, layered behind the button's content.
4
+
5
+ ## Usage
6
+
7
+ ```svelte
8
+ <script lang="ts">
9
+ import { RippleButton } from "fancy-ui-svelte";
10
+ </script>
11
+
12
+ <RippleButton onclick={() => console.log("clicked")}>Click me</RippleButton>
13
+
14
+ <RippleButton rippleColor="#f472b6" duration={800} class="bg-primary text-primary-foreground">
15
+ Custom ripple
16
+ </RippleButton>
17
+ ```
18
+
19
+ ## Props
20
+
21
+ | Prop | Type | Default | Description |
22
+ | ------------- | --------- | ----------- | ---------------------------------- |
23
+ | `rippleColor` | `string` | `"#ADD8E6"` | Color of the ripple effect |
24
+ | `duration` | `number` | `600` | Animation duration in milliseconds |
25
+ | `class` | `string` | — | Additional CSS classes |
26
+ | `children` | `Snippet` | — | Button content |
27
+
28
+ `RippleButtonProps` also extends `HTMLButtonAttributes` (minus `class`), and any attributes not explicitly listed above (e.g. `type`, `aria-*`, `data-*`) are spread onto the rendered `<button>` via `...restProps`.
29
+
30
+ ## Implementation notes
31
+
32
+ - `onclick` is intercepted, not overwritten: the component's own `handleClick` always creates the ripple first, then calls the caller's `onclick` (if provided) with the original event — passing `onclick` works exactly as it would on a plain `<button>`.
33
+ - Ripple size is `Math.max(width, height)` of the button so a single circle always covers the whole button regardless of click position; position is computed from `event.clientX/Y` relative to the button's `getBoundingClientRect()`, centered on the click point.
34
+ - Each ripple is tracked in a `ripples` array keyed by `Date.now()` and removed via `setTimeout(duration)` after the animation finishes — rapid repeated clicks stack multiple concurrent ripple elements rather than restarting a single one.
35
+ - `duration` is threaded into the CSS animation via the `--ripple-duration` custom property, so the visual animation and the removal timeout always stay in sync even when `duration` changes between renders.
36
+ - No `prefers-reduced-motion` handling — the ripple scale/opacity animation plays on every click regardless of user preference.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,45 @@
1
+ import { render, cleanup, fireEvent } from "@testing-library/svelte";
2
+ import { createRawSnippet } from "svelte";
3
+ import { afterEach, describe, it, expect } from "vitest";
4
+ import SmoothCursor from "./SmoothCursor.svelte";
5
+ function textSnippet(html) {
6
+ return createRawSnippet(() => ({ render: () => html }));
7
+ }
8
+ describe("SmoothCursor", () => {
9
+ afterEach(cleanup);
10
+ it("renders hidden (opacity-0) before any pointer movement", () => {
11
+ const { container } = render(SmoothCursor);
12
+ const cursor = container.firstElementChild;
13
+ expect(cursor.className).toContain("opacity-0");
14
+ expect(cursor.className).not.toContain("opacity-100");
15
+ });
16
+ it("applies custom class names", () => {
17
+ const { container } = render(SmoothCursor, { props: { class: "my-cursor" } });
18
+ const cursor = container.firstElementChild;
19
+ expect(cursor.className).toContain("my-cursor");
20
+ });
21
+ it("renders the default arrow svg when no cursor snippet is provided", () => {
22
+ const { container } = render(SmoothCursor);
23
+ expect(container.querySelector("svg")).toBeTruthy();
24
+ });
25
+ it("renders a custom cursor snippet instead of the default arrow", () => {
26
+ const { container } = render(SmoothCursor, {
27
+ props: { cursor: textSnippet("<div data-testid='custom-cursor'></div>") },
28
+ });
29
+ expect(container.querySelector("svg")).toBeFalsy();
30
+ expect(container.querySelector("[data-testid='custom-cursor']")).toBeTruthy();
31
+ });
32
+ it("hides the native cursor on mount and restores it on unmount", () => {
33
+ const { unmount } = render(SmoothCursor);
34
+ expect(document.body.style.cursor).toBe("none");
35
+ unmount();
36
+ expect(document.body.style.cursor).toBe("");
37
+ });
38
+ it("becomes visible once the pointer moves, and unmount does not throw once the animation loop is running", async () => {
39
+ const { container, unmount } = render(SmoothCursor);
40
+ const cursor = container.firstElementChild;
41
+ await fireEvent.mouseMove(document, { clientX: 120, clientY: 80 });
42
+ expect(cursor.className).toContain("opacity-100");
43
+ expect(() => unmount()).not.toThrow();
44
+ });
45
+ });
@@ -0,0 +1,48 @@
1
+ # TerminalText
2
+
3
+ Streams an array of lines onto the screen character by character, terminal-style, with a blinking block cursor and an optional post-stream "glitch" effect that randomly swaps a visible character for a glyph for a moment.
4
+
5
+ ## Usage
6
+
7
+ ```svelte
8
+ <script lang="ts">
9
+ import { TerminalText } from "fancy-ui-svelte";
10
+ </script>
11
+
12
+ <div class="rounded-lg border bg-black p-5 text-green-400">
13
+ <TerminalText
14
+ lines={[
15
+ "[INFO] boot sequence complete",
16
+ "[WARN] memory fragmentation at sector 0x4A2F",
17
+ "[OK] all systems nominal",
18
+ ]}
19
+ speed={25}
20
+ />
21
+ </div>
22
+ ```
23
+
24
+ ```svelte
25
+ <!-- Glitch effect after streaming finishes, and a completion callback -->
26
+ <TerminalText lines={["Connection established."]} glitch onComplete={() => console.log("done")} />
27
+ ```
28
+
29
+ ## Props
30
+
31
+ | Prop | Type | Default | Description |
32
+ | ------------ | ------------ | ------- | ---------------------------------------------------------- |
33
+ | `lines` | `string[]` | — | Array of lines to stream character by character (required) |
34
+ | `speed` | `number` | `40` | Delay between each character in ms |
35
+ | `delay` | `number` | `0` | Initial delay before streaming starts, in ms |
36
+ | `cursor` | `boolean` | `true` | Show the blinking cursor |
37
+ | `cursorChar` | `string` | `"█"` | Character used as the cursor |
38
+ | `glitch` | `boolean` | `false` | Enable random character glitching after streaming |
39
+ | `class` | `string` | — | Additional CSS classes |
40
+ | `onComplete` | `() => void` | — | Called once, after the last line finishes streaming |
41
+
42
+ ## Implementation notes
43
+
44
+ - Streaming is scheduled with a flat list of `setTimeout` calls computed up front from a running `totalDelay` counter (one timeout per character, plus a `speed * 3` pause between lines), not a single interval — every timeout is tracked and cleared together (`clearAllTimeouts`) so restarting the stream never leaves stray callbacks running.
45
+ - Streaming and glitching are two independent `$effect`s. The streaming effect depends on `lines`, `speed`, and `delay` only (`void`-read to declare the dependency without using the value), so toggling `glitch` mid-stream does not restart the text. The glitch effect depends only on `glitch` and starts/stops its own self-rescheduling loop (`scheduleGlitch`, random 2–4s interval).
46
+ - The glitch loop picks a random visible character from a random non-empty line, swaps in a random glyph from a fixed set, and restores the original character after 100ms — it checks that the same `(lineIdx, charIdx)` is still the active glitch before restoring, so it can't clobber a different glitch that started in between.
47
+ - The blinking cursor is CSS-only (`animation: blink 1s step-end infinite`) and is rendered after whichever line is currently last — while streaming that's the in-progress line, once `done` it moves to its own trailing line.
48
+ - No `prefers-reduced-motion` handling — lines always stream character-by-character and the cursor always blinks, regardless of user preference.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,88 @@
1
+ import { render, cleanup } from "@testing-library/svelte";
2
+ import { afterEach, describe, it, expect, vi } from "vitest";
3
+ import TerminalText from "./TerminalText.svelte";
4
+ // The template has static whitespace between its {#each} and {#if} blocks
5
+ // that shows up as stray spaces in textContent depending on which branch is
6
+ // active. Stripping all whitespace isolates the actual streamed characters.
7
+ function text(el) {
8
+ return (el.textContent ?? "").replace(/\s+/g, "");
9
+ }
10
+ describe("TerminalText", () => {
11
+ afterEach(() => {
12
+ cleanup();
13
+ vi.useRealTimers();
14
+ });
15
+ it("renders nothing until the first scheduled tick, then streams characters over time and completes", async () => {
16
+ vi.useFakeTimers();
17
+ const onComplete = vi.fn();
18
+ const { container } = render(TerminalText, {
19
+ props: { lines: ["hi"], speed: 10, onComplete },
20
+ });
21
+ const wrapper = container.firstElementChild;
22
+ // The effect only schedules timeouts synchronously; none have fired yet.
23
+ expect(text(wrapper)).toBe("");
24
+ // t=0: empty line placeholder + blinking cursor appear.
25
+ await vi.advanceTimersByTimeAsync(1);
26
+ expect(text(wrapper)).toBe("█");
27
+ expect(onComplete).not.toHaveBeenCalled();
28
+ // t=10: first character streamed in.
29
+ await vi.advanceTimersByTimeAsync(9);
30
+ expect(text(wrapper)).toBe("h█");
31
+ // t=20: second character streamed in.
32
+ await vi.advanceTimersByTimeAsync(10);
33
+ expect(text(wrapper)).toBe("hi█");
34
+ expect(onComplete).not.toHaveBeenCalled();
35
+ // t=50: stream marked done, onComplete fires exactly once.
36
+ await vi.advanceTimersByTimeAsync(30);
37
+ expect(text(wrapper)).toBe("hi█");
38
+ expect(onComplete).toHaveBeenCalledTimes(1);
39
+ });
40
+ it("renders no cursor glyph anywhere when cursor is false", async () => {
41
+ vi.useFakeTimers();
42
+ const { container } = render(TerminalText, {
43
+ props: { lines: ["ok"], speed: 5, cursor: false },
44
+ });
45
+ await vi.advanceTimersByTimeAsync(25);
46
+ const wrapper = container.firstElementChild;
47
+ expect(text(wrapper)).toBe("ok");
48
+ expect(container.querySelector(".cursor-blink")).toBeFalsy();
49
+ });
50
+ it("streams multiple lines in order, one div per line", async () => {
51
+ vi.useFakeTimers();
52
+ const { container } = render(TerminalText, {
53
+ props: { lines: ["a", "bb"], speed: 10, cursor: false },
54
+ });
55
+ await vi.advanceTimersByTimeAsync(90);
56
+ const wrapper = container.firstElementChild;
57
+ const lineDivs = Array.from(wrapper.children);
58
+ expect(lineDivs).toHaveLength(2);
59
+ expect(lineDivs[0].textContent).toBe("a");
60
+ expect(lineDivs[1].textContent).toBe("bb");
61
+ });
62
+ it("applies custom class names alongside the base classes", () => {
63
+ const { container } = render(TerminalText, {
64
+ props: { lines: ["x"], class: "my-terminal" },
65
+ });
66
+ const wrapper = container.firstElementChild;
67
+ expect(wrapper.className).toContain("my-terminal");
68
+ expect(wrapper.className).toContain("font-mono");
69
+ });
70
+ it("uses a custom cursor character once the stream is done", async () => {
71
+ vi.useFakeTimers();
72
+ const { container } = render(TerminalText, {
73
+ props: { lines: ["a"], speed: 5, cursorChar: "_" },
74
+ });
75
+ // push@0, char@5, done@20 (5 + speed*3=15)
76
+ await vi.advanceTimersByTimeAsync(20);
77
+ const wrapper = container.firstElementChild;
78
+ expect(text(wrapper)).toBe("a_");
79
+ });
80
+ it("does not throw or leave dangling timers when glitch is enabled", async () => {
81
+ vi.useFakeTimers();
82
+ const { unmount } = render(TerminalText, {
83
+ props: { lines: ["glitchy line"], speed: 5, glitch: true },
84
+ });
85
+ await vi.advanceTimersByTimeAsync(5000);
86
+ expect(() => unmount()).not.toThrow();
87
+ });
88
+ });
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "fancy-ui-svelte",
3
3
  "private": false,
4
- "version": "0.7.0",
4
+ "version": "0.9.0",
5
+ "description": "61 animated UI components for Svelte 5 — WebGL, GSAP and CSS motion, styled with Tailwind CSS v4.",
6
+ "author": "Rama Herbin",
7
+ "funding": "https://github.com/sponsors/RamaHerbin",
5
8
  "type": "module",
6
9
  "license": "MIT",
7
10
  "repository": {
@@ -15,10 +18,18 @@
15
18
  "keywords": [
16
19
  "svelte",
17
20
  "svelte5",
21
+ "sveltekit",
18
22
  "components",
23
+ "component-library",
19
24
  "animations",
25
+ "animated-components",
26
+ "motion",
20
27
  "ui",
21
- "tailwind"
28
+ "ui-library",
29
+ "tailwind",
30
+ "tailwindcss",
31
+ "webgl",
32
+ "typescript"
22
33
  ],
23
34
  "sideEffects": [
24
35
  "**/*.css",
@@ -60,7 +71,6 @@
60
71
  },
61
72
  "devDependencies": {
62
73
  "@changesets/cli": "^2.30.0",
63
- "@vercel/analytics": "^2.0.1",
64
74
  "@internationalized/date": "^3.10.1",
65
75
  "@lucide/svelte": "^0.562.0",
66
76
  "@playwright/test": "^1.58.2",
@@ -80,12 +90,13 @@
80
90
  "@types/canvas-confetti": "^1.9.0",
81
91
  "@types/node": "^25.2.3",
82
92
  "@types/three": "^0.183.1",
93
+ "@vercel/analytics": "^2.0.1",
94
+ "@webgpu/types": "^0.1.71",
83
95
  "arctic": "^3.7.0",
84
96
  "bits-ui": "^2.15.4",
85
97
  "isomorphic-dompurify": "^3.7.1",
86
98
  "jsdom": "^28.0.0",
87
99
  "marked": "^17.0.3",
88
- "mdsvex": "^0.12.7",
89
100
  "nanoid": "^5.1.6",
90
101
  "prettier": "^3.8.1",
91
102
  "prettier-plugin-svelte": "^3.5.0",
@@ -109,6 +120,9 @@
109
120
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
110
121
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
111
122
  "check:registry": "node scripts/check-registry.mjs",
123
+ "check:i18n": "node scripts/check-i18n.mjs",
124
+ "build:assets": "node scripts/build-synthwave-assets.mjs",
125
+ "build:og": "node scripts/build-og-images.mjs",
112
126
  "test": "vitest run",
113
127
  "test:watch": "vitest",
114
128
  "test:e2e": "playwright test",