fancy-ui-svelte 0.6.0 → 0.8.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.
- package/README.md +84 -71
- package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte +1132 -1111
- package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte.d.ts +3 -5
- package/dist/fancy-ui/fluid-cursor/FluidCursor.test.js +29 -0
- package/dist/fancy-ui/fluid-cursor/README.md +57 -6
- package/dist/fancy-ui/fluid-cursor/fluid-shared.d.ts +40 -0
- package/dist/fancy-ui/fluid-cursor/fluid-shared.js +125 -0
- package/dist/fancy-ui/fluid-cursor/fluid-shared.test.d.ts +1 -0
- package/dist/fancy-ui/fluid-cursor/fluid-shared.test.js +39 -0
- package/dist/fancy-ui/fluid-cursor/webgpu-engine.d.ts +29 -0
- package/dist/fancy-ui/fluid-cursor/webgpu-engine.js +847 -0
- package/dist/fancy-ui/frosted-glass/FrostedGlass.svelte +204 -0
- package/dist/fancy-ui/frosted-glass/FrostedGlass.svelte.d.ts +20 -0
- package/dist/fancy-ui/frosted-glass/FrostedGlass.test.d.ts +1 -0
- package/dist/fancy-ui/frosted-glass/FrostedGlass.test.js +48 -0
- package/dist/fancy-ui/frosted-glass/README.md +61 -0
- package/dist/fancy-ui/frosted-glass/index.d.ts +1 -0
- package/dist/fancy-ui/frosted-glass/index.js +1 -0
- package/dist/fancy-ui/index.d.ts +2 -0
- package/dist/fancy-ui/index.js +2 -0
- package/dist/fancy-ui/liquid-glass/LiquidGlass.svelte +19 -2
- package/dist/fancy-ui/liquid-glass/LiquidGlass.svelte.d.ts +4 -0
- package/dist/fancy-ui/liquid-glass/LiquidGlass.test.js +14 -0
- package/dist/fancy-ui/liquid-glass/README.md +7 -1
- package/dist/fancy-ui/liquid-text/LiquidText.svelte +1151 -0
- package/dist/fancy-ui/liquid-text/LiquidText.svelte.d.ts +37 -0
- package/dist/fancy-ui/liquid-text/LiquidText.test.d.ts +1 -0
- package/dist/fancy-ui/liquid-text/LiquidText.test.js +219 -0
- package/dist/fancy-ui/liquid-text/README.md +139 -0
- package/dist/fancy-ui/liquid-text/index.d.ts +2 -0
- package/dist/fancy-ui/liquid-text/index.js +1 -0
- package/dist/fancy-ui/registry.js +192 -0
- package/dist/fancy-ui/themes.d.ts +38 -0
- package/dist/fancy-ui/themes.js +356 -0
- package/dist/utils/animation.d.ts +109 -0
- package/dist/utils/animation.js +198 -0
- package/dist/utils/color.d.ts +128 -0
- package/dist/utils/color.js +314 -0
- package/dist/utils/geometry.d.ts +125 -0
- package/dist/utils/geometry.js +250 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.js +39 -0
- package/package.json +13 -4
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from "../../utils";
|
|
3
|
+
import { onMount } from "svelte";
|
|
4
|
+
import type { Snippet } from "svelte";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
radius?: number;
|
|
8
|
+
baseFrequency?: number;
|
|
9
|
+
numOctaves?: number;
|
|
10
|
+
seed?: number;
|
|
11
|
+
noiseBlur?: number;
|
|
12
|
+
scale?: number;
|
|
13
|
+
tint?: string;
|
|
14
|
+
highlight?: string;
|
|
15
|
+
border?: boolean;
|
|
16
|
+
fallbackBlur?: number;
|
|
17
|
+
fallbackSaturation?: number;
|
|
18
|
+
class?: string;
|
|
19
|
+
containerClass?: string;
|
|
20
|
+
children?: Snippet;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
radius = 24,
|
|
25
|
+
baseFrequency = 0.008,
|
|
26
|
+
numOctaves = 2,
|
|
27
|
+
seed = 92,
|
|
28
|
+
noiseBlur = 2,
|
|
29
|
+
scale = 70,
|
|
30
|
+
tint = "hsla(0, 0%, 100%, 0.25)",
|
|
31
|
+
highlight = "hsla(0, 0%, 100%, 0.75)",
|
|
32
|
+
border = true,
|
|
33
|
+
fallbackBlur = 20,
|
|
34
|
+
fallbackSaturation = 180,
|
|
35
|
+
class: className = "",
|
|
36
|
+
containerClass = "",
|
|
37
|
+
children,
|
|
38
|
+
}: Props = $props();
|
|
39
|
+
|
|
40
|
+
let filterId = $state("");
|
|
41
|
+
|
|
42
|
+
let containerStyle = $derived(
|
|
43
|
+
`border-radius:${radius}px;--fg-tint:${tint};--fg-highlight:${highlight};` +
|
|
44
|
+
`--fg-fallback-blur:${fallbackBlur}px;--fg-fallback-saturation:${fallbackSaturation}%;`
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
onMount(() => {
|
|
48
|
+
filterId = `fg-dist-${Math.random().toString(36).slice(2, 8)}`;
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<div
|
|
53
|
+
class={cn("frosted-glass", border && "frosted-glass-border", containerClass)}
|
|
54
|
+
style={containerStyle}
|
|
55
|
+
>
|
|
56
|
+
{#if filterId}
|
|
57
|
+
<div class="frosted-glass-filter" style="filter:url(#{filterId});"></div>
|
|
58
|
+
|
|
59
|
+
<svg class="frosted-glass-defs" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
60
|
+
<defs>
|
|
61
|
+
<filter
|
|
62
|
+
id={filterId}
|
|
63
|
+
x="-50%"
|
|
64
|
+
y="-50%"
|
|
65
|
+
width="200%"
|
|
66
|
+
height="200%"
|
|
67
|
+
filterUnits="objectBoundingBox"
|
|
68
|
+
primitiveUnits="userSpaceOnUse"
|
|
69
|
+
color-interpolation-filters="linearRGB"
|
|
70
|
+
>
|
|
71
|
+
<feTurbulence
|
|
72
|
+
type="fractalNoise"
|
|
73
|
+
baseFrequency="{baseFrequency} {baseFrequency}"
|
|
74
|
+
{numOctaves}
|
|
75
|
+
{seed}
|
|
76
|
+
stitchTiles="stitch"
|
|
77
|
+
result="noise"
|
|
78
|
+
/>
|
|
79
|
+
<feGaussianBlur in="noise" stdDeviation={noiseBlur} result="blurred" />
|
|
80
|
+
<feDisplacementMap
|
|
81
|
+
in="SourceGraphic"
|
|
82
|
+
in2="blurred"
|
|
83
|
+
{scale}
|
|
84
|
+
xChannelSelector="R"
|
|
85
|
+
yChannelSelector="G"
|
|
86
|
+
/>
|
|
87
|
+
</filter>
|
|
88
|
+
</defs>
|
|
89
|
+
</svg>
|
|
90
|
+
{/if}
|
|
91
|
+
|
|
92
|
+
<div class="frosted-glass-overlay"></div>
|
|
93
|
+
<div class="frosted-glass-specular"></div>
|
|
94
|
+
|
|
95
|
+
<div class={cn("frosted-glass-content", className)}>
|
|
96
|
+
{#if children}
|
|
97
|
+
{@render children()}
|
|
98
|
+
{/if}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<style>
|
|
103
|
+
.frosted-glass {
|
|
104
|
+
position: relative;
|
|
105
|
+
display: flex;
|
|
106
|
+
background: transparent;
|
|
107
|
+
overflow: hidden;
|
|
108
|
+
contain: layout paint;
|
|
109
|
+
box-shadow:
|
|
110
|
+
0 2px 4px rgba(0, 0, 0, 0.1),
|
|
111
|
+
0 0 8px rgba(0, 0, 0, 0.05);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.frosted-glass-border::before {
|
|
115
|
+
content: "";
|
|
116
|
+
position: absolute;
|
|
117
|
+
inset: 0;
|
|
118
|
+
z-index: 4;
|
|
119
|
+
border-radius: inherit;
|
|
120
|
+
padding: 1px;
|
|
121
|
+
background:
|
|
122
|
+
conic-gradient(
|
|
123
|
+
from -75deg at 50% 50%,
|
|
124
|
+
rgba(20, 60, 120, 0.5),
|
|
125
|
+
rgba(150, 200, 255, 0.1) 5% 40%,
|
|
126
|
+
rgba(20, 60, 120, 0.5) 50%,
|
|
127
|
+
rgba(150, 200, 255, 0.1) 60% 95%,
|
|
128
|
+
rgba(20, 60, 120, 0.5)
|
|
129
|
+
),
|
|
130
|
+
linear-gradient(180deg, rgba(220, 240, 255, 0.5), rgba(240, 250, 255, 0.5));
|
|
131
|
+
box-shadow: inset 0 0 0 0.5px rgba(220, 240, 255, 0.5);
|
|
132
|
+
pointer-events: none;
|
|
133
|
+
mask:
|
|
134
|
+
linear-gradient(#fff 0 0) content-box,
|
|
135
|
+
linear-gradient(#fff 0 0);
|
|
136
|
+
mask-composite: exclude;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* backdrop-filter: blur(0) pulls the backdrop into this layer so the SVG
|
|
140
|
+
displacement filter can distort it */
|
|
141
|
+
.frosted-glass-filter {
|
|
142
|
+
position: absolute;
|
|
143
|
+
inset: 0;
|
|
144
|
+
z-index: 0;
|
|
145
|
+
backdrop-filter: blur(0);
|
|
146
|
+
isolation: isolate;
|
|
147
|
+
transform: translateZ(0);
|
|
148
|
+
will-change: filter;
|
|
149
|
+
animation: frosted-glass-settle 0.22s ease-out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@keyframes frosted-glass-settle {
|
|
153
|
+
0% {
|
|
154
|
+
opacity: 0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
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)) {
|
|
162
|
+
.frosted-glass-filter {
|
|
163
|
+
filter: none !important;
|
|
164
|
+
-webkit-backdrop-filter: blur(var(--fg-fallback-blur)) saturate(var(--fg-fallback-saturation));
|
|
165
|
+
backdrop-filter: blur(var(--fg-fallback-blur)) saturate(var(--fg-fallback-saturation));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.frosted-glass-overlay {
|
|
169
|
+
background: hsla(0, 0%, 100%, 0.5);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.frosted-glass-defs {
|
|
174
|
+
position: absolute;
|
|
175
|
+
width: 0;
|
|
176
|
+
height: 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.frosted-glass-overlay {
|
|
180
|
+
position: absolute;
|
|
181
|
+
inset: 0;
|
|
182
|
+
z-index: 1;
|
|
183
|
+
background: var(--fg-tint);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.frosted-glass-specular {
|
|
187
|
+
position: absolute;
|
|
188
|
+
inset: 0;
|
|
189
|
+
z-index: 2;
|
|
190
|
+
border-radius: inherit;
|
|
191
|
+
overflow: hidden;
|
|
192
|
+
box-shadow:
|
|
193
|
+
inset 1px 1px 0 var(--fg-highlight),
|
|
194
|
+
inset 0 0 5px var(--fg-highlight);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.frosted-glass-content {
|
|
198
|
+
position: relative;
|
|
199
|
+
z-index: 3;
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
width: 100%;
|
|
203
|
+
}
|
|
204
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
interface Props {
|
|
3
|
+
radius?: number;
|
|
4
|
+
baseFrequency?: number;
|
|
5
|
+
numOctaves?: number;
|
|
6
|
+
seed?: number;
|
|
7
|
+
noiseBlur?: number;
|
|
8
|
+
scale?: number;
|
|
9
|
+
tint?: string;
|
|
10
|
+
highlight?: string;
|
|
11
|
+
border?: boolean;
|
|
12
|
+
fallbackBlur?: number;
|
|
13
|
+
fallbackSaturation?: number;
|
|
14
|
+
class?: string;
|
|
15
|
+
containerClass?: string;
|
|
16
|
+
children?: Snippet;
|
|
17
|
+
}
|
|
18
|
+
declare const FrostedGlass: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type FrostedGlass = ReturnType<typeof FrostedGlass>;
|
|
20
|
+
export default FrostedGlass;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { render, cleanup } from "@testing-library/svelte";
|
|
2
|
+
import { afterEach, describe, it, expect } from "vitest";
|
|
3
|
+
import FrostedGlass from "./FrostedGlass.svelte";
|
|
4
|
+
describe("FrostedGlass", () => {
|
|
5
|
+
afterEach(cleanup);
|
|
6
|
+
it("renders the component", () => {
|
|
7
|
+
const { container } = render(FrostedGlass);
|
|
8
|
+
const wrapper = container.querySelector(".frosted-glass");
|
|
9
|
+
expect(wrapper).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
it("renders the layer stack", () => {
|
|
12
|
+
const { container } = render(FrostedGlass);
|
|
13
|
+
expect(container.querySelector(".frosted-glass-overlay")).toBeInTheDocument();
|
|
14
|
+
expect(container.querySelector(".frosted-glass-specular")).toBeInTheDocument();
|
|
15
|
+
expect(container.querySelector(".frosted-glass-content")).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
it("applies custom container class", () => {
|
|
18
|
+
const { container } = render(FrostedGlass, {
|
|
19
|
+
props: { containerClass: "my-container" },
|
|
20
|
+
});
|
|
21
|
+
const wrapper = container.querySelector(".frosted-glass");
|
|
22
|
+
expect(wrapper?.className).toContain("my-container");
|
|
23
|
+
});
|
|
24
|
+
it("applies custom class to content", () => {
|
|
25
|
+
const { container } = render(FrostedGlass, { props: { class: "my-content" } });
|
|
26
|
+
const content = container.querySelector(".frosted-glass-content");
|
|
27
|
+
expect(content?.className).toContain("my-content");
|
|
28
|
+
});
|
|
29
|
+
it("sets border radius", () => {
|
|
30
|
+
const { container } = render(FrostedGlass, { props: { radius: 9999 } });
|
|
31
|
+
const wrapper = container.querySelector(".frosted-glass");
|
|
32
|
+
expect(wrapper.style.borderRadius).toBe("9999px");
|
|
33
|
+
});
|
|
34
|
+
it("sets tint CSS variable", () => {
|
|
35
|
+
const { container } = render(FrostedGlass, {
|
|
36
|
+
props: { tint: "hsla(0, 0%, 0%, 0.3)" },
|
|
37
|
+
});
|
|
38
|
+
const wrapper = container.querySelector(".frosted-glass");
|
|
39
|
+
expect(wrapper.style.getPropertyValue("--fg-tint")).toBe("hsla(0, 0%, 0%, 0.3)");
|
|
40
|
+
});
|
|
41
|
+
it("renders border layer by default and hides it when border=false", () => {
|
|
42
|
+
const withBorder = render(FrostedGlass);
|
|
43
|
+
expect(withBorder.container.querySelector(".frosted-glass-border")).toBeInTheDocument();
|
|
44
|
+
cleanup();
|
|
45
|
+
const withoutBorder = render(FrostedGlass, { props: { border: false } });
|
|
46
|
+
expect(withoutBorder.container.querySelector(".frosted-glass-border")).not.toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# FrostedGlass
|
|
2
|
+
|
|
3
|
+
Frosted glass surface with an organic, wavy refraction of the content behind it. An alternative to [LiquidGlass](../liquid-glass/README.md): where LiquidGlass produces chromatic (rainbow-edge) refraction from gradient displacement maps, FrostedGlass distorts the backdrop with fractal turbulence noise for a softer, rippled-glass look — the effect used by glassy pill navbars.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
The effect stacks four layers inside a rounded container:
|
|
8
|
+
|
|
9
|
+
1. **Filter layer** — `backdrop-filter: blur(0)` pulls the backdrop into the element's own rendering, then a CSS `filter: url(#...)` pointing at an inline SVG filter distorts it: `feTurbulence` (fractal noise) → `feGaussianBlur` → `feDisplacementMap`.
|
|
10
|
+
2. **Overlay** — translucent tint (`tint` prop).
|
|
11
|
+
3. **Specular** — inset highlights simulating a lit glass rim (`highlight` prop).
|
|
12
|
+
4. **Content** — your slotted content.
|
|
13
|
+
|
|
14
|
+
An optional conic-gradient border (`border` prop) frames the container.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```svelte
|
|
19
|
+
<script>
|
|
20
|
+
import { FrostedGlass } from "fancy-ui-svelte";
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<FrostedGlass radius={9999}>
|
|
24
|
+
<nav class="flex w-full items-center justify-between px-7 py-4">
|
|
25
|
+
<span class="font-semibold">Brand</span>
|
|
26
|
+
<a href="/download">Download</a>
|
|
27
|
+
</nav>
|
|
28
|
+
</FrostedGlass>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Props
|
|
32
|
+
|
|
33
|
+
| Prop | Type | Default | Description |
|
|
34
|
+
| -------------------- | --------- | --------------------------- | ------------------------------------------------- |
|
|
35
|
+
| `radius` | `number` | `24` | Border radius in pixels |
|
|
36
|
+
| `baseFrequency` | `number` | `0.008` | Turbulence noise frequency (lower = wider waves) |
|
|
37
|
+
| `numOctaves` | `number` | `2` | Turbulence octaves (detail of the noise) |
|
|
38
|
+
| `seed` | `number` | `92` | Turbulence random seed |
|
|
39
|
+
| `noiseBlur` | `number` | `2` | Gaussian blur softening the noise before displace |
|
|
40
|
+
| `scale` | `number` | `70` | Displacement intensity |
|
|
41
|
+
| `tint` | `string` | `"hsla(0, 0%, 100%, 0.25)"` | Overlay tint color |
|
|
42
|
+
| `highlight` | `string` | `"hsla(0, 0%, 100%, 0.75)"` | Specular rim highlight color |
|
|
43
|
+
| `border` | `boolean` | `true` | Show the conic-gradient glass border |
|
|
44
|
+
| `fallbackBlur` | `number` | `20` | Backdrop blur (px) for the Safari fallback |
|
|
45
|
+
| `fallbackSaturation` | `number` | `180` | Backdrop saturation (%) for the Safari fallback |
|
|
46
|
+
| `class` | `string` | `""` | CSS classes for the content layer |
|
|
47
|
+
| `containerClass` | `string` | `""` | CSS classes for the outer container |
|
|
48
|
+
|
|
49
|
+
## Slots
|
|
50
|
+
|
|
51
|
+
| Slot | Description |
|
|
52
|
+
| ---------- | ------------------------------------ |
|
|
53
|
+
| `children` | Content rendered on top of the glass |
|
|
54
|
+
|
|
55
|
+
## Notes
|
|
56
|
+
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FrostedGlass } from "./FrostedGlass.svelte";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FrostedGlass } from "./FrostedGlass.svelte";
|
package/dist/fancy-ui/index.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export * from "./text-reveal-card/index.js";
|
|
|
42
42
|
export * from "./container-scroll/index.js";
|
|
43
43
|
export * from "./container-text-flip/index.js";
|
|
44
44
|
export * from "./focus/index.js";
|
|
45
|
+
export * from "./frosted-glass/index.js";
|
|
45
46
|
export * from "./liquid-glass/index.js";
|
|
46
47
|
export * from "./smooth-cursor/index.js";
|
|
47
48
|
export * from "./sparkles/index.js";
|
|
@@ -57,4 +58,5 @@ export * from "./terminal-text/index.js";
|
|
|
57
58
|
export * from "./noise-reveal/index.js";
|
|
58
59
|
export * from "./line-reveal/index.js";
|
|
59
60
|
export * from "./editorial-engine/index.js";
|
|
61
|
+
export * from "./liquid-text/index.js";
|
|
60
62
|
export { registry, categories, categoryLabels, categoryDescriptions, getAllComponents, getComponent, getComponentsByStatus, getComponentsByCategory, getComponentsGroupedByCategory, getComponentsGroupedByStatus, searchComponents, getStats, hasComponent, getComponentCategory, } from "./registry.js";
|
package/dist/fancy-ui/index.js
CHANGED
|
@@ -47,6 +47,7 @@ export * from "./text-reveal-card/index.js";
|
|
|
47
47
|
export * from "./container-scroll/index.js";
|
|
48
48
|
export * from "./container-text-flip/index.js";
|
|
49
49
|
export * from "./focus/index.js";
|
|
50
|
+
export * from "./frosted-glass/index.js";
|
|
50
51
|
export * from "./liquid-glass/index.js";
|
|
51
52
|
export * from "./smooth-cursor/index.js";
|
|
52
53
|
export * from "./sparkles/index.js";
|
|
@@ -62,6 +63,7 @@ export * from "./terminal-text/index.js";
|
|
|
62
63
|
export * from "./noise-reveal/index.js";
|
|
63
64
|
export * from "./line-reveal/index.js";
|
|
64
65
|
export * from "./editorial-engine/index.js";
|
|
66
|
+
export * from "./liquid-text/index.js";
|
|
65
67
|
// =============================================================================
|
|
66
68
|
// Registry
|
|
67
69
|
// =============================================================================
|
|
@@ -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:
|
|
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.
|