fancy-ui-svelte 0.8.0 → 0.9.1

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 -89
  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/fireworks-hdr/FireworksHdr.svelte +629 -0
  17. package/dist/fancy-ui/fireworks-hdr/FireworksHdr.svelte.d.ts +56 -0
  18. package/dist/fancy-ui/fireworks-hdr/FireworksHdr.test.d.ts +1 -0
  19. package/dist/fancy-ui/fireworks-hdr/FireworksHdr.test.js +74 -0
  20. package/dist/fancy-ui/fireworks-hdr/README.md +492 -0
  21. package/dist/fancy-ui/fireworks-hdr/fireworks-shared.d.ts +444 -0
  22. package/dist/fancy-ui/fireworks-hdr/fireworks-shared.js +1520 -0
  23. package/dist/fancy-ui/fireworks-hdr/fireworks-shared.test.d.ts +1 -0
  24. package/dist/fancy-ui/fireworks-hdr/fireworks-shared.test.js +1054 -0
  25. package/dist/fancy-ui/fireworks-hdr/index.d.ts +5 -0
  26. package/dist/fancy-ui/fireworks-hdr/index.js +5 -0
  27. package/dist/fancy-ui/fireworks-hdr/webgl2-renderer.d.ts +20 -0
  28. package/dist/fancy-ui/fireworks-hdr/webgl2-renderer.js +634 -0
  29. package/dist/fancy-ui/fireworks-hdr/webgl2-renderer.test.d.ts +1 -0
  30. package/dist/fancy-ui/fireworks-hdr/webgl2-renderer.test.js +33 -0
  31. package/dist/fancy-ui/fireworks-hdr/webgpu-renderer.d.ts +51 -0
  32. package/dist/fancy-ui/fireworks-hdr/webgpu-renderer.js +538 -0
  33. package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte +130 -9
  34. package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte.d.ts +9 -1
  35. package/dist/fancy-ui/fluid-cursor/FluidCursor.test.js +19 -0
  36. package/dist/fancy-ui/fluid-cursor/fluid-shared.d.ts +32 -0
  37. package/dist/fancy-ui/fluid-cursor/index.d.ts +1 -0
  38. package/dist/fancy-ui/fluid-cursor/webgpu-engine.d.ts +11 -4
  39. package/dist/fancy-ui/fluid-cursor/webgpu-engine.js +113 -34
  40. package/dist/fancy-ui/index.d.ts +1 -0
  41. package/dist/fancy-ui/index.js +1 -0
  42. package/dist/fancy-ui/line-hover-link/README.md +37 -0
  43. package/dist/fancy-ui/line-reveal/LineReveal.test.d.ts +1 -0
  44. package/dist/fancy-ui/line-reveal/LineReveal.test.js +77 -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 +99 -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 +15 -2
