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
@@ -155,11 +155,13 @@
155
155
  }
156
156
  }
157
157
 
158
- /* Safari can't combine filter:url() with backdrop-filter — fall back to
159
- plain frosted blur */
160
- @supports (-webkit-hyphens: none) {
158
+ /* Safari can't combine filter:url() with backdrop-filter, so it needs a plain-blur
159
+ fallback. -webkit-named-image is a WebKit-only feature query (Chromium and Firefox
160
+ return false). Ship the -webkit- prefixed backdrop-filter too for Safari and iOS 17 and older. */
161
+ @supports (background: -webkit-named-image(i)) {
161
162
  .frosted-glass-filter {
162
163
  filter: none !important;
164
+ -webkit-backdrop-filter: blur(var(--fg-fallback-blur)) saturate(var(--fg-fallback-saturation));
163
165
  backdrop-filter: blur(var(--fg-fallback-blur)) saturate(var(--fg-fallback-saturation));
164
166
  }
165
167
 
@@ -13,8 +13,6 @@ The effect stacks four layers inside a rounded container:
13
13
 
14
14
  An optional conic-gradient border (`border` prop) frames the container.
15
15
 
16
- Safari cannot combine `filter: url()` with `backdrop-filter`, so it falls back to a plain `blur() saturate()` frosted look (tunable via `fallbackBlur` / `fallbackSaturation`).
17
-
18
16
  ## Usage
19
17
 
20
18
  ```svelte
@@ -57,3 +55,7 @@ Safari cannot combine `filter: url()` with `backdrop-filter`, so it falls back t
57
55
  ## Notes
58
56
 
59
57
  - The displacement only shows over content that scrolls or moves behind the glass; over a flat background the effect reads as a subtle frost.
58
+
59
+ ## Browser support
60
+
61
+ Safari (WebKit) cannot combine an SVG `filter: url(#…)` with `backdrop-filter`, so the turbulence displacement silently disappears there. To keep the component usable, FrostedGlass automatically detects Safari (via a WebKit-only `@supports` feature query) and falls back to a plain frosted `blur()` + `saturate()` (both `-webkit-` prefixed and unprefixed), tunable via the `fallbackBlur` and `fallbackSaturation` props. Chromium and Firefox get the full turbulence refraction.
@@ -58,4 +58,5 @@ export * from "./terminal-text/index.js";
58
58
  export * from "./noise-reveal/index.js";
59
59
  export * from "./line-reveal/index.js";
60
60
  export * from "./editorial-engine/index.js";
61
+ export * from "./liquid-text/index.js";
61
62
  export { registry, categories, categoryLabels, categoryDescriptions, getAllComponents, getComponent, getComponentsByStatus, getComponentsByCategory, getComponentsGroupedByCategory, getComponentsGroupedByStatus, searchComponents, getStats, hasComponent, getComponentCategory, } from "./registry.js";
@@ -63,6 +63,7 @@ export * from "./terminal-text/index.js";
63
63
  export * from "./noise-reveal/index.js";
64
64
  export * from "./line-reveal/index.js";
65
65
  export * from "./editorial-engine/index.js";
66
+ export * from "./liquid-text/index.js";
66
67
  // =============================================================================
67
68
  // Registry
68
69
  // =============================================================================
@@ -0,0 +1,37 @@
1
+ # LineHoverLink
2
+
3
+ An anchor with a pure-CSS underline hover effect, selectable from 11 variants. No JavaScript runs on hover — every variant is `::before`/`::after` pseudo-elements or an inline SVG stroke animated with CSS `transition`/`transform`/`stroke-dashoffset`, triggered by `:hover`/`:focus-visible`.
4
+
5
+ ## Usage
6
+
7
+ ```svelte
8
+ <script lang="ts">
9
+ import { LineHoverLink } from "fancy-ui-svelte";
10
+ </script>
11
+
12
+ <LineHoverLink href="/about" variant="slide">About</LineHoverLink>
13
+ <LineHoverLink href="/contact" variant="scribble" target="_blank">Contact</LineHoverLink>
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ | ------------ | ------------------ | --------- | ---------------------- |
20
+ | `variant` | `LineHoverVariant` | `"slide"` | The animation variant |
21
+ | `href` | `string` | `"#"` | Link href |
22
+ | `target` | `string` | — | Link target |
23
+ | `rel` | `string` | — | Link rel |
24
+ | `aria-label` | `string` | — | Accessible label |
25
+ | `class` | `string` | `""` | Additional CSS classes |
26
+
27
+ Children (slot content) are the link's visible text/label.
28
+
29
+ `LineHoverVariant` is one of: `"slide" | "double" | "grow" | "strike" | "fade" | "pulse" | "swap" | "sweep" | "bounce" | "arc" | "scribble"`.
30
+
31
+ ## Implementation notes
32
+
33
+ - The component has no `<script>`-side reactive state beyond two `$derived` values — everything visual is CSS selected by a `link-hover--<variant>` class.
34
+ - `arc` and `scribble` render an inline `<svg>` with a single `<path pathLength="1">` instead of a pseudo-element underline; both use the same technique (`stroke-dasharray: 1; stroke-dashoffset: 1` at rest, animated to `0` on hover/focus via `transition: stroke-dashoffset`).
35
+ - `strike`, `bounce`, `arc`, and `scribble` wrap the link text in an inner `<span>` (via the `needsSpan` derived flag) because those variants also transform the text itself (scale for `strike`, translate for `bounce`) independently of the underline graphic.
36
+ - `rel` defaults to `"noopener noreferrer"` automatically when `target="_blank"` and no explicit `rel` was passed, via the `relValue` derived value — an explicit `rel` prop is never overridden.
37
+ - All variants respond to both `:hover` and `:focus-visible`, so the effect is keyboard-navigable, not mouse-only.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,77 @@
1
+ import { render, cleanup } from "@testing-library/svelte";
2
+ import { afterEach, beforeEach, describe, it, expect, vi } from "vitest";
3
+ import LineReveal from "./LineReveal.svelte";
4
+ // pretext's prepareWithSegments() needs a canvas 2d context or OffscreenCanvas
5
+ // for text measurement — neither exists under jsdom (getContext is mocked to
6
+ // return null in src/test-setup.ts). Stubbing document.fonts.load() with a
7
+ // promise that never resolves keeps the component on its pre-measurement
8
+ // fallback branch instead of letting the effect's `.then()` handler call
9
+ // into pretext and throw "Text measurement requires OffscreenCanvas or a DOM
10
+ // canvas context."
11
+ function stubNeverLoadingFonts() {
12
+ Object.defineProperty(document, "fonts", {
13
+ configurable: true,
14
+ writable: true,
15
+ value: { load: () => new Promise(() => { }) },
16
+ });
17
+ }
18
+ class MockIntersectionObserver {
19
+ callback;
20
+ static instances = [];
21
+ constructor(callback) {
22
+ this.callback = callback;
23
+ MockIntersectionObserver.instances.push(this);
24
+ }
25
+ observe = vi.fn();
26
+ disconnect = vi.fn();
27
+ unobserve = vi.fn();
28
+ }
29
+ vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
30
+ describe("LineReveal", () => {
31
+ beforeEach(stubNeverLoadingFonts);
32
+ afterEach(() => {
33
+ cleanup();
34
+ MockIntersectionObserver.instances = [];
35
+ });
36
+ it("mounts and renders the root wrapper", () => {
37
+ const { container } = render(LineReveal, { props: { text: "Hello there" } });
38
+ const root = container.firstElementChild;
39
+ expect(root.className).toContain("relative");
40
+ expect(root.className).toContain("w-full");
41
+ });
42
+ it("applies custom class names", () => {
43
+ const { container } = render(LineReveal, {
44
+ props: { text: "Hello", class: "my-reveal" },
45
+ });
46
+ const root = container.firstElementChild;
47
+ expect(root.className).toContain("my-reveal");
48
+ });
49
+ it("keeps the full text available to screen readers via a sr-only span", () => {
50
+ const { container } = render(LineReveal, { props: { text: "Accessible copy" } });
51
+ const srOnly = container.querySelector(".sr-only");
52
+ expect(srOnly?.textContent).toBe("Accessible copy");
53
+ });
54
+ it("renders the pre-measure fallback (hidden text) before pretext has measured", () => {
55
+ const { container } = render(LineReveal, { props: { text: "Falls back" } });
56
+ const fallback = container.querySelector('[aria-hidden="true"]');
57
+ expect(fallback).toBeTruthy();
58
+ expect(fallback.textContent).toBe("Falls back");
59
+ expect(fallback.style.visibility).toBe("hidden");
60
+ });
61
+ it("sets the font style from the font prop", () => {
62
+ const { container } = render(LineReveal, {
63
+ props: { text: "Styled", font: "700 24px Georgia, serif" },
64
+ });
65
+ const root = container.firstElementChild;
66
+ expect(root.getAttribute("style") ?? "").toContain("24px");
67
+ });
68
+ it("observes the container element for intersection", () => {
69
+ render(LineReveal, { props: { text: "Watch me" } });
70
+ const lastInstance = MockIntersectionObserver.instances.at(-1);
71
+ expect(lastInstance?.observe).toHaveBeenCalled();
72
+ });
73
+ it("unmounts cleanly", () => {
74
+ const { unmount } = render(LineReveal, { props: { text: "Bye" } });
75
+ expect(() => unmount()).not.toThrow();
76
+ });
77
+ });
@@ -18,6 +18,10 @@
18
18
  bOffset?: number;
19
19
  scale?: number;
20
20
  frost?: number;
21
+ /** Backdrop blur in pixels for the Safari fallback */
22
+ fallbackBlur?: number;
23
+ /** Backdrop saturation percentage for the Safari fallback */
24
+ fallbackSaturation?: number;
21
25
  class?: string;
22
26
  containerClass?: string;
23
27
  children?: Snippet;
@@ -38,6 +42,8 @@
38
42
  bOffset = 20,
39
43
  scale = -180,
40
44
  frost = 0.05,
45
+ fallbackBlur = 20,
46
+ fallbackSaturation = 180,
41
47
  class: className = "",
42
48
  containerClass = "",
43
49
  children,
@@ -57,8 +63,8 @@
57
63
 
58
64
  let backdropStyle = $derived(
59
65
  filterId
60
- ? `--frost:${frost};border-radius:${radius}px;backdrop-filter:url(#displacementFilter-${filterId});`
61
- : `--frost:${frost};border-radius:${radius}px;`
66
+ ? `--frost:${frost};border-radius:${radius}px;backdrop-filter:url(#displacementFilter-${filterId});--lg-fallback-blur:${fallbackBlur}px;--lg-fallback-saturation:${fallbackSaturation}%;`
67
+ : `--frost:${frost};border-radius:${radius}px;--lg-fallback-blur:${fallbackBlur}px;--lg-fallback-saturation:${fallbackSaturation}%;`
62
68
  );
63
69
 
64
70
  onMount(() => {
@@ -187,6 +193,17 @@
187
193
  0px 16px 56px rgba(17, 17, 26, 0.05) inset;
188
194
  }
189
195
 
196
+ /* Safari cannot resolve SVG url() references inside backdrop-filter, so it needs a
197
+ plain-blur fallback. -webkit-named-image is a WebKit-only feature query (Chromium
198
+ and Firefox return false), so this never overrides Chromium's working SVG filter.
199
+ Ship the -webkit- prefixed backdrop-filter too for Safari and iOS 17 and older. */
200
+ @supports (background: -webkit-named-image(i)) {
201
+ .liquid-glass-effect {
202
+ -webkit-backdrop-filter: blur(var(--lg-fallback-blur)) saturate(var(--lg-fallback-saturation)) !important;
203
+ backdrop-filter: blur(var(--lg-fallback-blur)) saturate(var(--lg-fallback-saturation)) !important;
204
+ }
205
+ }
206
+
190
207
  .liquid-glass-slot {
191
208
  width: 100%;
192
209
  height: 100%;
@@ -14,6 +14,10 @@ interface Props {
14
14
  bOffset?: number;
15
15
  scale?: number;
16
16
  frost?: number;
17
+ /** Backdrop blur in pixels for the Safari fallback */
18
+ fallbackBlur?: number;
19
+ /** Backdrop saturation percentage for the Safari fallback */
20
+ fallbackSaturation?: number;
17
21
  class?: string;
18
22
  containerClass?: string;
19
23
  children?: Snippet;
@@ -45,4 +45,18 @@ describe("LiquidGlass", () => {
45
45
  expect(wrapper.getAttribute("style")).toContain("border-radius");
46
46
  expect(wrapper.getAttribute("style")).toContain("24px");
47
47
  });
48
+ it("exposes Safari fallback CSS variables from props", () => {
49
+ const { container: defaultContainer } = render(LiquidGlass);
50
+ const defaultWrapper = defaultContainer.querySelector(".liquid-glass-effect");
51
+ const defaultStyle = defaultWrapper.getAttribute("style") ?? "";
52
+ expect(defaultStyle).toMatch(/--lg-fallback-blur:\s*20px/);
53
+ expect(defaultStyle).toMatch(/--lg-fallback-saturation:\s*180%/);
54
+ const { container: customContainer } = render(LiquidGlass, {
55
+ props: { fallbackBlur: 8, fallbackSaturation: 120 },
56
+ });
57
+ const customWrapper = customContainer.querySelector(".liquid-glass-effect");
58
+ const customStyle = customWrapper.getAttribute("style") ?? "";
59
+ expect(customStyle).toMatch(/--lg-fallback-blur:\s*8px/);
60
+ expect(customStyle).toMatch(/--lg-fallback-saturation:\s*120%/);
61
+ });
48
62
  });
@@ -20,6 +20,8 @@ A glass-like visual effect using SVG filters for chromatic displacement, creatin
20
20
  | `bOffset` | `number` | `20` | Blue channel offset |
21
21
  | `scale` | `number` | `-180` | Displacement scale |
22
22
  | `frost` | `number` | `0.05` | Frosted overlay opacity |
23
+ | `fallbackBlur` | `number` | `20` | Backdrop blur in pixels for the Safari fallback |
24
+ | `fallbackSaturation` | `number` | `180` | Backdrop saturation percentage for the Safari fallback |
23
25
  | `class` | `string` | `""` | CSS classes for inner container |
24
26
  | `containerClass` | `string` | `""` | CSS classes for outer container |
25
27
 
@@ -31,4 +33,8 @@ A glass-like visual effect using SVG filters for chromatic displacement, creatin
31
33
  </LiquidGlass>
32
34
  ```
33
35
 
34
- **Note**: This component uses `position: fixed` and `backdrop-filter` with SVG filters. Place it over content to see the refraction effect.
36
+ **Note**: This component uses `position: relative` and `backdrop-filter` with SVG filters. Place it over content to see the refraction effect.
37
+
38
+ ## Browser support
39
+
40
+ Safari (WebKit) cannot resolve an SVG `url(#…)` filter reference inside `backdrop-filter`, so the chromatic displacement silently disappears there. To keep the component usable, LiquidGlass automatically detects Safari (via a WebKit-only `@supports` feature query) and falls back to a plain frosted `blur()` + `saturate()` (both `-webkit-` prefixed and unprefixed), tunable via the `fallbackBlur` and `fallbackSaturation` props. Chromium and Firefox get the full chromatic refraction.