@oalacea/chaosui 0.4.0 → 0.5.1

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 (178) hide show
  1. package/buttons/cta-brutal/css/cta-brutal.module.css +175 -0
  2. package/buttons/cta-brutal/css/index.tsx +78 -0
  3. package/buttons/cta-brutal/tailwind/index.tsx +85 -0
  4. package/buttons/dead-button/css/dead-button.module.css +216 -0
  5. package/buttons/dead-button/css/index.tsx +47 -0
  6. package/buttons/dead-button/tailwind/index.tsx +90 -0
  7. package/buttons/deeper-button/css/deeper-button.module.css +141 -0
  8. package/buttons/deeper-button/css/index.tsx +62 -0
  9. package/buttons/deeper-button/tailwind/index.tsx +78 -0
  10. package/buttons/dual-choice/css/dual-choice.module.css +176 -0
  11. package/buttons/dual-choice/css/index.tsx +56 -0
  12. package/buttons/dual-choice/tailwind/index.tsx +89 -0
  13. package/buttons/tension-bar/css/index.tsx +96 -0
  14. package/buttons/tension-bar/css/tension-bar.module.css +204 -0
  15. package/buttons/tension-bar/tailwind/index.tsx +122 -0
  16. package/decorative/inscription/css/index.tsx +66 -0
  17. package/decorative/inscription/css/inscription.module.css +160 -0
  18. package/decorative/inscription/tailwind/index.tsx +91 -0
  19. package/decorative/ornaments/css/index.tsx +136 -0
  20. package/decorative/ornaments/css/ornaments.module.css +144 -0
  21. package/decorative/ornaments/tailwind/index.tsx +122 -0
  22. package/decorative/rune-symbols/css/index.tsx +116 -0
  23. package/decorative/rune-symbols/css/rune-symbols.module.css +116 -0
  24. package/decorative/rune-symbols/tailwind/index.tsx +108 -0
  25. package/decorative/sheet-music/css/index.tsx +144 -0
  26. package/decorative/sheet-music/css/sheet-music.module.css +152 -0
  27. package/decorative/sheet-music/tailwind/index.tsx +111 -0
  28. package/layout/horizontal-scroll/css/horizontal-scroll.module.css +93 -0
  29. package/layout/horizontal-scroll/css/index.tsx +114 -0
  30. package/layout/horizontal-scroll/tailwind/index.tsx +108 -0
  31. package/layout/spec-grid/css/index.tsx +103 -0
  32. package/layout/spec-grid/css/spec-grid.module.css +125 -0
  33. package/layout/spec-grid/tailwind/index.tsx +91 -0
  34. package/layout/tower-pricing/css/index.tsx +91 -0
  35. package/layout/tower-pricing/css/tower-pricing.module.css +177 -0
  36. package/layout/tower-pricing/tailwind/index.tsx +106 -0
  37. package/layout/tracklist/css/index.tsx +96 -0
  38. package/layout/tracklist/css/tracklist.module.css +141 -0
  39. package/layout/tracklist/tailwind/index.tsx +92 -0
  40. package/layout/void-frame/css/index.tsx +60 -0
  41. package/layout/void-frame/css/void-frame.module.css +141 -0
  42. package/layout/void-frame/tailwind/index.tsx +64 -0
  43. package/navigation/brutal-nav/css/brutal-nav.module.css +178 -0
  44. package/navigation/brutal-nav/css/index.tsx +80 -0
  45. package/navigation/brutal-nav/tailwind/index.tsx +95 -0
  46. package/navigation/progress-dots/css/index.tsx +60 -0
  47. package/navigation/progress-dots/css/progress-dots.module.css +152 -0
  48. package/navigation/progress-dots/tailwind/index.tsx +105 -0
  49. package/navigation/scattered-nav/css/index.tsx +59 -0
  50. package/navigation/scattered-nav/css/scattered-nav.module.css +121 -0
  51. package/navigation/scattered-nav/tailwind/index.tsx +77 -0
  52. package/navigation/scroll-indicator/css/index.tsx +72 -0
  53. package/navigation/scroll-indicator/css/scroll-indicator.module.css +129 -0
  54. package/navigation/scroll-indicator/tailwind/index.tsx +107 -0
  55. package/navigation/vertical-nav/css/index.tsx +69 -0
  56. package/navigation/vertical-nav/css/vertical-nav.module.css +117 -0
  57. package/navigation/vertical-nav/tailwind/index.tsx +91 -0
  58. package/package.json +8 -33
  59. package/text/ascii-art/css/ascii-art.module.css +173 -0
  60. package/text/ascii-art/css/index.tsx +116 -0
  61. package/text/ascii-art/tailwind/index.tsx +124 -0
  62. package/text/blood-drip/css/blood-drip.module.css +142 -0
  63. package/text/blood-drip/css/index.tsx +113 -0
  64. package/text/blood-drip/tailwind/index.tsx +133 -0
  65. package/text/char-glitch/css/char-glitch.module.css +124 -0
  66. package/text/char-glitch/css/index.tsx +153 -0
  67. package/text/char-glitch/tailwind/index.tsx +126 -0
  68. package/text/countdown-display/css/countdown-display.module.css +179 -0
  69. package/text/countdown-display/css/index.tsx +190 -0
  70. package/text/countdown-display/tailwind/index.tsx +155 -0
  71. package/text/giant-layers/css/giant-layers.module.css +156 -0
  72. package/text/giant-layers/css/index.tsx +97 -0
  73. package/text/giant-layers/tailwind/index.tsx +111 -0
  74. package/text/reveal-text/css/index.tsx +180 -0
  75. package/text/reveal-text/css/reveal-text.module.css +129 -0
  76. package/text/reveal-text/tailwind/index.tsx +135 -0
  77. package/text/rotate-text/css/index.tsx +139 -0
  78. package/text/rotate-text/css/rotate-text.module.css +162 -0
  79. package/text/rotate-text/tailwind/index.tsx +127 -0
  80. package/text/strike-reveal/css/index.tsx +124 -0
  81. package/text/strike-reveal/css/strike-reveal.module.css +139 -0
  82. package/text/strike-reveal/tailwind/index.tsx +138 -0
  83. package/text/terminal-output/css/index.tsx +179 -0
  84. package/text/terminal-output/css/terminal-output.module.css +203 -0
  85. package/text/terminal-output/tailwind/index.tsx +174 -0
  86. package/text/typing-text/css/index.tsx +115 -0
  87. package/text/typing-text/css/typing-text.module.css +84 -0
  88. package/text/typing-text/tailwind/index.tsx +126 -0
  89. package/bin/cli.js +0 -278
  90. package/components/backgrounds/light-beams/index.tsx +0 -80
  91. package/components/backgrounds/light-beams/light-beams.module.css +0 -27
  92. package/components/navigation/hexagon-menu/css/hexagon-menu.module.css +0 -55
  93. package/components/navigation/hexagon-menu/css/index.tsx +0 -35
  94. package/components/navigation/hexagon-menu/tailwind/index.tsx +0 -53
  95. /package/{components/backgrounds → backgrounds}/glow-orbs/glow-orbs.module.css +0 -0
  96. /package/{components/backgrounds → backgrounds}/glow-orbs/index.tsx +0 -0
  97. /package/{components/backgrounds → backgrounds}/noise-canvas/index.tsx +0 -0
  98. /package/{components/backgrounds → backgrounds}/noise-canvas/noise-canvas.module.css +0 -0
  99. /package/{components/backgrounds → backgrounds}/particle-field/index.tsx +0 -0
  100. /package/{components/backgrounds → backgrounds}/particle-field/particle-field.module.css +0 -0
  101. /package/{components/buttons → buttons}/chaos-button/chaos-button.module.css +0 -0
  102. /package/{components/buttons → buttons}/chaos-button/index.tsx +0 -0
  103. /package/{components/buttons → buttons}/glitch-button/glitch-button.module.css +0 -0
  104. /package/{components/buttons → buttons}/glitch-button/index.tsx +0 -0
  105. /package/{components/chaos-vars.css → chaos-vars.css} +0 -0
  106. /package/{components/cyber → cyber}/cyber-avatar/css/cyber-avatar.module.css +0 -0
  107. /package/{components/cyber → cyber}/cyber-avatar/css/index.tsx +0 -0
  108. /package/{components/cyber → cyber}/cyber-avatar/tailwind/index.tsx +0 -0
  109. /package/{components/cyber → cyber}/cyber-input/css/cyber-input.module.css +0 -0
  110. /package/{components/cyber → cyber}/cyber-input/css/index.tsx +0 -0
  111. /package/{components/cyber → cyber}/cyber-input/tailwind/index.tsx +0 -0
  112. /package/{components/cyber → cyber}/cyber-loader/css/cyber-loader.module.css +0 -0
  113. /package/{components/cyber → cyber}/cyber-loader/css/index.tsx +0 -0
  114. /package/{components/cyber → cyber}/cyber-loader/tailwind/index.tsx +0 -0
  115. /package/{components/cyber → cyber}/cyber-modal/css/cyber-modal.module.css +0 -0
  116. /package/{components/cyber → cyber}/cyber-modal/css/index.tsx +0 -0
  117. /package/{components/cyber → cyber}/cyber-modal/tailwind/index.tsx +0 -0
  118. /package/{components/cyber → cyber}/cyber-slider/css/cyber-slider.module.css +0 -0
  119. /package/{components/cyber → cyber}/cyber-slider/css/index.tsx +0 -0
  120. /package/{components/cyber → cyber}/cyber-slider/tailwind/index.tsx +0 -0
  121. /package/{components/cyber → cyber}/cyber-tooltip/css/cyber-tooltip.module.css +0 -0
  122. /package/{components/cyber → cyber}/cyber-tooltip/css/index.tsx +0 -0
  123. /package/{components/cyber → cyber}/cyber-tooltip/tailwind/index.tsx +0 -0
  124. /package/{components/effects → effects}/cursor-follower/cursor-follower.module.css +0 -0
  125. /package/{components/effects → effects}/cursor-follower/index.tsx +0 -0
  126. /package/{components/effects → effects}/glitch-image/css/glitch-image.module.css +0 -0
  127. /package/{components/effects → effects}/glitch-image/css/index.tsx +0 -0
  128. /package/{components/effects → effects}/glitch-image/tailwind/index.tsx +0 -0
  129. /package/{components/effects → effects}/glowing-border/css/glowing-border.module.css +0 -0
  130. /package/{components/effects → effects}/glowing-border/css/index.tsx +0 -0
  131. /package/{components/effects → effects}/glowing-border/tailwind/index.tsx +0 -0
  132. /package/{components/effects → effects}/screen-distortion/index.tsx +0 -0
  133. /package/{components/effects → effects}/screen-distortion/screen-distortion.module.css +0 -0
  134. /package/{components/effects → effects}/warning-tape/index.tsx +0 -0
  135. /package/{components/effects → effects}/warning-tape/warning-tape.module.css +0 -0
  136. /package/{components/layout → layout}/data-grid/css/data-grid.module.css +0 -0
  137. /package/{components/layout → layout}/data-grid/css/index.tsx +0 -0
  138. /package/{components/layout → layout}/data-grid/tailwind/index.tsx +0 -0
  139. /package/{components/layout → layout}/hologram-card/css/hologram-card.module.css +0 -0
  140. /package/{components/layout → layout}/hologram-card/css/index.tsx +0 -0
  141. /package/{components/layout → layout}/hologram-card/tailwind/index.tsx +0 -0
  142. /package/{components/neon → neon}/neon-alert/css/index.tsx +0 -0
  143. /package/{components/neon → neon}/neon-alert/css/neon-alert.module.css +0 -0
  144. /package/{components/neon → neon}/neon-alert/tailwind/index.tsx +0 -0
  145. /package/{components/neon → neon}/neon-badge/css/index.tsx +0 -0
  146. /package/{components/neon → neon}/neon-badge/css/neon-badge.module.css +0 -0
  147. /package/{components/neon → neon}/neon-badge/tailwind/index.tsx +0 -0
  148. /package/{components/neon → neon}/neon-button/css/index.tsx +0 -0
  149. /package/{components/neon → neon}/neon-button/css/neon-button.module.css +0 -0
  150. /package/{components/neon → neon}/neon-button/tailwind/index.tsx +0 -0
  151. /package/{components/neon → neon}/neon-divider/css/index.tsx +0 -0
  152. /package/{components/neon → neon}/neon-divider/css/neon-divider.module.css +0 -0
  153. /package/{components/neon → neon}/neon-divider/tailwind/index.tsx +0 -0
  154. /package/{components/neon → neon}/neon-progress/css/index.tsx +0 -0
  155. /package/{components/neon → neon}/neon-progress/css/neon-progress.module.css +0 -0
  156. /package/{components/neon → neon}/neon-progress/tailwind/index.tsx +0 -0
  157. /package/{components/neon → neon}/neon-tabs/css/index.tsx +0 -0
  158. /package/{components/neon → neon}/neon-tabs/css/neon-tabs.module.css +0 -0
  159. /package/{components/neon → neon}/neon-tabs/tailwind/index.tsx +0 -0
  160. /package/{components/neon → neon}/neon-toggle/css/index.tsx +0 -0
  161. /package/{components/neon → neon}/neon-toggle/css/neon-toggle.module.css +0 -0
  162. /package/{components/neon → neon}/neon-toggle/tailwind/index.tsx +0 -0
  163. /package/{components/overlays → overlays}/noise-overlay/index.tsx +0 -0
  164. /package/{components/overlays → overlays}/noise-overlay/noise-overlay.module.css +0 -0
  165. /package/{components/overlays → overlays}/scanlines/index.tsx +0 -0
  166. /package/{components/overlays → overlays}/scanlines/scanlines.module.css +0 -0
  167. /package/{components/overlays → overlays}/static-flicker/index.tsx +0 -0
  168. /package/{components/overlays → overlays}/static-flicker/static-flicker.module.css +0 -0
  169. /package/{components/overlays → overlays}/vignette/index.tsx +0 -0
  170. /package/{components/overlays → overlays}/vignette/vignette.module.css +0 -0
  171. /package/{components/text → text}/distortion-text/distortion-text.module.css +0 -0
  172. /package/{components/text → text}/distortion-text/index.tsx +0 -0
  173. /package/{components/text → text}/falling-text/falling-text.module.css +0 -0
  174. /package/{components/text → text}/falling-text/index.tsx +0 -0
  175. /package/{components/text → text}/flicker-text/flicker-text.module.css +0 -0
  176. /package/{components/text → text}/flicker-text/index.tsx +0 -0
  177. /package/{components/text → text}/glitch-text/glitch-text.module.css +0 -0
  178. /package/{components/text → text}/glitch-text/index.tsx +0 -0