@@ -0,0 +1,118 @@
1
+ import { render, cleanup, fireEvent } from "@testing-library/svelte";
2
+ import { createRawSnippet } from "svelte";
3
+ import { afterEach, describe, it, expect } from "vitest";
4
+ import CardContainer from "./CardContainer.svelte";
5
+ import CardBody from "./CardBody.svelte";
6
+ import CardItem from "./CardItem.svelte";
7
+ function textSnippet(html) {
8
+ return createRawSnippet(() => ({ render: () => html }));
9
+ }
10
+ describe("CardContainer", () => {
11
+ afterEach(cleanup);
12
+ it("renders the perspective wrapper and the 3d inner container", () => {
13
+ const { container } = render(CardContainer);
14
+ const outer = container.firstElementChild;
15
+ const inner = outer.firstElementChild;
16
+ expect(outer.getAttribute("style")).toContain("perspective: 1000px");
17
+ expect(inner.getAttribute("style")).toContain("transform-style: preserve-3d");
18
+ });
19
+ it("applies containerClass to the outer wrapper and class to the inner container", () => {
20
+ const { container } = render(CardContainer, {
21
+ props: { containerClass: "my-outer", class: "my-inner" },
22
+ });
23
+ const outer = container.firstElementChild;
24
+ const inner = outer.firstElementChild;
25
+ expect(outer.className).toContain("my-outer");
26
+ expect(inner.className).toContain("my-inner");
27
+ });
28
+ it("renders children content", () => {
29
+ const { container } = render(CardContainer, {
30
+ props: { children: textSnippet("<p data-testid='content'>card body</p>") },
31
+ });
32
+ expect(container.querySelector("[data-testid='content']")?.textContent).toBe("card body");
33
+ });
34
+ it("rotates the inner container on mousemove and resets on mouseleave", async () => {
35
+ const { container } = render(CardContainer);
36
+ const inner = container.firstElementChild.firstElementChild;
37
+ // jsdom's getBoundingClientRect() is all zeros, so rotateY = clientX / 25
38
+ // and rotateX = clientY / 25 for this component's handler.
39
+ await fireEvent.mouseMove(inner, { clientX: 50, clientY: 25 });
40
+ expect(inner.style.transform).toBe("rotateY(2deg) rotateX(1deg)");
41
+ await fireEvent.mouseLeave(inner);
42
+ expect(inner.style.transform).toBe("rotateY(0deg) rotateX(0deg)");
43
+ });
44
+ });
45
+ describe("CardBody", () => {
46
+ afterEach(cleanup);
47
+ it("renders with the fixed 3d-card dimensions", () => {
48
+ const { container } = render(CardBody);
49
+ const body = container.firstElementChild;
50
+ expect(body.className).toContain("h-96");
51
+ expect(body.className).toContain("w-96");
52
+ expect(body.getAttribute("style")).toContain("transform-style: preserve-3d");
53
+ });
54
+ it("applies custom class names", () => {
55
+ const { container } = render(CardBody, { props: { class: "my-body" } });
56
+ const body = container.firstElementChild;
57
+ expect(body.className).toContain("my-body");
58
+ });
59
+ it("renders children content", () => {
60
+ const { container } = render(CardBody, {
61
+ props: { children: textSnippet("<span data-testid='inner'>x</span>") },
62
+ });
63
+ expect(container.querySelector("[data-testid='inner']")).toBeTruthy();
64
+ });
65
+ });
66
+ describe("CardItem", () => {
67
+ afterEach(cleanup);
68
+ // CardItem reads its hover state from CardContainer via setContext/getContext
69
+ // under the "card3d:mouseEntered" key. @testing-library/svelte forwards a
70
+ // `context` mount option straight to Svelte's mount(), so it can be tested
71
+ // standalone without mounting a full CardContainer tree.
72
+ function withContext(mouseEntered) {
73
+ return new Map([["card3d:mouseEntered", () => mouseEntered]]);
74
+ }
75
+ it("sits at the zero transform when the container reports no hover", () => {
76
+ const { container } = render(CardItem, {
77
+ props: { translateX: 20, rotateZ: 10 },
78
+ context: withContext(false),
79
+ });
80
+ const el = container.firstElementChild;
81
+ expect(el.style.transform).toContain("translateX(0px)");
82
+ expect(el.style.transform).toContain("rotateZ(0deg)");
83
+ });
84
+ it("applies the configured translate/rotate values once the container reports hover", () => {
85
+ const { container } = render(CardItem, {
86
+ props: { translateX: 20, translateY: -10, rotateZ: 10 },
87
+ context: withContext(true),
88
+ });
89
+ const el = container.firstElementChild;
90
+ expect(el.style.transform).toContain("translateX(20px)");
91
+ expect(el.style.transform).toContain("translateY(-10px)");
92
+ expect(el.style.transform).toContain("rotateZ(10deg)");
93
+ });
94
+ it("renders the element specified by the as prop", () => {
95
+ const { container } = render(CardItem, {
96
+ props: { as: "span" },
97
+ context: withContext(false),
98
+ });
99
+ expect(container.firstElementChild?.tagName.toLowerCase()).toBe("span");
100
+ });
101
+ it("defaults to a div and merges custom class names", () => {
102
+ const { container } = render(CardItem, {
103
+ props: { class: "my-item" },
104
+ context: withContext(false),
105
+ });
106
+ const el = container.firstElementChild;
107
+ expect(el.tagName.toLowerCase()).toBe("div");
108
+ expect(el.className).toContain("my-item");
109
+ expect(el.className).toContain("w-fit");
110
+ });
111
+ it("renders children content", () => {
112
+ const { container } = render(CardItem, {
113
+ props: { children: textSnippet("<em data-testid='item-child'>x</em>") },
114
+ context: withContext(false),
115
+ });
116
+ expect(container.querySelector("[data-testid='item-child']")).toBeTruthy();
117
+ });
118
+ });
@@ -0,0 +1,58 @@
1
+ import { render, cleanup, fireEvent } from "@testing-library/svelte";
2
+ import { createRawSnippet } from "svelte";
3
+ import { afterEach, describe, it, expect } from "vitest";
4
+ import CardSpotlight from "./CardSpotlight.svelte";
5
+ function textSnippet(html) {
6
+ return createRawSnippet(() => ({ render: () => html }));
7
+ }
8
+ describe("CardSpotlight", () => {
9
+ afterEach(cleanup);
10
+ it("renders the wrapper with the base card classes", () => {
11
+ const { container } = render(CardSpotlight);
12
+ const wrapper = container.firstElementChild;
13
+ expect(wrapper.className).toContain("rounded-xl");
14
+ expect(wrapper.className).toContain("overflow-hidden");
15
+ });
16
+ it("applies custom class names to the wrapper", () => {
17
+ const { container } = render(CardSpotlight, { props: { class: "my-spotlight" } });
18
+ const wrapper = container.firstElementChild;
19
+ expect(wrapper.className).toContain("my-spotlight");
20
+ });
21
+ it("applies slotClass to the content wrapper", () => {
22
+ const { container } = render(CardSpotlight, { props: { slotClass: "my-slot" } });
23
+ const slot = container.querySelector(".relative.z-10");
24
+ expect(slot.className).toContain("my-slot");
25
+ });
26
+ it("renders children content", () => {
27
+ const { container } = render(CardSpotlight, {
28
+ props: { children: textSnippet("<p data-testid='content'>hi</p>") },
29
+ });
30
+ expect(container.querySelector("[data-testid='content']")).toBeTruthy();
31
+ });
32
+ // The overlay's `style="background: {backgroundStyle}; opacity: {gradientOpacity}"`
33
+ // attribute never materializes under this repo's vitest+jsdom setup — verified
34
+ // with a from-scratch minimal component reproduction, unrelated to mouse
35
+ // tracking or gradientSize/gradientColor: any style attribute shaped like
36
+ // `style="prop: {dyn}; prop2: value"` on a plain element renders with no
37
+ // style attribute at all (confirmed via getAttribute, .style.cssText, and
38
+ // the individual .style.* properties, all empty). Because vite.config.ts
39
+ // forces `resolve: { conditions: ['browser'] }`, tests compile the same
40
+ // client output real browsers get, so this is worth a maintainer checking
41
+ // in an actual browser rather than something to chase further here — see
42
+ // the gradient/opacity plumbing tests below, which stay on structure only.
43
+ it("accepts gradientSize/gradientColor/gradientOpacity without throwing and keeps the overlay in the DOM", () => {
44
+ const { container } = render(CardSpotlight, {
45
+ props: { gradientSize: 300, gradientColor: "#ff0000", gradientOpacity: 0.5 },
46
+ });
47
+ const overlay = container.querySelector(".pointer-events-none.absolute.inset-0");
48
+ expect(overlay).toBeTruthy();
49
+ });
50
+ it("responds to mousemove and mouseleave without throwing", async () => {
51
+ const { container } = render(CardSpotlight);
52
+ const wrapper = container.firstElementChild;
53
+ await fireEvent.mouseMove(wrapper, { clientX: 40, clientY: 30 });
54
+ await fireEvent.mouseLeave(wrapper);
55
+ // The awaits above would reject if either handler threw.
56
+ expect(container.querySelector(".pointer-events-none.absolute.inset-0")).toBeTruthy();
57
+ });
58
+ });
@@ -0,0 +1,39 @@
1
+ # DisplacementText
2
+
3
+ Large text rendered as a Three.js plane whose surface bulges toward the viewer near the cursor, via a vertex-shader displacement driven by raycasting against an invisible hit-plane. The text itself is rasterized to a canvas texture, not drawn with real DOM glyphs.
4
+
5
+ ## Usage
6
+
7
+ ```svelte
8
+ <script lang="ts">
9
+ import { DisplacementText } from "fancy-ui-svelte";
10
+ </script>
11
+
12
+ <DisplacementText text="Hover Me" fontSize={220} class="h-[400px] w-full" />
13
+ ```
14
+
15
+ ```svelte
16
+ <!-- Fixed color instead of following light/dark mode -->
17
+ <DisplacementText text="Fancy" color="#ff4d4d" font="Georgia, serif" />
18
+ ```
19
+
20
+ ## Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ | ------------ | -------- | --------------------- | ------------------------------------------------------------------------------------ |
24
+ | `text` | `string` | `"Hover Me"` | Text to display |
25
+ | `fontSize` | `number` | `200` | Font size in pixels, used when rasterizing the text texture |
26
+ | `font` | `string` | `"Inter, sans-serif"` | Font family |
27
+ | `color` | `string` | — | Fixed text color; overrides `lightColor`/`darkColor` and disables dark-mode tracking |
28
+ | `lightColor` | `string` | `"#000000"` | Text color in light mode |
29
+ | `darkColor` | `string` | `"#ffffff"` | Text color in dark mode |
30
+ | `class` | `string` | `""` | Additional CSS classes on the root |
31
+
32
+ ## Implementation notes
33
+
34
+ - The whole Three.js scene (renderer, camera, geometry, material) is created and torn down inside a single `$effect` that depends on `text`, `fontSize`, `font`, `color`, `lightColor`, and `darkColor` — any of those changing disposes the old scene and builds a new one from scratch rather than updating it incrementally. This is simple but not cheap; avoid binding these props to something that changes every frame.
35
+ - Displacement comes from a plane geometry (100×100 segments) whose vertex shader offsets `z` based on distance from a `uDisplacement` uniform, updated every `pointermove` by raycasting against a large (500×500), fully transparent hit-plane placed in the same scene — the visible text plane itself is not the raycast target.
36
+ - Text is rasterized once per texture rebuild via a 2048×2048 `<canvas>` with `ctx.fillText`, then uploaded as a `THREE.CanvasTexture`.
37
+ - When `color` is not set, a `MutationObserver` watches `document.documentElement`'s `class` attribute (the app's dark-mode toggle) and regenerates the text texture — disposing the old one — whenever the resolved color actually changes. This observer is not created at all when `color` is set.
38
+ - Cleanup on effect teardown: removes the `pointermove` and `resize` listeners, cancels the animation frame, disconnects the `MutationObserver`, removes the renderer's canvas from the DOM, and disposes the renderer, geometry, material, and texture.
39
+ - No `prefers-reduced-motion` handling — the render loop and pointer tracking run regardless of user preference.
@@ -1,6 +1,6 @@
1
1
  # Dock
