fancy-ui-svelte 0.5.0 → 0.7.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 +98 -74
- package/dist/fancy-ui/dock/index.d.ts +1 -1
- package/dist/fancy-ui/dock/index.js +0 -1
- package/dist/fancy-ui/editorial-engine/EditorialEngine.svelte +181 -0
- package/dist/fancy-ui/editorial-engine/EditorialEngine.svelte.d.ts +26 -0
- package/dist/fancy-ui/editorial-engine/README.md +56 -0
- package/dist/fancy-ui/editorial-engine/engine.d.ts +7 -0
- package/dist/fancy-ui/editorial-engine/engine.js +581 -0
- package/dist/fancy-ui/editorial-engine/index.d.ts +2 -0
- package/dist/fancy-ui/editorial-engine/index.js +1 -0
- package/dist/fancy-ui/frosted-glass/FrostedGlass.svelte +202 -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 +59 -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 +5 -0
- package/dist/fancy-ui/index.js +5 -0
- package/dist/fancy-ui/line-reveal/LineReveal.svelte +168 -0
- package/dist/fancy-ui/line-reveal/LineReveal.svelte.d.ts +33 -0
- package/dist/fancy-ui/line-reveal/README.md +42 -0
- package/dist/fancy-ui/line-reveal/index.d.ts +2 -0
- package/dist/fancy-ui/line-reveal/index.js +1 -0
- package/dist/fancy-ui/noise-reveal/NoiseReveal.svelte +323 -0
- package/dist/fancy-ui/noise-reveal/NoiseReveal.svelte.d.ts +19 -0
- package/dist/fancy-ui/noise-reveal/NoiseReveal.test.d.ts +1 -0
- package/dist/fancy-ui/noise-reveal/NoiseReveal.test.js +123 -0
- package/dist/fancy-ui/noise-reveal/index.d.ts +2 -0
- package/dist/fancy-ui/noise-reveal/index.js +1 -0
- package/dist/fancy-ui/registry.js +196 -11
- package/dist/fancy-ui/themes.d.ts +38 -0
- package/dist/fancy-ui/themes.js +356 -0
- package/dist/types.d.ts +2 -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 -3
|
@@ -0,0 +1,202 @@
|
|
|
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 — fall back to
|
|
159
|
+
plain frosted blur */
|
|
160
|
+
@supports (-webkit-hyphens: none) {
|
|
161
|
+
.frosted-glass-filter {
|
|
162
|
+
filter: none !important;
|
|
163
|
+
backdrop-filter: blur(var(--fg-fallback-blur)) saturate(var(--fg-fallback-saturation));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.frosted-glass-overlay {
|
|
167
|
+
background: hsla(0, 0%, 100%, 0.5);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.frosted-glass-defs {
|
|
172
|
+
position: absolute;
|
|
173
|
+
width: 0;
|
|
174
|
+
height: 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.frosted-glass-overlay {
|
|
178
|
+
position: absolute;
|
|
179
|
+
inset: 0;
|
|
180
|
+
z-index: 1;
|
|
181
|
+
background: var(--fg-tint);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.frosted-glass-specular {
|
|
185
|
+
position: absolute;
|
|
186
|
+
inset: 0;
|
|
187
|
+
z-index: 2;
|
|
188
|
+
border-radius: inherit;
|
|
189
|
+
overflow: hidden;
|
|
190
|
+
box-shadow:
|
|
191
|
+
inset 1px 1px 0 var(--fg-highlight),
|
|
192
|
+
inset 0 0 5px var(--fg-highlight);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.frosted-glass-content {
|
|
196
|
+
position: relative;
|
|
197
|
+
z-index: 3;
|
|
198
|
+
display: flex;
|
|
199
|
+
align-items: center;
|
|
200
|
+
width: 100%;
|
|
201
|
+
}
|
|
202
|
+
</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,59 @@
|
|
|
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
|
+
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
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```svelte
|
|
21
|
+
<script>
|
|
22
|
+
import { FrostedGlass } from "fancy-ui-svelte";
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<FrostedGlass radius={9999}>
|
|
26
|
+
<nav class="flex w-full items-center justify-between px-7 py-4">
|
|
27
|
+
<span class="font-semibold">Brand</span>
|
|
28
|
+
<a href="/download">Download</a>
|
|
29
|
+
</nav>
|
|
30
|
+
</FrostedGlass>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Props
|
|
34
|
+
|
|
35
|
+
| Prop | Type | Default | Description |
|
|
36
|
+
| -------------------- | --------- | --------------------------- | ------------------------------------------------- |
|
|
37
|
+
| `radius` | `number` | `24` | Border radius in pixels |
|
|
38
|
+
| `baseFrequency` | `number` | `0.008` | Turbulence noise frequency (lower = wider waves) |
|
|
39
|
+
| `numOctaves` | `number` | `2` | Turbulence octaves (detail of the noise) |
|
|
40
|
+
| `seed` | `number` | `92` | Turbulence random seed |
|
|
41
|
+
| `noiseBlur` | `number` | `2` | Gaussian blur softening the noise before displace |
|
|
42
|
+
| `scale` | `number` | `70` | Displacement intensity |
|
|
43
|
+
| `tint` | `string` | `"hsla(0, 0%, 100%, 0.25)"` | Overlay tint color |
|
|
44
|
+
| `highlight` | `string` | `"hsla(0, 0%, 100%, 0.75)"` | Specular rim highlight color |
|
|
45
|
+
| `border` | `boolean` | `true` | Show the conic-gradient glass border |
|
|
46
|
+
| `fallbackBlur` | `number` | `20` | Backdrop blur (px) for the Safari fallback |
|
|
47
|
+
| `fallbackSaturation` | `number` | `180` | Backdrop saturation (%) for the Safari fallback |
|
|
48
|
+
| `class` | `string` | `""` | CSS classes for the content layer |
|
|
49
|
+
| `containerClass` | `string` | `""` | CSS classes for the outer container |
|
|
50
|
+
|
|
51
|
+
## Slots
|
|
52
|
+
|
|
53
|
+
| Slot | Description |
|
|
54
|
+
| ---------- | ------------------------------------ |
|
|
55
|
+
| `children` | Content rendered on top of the glass |
|
|
56
|
+
|
|
57
|
+
## Notes
|
|
58
|
+
|
|
59
|
+
- The displacement only shows over content that scrolls or moves behind the glass; over a flat background the effect reads as a subtle frost.
|
|
@@ -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
|
@@ -11,6 +11,7 @@ export * from "./border-beam/index.js";
|
|
|
11
11
|
export * from "./glow-border/index.js";
|
|
12
12
|
export * from "./marquee/index.js";
|
|
13
13
|
export * from "./compare/index.js";
|
|
14
|
+
export * from "./dock/index.js";
|
|
14
15
|
export * from "./image-trail-cursor/index.js";
|
|
15
16
|
export * from "./interactive-grid-pattern/index.js";
|
|
16
17
|
export * from "./interactive-hover-button/index.js";
|
|
@@ -41,6 +42,7 @@ export * from "./text-reveal-card/index.js";
|
|
|
41
42
|
export * from "./container-scroll/index.js";
|
|
42
43
|
export * from "./container-text-flip/index.js";
|
|
43
44
|
export * from "./focus/index.js";
|
|
45
|
+
export * from "./frosted-glass/index.js";
|
|
44
46
|
export * from "./liquid-glass/index.js";
|
|
45
47
|
export * from "./smooth-cursor/index.js";
|
|
46
48
|
export * from "./sparkles/index.js";
|
|
@@ -53,4 +55,7 @@ export * from "./tracing-beam/index.js";
|
|
|
53
55
|
export * from "./displacement-text/index.js";
|
|
54
56
|
export * from "./matrix-rain/index.js";
|
|
55
57
|
export * from "./terminal-text/index.js";
|
|
58
|
+
export * from "./noise-reveal/index.js";
|
|
59
|
+
export * from "./line-reveal/index.js";
|
|
60
|
+
export * from "./editorial-engine/index.js";
|
|
56
61
|
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
|
@@ -16,6 +16,7 @@ export * from "./border-beam/index.js";
|
|
|
16
16
|
export * from "./glow-border/index.js";
|
|
17
17
|
export * from "./marquee/index.js";
|
|
18
18
|
export * from "./compare/index.js";
|
|
19
|
+
export * from "./dock/index.js";
|
|
19
20
|
export * from "./image-trail-cursor/index.js";
|
|
20
21
|
export * from "./interactive-grid-pattern/index.js";
|
|
21
22
|
export * from "./interactive-hover-button/index.js";
|
|
@@ -46,6 +47,7 @@ export * from "./text-reveal-card/index.js";
|
|
|
46
47
|
export * from "./container-scroll/index.js";
|
|
47
48
|
export * from "./container-text-flip/index.js";
|
|
48
49
|
export * from "./focus/index.js";
|
|
50
|
+
export * from "./frosted-glass/index.js";
|
|
49
51
|
export * from "./liquid-glass/index.js";
|
|
50
52
|
export * from "./smooth-cursor/index.js";
|
|
51
53
|
export * from "./sparkles/index.js";
|
|
@@ -58,6 +60,9 @@ export * from "./tracing-beam/index.js";
|
|
|
58
60
|
export * from "./displacement-text/index.js";
|
|
59
61
|
export * from "./matrix-rain/index.js";
|
|
60
62
|
export * from "./terminal-text/index.js";
|
|
63
|
+
export * from "./noise-reveal/index.js";
|
|
64
|
+
export * from "./line-reveal/index.js";
|
|
65
|
+
export * from "./editorial-engine/index.js";
|
|
61
66
|
// =============================================================================
|
|
62
67
|
// Registry
|
|
63
68
|
// =============================================================================
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* Props for LineReveal
|
|
4
|
+
*
|
|
5
|
+
* Reveals text line by line with a staggered slide-up animation.
|
|
6
|
+
* Line breaks are computed with @chenglou/pretext (canvas text
|
|
7
|
+
* measurement), so no DOM splitting or reflow measurement is needed.
|
|
8
|
+
*/
|
|
9
|
+
export interface LineRevealProps {
|
|
10
|
+
/** Text to reveal */
|
|
11
|
+
text: string;
|
|
12
|
+
/**
|
|
13
|
+
* CSS font shorthand used for both measurement and rendering
|
|
14
|
+
* (canvas `ctx.font` format, e.g. "600 32px Inter, sans-serif").
|
|
15
|
+
* Prefer named families: `system-ui` can resolve differently between
|
|
16
|
+
* canvas measurement and DOM rendering.
|
|
17
|
+
*/
|
|
18
|
+
font?: string;
|
|
19
|
+
/** Line height in pixels (defaults to 1.2 × font size) */
|
|
20
|
+
lineHeight?: number;
|
|
21
|
+
/** Delay between lines in seconds */
|
|
22
|
+
stagger?: number;
|
|
23
|
+
/** Animation duration per line in seconds */
|
|
24
|
+
duration?: number;
|
|
25
|
+
/** Initial delay before the first line in seconds */
|
|
26
|
+
delay?: number;
|
|
27
|
+
/** Animate only the first time the component enters the viewport */
|
|
28
|
+
once?: boolean;
|
|
29
|
+
/** Additional CSS classes */
|
|
30
|
+
class?: string;
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<script lang="ts">
|
|
35
|
+
import { cn } from "../../utils.js";
|
|
36
|
+
import { onMount } from "svelte";
|
|
37
|
+
import {
|
|
38
|
+
prepareWithSegments,
|
|
39
|
+
layoutWithLines,
|
|
40
|
+
type PreparedTextWithSegments,
|
|
41
|
+
} from "@chenglou/pretext";
|
|
42
|
+
|
|
43
|
+
let {
|
|
44
|
+
text,
|
|
45
|
+
// Named families only: canvas and CSS can resolve `system-ui` to
|
|
46
|
+
// different fonts (notably on macOS), which skews measured widths.
|
|
47
|
+
font = '600 32px "Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
48
|
+
lineHeight,
|
|
49
|
+
stagger = 0.08,
|
|
50
|
+
duration = 0.7,
|
|
51
|
+
delay = 0,
|
|
52
|
+
once = true,
|
|
53
|
+
class: className,
|
|
54
|
+
}: LineRevealProps = $props();
|
|
55
|
+
|
|
56
|
+
const fontSize = $derived(Number(/(\d+(?:\.\d+)?)px/.exec(font)?.[1] ?? 32));
|
|
57
|
+
const resolvedLineHeight = $derived(lineHeight ?? Math.round(fontSize * 1.2));
|
|
58
|
+
|
|
59
|
+
let containerRef: HTMLDivElement;
|
|
60
|
+
let containerWidth = $state(0);
|
|
61
|
+
let revealed = $state(false);
|
|
62
|
+
let prepared = $state<PreparedTextWithSegments | null>(null);
|
|
63
|
+
|
|
64
|
+
// Measurement requires a canvas context, so this only runs in the browser
|
|
65
|
+
// ($effect never runs during SSR). Web fonts must be loaded first or the
|
|
66
|
+
// canvas silently measures with the fallback font.
|
|
67
|
+
$effect(() => {
|
|
68
|
+
const currentText = text;
|
|
69
|
+
const currentFont = font;
|
|
70
|
+
let cancelled = false;
|
|
71
|
+
|
|
72
|
+
document.fonts
|
|
73
|
+
.load(currentFont)
|
|
74
|
+
.catch(() => {})
|
|
75
|
+
.then(() => {
|
|
76
|
+
if (!cancelled) {
|
|
77
|
+
prepared = prepareWithSegments(currentText, currentFont);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
cancelled = true;
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Pure arithmetic on cached measurements — cheap to re-run on every resize
|
|
87
|
+
// (containerWidth is tracked by Svelte's bind:clientWidth ResizeObserver).
|
|
88
|
+
const layoutResult = $derived(
|
|
89
|
+
prepared && containerWidth > 0
|
|
90
|
+
? layoutWithLines(prepared, containerWidth, resolvedLineHeight)
|
|
91
|
+
: null
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
onMount(() => {
|
|
95
|
+
const observer = new IntersectionObserver(
|
|
96
|
+
(entries) => {
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
if (entry.isIntersecting) {
|
|
99
|
+
revealed = true;
|
|
100
|
+
if (once) observer.disconnect();
|
|
101
|
+
} else if (!once) {
|
|
102
|
+
revealed = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{ threshold: 0.1 }
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
observer.observe(containerRef);
|
|
110
|
+
|
|
111
|
+
return () => observer.disconnect();
|
|
112
|
+
});
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<div
|
|
116
|
+
bind:this={containerRef}
|
|
117
|
+
bind:clientWidth={containerWidth}
|
|
118
|
+
class={cn("relative w-full", className)}
|
|
119
|
+
style:font
|
|
120
|
+
style:line-height="{resolvedLineHeight}px"
|
|
121
|
+
style:height={layoutResult ? `${layoutResult.height}px` : undefined}
|
|
122
|
+
>
|
|
123
|
+
<span class="sr-only">{text}</span>
|
|
124
|
+
{#if layoutResult}
|
|
125
|
+
{#each layoutResult.lines as line, i (i)}
|
|
126
|
+
<div class="line-mask" style:height="{resolvedLineHeight}px" aria-hidden="true">
|
|
127
|
+
<div
|
|
128
|
+
class="line"
|
|
129
|
+
class:revealed
|
|
130
|
+
style:transition-duration="{duration}s"
|
|
131
|
+
style:transition-delay="{delay + i * stagger}s"
|
|
132
|
+
>
|
|
133
|
+
{line.text}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
{/each}
|
|
137
|
+
{:else}
|
|
138
|
+
<!-- SSR / pre-measure fallback: keeps text in the HTML and reserves space -->
|
|
139
|
+
<div style:visibility="hidden" aria-hidden="true">{text}</div>
|
|
140
|
+
{/if}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<style>
|
|
144
|
+
.line-mask {
|
|
145
|
+
overflow: hidden;
|
|
146
|
+
/* preserve exact spacing so rendered width matches pretext measurement */
|
|
147
|
+
white-space: pre;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.line {
|
|
151
|
+
opacity: 0;
|
|
152
|
+
transform: translateY(105%);
|
|
153
|
+
transition-property: transform, opacity;
|
|
154
|
+
transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.line.revealed {
|
|
158
|
+
opacity: 1;
|
|
159
|
+
transform: translateY(0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@media (prefers-reduced-motion: reduce) {
|
|
163
|
+
.line {
|
|
164
|
+
transition-duration: 0.1s !important;
|
|
165
|
+
transition-delay: 0s !important;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Props for LineReveal
|
|
3
|
+
*
|
|
4
|
+
* Reveals text line by line with a staggered slide-up animation.
|
|
5
|
+
* Line breaks are computed with @chenglou/pretext (canvas text
|
|
6
|
+
* measurement), so no DOM splitting or reflow measurement is needed.
|
|
7
|
+
*/
|
|
8
|
+
export interface LineRevealProps {
|
|
9
|
+
/** Text to reveal */
|
|
10
|
+
text: string;
|
|
11
|
+
/**
|
|
12
|
+
* CSS font shorthand used for both measurement and rendering
|
|
13
|
+
* (canvas `ctx.font` format, e.g. "600 32px Inter, sans-serif").
|
|
14
|
+
* Prefer named families: `system-ui` can resolve differently between
|
|
15
|
+
* canvas measurement and DOM rendering.
|
|
16
|
+
*/
|
|
17
|
+
font?: string;
|
|
18
|
+
/** Line height in pixels (defaults to 1.2 × font size) */
|
|
19
|
+
lineHeight?: number;
|
|
20
|
+
/** Delay between lines in seconds */
|
|
21
|
+
stagger?: number;
|
|
22
|
+
/** Animation duration per line in seconds */
|
|
23
|
+
duration?: number;
|
|
24
|
+
/** Initial delay before the first line in seconds */
|
|
25
|
+
delay?: number;
|
|
26
|
+
/** Animate only the first time the component enters the viewport */
|
|
27
|
+
once?: boolean;
|
|
28
|
+
/** Additional CSS classes */
|
|
29
|
+
class?: string;
|
|
30
|
+
}
|
|
31
|
+
declare const LineReveal: import("svelte").Component<LineRevealProps, {}, "">;
|
|
32
|
+
type LineReveal = ReturnType<typeof LineReveal>;
|
|
33
|
+
export default LineReveal;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# LineReveal
|
|
2
|
+
|
|
3
|
+
Reveals text line by line with a staggered slide-up animation.
|
|
4
|
+
|
|
5
|
+
Unlike DOM-splitting approaches, line breaks are computed with
|
|
6
|
+
[@chenglou/pretext](https://github.com/chenglou/pretext) — canvas-accurate text
|
|
7
|
+
measurement in pure JS. Each computed line is rendered as its own masked
|
|
8
|
+
element, so the per-line stagger works without measuring or mutating the DOM,
|
|
9
|
+
and lines re-wrap instantly on container resize.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```svelte
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import { LineReveal } from "$lib/fancy-ui/line-reveal";
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<LineReveal
|
|
19
|
+
text="Text measurement and layout without touching the DOM."
|
|
20
|
+
font="700 40px Helvetica, Arial, sans-serif"
|
|
21
|
+
/>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Props
|
|
25
|
+
|
|
26
|
+
| Prop | Type | Default | Description |
|
|
27
|
+
| ------------ | --------- | -------------------------------- | ------------------------------------------------------------ |
|
|
28
|
+
| `text` | `string` | — (required) | Text to reveal |
|
|
29
|
+
| `font` | `string` | `"600 32px \"Helvetica Neue\", Helvetica, Arial, sans-serif"` | CSS font shorthand, used for both measurement and rendering |
|
|
30
|
+
| `lineHeight` | `number` | `1.2 × font size` | Line height in pixels |
|
|
31
|
+
| `stagger` | `number` | `0.08` | Delay between lines in seconds |
|
|
32
|
+
| `duration` | `number` | `0.7` | Animation duration per line in seconds |
|
|
33
|
+
| `delay` | `number` | `0` | Initial delay in seconds |
|
|
34
|
+
| `once` | `boolean` | `true` | Animate only on first viewport entry |
|
|
35
|
+
| `class` | `string` | — | Additional CSS classes |
|
|
36
|
+
|
|
37
|
+
## Notes
|
|
38
|
+
|
|
39
|
+
- `font` must be a valid canvas `ctx.font` shorthand (weight, size in px, family).
|
|
40
|
+
- Measurement happens client-side only (pretext needs a canvas); during SSR the
|
|
41
|
+
raw text is rendered hidden so content stays in the HTML.
|
|
42
|
+
- Resizing the container re-wraps lines without replaying the animation.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as LineReveal } from "./LineReveal.svelte";
|