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.
Files changed (43) hide show
  1. package/README.md +84 -71
  2. package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte +1132 -1111
  3. package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte.d.ts +3 -5
  4. package/dist/fancy-ui/fluid-cursor/FluidCursor.test.js +29 -0
  5. package/dist/fancy-ui/fluid-cursor/README.md +57 -6
  6. package/dist/fancy-ui/fluid-cursor/fluid-shared.d.ts +40 -0
  7. package/dist/fancy-ui/fluid-cursor/fluid-shared.js +125 -0
  8. package/dist/fancy-ui/fluid-cursor/fluid-shared.test.d.ts +1 -0
  9. package/dist/fancy-ui/fluid-cursor/fluid-shared.test.js +39 -0
  10. package/dist/fancy-ui/fluid-cursor/webgpu-engine.d.ts +29 -0
  11. package/dist/fancy-ui/fluid-cursor/webgpu-engine.js +847 -0
  12. package/dist/fancy-ui/frosted-glass/FrostedGlass.svelte +204 -0
  13. package/dist/fancy-ui/frosted-glass/FrostedGlass.svelte.d.ts +20 -0
  14. package/dist/fancy-ui/frosted-glass/FrostedGlass.test.d.ts +1 -0
  15. package/dist/fancy-ui/frosted-glass/FrostedGlass.test.js +48 -0
  16. package/dist/fancy-ui/frosted-glass/README.md +61 -0
  17. package/dist/fancy-ui/frosted-glass/index.d.ts +1 -0
  18. package/dist/fancy-ui/frosted-glass/index.js +1 -0
  19. package/dist/fancy-ui/index.d.ts +2 -0
  20. package/dist/fancy-ui/index.js +2 -0
  21. package/dist/fancy-ui/liquid-glass/LiquidGlass.svelte +19 -2
  22. package/dist/fancy-ui/liquid-glass/LiquidGlass.svelte.d.ts +4 -0
  23. package/dist/fancy-ui/liquid-glass/LiquidGlass.test.js +14 -0
  24. package/dist/fancy-ui/liquid-glass/README.md +7 -1
  25. package/dist/fancy-ui/liquid-text/LiquidText.svelte +1151 -0
  26. package/dist/fancy-ui/liquid-text/LiquidText.svelte.d.ts +37 -0
  27. package/dist/fancy-ui/liquid-text/LiquidText.test.d.ts +1 -0
  28. package/dist/fancy-ui/liquid-text/LiquidText.test.js +219 -0
  29. package/dist/fancy-ui/liquid-text/README.md +139 -0
  30. package/dist/fancy-ui/liquid-text/index.d.ts +2 -0
  31. package/dist/fancy-ui/liquid-text/index.js +1 -0
  32. package/dist/fancy-ui/registry.js +192 -0
  33. package/dist/fancy-ui/themes.d.ts +38 -0
  34. package/dist/fancy-ui/themes.js +356 -0
  35. package/dist/utils/animation.d.ts +109 -0
  36. package/dist/utils/animation.js +198 -0
  37. package/dist/utils/color.d.ts +128 -0
  38. package/dist/utils/color.js +314 -0
  39. package/dist/utils/geometry.d.ts +125 -0
  40. package/dist/utils/geometry.js +250 -0
  41. package/dist/utils/index.d.ts +8 -0
  42. package/dist/utils/index.js +39 -0
  43. package/package.json +13 -4