2
2
 
3
- A macOS-style dock component with icon magnification on hover.
3
+ An icon dock component whose items magnify smoothly as the cursor approaches.
4
4
 
5
5
  ## Components
6
6
 
@@ -0,0 +1,54 @@
1
+ import { render, cleanup } from "@testing-library/svelte";
2
+ import { afterEach, beforeEach, describe, it, expect } from "vitest";
3
+ import EditorialEngine from "./EditorialEngine.svelte";
4
+ // The real engine measures text via @chenglou/pretext, which needs a canvas
5
+ // 2d context or OffscreenCanvas — neither exists under jsdom
6
+ // (HTMLCanvasElement.prototype.getContext is mocked to return null in
7
+ // src/test-setup.ts). Stubbing `document.fonts.ready` with a promise that
8
+ // never resolves keeps the component pinned on its pre-engine fallback
9
+ // branch (the only branch that's meaningfully testable under jsdom) instead
10
+ // of letting the effect's `.then()` handler call into pretext and throw.
11
+ function stubNeverReadyFonts() {
12
+ Object.defineProperty(document, "fonts", {
13
+ configurable: true,
14
+ writable: true,
15
+ value: { ready: new Promise(() => { }) },
16
+ });
17
+ }
18
+ describe("EditorialEngine", () => {
19
+ beforeEach(stubNeverReadyFonts);
20
+ afterEach(cleanup);
21
+ it("mounts and renders the stage without throwing", () => {
22
+ const { container } = render(EditorialEngine);
23
+ expect(container.querySelector(".ee-stage")).toBeTruthy();
24
+ });
25
+ it("applies custom class names to the stage", () => {
26
+ const { container } = render(EditorialEngine, { props: { class: "my-stage" } });
27
+ const stage = container.querySelector(".ee-stage");
28
+ expect(stage.className).toContain("my-stage");
29
+ });
30
+ it("sets the font-family style from the fontFamily prop", () => {
31
+ const { container } = render(EditorialEngine, { props: { fontFamily: "Georgia, serif" } });
32
+ const stage = container.querySelector(".ee-stage");
33
+ expect(stage.style.fontFamily).toContain("Georgia");
34
+ });
35
+ it("renders the pre-engine fallback with the headline and body before the engine is ready", () => {
36
+ const { container } = render(EditorialEngine, {
37
+ props: { headline: "HELLO WORLD", body: "Some body copy." },
38
+ });
39
+ const fallback = container.querySelector(".ee-fallback");
40
+ expect(fallback).toBeTruthy();
41
+ expect(fallback?.querySelector("h2")?.textContent).toBe("HELLO WORLD");
42
+ expect(fallback?.querySelector("p")?.textContent).toBe("Some body copy.");
43
+ });
44
+ it("does not carry the ready class while pretext has not measured yet", () => {
45
+ const { container } = render(EditorialEngine);
46
+ const stage = container.querySelector(".ee-stage");
47
+ expect(stage.classList.contains("ready")).toBe(false);
48
+ });
49
+ it("unmounts cleanly", () => {
50
+ const { container, unmount } = render(EditorialEngine);
51
+ expect(() => unmount()).not.toThrow();
52
+ expect(container.querySelector(".ee-stage")).toBeFalsy();
53
+ });
54
+ });