@oalacea/chaosui 0.4.0 → 0.5.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 (68) hide show
  1. package/bin/cli.js +32 -2
  2. package/components/buttons/cta-brutal/cta-brutal.module.css +81 -0
  3. package/components/buttons/cta-brutal/index.tsx +56 -0
  4. package/components/buttons/dead-button/dead-button.module.css +111 -0
  5. package/components/buttons/dead-button/index.tsx +47 -0
  6. package/components/buttons/deeper-button/deeper-button.module.css +76 -0
  7. package/components/buttons/deeper-button/index.tsx +51 -0
  8. package/components/buttons/dual-choice/dual-choice.module.css +90 -0
  9. package/components/buttons/dual-choice/index.tsx +54 -0
  10. package/components/buttons/tension-bar/index.tsx +79 -0
  11. package/components/buttons/tension-bar/tension-bar.module.css +105 -0
  12. package/components/decorative/coffee-stain/coffee-stain.module.css +24 -0
  13. package/components/decorative/coffee-stain/index.tsx +55 -0
  14. package/components/decorative/ornaments/index.tsx +51 -0
  15. package/components/decorative/ornaments/ornaments.module.css +33 -0
  16. package/components/decorative/rune-symbols/index.tsx +55 -0
  17. package/components/decorative/rune-symbols/rune-symbols.module.css +22 -0
  18. package/components/layout/horizontal-scroll/horizontal-scroll.module.css +30 -0
  19. package/components/layout/horizontal-scroll/index.tsx +78 -0
  20. package/components/layout/spec-grid/index.tsx +56 -0
  21. package/components/layout/spec-grid/spec-grid.module.css +21 -0
  22. package/components/layout/tower-pricing/index.tsx +56 -0
  23. package/components/layout/tower-pricing/tower-pricing.module.css +27 -0
  24. package/components/layout/tracklist/index.tsx +45 -0
  25. package/components/layout/tracklist/tracklist.module.css +24 -0
  26. package/components/layout/void-frame/index.tsx +32 -0
  27. package/components/layout/void-frame/void-frame.module.css +38 -0
  28. package/components/navigation/brutal-nav/brutal-nav.module.css +85 -0
  29. package/components/navigation/brutal-nav/index.tsx +71 -0
  30. package/components/navigation/progress-dots/index.tsx +55 -0
  31. package/components/navigation/progress-dots/progress-dots.module.css +91 -0
  32. package/components/navigation/scattered-nav/index.tsx +59 -0
  33. package/components/navigation/scattered-nav/scattered-nav.module.css +113 -0
  34. package/components/navigation/scroll-indicator/index.tsx +58 -0
  35. package/components/navigation/scroll-indicator/scroll-indicator.module.css +82 -0
  36. package/components/navigation/vertical-nav/index.tsx +59 -0
  37. package/components/navigation/vertical-nav/vertical-nav.module.css +98 -0
  38. package/components/text/ascii-art/css/ascii-art.module.css +173 -0
  39. package/components/text/ascii-art/css/index.tsx +116 -0
  40. package/components/text/ascii-art/tailwind/index.tsx +124 -0
  41. package/components/text/blood-drip/css/blood-drip.module.css +142 -0
  42. package/components/text/blood-drip/css/index.tsx +113 -0
  43. package/components/text/blood-drip/tailwind/index.tsx +133 -0
  44. package/components/text/char-glitch/css/char-glitch.module.css +124 -0
  45. package/components/text/char-glitch/css/index.tsx +153 -0
  46. package/components/text/char-glitch/tailwind/index.tsx +126 -0
  47. package/components/text/countdown-display/css/countdown-display.module.css +179 -0
  48. package/components/text/countdown-display/css/index.tsx +190 -0
  49. package/components/text/countdown-display/tailwind/index.tsx +155 -0
  50. package/components/text/giant-layers/css/giant-layers.module.css +156 -0
  51. package/components/text/giant-layers/css/index.tsx +97 -0
  52. package/components/text/giant-layers/tailwind/index.tsx +111 -0
  53. package/components/text/reveal-text/css/index.tsx +180 -0
  54. package/components/text/reveal-text/css/reveal-text.module.css +129 -0
  55. package/components/text/reveal-text/tailwind/index.tsx +135 -0
  56. package/components/text/rotate-text/css/index.tsx +139 -0
  57. package/components/text/rotate-text/css/rotate-text.module.css +162 -0
  58. package/components/text/rotate-text/tailwind/index.tsx +127 -0
  59. package/components/text/strike-reveal/css/index.tsx +124 -0
  60. package/components/text/strike-reveal/css/strike-reveal.module.css +139 -0
  61. package/components/text/strike-reveal/tailwind/index.tsx +138 -0
  62. package/components/text/terminal-output/css/index.tsx +179 -0
  63. package/components/text/terminal-output/css/terminal-output.module.css +203 -0
  64. package/components/text/terminal-output/tailwind/index.tsx +174 -0
  65. package/components/text/typing-text/css/index.tsx +115 -0
  66. package/components/text/typing-text/css/typing-text.module.css +84 -0
  67. package/components/text/typing-text/tailwind/index.tsx +126 -0
  68. package/package.json +1 -1