@@ -0,0 +1,37 @@
1
+ export interface LiquidTextProps {
2
+ /** Text rendered into the fluid texture (or as static fallback text). */
3
+ text?: string;
4
+ /** CSS font-family. Empty string resolves to getComputedStyle(host).fontFamily. */
5
+ font?: string;
6
+ /** Font size in px. 0 auto-fits the text to the container's width. */
7
+ fontSize?: number;
8
+ /** CSS font-weight for the rasterized/fallback text. */
9
+ fontWeight?: number | string;
10
+ /** Text color used in light mode. */
11
+ lightColor?: string;
12
+ /** Text color used in dark mode. */
13
+ darkColor?: string;
14
+ /** Additional CSS classes applied to the root element. */
15
+ class?: string;
16
+ /** Geometric UV warp gain — how far the fluid velocity displaces the text's UVs. */
17
+ strength?: number;
18
+ /** Splat radius in screen pixels around the pointer. */
19
+ radius?: number;
20
+ /** Multiplier from mouse-delta-per-frame to splat force. */
21
+ forceGain?: number;
22
+ /** Per-frame velocity decay factor (relax-back rate for the smear). */
23
+ dissipation?: number;
24
+ /** Viscous diffusion strength (Jacobi iteration, 8 iters). */
25
+ viscosity?: number;
26
+ /** Chromatic offset = warp amount x this ratio. */
27
+ chromaticRatio?: number;
28
+ /** If window.innerWidth <= staticBelow at mount, render static DOM text instead. */
29
+ staticBelow?: number;
30
+ /** Whether the fluid sim reacts to pointer movement. */
31
+ interactive?: boolean;
32
+ /** Pause the render loop via visibilitychange when the tab/page is hidden. */
33
+ pauseWhenHidden?: boolean;
34
+ }
35
+ declare const LiquidText: import("svelte").Component<LiquidTextProps, {}, "">;
36
+ type LiquidText = ReturnType<typeof LiquidText>;
37
+ export default LiquidText;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,219 @@
1
+ import { render, screen, cleanup } from "@testing-library/svelte";
2
+ import { afterEach, describe, it, expect, vi } from "vitest";
3
+ import LiquidText from "./LiquidText.svelte";
4
+ /**
5
+ * Ambient test environment (see src/test-setup.ts) already stubs:
6
+ * - HTMLCanvasElement.prototype.getContext -> null (WebGL "unavailable")
7
+ * - window.matchMedia -> { matches: false, ... }
8
+ * - ResizeObserver / IntersectionObserver -> no-op classes
9
+ *
10
+ * Per the frozen LiquidText contract, "no WebGL" and "no reduced-motion
11
+ * preference" both drive the component into its DOM-fallback path. Tests
12
+ * below that specifically target the fallback/no-WebGL/reduced-motion
13
+ * behavior re-assert these overrides explicitly (mirroring the ambient
14
+ * default) so intent is self-documenting and independent of test-setup.ts.
15
+ */
16
+ describe("LiquidText", () => {
17
+ afterEach(cleanup);
18
+ describe("accessible text node", () => {
19
+ it("renders the text prop as a real text node in the DOM", () => {
20
+ render(LiquidText, { props: { text: "Hello Liquid" } });
21
+ const textNode = screen.getByText("Hello Liquid");
22
+ expect(textNode).toBeInTheDocument();
23
+ // The contract only requires the *canvas* to be aria-hidden; the
24
+ // real text node itself must stay in the accessibility tree,
25
+ // whether it is the sr-only span (canvas active) or the visible
26
+ // fallback text.
27
+ expect(textNode.getAttribute("aria-hidden")).not.toBe("true");
28
+ });
29
+ it('falls back to the default text "Liquid" when no text prop is passed', () => {
30
+ render(LiquidText);
31
+ expect(screen.getByText("Liquid")).toBeInTheDocument();
32
+ });
33
+ it("marks the canvas aria-hidden when a canvas is rendered", () => {
34
+ const { container } = render(LiquidText, { props: { text: "Canvas Check" } });
35
+ const canvas = container.querySelector("canvas");
36
+ // The contract does not pin down whether <canvas> is always present
37
+ // in the markup or only mounted on the interactive/WebGL path, so
38
+ // this only asserts when one is actually found.
39
+ if (canvas) {
40
+ expect(canvas.getAttribute("aria-hidden")).toBe("true");
41
+ }
42
+ });
43
+ });
44
+ describe("WebGL unavailable fallback", () => {
45
+ it("renders visible styled fallback text, starts no rAF render loop, and logs no console errors", () => {
46
+ const originalGetContext = HTMLCanvasElement.prototype.getContext;
47
+ HTMLCanvasElement.prototype.getContext = (() => null);
48
+ const rafSpy = vi.spyOn(window, "requestAnimationFrame");
49
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
50
+ render(LiquidText, {
51
+ props: {
52
+ text: "No WebGL",
53
+ font: "Georgia, serif",
54
+ fontSize: 48,
55
+ fontWeight: 600,
56
+ lightColor: "#123456",
57
+ },
58
+ });
59
+ const textNode = screen.getByText("No WebGL");
60
+ expect(textNode).toBeInTheDocument();
61
+ // Per contract: "visible styled text in fallback modes", as opposed
62
+ // to the sr-only span used when the canvas is actively driven. It
63
+ // must actually carry the prop-driven font/size/color styling, not
64
+ // just be present and unstyled.
65
+ expect(textNode.className).not.toContain("sr-only");
66
+ expect(textNode.style.fontFamily).toBe("Georgia, serif");
67
+ expect(textNode.style.fontSize).toBe("48px");
68
+ expect(textNode.style.fontWeight).toBe("600");
69
+ expect(textNode.style.color).toBe("rgb(18, 52, 86)");
70
+ expect(rafSpy).not.toHaveBeenCalled();
71
+ expect(errorSpy).not.toHaveBeenCalled();
72
+ rafSpy.mockRestore();
73
+ errorSpy.mockRestore();
74
+ HTMLCanvasElement.prototype.getContext = originalGetContext;
75
+ });
76
+ });
77
+ describe("prefers-reduced-motion fallback", () => {
78
+ it("renders static fallback text and starts no rAF render loop when reduced motion is preferred", () => {
79
+ const originalMatchMedia = window.matchMedia;
80
+ Object.defineProperty(window, "matchMedia", {
81
+ writable: true,
82
+ configurable: true,
83
+ value: (query) => ({
84
+ matches: query.includes("prefers-reduced-motion"),
85
+ media: query,
86
+ onchange: null,
87
+ addEventListener: () => { },
88
+ removeEventListener: () => { },
89
+ dispatchEvent: () => false,
90
+ addListener: () => { },
91
+ removeListener: () => { },
92
+ }),
93
+ });
94
+ const rafSpy = vi.spyOn(window, "requestAnimationFrame");
95
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
96
+ render(LiquidText, {
97
+ props: {
98
+ text: "Reduced Motion",
99
+ font: "Georgia, serif",
100
+ fontSize: 32,
101
+ fontWeight: 500,
102
+ lightColor: "#654321",
103
+ },
104
+ });
105
+ const textNode = screen.getByText("Reduced Motion");
106
+ expect(textNode).toBeInTheDocument();
107
+ expect(textNode.className).not.toContain("sr-only");
108
+ // Same requirement as the no-WebGL fallback: styled per the props,
109
+ // not just present.
110
+ expect(textNode.style.fontFamily).toBe("Georgia, serif");
111
+ expect(textNode.style.fontSize).toBe("32px");
112
+ expect(textNode.style.fontWeight).toBe("500");
113
+ expect(textNode.style.color).toBe("rgb(101, 67, 33)");
114
+ expect(rafSpy).not.toHaveBeenCalled();
115
+ expect(errorSpy).not.toHaveBeenCalled();
116
+ rafSpy.mockRestore();
117
+ errorSpy.mockRestore();
118
+ Object.defineProperty(window, "matchMedia", {
119
+ writable: true,
120
+ configurable: true,
121
+ value: originalMatchMedia,
122
+ });
123
+ });
124
+ });
125
+ describe("cleanup on repeated mount/unmount", () => {
126
+ it("leaves window/document/body listener counts balanced and no dangling rAF after 5 mount/unmount cycles", () => {
127
+ const targets = [window, document, document.body];
128
+ const addSpies = targets.map((t) => vi.spyOn(t, "addEventListener"));
129
+ const removeSpies = targets.map((t) => vi.spyOn(t, "removeEventListener"));
130
+ let nextRafId = 1;
131
+ const pendingRafIds = new Set();
132
+ const rafSpy = vi.spyOn(window, "requestAnimationFrame").mockImplementation(() => {
133
+ const id = nextRafId++;
134
+ pendingRafIds.add(id);
135
+ return id;
136
+ });
137
+ const cafSpy = vi
138
+ .spyOn(window, "cancelAnimationFrame")
139
+ .mockImplementation((id) => {
140
+ pendingRafIds.delete(id);
141
+ });
142
+ for (let i = 0; i < 5; i++) {
143
+ const { unmount } = render(LiquidText, { props: { text: `pass-${i}` } });
144
+ unmount();
145
+ }
146
+ // Per-event-name balance on each target: every addEventListener("x", …)
147
+ // must be matched by a removeEventListener("x", …) across the 5
148
+ // mount/unmount cycles. Checking per event name (not just raw totals)
149
+ // catches leaks that would otherwise cancel out coincidentally.
150
+ targets.forEach((_, i) => {
151
+ const counts = new Map();
152
+ for (const call of addSpies[i].mock.calls) {
153
+ const name = call[0];
154
+ counts.set(name, (counts.get(name) ?? 0) + 1);
155
+ }
156
+ for (const call of removeSpies[i].mock.calls) {
157
+ const name = call[0];
158
+ counts.set(name, (counts.get(name) ?? 0) - 1);
159
+ }
160
+ for (const [eventName, balance] of counts) {
161
+ expect(balance, `"${eventName}" listener leak on target #${i}`).toBe(0);
162
+ }
163
+ });
164
+ expect(pendingRafIds.size).toBe(0);
165
+ addSpies.forEach((s) => s.mockRestore());
166
+ removeSpies.forEach((s) => s.mockRestore());
167
+ rafSpy.mockRestore();
168
+ cafSpy.mockRestore();
169
+ });
170
+ });
171
+ describe("class prop", () => {
172
+ it("applies the class prop to the root element", () => {
173
+ const { container } = render(LiquidText, { props: { class: "my-liquid-class" } });
174
+ const root = container.firstElementChild;
175
+ expect(root.className).toContain("my-liquid-class");
176
+ });
177
+ it("still renders a root element with the default empty class", () => {
178
+ const { container } = render(LiquidText);
179
+ expect(container.firstElementChild).toBeInTheDocument();
180
+ });
181
+ });
182
+ describe("prop defaults and full prop surface (smoke)", () => {
183
+ it("mounts without error using only defaults", () => {
184
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
185
+ const { container } = render(LiquidText);
186
+ expect(container.firstElementChild).toBeInTheDocument();
187
+ expect(screen.getByText("Liquid")).toBeInTheDocument();
188
+ expect(errorSpy).not.toHaveBeenCalled();
189
+ errorSpy.mockRestore();
190
+ });
191
+ it("mounts without error with every documented prop explicitly set", () => {
192
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
193
+ const { container } = render(LiquidText, {
194
+ props: {
195
+ text: "Smoke Test",
196
+ font: "Arial, sans-serif",
197
+ fontSize: 96,
198
+ fontWeight: 800,
199
+ lightColor: "#111111",
200
+ darkColor: "#eeeeee",
201
+ class: "smoke-root",
202
+ strength: 0.75,
203
+ radius: 220,
204
+ forceGain: 24,
205
+ dissipation: 0.95,
206
+ viscosity: 6,
207
+ chromaticRatio: 0.35,
208
+ staticBelow: 768,
209
+ interactive: false,
210
+ pauseWhenHidden: false,
211
+ },
212
+ });
213
+ expect(container.firstElementChild).toBeInTheDocument();
214
+ expect(screen.getByText("Smoke Test")).toBeInTheDocument();
215
+ expect(errorSpy).not.toHaveBeenCalled();
216
+ errorSpy.mockRestore();
217
+ });
218
+ });
219
+ });
@@ -0,0 +1,139 @@
1
+ # LiquidText
2
+
3
+ Large display text that liquefies under the cursor: a raw-WebGL fluid velocity
4
+ field displaces the text's UVs along the pointer's trail, with RGB chromatic
5
+ fringing on the warped edges, then relaxes back to rest as the fluid decays.
6
+
7
+ ## Features
8
+
9
+ - Raw WebGL2 (WebGL1 fallback) fluid simulation — no Three.js dependency
10
+ - Any string rendered through the sim: text is rasterized to a texture via
11
+ Canvas2D (`fillText`), not a pre-baked image
12
+ - Mouse-velocity-driven splats: faster cursor movement = stronger smear
13
+ - Chromatic aberration on displaced edges (3-tap R/G/B sampling)
14
+ - Self-relaxing: velocity field decays every frame, so the smear resolves on
15
+ its own roughly a second after the cursor moves away
16
+ - Auto-fit font size to the container width when `fontSize` is unset
17
+ - Graceful fallbacks: `prefers-reduced-motion`, no WebGL, and small viewports
18
+ (`staticBelow`) all render plain styled static text instead of the sim
19
+ - Accessible by construction: a real text node is always present in the DOM;
20
+ only the `<canvas>` is `aria-hidden`
21
+ - Single `requestAnimationFrame` loop, paused via `visibilitychange` when the
22
+ tab is hidden (`pauseWhenHidden`)
23
+ - Exhaustive cleanup on unmount: cancels the rAF loop, removes every
24
+ listener, disconnects the `ResizeObserver`, and calls `WEBGL_lose_context`
25
+
26
+ ## Usage
27
+
28
+ ```svelte
29
+ <script>
30
+ import { LiquidText } from "fancy-ui-svelte";
31
+ </script>
32
+
33
+ <LiquidText text="Liquid" class="text-7xl font-bold" />
34
+ ```
35
+
36
+ ```svelte
37
+ <!-- Tuned for a subtler, slower-decaying smear -->
38
+ <LiquidText
39
+ text="Rama Herbin"
40
+ font="Bricolage Grotesque, sans-serif"
41
+ fontWeight={700}
42
+ strength={0.35}
43
+ radius={120}
44
+ dissipation={0.985}
45
+ lightColor="#0a0a0a"
46
+ darkColor="#fafafa"
47
+ />
48
+ ```
49
+
50
+ ```svelte
51
+ <!-- Force the static DOM fallback (e.g. inside a `prefers-reduced-motion`
52
+ conscious layout, or below a custom breakpoint) -->
53
+ <LiquidText text="Liquid" interactive={false} staticBelow={1280} />
54
+ ```
55
+
56
+ ## Props
57
+
58
+ | Prop | Type | Default | Description |
59
+ | ---------------- | -------------------- | ----------- | -------------------------------------------------------------------------------------- |
60
+ | `text` | `string` | `"Liquid"` | The text rendered into the fluid texture (or as static fallback text). |
61
+ | `font` | `string` | `""` | CSS font-family for the text. Empty string resolves to `getComputedStyle(host).fontFamily`. |
62
+ | `fontSize` | `number` | `0` | Font size in px. `0` auto-fits the text to the container's width. |
63
+ | `fontWeight` | `number \| string` | `700` | CSS font-weight for the rasterized/fallback text. |
64
+ | `lightColor` | `string` | `"#000000"` | Text color used in light mode. |
65
+ | `darkColor` | `string` | `"#ffffff"` | Text color used in dark mode. |
66
+ | `class` | `string` | `""` | Additional class names applied to the root element. |
67
+ | `strength` | `number` | `0.5` | Geometric UV warp gain — how far the fluid velocity displaces the text's UVs. |
68
+ | `radius` | `number` | `160` | Splat radius in screen pixels around the pointer. |
69
+ | `forceGain` | `number` | `17` | Multiplier from mouse-delta-per-frame to splat force. |
70
+ | `dissipation` | `number` | `0.98` | Per-frame velocity decay factor (relax-back rate for the smear). |
71
+ | `viscosity` | `number` | `4` | Viscous diffusion strength (Jacobi iteration, 8 iters). |
72
+ | `chromaticRatio` | `number` | `0.2` | Chromatic offset = warp amount × this ratio. |
73
+ | `staticBelow` | `number` | `1024` | If `window.innerWidth <= staticBelow` at mount, render static DOM text instead. |
74
+ | `interactive` | `boolean` | `true` | Whether the fluid sim reacts to pointer movement. |
75
+ | `pauseWhenHidden`| `boolean` | `true` | Pause the render loop via `visibilitychange` when the tab/page is hidden. |
76
+
77
+ ## Behavior contract
78
+
79
+ - A real text node is **always** in the DOM: a `sr-only` span while the
80
+ canvas is actively driven, or visible styled text in any fallback mode.
81
+ - The `<canvas>` is always `aria-hidden="true"` — it is a visual effect
82
+ layer, not content.
83
+ - The component falls back to plain styled DOM text (no WebGL sim, no rAF
84
+ loop) when any of these are true at mount: `prefers-reduced-motion` is
85
+ preferred, WebGL is unavailable, or `window.innerWidth <= staticBelow`.
86
+ - Root markup shape: `<div class={...}>` wrapping the canvas and the text
87
+ node.
88
+
89
+ ## Implementation notes
90
+
91
+ ### Fluid simulation
92
+
93
+ - Runs the velocity solve at **0.25× the canvas resolution** to keep the
94
+ per-frame cost low for a text-sized effect (this doesn't need dye-buffer
95
+ resolution the way a full-screen cursor effect does).
96
+ - Pipeline per frame: self-advection with `dissipation` decay → pointer-velocity
97
+ splat (screen-px falloff `(1 - dist/radius)² × 1.5`, force ∝ mouse delta ×
98
+ `forceGain`) → viscous diffusion (Jacobi, `viscosity`, 8 iterations) →
99
+ divergence → pressure solve (Jacobi, 16 iterations) → gradient
100
+ subtraction/projection. Vorticity confinement is intentionally left out —
101
+ it isn't part of the reference behavior being reproduced and would add
102
+ cost without visible benefit at this scale. `dt` is fixed rather than
103
+ measured from real frame time, so the decay rate stays predictable
104
+ regardless of frame rate.
105
+ - Text is rasterized once per (text, font, fontSize, fontWeight) combination
106
+ via Canvas2D `fillText` into a texture, and re-rasterized on `ResizeObserver`
107
+ changes that affect layout (e.g. auto-fit font size).
108
+
109
+ ### Chromatic sampling
110
+
111
+ - The fragment shader reads the velocity field at the screen UV, derives a
112
+ geometric warp (`velocity * strength`) and a chromatic offset
113
+ (`warp * chromaticRatio`), then does 3 independent texture taps: red at
114
+ `uv - offset`, green at `uv`, blue at `uv + offset`. The result is a
115
+ visible RGB fringe exactly where the text is being smeared, which
116
+ disappears as the velocity field dissipates.
117
+
118
+ ### Fallbacks
119
+
120
+ - `prefers-reduced-motion: reduce` and "no WebGL context available" both
121
+ short-circuit straight to the static DOM text path — no canvas sim is
122
+ initialized, no rAF loop starts.
123
+ - `staticBelow` is checked once against `window.innerWidth` at mount (not
124
+ reactively re-checked on resize) — crossing the breakpoint after mount
125
+ does not retroactively swap modes.
126
+ - All three fallback conditions render the same static markup: visible,
127
+ normally-styled text using `lightColor`/`darkColor` per color scheme.
128
+
129
+ ### Cleanup
130
+
131
+ - A single `requestAnimationFrame` loop drives the sim; it is paused (not
132
+ cancelled) on `document.visibilitychange` when `pauseWhenHidden` is true,
133
+ and resumed when the tab becomes visible again.
134
+ - On unmount: cancels the pending rAF, removes every `window`/`document`
135
+ listener the component registered, disconnects the `ResizeObserver`, and
136
+ explicitly calls the `WEBGL_lose_context` extension's `loseContext()` to
137
+ release GPU resources deterministically rather than waiting on GC.
138
+ - Verified safe under repeated mount/unmount (no leaked listeners, no
139
+ dangling animation frames) — see `LiquidText.test.ts`.
@@ -0,0 +1,2 @@
1
+ export { default as LiquidText } from "./LiquidText.svelte";
2
+ export type { LiquidTextProps } from "./LiquidText.svelte";
@@ -0,0 +1 @@
1
+ export { default as LiquidText } from "./LiquidText.svelte";
@@ -935,6 +935,18 @@ 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": {
@@ -1634,6 +1646,74 @@ export const registry = {
1634
1646
  },
