fancy-ui-svelte 0.2.1 → 0.4.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 +18 -4
- package/dist/fancy-ui/compare/Compare.svelte +1 -1
- package/dist/fancy-ui/flip-words/FlipWords.svelte +45 -13
- package/dist/fancy-ui/index.d.ts +2 -0
- package/dist/fancy-ui/index.js +2 -0
- package/dist/fancy-ui/matrix-rain/MatrixRain.svelte +127 -0
- package/dist/fancy-ui/matrix-rain/MatrixRain.svelte.d.ts +11 -0
- package/dist/fancy-ui/matrix-rain/index.d.ts +3 -0
- package/dist/fancy-ui/matrix-rain/index.js +2 -0
- package/dist/fancy-ui/registry.js +14 -0
- package/dist/fancy-ui/tailwind.css +2 -2
- package/dist/fancy-ui/terminal-text/TerminalText.svelte +187 -0
- package/dist/fancy-ui/terminal-text/TerminalText.svelte.d.ts +13 -0
- package/dist/fancy-ui/terminal-text/index.d.ts +3 -0
- package/dist/fancy-ui/terminal-text/index.js +2 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -116,11 +116,25 @@ Beautiful animation and UI components for **Svelte 5**.
|
|
|
116
116
|
|
|
117
117
|
## Quick Start
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
> or by cloning the repo. A registry-based install (`npx shadcn-svelte@latest add`) is
|
|
121
|
-
> planned for v0.2.
|
|
119
|
+
**Install via npm:**
|
|
122
120
|
|
|
123
|
-
|
|
121
|
+
```bash
|
|
122
|
+
npm install fancy-ui
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Then import any component:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { Marquee, BorderBeam, Confetti } from 'fancy-ui';
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
To include Tailwind classes, add this to your app's CSS:
|
|
132
|
+
|
|
133
|
+
```css
|
|
134
|
+
@import "fancy-ui/tailwind.css";
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Or browse and copy a component:**
|
|
124
138
|
1. Find the component you need in the [live demo](https://fancy-ui.rama.app)
|
|
125
139
|
2. Copy the source from `src/lib/fancy-ui/[component-name]/`
|
|
126
140
|
3. Paste into your project
|
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
<!-- Second Content -->
|
|
286
286
|
<div
|
|
287
287
|
class={cn(
|
|
288
|
-
"absolute top-0 left-0 z-[19] h-full w-full rounded-2xl select-none",
|
|
288
|
+
"absolute top-0 left-0 z-[19] h-full w-full overflow-hidden rounded-2xl select-none",
|
|
289
289
|
secondContentClass
|
|
290
290
|
)}
|
|
291
291
|
style:pointer-events={isInteracting ? "none" : "auto"}
|
|
@@ -20,11 +20,14 @@
|
|
|
20
20
|
import { cn } from "../../utils.js";
|
|
21
21
|
import { onMount } from "svelte";
|
|
22
22
|
|
|
23
|
+
const EXIT_MS = 600;
|
|
24
|
+
|
|
23
25
|
let { words, duration = 3000, class: className }: FlipWordsProps = $props();
|
|
24
26
|
|
|
25
27
|
let currentIndex = $state(0);
|
|
26
|
-
let
|
|
28
|
+
let isExiting = $state(false);
|
|
27
29
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
30
|
+
let exitTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
28
31
|
|
|
29
32
|
let currentWord = $derived(words[currentIndex] ?? "");
|
|
30
33
|
let splitWords = $derived(
|
|
@@ -35,37 +38,45 @@
|
|
|
35
38
|
);
|
|
36
39
|
|
|
37
40
|
function startAnimation() {
|
|
38
|
-
|
|
41
|
+
if (words.length < 2) return;
|
|
42
|
+
|
|
43
|
+
isExiting = true;
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
if (exitTimeoutId) clearTimeout(exitTimeoutId);
|
|
46
|
+
exitTimeoutId = setTimeout(() => {
|
|
47
|
+
isExiting = false;
|
|
41
48
|
currentIndex = (currentIndex + 1) % words.length;
|
|
42
|
-
|
|
43
|
-
}, 600);
|
|
49
|
+
}, EXIT_MS);
|
|
44
50
|
}
|
|
45
51
|
|
|
46
|
-
function scheduleNext() {
|
|
52
|
+
function scheduleNext(index: number, d: number) {
|
|
53
|
+
if (words.length < 2) return;
|
|
47
54
|
if (timeoutId) clearTimeout(timeoutId);
|
|
48
|
-
timeoutId = setTimeout(startAnimation,
|
|
55
|
+
timeoutId = setTimeout(startAnimation, d);
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
$effect(() => {
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
// Re-schedule whenever a new word becomes active (not during exit).
|
|
60
|
+
// Reference currentIndex and duration directly so the effect re-runs on changes.
|
|
61
|
+
if (!isExiting) {
|
|
62
|
+
scheduleNext(currentIndex, duration);
|
|
54
63
|
}
|
|
55
64
|
});
|
|
56
65
|
|
|
57
66
|
onMount(() => {
|
|
58
67
|
return () => {
|
|
59
68
|
if (timeoutId) clearTimeout(timeoutId);
|
|
69
|
+
if (exitTimeoutId) clearTimeout(exitTimeoutId);
|
|
60
70
|
};
|
|
61
71
|
});
|
|
62
72
|
</script>
|
|
63
73
|
|
|
64
74
|
<div class="flip-words relative inline-block px-2">
|
|
65
|
-
{#
|
|
75
|
+
{#key currentIndex}
|
|
66
76
|
<div
|
|
67
77
|
class={cn(
|
|
68
|
-
"
|
|
78
|
+
"relative z-10 inline-block text-left text-neutral-900 dark:text-neutral-100",
|
|
79
|
+
isExiting ? "flip-words-exit" : "flip-words-enter",
|
|
69
80
|
className
|
|
70
81
|
)}
|
|
71
82
|
>
|
|
@@ -86,7 +97,7 @@
|
|
|
86
97
|
</span>
|
|
87
98
|
{/each}
|
|
88
99
|
</div>
|
|
89
|
-
{/
|
|
100
|
+
{/key}
|
|
90
101
|
</div>
|
|
91
102
|
|
|
92
103
|
<style>
|
|
@@ -127,9 +138,30 @@
|
|
|
127
138
|
transform: translateY(0);
|
|
128
139
|
}
|
|
129
140
|
}
|
|
141
|
+
|
|
142
|
+
@keyframes flipExitWord {
|
|
143
|
+
0% {
|
|
144
|
+
opacity: 1;
|
|
145
|
+
transform: translateY(0);
|
|
146
|
+
filter: blur(0);
|
|
147
|
+
}
|
|
148
|
+
100% {
|
|
149
|
+
opacity: 0;
|
|
150
|
+
transform: translateY(-10px);
|
|
151
|
+
filter: blur(8px);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.flip-words {
|
|
157
|
+
--flip-words-ms: 0.6s;
|
|
130
158
|
}
|
|
131
159
|
|
|
132
160
|
.flip-words-enter {
|
|
133
|
-
animation: flipEnterWord
|
|
161
|
+
animation: flipEnterWord var(--flip-words-ms) ease-in-out forwards;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.flip-words-exit {
|
|
165
|
+
animation: flipExitWord var(--flip-words-ms) ease-in-out forwards;
|
|
134
166
|
}
|
|
135
167
|
</style>
|
package/dist/fancy-ui/index.d.ts
CHANGED
|
@@ -49,4 +49,6 @@ export * from "./text-generate-effect/index.js";
|
|
|
49
49
|
export * from "./line-shadow-text/index.js";
|
|
50
50
|
export * from "./tracing-beam/index.js";
|
|
51
51
|
export * from "./displacement-text/index.js";
|
|
52
|
+
export * from "./matrix-rain/index.js";
|
|
53
|
+
export * from "./terminal-text/index.js";
|
|
52
54
|
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
|
@@ -54,6 +54,8 @@ export * from "./text-generate-effect/index.js";
|
|
|
54
54
|
export * from "./line-shadow-text/index.js";
|
|
55
55
|
export * from "./tracing-beam/index.js";
|
|
56
56
|
export * from "./displacement-text/index.js";
|
|
57
|
+
export * from "./matrix-rain/index.js";
|
|
58
|
+
export * from "./terminal-text/index.js";
|
|
57
59
|
// =============================================================================
|
|
58
60
|
// Registry
|
|
59
61
|
// =============================================================================
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export interface MatrixRainProps {
|
|
3
|
+
color?: string;
|
|
4
|
+
speed?: number;
|
|
5
|
+
density?: number;
|
|
6
|
+
glyphSize?: number;
|
|
7
|
+
fadeOpacity?: number;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
import { cn } from "../../utils.js";
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
color = "#00ff41",
|
|
17
|
+
speed = 1.0,
|
|
18
|
+
density = 1.0,
|
|
19
|
+
glyphSize = 16,
|
|
20
|
+
fadeOpacity = 0.05,
|
|
21
|
+
class: className,
|
|
22
|
+
}: MatrixRainProps = $props();
|
|
23
|
+
|
|
24
|
+
const GLYPHS =
|
|
25
|
+
"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&";
|
|
26
|
+
|
|
27
|
+
let canvas: HTMLCanvasElement;
|
|
28
|
+
let ctx: CanvasRenderingContext2D | null = null;
|
|
29
|
+
let rafId: number;
|
|
30
|
+
let columns: number[] = [];
|
|
31
|
+
let frameCount = 0;
|
|
32
|
+
let canvasW = 0;
|
|
33
|
+
let canvasH = 0;
|
|
34
|
+
|
|
35
|
+
function randomGlyph(): string {
|
|
36
|
+
return GLYPHS[Math.floor(Math.random() * GLYPHS.length)];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function init(w: number, h: number) {
|
|
40
|
+
const safeGlyphSize = Math.max(1, glyphSize);
|
|
41
|
+
const safeDensity = Math.max(0.1, density);
|
|
42
|
+
const colCount = Math.max(1, Math.floor(w / (safeGlyphSize * safeDensity)));
|
|
43
|
+
columns = Array.from({ length: colCount }, () =>
|
|
44
|
+
Math.floor(Math.random() * (h / safeGlyphSize))
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function draw() {
|
|
49
|
+
if (!ctx) return;
|
|
50
|
+
|
|
51
|
+
const w = canvasW;
|
|
52
|
+
const h = canvasH;
|
|
53
|
+
|
|
54
|
+
// Fade trail
|
|
55
|
+
ctx.fillStyle = `rgba(0, 0, 0, ${fadeOpacity})`;
|
|
56
|
+
ctx.fillRect(0, 0, w, h);
|
|
57
|
+
|
|
58
|
+
ctx.font = `${glyphSize}px monospace`;
|
|
59
|
+
ctx.shadowBlur = 8;
|
|
60
|
+
ctx.shadowColor = color;
|
|
61
|
+
|
|
62
|
+
const rowsPerFrame = Math.max(1, speed);
|
|
63
|
+
frameCount++;
|
|
64
|
+
|
|
65
|
+
// Only advance every N frames to control speed below 1x
|
|
66
|
+
const shouldAdvance = speed >= 1 || frameCount % Math.round(1 / speed) === 0;
|
|
67
|
+
|
|
68
|
+
if (shouldAdvance) {
|
|
69
|
+
for (let i = 0; i < columns.length; i++) {
|
|
70
|
+
const x = i * glyphSize * density;
|
|
71
|
+
const y = columns[i] * glyphSize;
|
|
72
|
+
|
|
73
|
+
// Head character (bright white)
|
|
74
|
+
ctx.fillStyle = "#ffffff";
|
|
75
|
+
ctx.fillText(randomGlyph(), x, y);
|
|
76
|
+
|
|
77
|
+
// Body glyph one step behind (dimmer)
|
|
78
|
+
if (columns[i] > 1) {
|
|
79
|
+
ctx.fillStyle = color;
|
|
80
|
+
ctx.fillText(randomGlyph(), x, y - glyphSize);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Reset column when it exits bottom, with random delay
|
|
84
|
+
if (y > h && Math.random() > 0.975) {
|
|
85
|
+
columns[i] = 0;
|
|
86
|
+
} else {
|
|
87
|
+
columns[i] += rowsPerFrame;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ctx.shadowBlur = 0;
|
|
93
|
+
rafId = requestAnimationFrame(draw);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
$effect(() => {
|
|
97
|
+
ctx = canvas.getContext("2d");
|
|
98
|
+
if (!ctx) return;
|
|
99
|
+
|
|
100
|
+
// Set canvas size to container size with HiDPI scaling
|
|
101
|
+
const resize = () => {
|
|
102
|
+
const dpr = window.devicePixelRatio || 1;
|
|
103
|
+
canvasW = canvas.clientWidth;
|
|
104
|
+
canvasH = canvas.clientHeight;
|
|
105
|
+
canvas.width = Math.floor(canvasW * dpr);
|
|
106
|
+
canvas.height = Math.floor(canvasH * dpr);
|
|
107
|
+
ctx!.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
108
|
+
ctx!.fillStyle = "black";
|
|
109
|
+
ctx!.fillRect(0, 0, canvasW, canvasH);
|
|
110
|
+
init(canvasW, canvasH);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
resize();
|
|
114
|
+
|
|
115
|
+
const observer = new ResizeObserver(resize);
|
|
116
|
+
observer.observe(canvas);
|
|
117
|
+
|
|
118
|
+
rafId = requestAnimationFrame(draw);
|
|
119
|
+
|
|
120
|
+
return () => {
|
|
121
|
+
cancelAnimationFrame(rafId);
|
|
122
|
+
observer.disconnect();
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<canvas bind:this={canvas} class={cn("block h-full w-full bg-black", className)}></canvas>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface MatrixRainProps {
|
|
2
|
+
color?: string;
|
|
3
|
+
speed?: number;
|
|
4
|
+
density?: number;
|
|
5
|
+
glyphSize?: number;
|
|
6
|
+
fadeOpacity?: number;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const MatrixRain: import("svelte").Component<MatrixRainProps, {}, "">;
|
|
10
|
+
type MatrixRain = ReturnType<typeof MatrixRain>;
|
|
11
|
+
export default MatrixRain;
|
|
@@ -553,6 +553,20 @@ export const registry = {
|
|
|
553
553
|
status: "done",
|
|
554
554
|
credits: [{ source: "VengenceUI", url: "https://vengenceui.com/docs/liquid-text" }],
|
|
555
555
|
},
|
|
556
|
+
"matrix-rain": {
|
|
557
|
+
name: "MatrixRain",
|
|
558
|
+
slug: "matrix-rain",
|
|
559
|
+
description: "Canvas-based Matrix-style falling glyph rain with configurable color, speed, and density",
|
|
560
|
+
category: "backgrounds",
|
|
561
|
+
status: "done",
|
|
562
|
+
},
|
|
563
|
+
"terminal-text": {
|
|
564
|
+
name: "TerminalText",
|
|
565
|
+
slug: "terminal-text",
|
|
566
|
+
description: "Terminal-style text streamer with per-character animation, blinking cursor, and glitch effect",
|
|
567
|
+
category: "text",
|
|
568
|
+
status: "done",
|
|
569
|
+
},
|
|
556
570
|
};
|
|
557
571
|
// =============================================================================
|
|
558
572
|
// Registry Helpers
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/* Import this file in your app's CSS to include fancy-ui
|
|
2
|
-
@import "fancy-ui
|
|
1
|
+
/* Import this file in your app's CSS to include fancy-ui Tailwind classes:
|
|
2
|
+
@import "fancy-ui/tailwind.css"; */
|
|
3
3
|
@source "..";
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export interface TerminalTextProps {
|
|
3
|
+
lines: string[];
|
|
4
|
+
speed?: number;
|
|
5
|
+
delay?: number;
|
|
6
|
+
cursor?: boolean;
|
|
7
|
+
cursorChar?: string;
|
|
8
|
+
glitch?: boolean;
|
|
9
|
+
class?: string;
|
|
10
|
+
onComplete?: () => void;
|
|
11
|
+
}
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import { cn } from "../../utils.js";
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
lines,
|
|
19
|
+
speed = 40,
|
|
20
|
+
delay = 0,
|
|
21
|
+
cursor = true,
|
|
22
|
+
cursorChar = "█",
|
|
23
|
+
glitch = false,
|
|
24
|
+
class: className,
|
|
25
|
+
onComplete,
|
|
26
|
+
}: TerminalTextProps = $props();
|
|
27
|
+
|
|
28
|
+
const GLITCH_GLYPHS = "アイウエオ@#$%&!?░▒▓█▄▀■□▪▫";
|
|
29
|
+
|
|
30
|
+
let displayedLines = $state<string[]>([]);
|
|
31
|
+
let done = $state(false);
|
|
32
|
+
let glitchState = $state<{ lineIdx: number; charIdx: number; original: string } | null>(null);
|
|
33
|
+
|
|
34
|
+
let timeouts: ReturnType<typeof setTimeout>[] = [];
|
|
35
|
+
|
|
36
|
+
function scheduleTimeout(fn: () => void, ms: number) {
|
|
37
|
+
const id = setTimeout(fn, ms);
|
|
38
|
+
timeouts.push(id);
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function clearAllTimeouts() {
|
|
43
|
+
timeouts.forEach(clearTimeout);
|
|
44
|
+
timeouts = [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function streamLines() {
|
|
48
|
+
clearAllTimeouts();
|
|
49
|
+
displayedLines = [];
|
|
50
|
+
done = false;
|
|
51
|
+
glitchState = null;
|
|
52
|
+
|
|
53
|
+
let totalDelay = delay;
|
|
54
|
+
|
|
55
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
56
|
+
const line = lines[lineIdx];
|
|
57
|
+
|
|
58
|
+
// Push empty line placeholder
|
|
59
|
+
const capturedIdx = lineIdx;
|
|
60
|
+
scheduleTimeout(() => {
|
|
61
|
+
displayedLines = [...displayedLines, ""];
|
|
62
|
+
}, totalDelay);
|
|
63
|
+
|
|
64
|
+
// Stream characters one by one
|
|
65
|
+
for (let charIdx = 0; charIdx < line.length; charIdx++) {
|
|
66
|
+
const capturedChar = charIdx;
|
|
67
|
+
const capturedLine = line;
|
|
68
|
+
totalDelay += speed;
|
|
69
|
+
scheduleTimeout(() => {
|
|
70
|
+
displayedLines = displayedLines.map((l, i) =>
|
|
71
|
+
i === capturedIdx ? capturedLine.slice(0, capturedChar + 1) : l
|
|
72
|
+
);
|
|
73
|
+
}, totalDelay);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Small pause between lines
|
|
77
|
+
totalDelay += speed * 3;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Mark done
|
|
81
|
+
scheduleTimeout(() => {
|
|
82
|
+
done = true;
|
|
83
|
+
onComplete?.();
|
|
84
|
+
}, totalDelay);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function startGlitchLoop() {
|
|
88
|
+
function glitchOnce() {
|
|
89
|
+
if (displayedLines.length === 0) return;
|
|
90
|
+
|
|
91
|
+
// Pick a random line with content
|
|
92
|
+
const linesWithContent = displayedLines
|
|
93
|
+
.map((l, i) => ({ l, i }))
|
|
94
|
+
.filter(({ l }) => l.length > 0);
|
|
95
|
+
if (linesWithContent.length === 0) return;
|
|
96
|
+
|
|
97
|
+
const { l, i } = linesWithContent[Math.floor(Math.random() * linesWithContent.length)];
|
|
98
|
+
const charIdx = Math.floor(Math.random() * l.length);
|
|
99
|
+
const original = l[charIdx];
|
|
100
|
+
const fakeGlyph = GLITCH_GLYPHS[Math.floor(Math.random() * GLITCH_GLYPHS.length)];
|
|
101
|
+
|
|
102
|
+
glitchState = { lineIdx: i, charIdx, original };
|
|
103
|
+
|
|
104
|
+
// Temporarily replace char in display via glitchState, restore after 100ms
|
|
105
|
+
displayedLines = displayedLines.map((line, idx) => {
|
|
106
|
+
if (idx !== i) return line;
|
|
107
|
+
return line.slice(0, charIdx) + fakeGlyph + line.slice(charIdx + 1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
scheduleTimeout(() => {
|
|
111
|
+
if (glitchState && glitchState.lineIdx === i && glitchState.charIdx === charIdx) {
|
|
112
|
+
displayedLines = displayedLines.map((line, idx) => {
|
|
113
|
+
if (idx !== i) return line;
|
|
114
|
+
return line.slice(0, charIdx) + original + line.slice(charIdx + 1);
|
|
115
|
+
});
|
|
116
|
+
glitchState = null;
|
|
117
|
+
}
|
|
118
|
+
}, 100);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function scheduleGlitch() {
|
|
122
|
+
// Random interval between 2s and 4s
|
|
123
|
+
const interval = 2000 + Math.random() * 2000;
|
|
124
|
+
scheduleTimeout(() => {
|
|
125
|
+
if (glitch) glitchOnce();
|
|
126
|
+
scheduleGlitch();
|
|
127
|
+
}, interval);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
scheduleGlitch();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
$effect(() => {
|
|
134
|
+
// React only to lines/speed/delay — changing glitch won't restart the stream
|
|
135
|
+
void lines;
|
|
136
|
+
void speed;
|
|
137
|
+
void delay;
|
|
138
|
+
|
|
139
|
+
streamLines();
|
|
140
|
+
|
|
141
|
+
return () => {
|
|
142
|
+
clearAllTimeouts();
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
$effect(() => {
|
|
147
|
+
// React to glitch changes without restarting the stream
|
|
148
|
+
void glitch;
|
|
149
|
+
|
|
150
|
+
if (glitch) {
|
|
151
|
+
startGlitchLoop();
|
|
152
|
+
} else {
|
|
153
|
+
glitchState = null;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
</script>
|
|
157
|
+
|
|
158
|
+
<div class={cn("font-mono text-sm leading-relaxed", className)}>
|
|
159
|
+
{#each displayedLines as line, i}
|
|
160
|
+
<div class="min-h-[1.4em]">
|
|
161
|
+
<span>{line}</span>{#if cursor && i === displayedLines.length - 1 && !done}<span
|
|
162
|
+
class="cursor-blink">{cursorChar}</span
|
|
163
|
+
>{/if}
|
|
164
|
+
</div>
|
|
165
|
+
{/each}
|
|
166
|
+
{#if cursor && done}
|
|
167
|
+
<div class="min-h-[1.4em]">
|
|
168
|
+
<span class="cursor-blink">{cursorChar}</span>
|
|
169
|
+
</div>
|
|
170
|
+
{/if}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<style>
|
|
174
|
+
.cursor-blink {
|
|
175
|
+
animation: blink 1s step-end infinite;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@keyframes blink {
|
|
179
|
+
0%,
|
|
180
|
+
100% {
|
|
181
|
+
opacity: 1;
|
|
182
|
+
}
|
|
183
|
+
50% {
|
|
184
|
+
opacity: 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface TerminalTextProps {
|
|
2
|
+
lines: string[];
|
|
3
|
+
speed?: number;
|
|
4
|
+
delay?: number;
|
|
5
|
+
cursor?: boolean;
|
|
6
|
+
cursorChar?: string;
|
|
7
|
+
glitch?: boolean;
|
|
8
|
+
class?: string;
|
|
9
|
+
onComplete?: () => void;
|
|
10
|
+
}
|
|
11
|
+
declare const TerminalText: import("svelte").Component<TerminalTextProps, {}, "">;
|
|
12
|
+
type TerminalText = ReturnType<typeof TerminalText>;
|
|
13
|
+
export default TerminalText;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fancy-ui-svelte",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"@types/three": "^0.183.1",
|
|
72
72
|
"arctic": "^3.7.0",
|
|
73
73
|
"bits-ui": "^2.15.4",
|
|
74
|
+
"isomorphic-dompurify": "^3.7.1",
|
|
74
75
|
"jsdom": "^28.0.0",
|
|
75
76
|
"marked": "^17.0.3",
|
|
76
77
|
"nanoid": "^5.1.6",
|