@@ -0,0 +1,179 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, useEffect, useState, useRef } from 'react';
4
+ import styles from './terminal-output.module.css';
5
+
6
+ export interface TerminalLine {
7
+ type: 'command' | 'output' | 'error' | 'success' | 'warning' | 'info';
8
+ content: string;
9
+ prompt?: string;
10
+ delay?: number;
11
+ }
12
+
13
+ export interface TerminalOutputProps extends HTMLAttributes<HTMLDivElement> {
14
+ /** Lines to display */
15
+ lines: TerminalLine[];
16
+ /** Default prompt string */
17
+ prompt?: string;
18
+ /** Visual variant */
19
+ variant?: 'default' | 'hacker' | 'blood' | 'cyber';
20
+ /** Show window header with dots */
21
+ showHeader?: boolean;
22
+ /** Window title */
23
+ title?: string;
24
+ /** Animate lines appearing */
25
+ animated?: boolean;
26
+ /** Base delay between animated lines (ms) */
27
+ animationDelay?: number;
28
+ /** Show typing cursor on last command */
29
+ typingCursor?: boolean;
30
+ /** Enable scanlines overlay */
31
+ scanlines?: boolean;
32
+ /** Enable glow effect */
33
+ glowing?: boolean;
34
+ /** Show interactive input */
35
+ showInput?: boolean;
36
+ /** Input placeholder */
37
+ inputPlaceholder?: string;
38
+ /** Callback when command is submitted */
39
+ onCommand?: (command: string) => void;
40
+ /** Auto-scroll to bottom */
41
+ autoScroll?: boolean;
42
+ }
43
+
44
+ export const TerminalOutput = forwardRef<HTMLDivElement, TerminalOutputProps>(
45
+ (
46
+ {
47
+ lines,
48
+ prompt = '❯',
49
+ variant = 'default',
50
+ showHeader = true,
51
+ title = 'terminal',
52
+ animated = false,
53
+ animationDelay = 100,
54
+ typingCursor = false,
55
+ scanlines = false,
56
+ glowing = false,
57
+ showInput = false,
58
+ inputPlaceholder = 'Type a command...',
59
+ onCommand,
60
+ autoScroll = true,
61
+ className,
62
+ ...props
63
+ },
64
+ ref
65
+ ) => {
66
+ const [visibleLines, setVisibleLines] = useState<number>(animated ? 0 : lines.length);
67
+ const [inputValue, setInputValue] = useState('');
68
+ const contentRef = useRef<HTMLDivElement>(null);
69
+
70
+ useEffect(() => {
71
+ if (animated) {
72
+ setVisibleLines(0);
73
+ let index = 0;
74
+
75
+ const showLine = () => {
76
+ if (index < lines.length) {
77
+ const line = lines[index];
78
+ const delay = line.delay ?? animationDelay;
79
+
80
+ setTimeout(() => {
81
+ setVisibleLines(index + 1);
82
+ index++;
83
+ showLine();
84
+ }, delay);
85
+ }
86
+ };
87
+
88
+ showLine();
89
+ } else {
90
+ setVisibleLines(lines.length);
91
+ }
92
+ }, [lines, animated, animationDelay]);
93
+
94
+ useEffect(() => {
95
+ if (autoScroll && contentRef.current) {
96
+ contentRef.current.scrollTop = contentRef.current.scrollHeight;
97
+ }
98
+ }, [visibleLines, autoScroll]);
99
+
100
+ const handleSubmit = (e: React.FormEvent) => {
101
+ e.preventDefault();
102
+ if (inputValue.trim()) {
103
+ onCommand?.(inputValue);
104
+ setInputValue('');
105
+ }
106
+ };
107
+
108
+ const containerClasses = [
109
+ styles.container,
110
+ styles[variant],
111
+ animated && styles.animated,
112
+ scanlines && styles.scanlines,
113
+ glowing && styles.glowing,
114
+ className
115
+ ].filter(Boolean).join(' ');
116
+
117
+ return (
118
+ <div ref={ref} className={containerClasses} {...props}>
119
+ {showHeader && (
120
+ <div className={styles.header}>
121
+ <div className={styles.dots}>
122
+ <span className={`${styles.dot} ${styles.red}`} />
123
+ <span className={`${styles.dot} ${styles.yellow}`} />
124
+ <span className={`${styles.dot} ${styles.green}`} />
125
+ </div>
126
+ <span className={styles.title}>{title}</span>
127
+ </div>
128
+ )}
129
+
130
+ <div ref={contentRef} className={styles.content}>
131
+ {lines.slice(0, visibleLines).map((line, i) => {
132
+ const isLastCommand = i === visibleLines - 1 && line.type === 'command' && typingCursor;
133
+ const lineClasses = [
134
+ styles.line,
135
+ line.type !== 'command' && line.type !== 'output' && styles[line.type]
136
+ ].filter(Boolean).join(' ');
137
+
138
+ return (
139
+ <div
140
+ key={i}
141
+ className={lineClasses}
142
+ style={animated ? { animationDelay: `${i * 50}ms` } : undefined}
143
+ >
144
+ {line.type === 'command' && (
145
+ <>
146
+ <span className={styles.prompt}>{line.prompt || prompt}</span>
147
+ <span className={`${styles.command} ${isLastCommand ? styles.typing : ''}`}>
148
+ {line.content}
149
+ </span>
150
+ </>
151
+ )}
152
+ {line.type !== 'command' && (
153
+ <span className={styles.output}>{line.content}</span>
154
+ )}
155
+ </div>
156
+ );
157
+ })}
158
+
159
+ {showInput && (
160
+ <form onSubmit={handleSubmit} className={styles.inputLine}>
161
+ <span className={styles.prompt}>{prompt}</span>
162
+ <input
163
+ type="text"
164
+ value={inputValue}
165
+ onChange={(e) => setInputValue(e.target.value)}
166
+ placeholder={inputPlaceholder}
167
+ className={styles.input}
168
+ autoFocus
169
+ />
170
+ </form>
171
+ )}
172
+ </div>
173
+ </div>
174
+ );
175
+ }
176
+ );
177
+
178
+ TerminalOutput.displayName = 'TerminalOutput';
179
+ export default TerminalOutput;
@@ -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;