1635
1647
  ],
1636
1648
  },
1649
+ "frosted-glass": {
1650
+ name: "FrostedGlass",
1651
+ slug: "frosted-glass",
1652
+ description: "Frosted glass surface with organic turbulence-noise refraction, an alternative to LiquidGlass",
1653
+ category: "effects",
1654
+ status: "done",
1655
+ tags: ["glass", "effect", "svg", "filter", "turbulence", "displacement", "blur", "navbar"],
1656
+ props: [
1657
+ { name: "radius", type: "number", default: "24", description: "Border radius in pixels" },
1658
+ {
1659
+ name: "baseFrequency",
1660
+ type: "number",
1661
+ default: "0.008",
1662
+ description: "Turbulence noise frequency (lower = wider waves)",
1663
+ },
1664
+ {
1665
+ name: "numOctaves",
1666
+ type: "number",
1667
+ default: "2",
1668
+ description: "Turbulence octaves controlling noise detail",
1669
+ },
1670
+ { name: "seed", type: "number", default: "92", description: "Turbulence random seed" },
1671
+ {
1672
+ name: "noiseBlur",
1673
+ type: "number",
1674
+ default: "2",
1675
+ description: "Gaussian blur softening the noise before displacement",
1676
+ },
1677
+ { name: "scale", type: "number", default: "70", description: "Displacement intensity" },
1678
+ {
1679
+ name: "tint",
1680
+ type: "string",
1681
+ default: '"hsla(0, 0%, 100%, 0.25)"',
1682
+ description: "Overlay tint color",
1683
+ },
1684
+ {
1685
+ name: "highlight",
1686
+ type: "string",
1687
+ default: '"hsla(0, 0%, 100%, 0.75)"',
1688
+ description: "Specular rim highlight color",
1689
+ },
1690
+ {
1691
+ name: "border",
1692
+ type: "boolean",
1693
+ default: "true",
1694
+ description: "Show the conic-gradient glass border",
1695
+ },
1696
+ {
1697
+ name: "fallbackBlur",
1698
+ type: "number",
1699
+ default: "20",
1700
+ description: "Backdrop blur in pixels for the Safari fallback",
1701
+ },
1702
+ {
1703
+ name: "fallbackSaturation",
1704
+ type: "number",
1705
+ default: "180",
1706
+ description: "Backdrop saturation percentage for the Safari fallback",
1707
+ },
1708
+ {
1709
+ name: "containerClass",
1710
+ type: "string",
1711
+ default: '""',
1712
+ description: "CSS classes for the outer container",
1713
+ },
1714
+ ],
1715
+ slots: [{ name: "children", description: "Content rendered on top of the glass" }],
1716
+ },
1637
1717
  "liquid-glass": {
1638
1718
  name: "LiquidGlass",
1639
1719
  slug: "liquid-glass",
@@ -1711,6 +1791,18 @@ export const registry = {
1711
1791
  },
1712
1792
  { name: "scale", type: "number", default: "-180", description: "Displacement scale" },
1713
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
+ },
1714
1806
  {
1715
1807
  name: "containerClass",
1716
1808
  type: "string",
@@ -2157,6 +2249,106 @@ export const registry = {
2157
2249
  },
2158
2250
  ],