@@ -0,0 +1,203 @@
1
+ .container {
2
+ font-family: 'Courier New', 'Monaco', 'Consolas', monospace;
3
+ background: #0a0a0a;
4
+ border: 1px solid #222;
5
+ border-radius: 4px;
6
+ overflow: hidden;
7
+ }
8
+
9
+ /* Header bar */
10
+ .header {
11
+ display: flex;
12
+ align-items: center;
13
+ gap: 0.5rem;
14
+ padding: 0.75rem 1rem;
15
+ background: #111;
16
+ border-bottom: 1px solid #222;
17
+ }
18
+
19
+ .dots {
20
+ display: flex;
21
+ gap: 6px;
22
+ }
23
+
24
+ .dot {
25
+ width: 12px;
26
+ height: 12px;
27
+ border-radius: 50%;
28
+ }
29
+
30
+ .dot.red { background: #ff5f56; }
31
+ .dot.yellow { background: #ffbd2e; }
32
+ .dot.green { background: #27c93f; }
33
+
34
+ .title {
35
+ font-size: 0.75rem;
36
+ color: #666;
37
+ margin-left: auto;
38
+ letter-spacing: 0.05em;
39
+ }
40
+
41
+ /* Content area */
42
+ .content {
43
+ padding: 1rem;
44
+ max-height: 400px;
45
+ overflow-y: auto;
46
+ scrollbar-width: thin;
47
+ scrollbar-color: #333 #0a0a0a;
48
+ }
49
+
50
+ .content::-webkit-scrollbar {
51
+ width: 6px;
52
+ }
53
+
54
+ .content::-webkit-scrollbar-track {
55
+ background: #0a0a0a;
56
+ }
57
+
58
+ .content::-webkit-scrollbar-thumb {
59
+ background: #333;
60
+ border-radius: 3px;
61
+ }
62
+
63
+ /* Lines */
64
+ .line {
65
+ display: flex;
66
+ gap: 0.5rem;
67
+ margin-bottom: 0.25rem;
68
+ line-height: 1.5;
69
+ font-size: 0.875rem;
70
+ }
71
+
72
+ .prompt {
73
+ color: #00ff00;
74
+ white-space: nowrap;
75
+ user-select: none;
76
+ }
77
+
78
+ .command {
79
+ color: #fafafa;
80
+ }
81
+
82
+ .output {
83
+ color: #888;
84
+ white-space: pre-wrap;
85
+ word-break: break-all;
86
+ }
87
+
88
+ /* Line types */
89
+ .line.error .output { color: #ff0040; }
90
+ .line.success .output { color: #00ff00; }
91
+ .line.warning .output { color: #ffaa00; }
92
+ .line.info .output { color: #00ffff; }
93
+
94
+ /* Typing animation */
95
+ .typing .command::after {
96
+ content: '█';
97
+ animation: blink 1s step-end infinite;
98
+ margin-left: 2px;
99
+ }
100
+
101
+ @keyframes blink {
102
+ 0%, 100% { opacity: 1; }
103
+ 50% { opacity: 0; }
104
+ }
105
+
106
+ /* Animated output */
107
+ .animated .line {
108
+ opacity: 0;
109
+ animation: lineIn 0.3s forwards;
110
+ }
111
+
112
+ @keyframes lineIn {
113
+ from { opacity: 0; transform: translateY(5px); }
114
+ to { opacity: 1; transform: translateY(0); }
115
+ }
116
+
117
+ /* Variants */
118
+ .hacker {
119
+ border-color: #00ff00;
120
+ }
121
+
122
+ .hacker .header {
123
+ background: #001100;
124
+ border-color: #00ff0040;
125
+ }
126
+
127
+ .hacker .prompt { color: #00ff00; }
128
+ .hacker .command { color: #00ff00; }
129
+ .hacker .output { color: #00aa00; }
130
+
131
+ .blood {
132
+ border-color: #ff0040;
133
+ }
134
+
135
+ .blood .header {
136
+ background: #110000;
137
+ border-color: #ff004040;
138
+ }
139
+
140
+ .blood .prompt { color: #ff0040; }
141
+ .blood .command { color: #ff6666; }
142
+ .blood .output { color: #aa4444; }
143
+
144
+ .cyber {
145
+ border-color: #00ffff;
146
+ }
147
+
148
+ .cyber .header {
149
+ background: #001111;
150
+ border-color: #00ffff40;
151
+ }
152
+
153
+ .cyber .prompt { color: #00ffff; }
154
+ .cyber .command { color: #88ffff; }
155
+ .cyber .output { color: #00aaaa; }
156
+
157
+ /* Scanlines overlay */
158
+ .scanlines::after {
159
+ content: '';
160
+ position: absolute;
161
+ top: 0;
162
+ left: 0;
163
+ right: 0;
164
+ bottom: 0;
165
+ background: repeating-linear-gradient(
166
+ 0deg,
167
+ transparent,
168
+ transparent 2px,
169
+ rgba(0, 0, 0, 0.1) 2px,
170
+ rgba(0, 0, 0, 0.1) 4px
171
+ );
172
+ pointer-events: none;
173
+ }
174
+
175
+ /* Glowing effect */
176
+ .glowing .prompt,
177
+ .glowing .command {
178
+ text-shadow: 0 0 5px currentColor;
179
+ }
180
+
181
+ /* Input line */
182
+ .inputLine {
183
+ display: flex;
184
+ gap: 0.5rem;
185
+ margin-top: 0.5rem;
186
+ padding-top: 0.5rem;
187
+ border-top: 1px solid #222;
188
+ }
189
+
190
+ .input {
191
+ flex: 1;
192
+ background: transparent;
193
+ border: none;
194
+ color: #fafafa;
195
+ font-family: inherit;
196
+ font-size: 0.875rem;
197
+ outline: none;
198
+ caret-color: #00ff00;
199
+ }
200
+
201
+ .input::placeholder {
202
+ color: #444;
203
+ }
@@ -0,0 +1,174 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, useEffect, useState, useRef } from 'react';
4
+
5
+ export interface TerminalLine {
6
+ type: 'command' | 'output' | 'error' | 'success' | 'warning' | 'info';
7
+ content: string;
8
+ prompt?: string;
9
+ delay?: number;
10
+ }
11
+
12
+ export interface TerminalOutputProps extends HTMLAttributes<HTMLDivElement> {
13
+ lines: TerminalLine[];
14
+ prompt?: string;
15
+ variant?: 'default' | 'hacker' | 'blood' | 'cyber';
16
+ showHeader?: boolean;
17
+ title?: string;
18
+ animated?: boolean;
19
+ animationDelay?: number;
20
+ typingCursor?: boolean;
21
+ scanlines?: boolean;
22
+ glowing?: boolean;
23
+ showInput?: boolean;
24
+ inputPlaceholder?: string;
25
+ onCommand?: (command: string) => void;
26
+ autoScroll?: boolean;
27
+ }
28
+
29
+ const variantStyles = {
30
+ default: { border: 'border-gray-800', header: 'bg-gray-900', prompt: 'text-green-400', command: 'text-gray-100', output: 'text-gray-500' },
31
+ hacker: { border: 'border-green-500', header: 'bg-green-950', prompt: 'text-green-400', command: 'text-green-400', output: 'text-green-700' },
32
+ blood: { border: 'border-rose-500', header: 'bg-rose-950', prompt: 'text-rose-500', command: 'text-rose-300', output: 'text-rose-700' },
33
+ cyber: { border: 'border-cyan-500', header: 'bg-cyan-950', prompt: 'text-cyan-400', command: 'text-cyan-200', output: 'text-cyan-700' },
34
+ };
35
+
36
+ const typeColors = { error: 'text-rose-500', success: 'text-green-400', warning: 'text-amber-500', info: 'text-cyan-400' };
37
+
38
+ export const TerminalOutput = forwardRef<HTMLDivElement, TerminalOutputProps>(
39
+ (
40
+ {
41
+ lines,
42
+ prompt = '❯',
43
+ variant = 'default',
44
+ showHeader = true,
45
+ title = 'terminal',
46
+ animated = false,
47
+ animationDelay = 100,
48
+ typingCursor = false,
49
+ scanlines = false,
50
+ glowing = false,
51
+ showInput = false,
52
+ inputPlaceholder = 'Type a command...',
53
+ onCommand,
54
+ autoScroll = true,
55
+ className = '',
56
+ ...props
57
+ },
58
+ ref
59
+ ) => {
60
+ const [visibleLines, setVisibleLines] = useState<number>(animated ? 0 : lines.length);
61
+ const [inputValue, setInputValue] = useState('');
62
+ const contentRef = useRef<HTMLDivElement>(null);
63
+ const styles = variantStyles[variant];
64
+
65
+ useEffect(() => {
66
+ if (animated) {
67
+ setVisibleLines(0);
68
+ let index = 0;
69
+
70
+ const showLine = () => {
71
+ if (index < lines.length) {
72
+ const line = lines[index];
73
+ setTimeout(() => {
74
+ setVisibleLines(index + 1);
75
+ index++;
76
+ showLine();
77
+ }, line.delay ?? animationDelay);
78
+ }
79
+ };
80
+ showLine();
81
+ } else {
82
+ setVisibleLines(lines.length);
83
+ }
84
+ }, [lines, animated, animationDelay]);
85
+
86
+ useEffect(() => {
87
+ if (autoScroll && contentRef.current) {
88
+ contentRef.current.scrollTop = contentRef.current.scrollHeight;
89
+ }
90
+ }, [visibleLines, autoScroll]);
91
+
92
+ const handleSubmit = (e: React.FormEvent) => {
93
+ e.preventDefault();
94
+ if (inputValue.trim()) {
95
+ onCommand?.(inputValue);
96
+ setInputValue('');
97
+ }
98
+ };
99
+
100
+ return (
101
+ <div
102
+ ref={ref}
103
+ className={`font-mono bg-[#0a0a0a] border ${styles.border} rounded overflow-hidden relative ${
104
+ scanlines ? 'after:absolute after:inset-0 after:bg-[repeating-linear-gradient(0deg,transparent,transparent_2px,rgba(0,0,0,0.1)_2px,rgba(0,0,0,0.1)_4px)] after:pointer-events-none' : ''
105
+ } ${className}`}
106
+ {...props}
107
+ >
108
+ {showHeader && (
109
+ <div className={`flex items-center gap-2 px-4 py-3 ${styles.header} border-b border-gray-800`}>
110
+ <div className="flex gap-1.5">
111
+ <span className="w-3 h-3 rounded-full bg-red-500" />
112
+ <span className="w-3 h-3 rounded-full bg-yellow-500" />
113
+ <span className="w-3 h-3 rounded-full bg-green-500" />
114
+ </div>
115
+ <span className="ml-auto text-xs text-gray-600 tracking-wide">{title}</span>
116
+ </div>
117
+ )}
118
+
119
+ <div ref={contentRef} className="p-4 max-h-[400px] overflow-y-auto scrollbar-thin scrollbar-track-[#0a0a0a] scrollbar-thumb-gray-700">
120
+ {lines.slice(0, visibleLines).map((line, i) => {
121
+ const isLastCommand = i === visibleLines - 1 && line.type === 'command' && typingCursor;
122
+ const outputColor = line.type !== 'command' && line.type !== 'output' ? typeColors[line.type] : styles.output;
123
+
124
+ return (
125
+ <div
126
+ key={i}
127
+ className={`flex gap-2 mb-1 leading-relaxed text-sm ${animated ? 'animate-[fadeInUp_0.3s_ease-out_forwards]' : ''}`}
128
+ style={animated ? { animationDelay: `${i * 50}ms`, opacity: 0 } : undefined}
129
+ >
130
+ {line.type === 'command' ? (
131
+ <>
132
+ <span className={`${styles.prompt} whitespace-nowrap select-none ${glowing ? 'drop-shadow-[0_0_5px_currentColor]' : ''}`}>
133
+ {line.prompt || prompt}
134
+ </span>
135
+ <span className={`${styles.command} ${glowing ? 'drop-shadow-[0_0_5px_currentColor]' : ''}`}>
136
+ {line.content}
137
+ {isLastCommand && <span className="animate-pulse ml-0.5">█</span>}
138
+ </span>
139
+ </>
140
+ ) : (
141
+ <span className={`${outputColor} whitespace-pre-wrap break-all`}>{line.content}</span>
142
+ )}
143
+ </div>
144
+ );
145
+ })}
146
+
147
+ {showInput && (
148
+ <form onSubmit={handleSubmit} className="flex gap-2 mt-2 pt-2 border-t border-gray-800">
149
+ <span className={`${styles.prompt} ${glowing ? 'drop-shadow-[0_0_5px_currentColor]' : ''}`}>{prompt}</span>
150
+ <input
151
+ type="text"
152
+ value={inputValue}
153
+ onChange={(e) => setInputValue(e.target.value)}
154
+ placeholder={inputPlaceholder}
155
+ className="flex-1 bg-transparent border-none text-gray-100 text-sm outline-none caret-green-400 placeholder:text-gray-700"
156
+ autoFocus
157
+ />
158
+ </form>
159
+ )}
160
+ </div>
161
+
162
+ <style>{`
163
+ @keyframes fadeInUp {
164
+ from { opacity: 0; transform: translateY(5px); }
165
+ to { opacity: 1; transform: translateY(0); }
166
+ }
167
+ `}</style>
168
+ </div>
169
+ );
170
+ }
171
+ );
172
+
173
+ TerminalOutput.displayName = 'TerminalOutput';
174
+ export default TerminalOutput;
@@ -0,0 +1,115 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, useEffect, useState, useCallback } from 'react';
4
+ import styles from './typing-text.module.css';
5
+
6
+ export interface TypingTextProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
7
+ /** Text to type out */
8
+ text: string;
9
+ /** Typing speed in ms per character */
10
+ speed?: number;
11
+ /** Delay before starting in ms */
12
+ delay?: number;
13
+ /** Show cursor */
14
+ showCursor?: boolean;
15
+ /** Cursor style */
16
+ cursorStyle?: 'block' | 'line' | 'underscore';
17
+ /** Visual variant */
18
+ variant?: 'default' | 'terminal' | 'hacker' | 'cyber' | 'ghost';
19
+ /** Loop the animation */
20
+ loop?: boolean;
21
+ /** Pause between loops in ms */
22
+ loopDelay?: number;
23
+ /** Delete speed when looping */
24
+ deleteSpeed?: number;
25
+ /** Callback when typing completes */
26
+ onComplete?: () => void;
27
+ }
28
+
29
+ export const TypingText = forwardRef<HTMLSpanElement, TypingTextProps>(
30
+ (
31
+ {
32
+ text,
33
+ speed = 50,
34
+ delay = 0,
35
+ showCursor = true,
36
+ cursorStyle = 'block',
37
+ variant = 'default',
38
+ loop = false,
39
+ loopDelay = 2000,
40
+ deleteSpeed = 30,
41
+ onComplete,
42
+ className,
43
+ ...props
44
+ },
45
+ ref
46
+ ) => {
47
+ const [displayText, setDisplayText] = useState('');
48
+ const [isTyping, setIsTyping] = useState(false);
49
+ const [isDeleting, setIsDeleting] = useState(false);
50
+
51
+ const typeText = useCallback(() => {
52
+ setIsTyping(true);
53
+ let index = 0;
54
+
55
+ const typeInterval = setInterval(() => {
56
+ if (index < text.length) {
57
+ setDisplayText(text.slice(0, index + 1));
58
+ index++;
59
+ } else {
60
+ clearInterval(typeInterval);
61
+ setIsTyping(false);
62
+
63
+ if (loop) {
64
+ setTimeout(() => {
65
+ setIsDeleting(true);
66
+ let deleteIndex = text.length;
67
+
68
+ const deleteInterval = setInterval(() => {
69
+ if (deleteIndex > 0) {
70
+ setDisplayText(text.slice(0, deleteIndex - 1));
71
+ deleteIndex--;
72
+ } else {
73
+ clearInterval(deleteInterval);
74
+ setIsDeleting(false);
75
+ setTimeout(typeText, delay);
76
+ }
77
+ }, deleteSpeed);
78
+ }, loopDelay);
79
+ } else {
80
+ onComplete?.();
81
+ }
82
+ }
83
+ }, speed);
84
+
85
+ return () => clearInterval(typeInterval);
86
+ }, [text, speed, loop, loopDelay, deleteSpeed, delay, onComplete]);
87
+
88
+ useEffect(() => {
89
+ const timeout = setTimeout(typeText, delay);
90
+ return () => clearTimeout(timeout);
91
+ }, [typeText, delay]);
92
+
93
+ const containerClasses = [
94
+ styles.container,
95
+ variant !== 'default' && styles[variant],
96
+ className
97
+ ].filter(Boolean).join(' ');
98
+
99
+ const cursorClasses = [
100
+ styles.cursor,
101
+ styles[cursorStyle],
102
+ !isTyping && !isDeleting && !loop && styles.cursorHidden
103
+ ].filter(Boolean).join(' ');
104
+
105
+ return (
106
+ <span ref={ref} className={containerClasses} {...props}>
107
+ <span className={styles.text}>{displayText}</span>
108
+ {showCursor && <span className={cursorClasses} />}
109
+ </span>
110
+ );
111
+ }
112
+ );
113
+
114
+ TypingText.displayName = 'TypingText';
115
+ export default TypingText;
@@ -0,0 +1,84 @@
1
+ .container {
2
+ display: inline-block;
3
+ font-family: 'Courier New', 'Monaco', monospace;
4
+ position: relative;
5
+ }
6
+
7
+ .text {
8
+ display: inline;
9
+ white-space: pre-wrap;
10
+ }
11
+
12
+ .cursor {
13
+ display: inline-block;
14
+ width: 0.6em;
15
+ height: 1.1em;
16
+ background: currentColor;
17
+ margin-left: 2px;
18
+ vertical-align: text-bottom;
19
+ animation: blink 1s step-end infinite;
20
+ }
21
+
22
+ .cursor.block {
23
+ width: 0.6em;
24
+ }
25
+
26
+ .cursor.line {
27
+ width: 2px;
28
+ }
29
+
30
+ .cursor.underscore {
31
+ width: 0.6em;
32
+ height: 2px;
33
+ vertical-align: baseline;
34
+ }
35
+
36
+ .cursorHidden {
37
+ opacity: 0;
38
+ }
39
+
40
+ /* Variants */
41
+ .terminal {
42
+ color: #00ff00;
43
+ text-shadow: 0 0 5px #00ff00, 0 0 10px #00ff0080;
44
+ }
45
+
46
+ .terminal .cursor {
47
+ background: #00ff00;
48
+ box-shadow: 0 0 5px #00ff00, 0 0 10px #00ff0080;
49
+ }
50
+
51
+ .hacker {
52
+ color: #ff0040;
53
+ text-shadow: 0 0 5px #ff0040, 0 0 15px #ff004080;
54
+ }
55
+
56
+ .hacker .cursor {
57
+ background: #ff0040;
58
+ box-shadow: 0 0 5px #ff0040;
59
+ }
60
+
61
+ .cyber {
62
+ color: #00ffff;
63
+ text-shadow: 0 0 5px #00ffff, 0 0 10px #00ffff80;
64
+ }
65
+
66
+ .cyber .cursor {
67
+ background: #00ffff;
68
+ box-shadow: 0 0 5px #00ffff;
69
+ }
70
+
71
+ .ghost {
72
+ color: #888;
73
+ opacity: 0.8;
74
+ }
75
+
76
+ .ghost .cursor {
77
+ background: #888;
78
+ animation: blink 2s step-end infinite;
79
+ }
80
+
81
+ @keyframes blink {
82
+ 0%, 100% { opacity: 1; }
83
+ 50% { opacity: 0; }
84
+ }
@@ -0,0 +1,126 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, useEffect, useState, useCallback } from 'react';
4
+
5
+ export interface TypingTextProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
6
+ text: string;
7
+ speed?: number;
8
+ delay?: number;
9
+ showCursor?: boolean;
10
+ cursorStyle?: 'block' | 'line' | 'underscore';
11
+ variant?: 'default' | 'terminal' | 'hacker' | 'cyber' | 'ghost';
12
+ loop?: boolean;
13
+ loopDelay?: number;
14
+ deleteSpeed?: number;
15
+ onComplete?: () => void;
16
+ }
17
+
18
+ const variantStyles = {
19
+ default: '',
20
+ terminal: 'text-green-400 drop-shadow-[0_0_5px_#00ff00]',
21
+ hacker: 'text-rose-500 drop-shadow-[0_0_5px_#ff0040]',
22
+ cyber: 'text-cyan-400 drop-shadow-[0_0_5px_#00ffff]',
23
+ ghost: 'text-gray-500 opacity-80',
24
+ };
25
+
26
+ const cursorVariants = {
27
+ default: 'bg-current',
28
+ terminal: 'bg-green-400 shadow-[0_0_5px_#00ff00]',
29
+ hacker: 'bg-rose-500 shadow-[0_0_5px_#ff0040]',
30
+ cyber: 'bg-cyan-400 shadow-[0_0_5px_#00ffff]',
31
+ ghost: 'bg-gray-500',
32
+ };
33
+
34
+ const cursorSizes = {
35
+ block: 'w-[0.6em] h-[1.1em]',
36
+ line: 'w-0.5 h-[1.1em]',
37
+ underscore: 'w-[0.6em] h-0.5',
38
+ };
39
+
40
+ export const TypingText = forwardRef<HTMLSpanElement, TypingTextProps>(
41
+ (
42
+ {
43
+ text,
44
+ speed = 50,
45
+ delay = 0,
46
+ showCursor = true,
47
+ cursorStyle = 'block',
48
+ variant = 'default',
49
+ loop = false,
50
+ loopDelay = 2000,
51
+ deleteSpeed = 30,
52
+ onComplete,
53
+ className = '',
54
+ ...props
55
+ },
56
+ ref
57
+ ) => {
58
+ const [displayText, setDisplayText] = useState('');
59
+ const [isTyping, setIsTyping] = useState(false);
60
+ const [isDeleting, setIsDeleting] = useState(false);
61
+
62
+ const typeText = useCallback(() => {
63
+ setIsTyping(true);
64
+ let index = 0;
65
+
66
+ const typeInterval = setInterval(() => {
67
+ if (index < text.length) {
68
+ setDisplayText(text.slice(0, index + 1));
69
+ index++;
70
+ } else {
71
+ clearInterval(typeInterval);
72
+ setIsTyping(false);
73
+
74
+ if (loop) {
75
+ setTimeout(() => {
76
+ setIsDeleting(true);
77
+ let deleteIndex = text.length;
78
+
79
+ const deleteInterval = setInterval(() => {
80
+ if (deleteIndex > 0) {
81
+ setDisplayText(text.slice(0, deleteIndex - 1));
82
+ deleteIndex--;
83
+ } else {
84
+ clearInterval(deleteInterval);
85
+ setIsDeleting(false);
86
+ setTimeout(typeText, delay);
87
+ }
88
+ }, deleteSpeed);
89
+ }, loopDelay);
90
+ } else {
91
+ onComplete?.();
92
+ }
93
+ }
94
+ }, speed);
95
+
96
+ return () => clearInterval(typeInterval);
97
+ }, [text, speed, loop, loopDelay, deleteSpeed, delay, onComplete]);
98
+
99
+ useEffect(() => {
100
+ const timeout = setTimeout(typeText, delay);
101
+ return () => clearTimeout(timeout);
102
+ }, [typeText, delay]);
103
+
104
+ const showBlinkingCursor = isTyping || isDeleting || loop;
105
+
106
+ return (
107
+ <span
108
+ ref={ref}
109
+ className={`inline-block font-mono ${variantStyles[variant]} ${className}`}
110
+ {...props}
111
+ >
112
+ <span className="whitespace-pre-wrap">{displayText}</span>
113
+ {showCursor && (
114
+ <span
115
+ className={`inline-block ml-0.5 align-text-bottom ${cursorSizes[cursorStyle]} ${cursorVariants[variant]} ${
116
+ showBlinkingCursor ? 'animate-pulse' : 'opacity-0'
117
+ }`}
118
+ />
119
+ )}
120
+ </span>
121
+ );
122
+ }
123
+ );
124
+
125
+ TypingText.displayName = 'TypingText';
126
+ export default TypingText;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oalacea/chaosui",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Glitch, noise, and distortion components for React. Copy-paste like shadcn.",
5
5
  "type": "module",
6
6
  "bin": {