2159
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
+ },
2160
2352
  };
2161
2353
  // =============================================================================
2162
2354
  // Registry Helpers
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Theme registry (docs-site theming data).
3
+ *
4
+ * A named theme is a set of CSS-variable overrides applied on top of the base
5
+ * light/dark token sets defined in `src/routes/layout.css`. The theme store
6
+ * (`src/lib/stores/theme.svelte.ts`) applies a theme by setting these variables
7
+ * inline on `<html>`, toggling the `.dark` class per `colorScheme`, and setting
8
+ * a `data-theme` attribute.
9
+ *
10
+ * NOTE: this module is docs/theming data only. It is NOT imported by any shipped
11
+ * component in `src/lib/fancy-ui/*`, so it does not affect the published package.
12
+ */
13
+ export type ColorScheme = "light" | "dark";
14
+ export interface ThemeDef {
15
+ /** Stable id, used as the `data-theme` value and persistence key. */
16
+ name: string;
17
+ /** Human label shown in the switcher. */
18
+ label: string;
19
+ /** Whether the theme reads as light or dark (drives the `.dark` class). */
20
+ colorScheme: ColorScheme;
21
+ /** CSS custom-property overrides applied on top of the base light/dark set. */
22
+ tokens: Record<string, string>;
23
+ /** Four representative colors for the switcher swatch. */
24
+ swatch: [string, string, string, string];
25
+ }
26
+ /** Every CSS variable a theme may set. The store clears these before applying a theme. */
27
+ export declare const MANAGED_VARS: readonly ["--background", "--foreground", "--card", "--card-foreground", "--popover", "--popover-foreground", "--primary", "--primary-foreground", "--secondary", "--secondary-foreground", "--muted", "--muted-foreground", "--accent", "--accent-foreground", "--border", "--input", "--ring", "--radius", "--rainbow-1", "--rainbow-2", "--rainbow-3", "--rainbow-4", "--rainbow-5", "--sidebar", "--sidebar-foreground", "--sidebar-primary", "--sidebar-primary-foreground", "--sidebar-accent", "--sidebar-accent-foreground", "--sidebar-border"];
28
+ /**
29
+ * The theme list.
30
+ *
31
+ * `light` and `dark` carry no token overrides — they are the base look defined
32
+ * by `:root` / `.dark` in `layout.css`. The remaining themes are self-contained.
33
+ * `sunset` / `ocean` / `mono` mirror the theme-generator presets.
34
+ */
35
+ export declare const themes: ThemeDef[];
36
+ export declare function getTheme(name: string): ThemeDef | undefined;
37
+ export declare const themeNames: string[];
38
+ export declare const DEFAULT_THEME = "light";