@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.
- package/bin/cli.js +32 -2
- package/components/buttons/cta-brutal/cta-brutal.module.css +81 -0
- package/components/buttons/cta-brutal/index.tsx +56 -0
- package/components/buttons/dead-button/dead-button.module.css +111 -0
- package/components/buttons/dead-button/index.tsx +47 -0
- package/components/buttons/deeper-button/deeper-button.module.css +76 -0
- package/components/buttons/deeper-button/index.tsx +51 -0
- package/components/buttons/dual-choice/dual-choice.module.css +90 -0
- package/components/buttons/dual-choice/index.tsx +54 -0
- package/components/buttons/tension-bar/index.tsx +79 -0
- package/components/buttons/tension-bar/tension-bar.module.css +105 -0
- package/components/decorative/coffee-stain/coffee-stain.module.css +24 -0
- package/components/decorative/coffee-stain/index.tsx +55 -0
- package/components/decorative/ornaments/index.tsx +51 -0
- package/components/decorative/ornaments/ornaments.module.css +33 -0
- package/components/decorative/rune-symbols/index.tsx +55 -0
- package/components/decorative/rune-symbols/rune-symbols.module.css +22 -0
- package/components/layout/horizontal-scroll/horizontal-scroll.module.css +30 -0
- package/components/layout/horizontal-scroll/index.tsx +78 -0
- package/components/layout/spec-grid/index.tsx +56 -0
- package/components/layout/spec-grid/spec-grid.module.css +21 -0
- package/components/layout/tower-pricing/index.tsx +56 -0
- package/components/layout/tower-pricing/tower-pricing.module.css +27 -0
- package/components/layout/tracklist/index.tsx +45 -0
- package/components/layout/tracklist/tracklist.module.css +24 -0
- package/components/layout/void-frame/index.tsx +32 -0
- package/components/layout/void-frame/void-frame.module.css +38 -0
- package/components/navigation/brutal-nav/brutal-nav.module.css +85 -0
- package/components/navigation/brutal-nav/index.tsx +71 -0
- package/components/navigation/progress-dots/index.tsx +55 -0
- package/components/navigation/progress-dots/progress-dots.module.css +91 -0
- package/components/navigation/scattered-nav/index.tsx +59 -0
- package/components/navigation/scattered-nav/scattered-nav.module.css +113 -0
- package/components/navigation/scroll-indicator/index.tsx +58 -0
- package/components/navigation/scroll-indicator/scroll-indicator.module.css +82 -0
- package/components/navigation/vertical-nav/index.tsx +59 -0
- package/components/navigation/vertical-nav/vertical-nav.module.css +98 -0
- package/components/text/ascii-art/css/ascii-art.module.css +173 -0
- package/components/text/ascii-art/css/index.tsx +116 -0
- package/components/text/ascii-art/tailwind/index.tsx +124 -0
- package/components/text/blood-drip/css/blood-drip.module.css +142 -0
- package/components/text/blood-drip/css/index.tsx +113 -0
- package/components/text/blood-drip/tailwind/index.tsx +133 -0
- package/components/text/char-glitch/css/char-glitch.module.css +124 -0
- package/components/text/char-glitch/css/index.tsx +153 -0
- package/components/text/char-glitch/tailwind/index.tsx +126 -0
- package/components/text/countdown-display/css/countdown-display.module.css +179 -0
- package/components/text/countdown-display/css/index.tsx +190 -0
- package/components/text/countdown-display/tailwind/index.tsx +155 -0
- package/components/text/giant-layers/css/giant-layers.module.css +156 -0
- package/components/text/giant-layers/css/index.tsx +97 -0
- package/components/text/giant-layers/tailwind/index.tsx +111 -0
- package/components/text/reveal-text/css/index.tsx +180 -0
- package/components/text/reveal-text/css/reveal-text.module.css +129 -0
- package/components/text/reveal-text/tailwind/index.tsx +135 -0
- package/components/text/rotate-text/css/index.tsx +139 -0
- package/components/text/rotate-text/css/rotate-text.module.css +162 -0
- package/components/text/rotate-text/tailwind/index.tsx +127 -0
- package/components/text/strike-reveal/css/index.tsx +124 -0
- package/components/text/strike-reveal/css/strike-reveal.module.css +139 -0
- package/components/text/strike-reveal/tailwind/index.tsx +138 -0
- package/components/text/terminal-output/css/index.tsx +179 -0
- package/components/text/terminal-output/css/terminal-output.module.css +203 -0
- package/components/text/terminal-output/tailwind/index.tsx +174 -0
- package/components/text/typing-text/css/index.tsx +115 -0
- package/components/text/typing-text/css/typing-text.module.css +84 -0
- package/components/text/typing-text/tailwind/index.tsx +126 -0
- package/package.json +1 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, HTMLAttributes, useEffect, useState, useRef } from 'react';
|
|
4
|
+
import styles from './strike-reveal.module.css';
|
|
5
|
+
|
|
6
|
+
export interface StrikeRevealProps extends HTMLAttributes<HTMLSpanElement> {
|
|
7
|
+
/** Original text to strike through */
|
|
8
|
+
children: string;
|
|
9
|
+
/** Text to reveal after strike */
|
|
10
|
+
revealText?: string;
|
|
11
|
+
/** Visual variant */
|
|
12
|
+
variant?: 'permanent' | 'crossout' | 'redacted' | 'censored' | 'glitch';
|
|
13
|
+
/** Color variant */
|
|
14
|
+
color?: 'default' | 'blood' | 'cyber' | 'acid' | 'void';
|
|
15
|
+
/** Trigger mode */
|
|
16
|
+
trigger?: 'auto' | 'hover' | 'scroll' | 'click';
|
|
17
|
+
/** Delay before animation (ms) */
|
|
18
|
+
delay?: number;
|
|
19
|
+
/** Double strike line */
|
|
20
|
+
double?: boolean;
|
|
21
|
+
/** Custom strike color */
|
|
22
|
+
strikeColor?: string;
|
|
23
|
+
/** Callback when reveal completes */
|
|
24
|
+
onReveal?: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const StrikeReveal = forwardRef<HTMLSpanElement, StrikeRevealProps>(
|
|
28
|
+
(
|
|
29
|
+
{
|
|
30
|
+
children,
|
|
31
|
+
revealText,
|
|
32
|
+
variant = 'permanent',
|
|
33
|
+
color = 'default',
|
|
34
|
+
trigger = 'auto',
|
|
35
|
+
delay = 0,
|
|
36
|
+
double = false,
|
|
37
|
+
strikeColor,
|
|
38
|
+
onReveal,
|
|
39
|
+
className,
|
|
40
|
+
style,
|
|
41
|
+
...props
|
|
42
|
+
},
|
|
43
|
+
ref
|
|
44
|
+
) => {
|
|
45
|
+
const [isActive, setIsActive] = useState(false);
|
|
46
|
+
const containerRef = useRef<HTMLSpanElement>(null);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (trigger === 'auto') {
|
|
50
|
+
const timeout = setTimeout(() => {
|
|
51
|
+
setIsActive(true);
|
|
52
|
+
setTimeout(() => onReveal?.(), 700);
|
|
53
|
+
}, delay);
|
|
54
|
+
return () => clearTimeout(timeout);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (trigger === 'scroll') {
|
|
58
|
+
const observer = new IntersectionObserver(
|
|
59
|
+
([entry]) => {
|
|
60
|
+
if (entry.isIntersecting) {
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
setIsActive(true);
|
|
63
|
+
setTimeout(() => onReveal?.(), 700);
|
|
64
|
+
}, delay);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{ threshold: 0.5 }
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (containerRef.current) {
|
|
71
|
+
observer.observe(containerRef.current);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return () => observer.disconnect();
|
|
75
|
+
}
|
|
76
|
+
}, [trigger, delay, onReveal]);
|
|
77
|
+
|
|
78
|
+
const handleClick = () => {
|
|
79
|
+
if (trigger === 'click') {
|
|
80
|
+
setIsActive(true);
|
|
81
|
+
setTimeout(() => onReveal?.(), 700);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const containerClasses = [
|
|
86
|
+
styles.container,
|
|
87
|
+
styles[variant],
|
|
88
|
+
color !== 'default' && styles[color],
|
|
89
|
+
double && styles.double,
|
|
90
|
+
trigger === 'hover' && styles.hover,
|
|
91
|
+
isActive && styles.active,
|
|
92
|
+
className
|
|
93
|
+
].filter(Boolean).join(' ');
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<span
|
|
97
|
+
ref={(node) => {
|
|
98
|
+
(containerRef as React.MutableRefObject<HTMLSpanElement | null>).current = node;
|
|
99
|
+
if (typeof ref === 'function') ref(node);
|
|
100
|
+
else if (ref) ref.current = node;
|
|
101
|
+
}}
|
|
102
|
+
className={containerClasses}
|
|
103
|
+
style={{
|
|
104
|
+
'--strike-color': strikeColor,
|
|
105
|
+
'--delay': `${delay}ms`,
|
|
106
|
+
...style
|
|
107
|
+
} as React.CSSProperties}
|
|
108
|
+
onClick={handleClick}
|
|
109
|
+
{...props}
|
|
110
|
+
>
|
|
111
|
+
<span className={styles.text}>
|
|
112
|
+
{children}
|
|
113
|
+
<span className={styles.strike} />
|
|
114
|
+
</span>
|
|
115
|
+
{revealText && (
|
|
116
|
+
<span className={styles.revealed}>{revealText}</span>
|
|
117
|
+
)}
|
|
118
|
+
</span>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
StrikeReveal.displayName = 'StrikeReveal';
|
|
124
|
+
export default StrikeReveal;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: inline-block;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.text {
|
|
7
|
+
position: relative;
|
|
8
|
+
display: inline;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* Strike line */
|
|
12
|
+
.strike {
|
|
13
|
+
position: absolute;
|
|
14
|
+
left: 0;
|
|
15
|
+
top: 50%;
|
|
16
|
+
width: 0;
|
|
17
|
+
height: 2px;
|
|
18
|
+
background: currentColor;
|
|
19
|
+
transform: translateY(-50%);
|
|
20
|
+
transition: width 0.4s cubic-bezier(0.19, 1, 0.22, 1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.container.active .strike {
|
|
24
|
+
width: 100%;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Hidden text that reveals */
|
|
28
|
+
.revealed {
|
|
29
|
+
display: block;
|
|
30
|
+
opacity: 0;
|
|
31
|
+
transform: translateY(10px);
|
|
32
|
+
transition: opacity 0.4s ease, transform 0.4s ease;
|
|
33
|
+
transition-delay: 0.3s;
|
|
34
|
+
margin-top: 0.5em;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.container.active .revealed {
|
|
38
|
+
opacity: 1;
|
|
39
|
+
transform: translateY(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Variant: strikethrough stays */
|
|
43
|
+
.permanent .strike {
|
|
44
|
+
background: var(--strike-color, #ff0040);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.permanent.active .text {
|
|
48
|
+
opacity: 0.5;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Variant: text fades then reveals */
|
|
52
|
+
.crossout .text {
|
|
53
|
+
transition: opacity 0.3s ease;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.crossout.active .text {
|
|
57
|
+
opacity: 0.3;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Variant: redacted style */
|
|
61
|
+
.redacted .strike {
|
|
62
|
+
height: 1em;
|
|
63
|
+
background: var(--strike-color, #0a0a0a);
|
|
64
|
+
top: 50%;
|
|
65
|
+
transform: translateY(-50%);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.redacted.active .text {
|
|
69
|
+
color: transparent;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Variant: censored with bars */
|
|
73
|
+
.censored {
|
|
74
|
+
position: relative;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.censored .strike {
|
|
78
|
+
height: 100%;
|
|
79
|
+
background: repeating-linear-gradient(
|
|
80
|
+
90deg,
|
|
81
|
+
var(--strike-color, #0a0a0a) 0px,
|
|
82
|
+
var(--strike-color, #0a0a0a) 8px,
|
|
83
|
+
transparent 8px,
|
|
84
|
+
transparent 12px
|
|
85
|
+
);
|
|
86
|
+
top: 0;
|
|
87
|
+
transform: none;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Variant: glitch strike */
|
|
91
|
+
.glitch .strike {
|
|
92
|
+
background: var(--strike-color, #ff0040);
|
|
93
|
+
animation: none;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.glitch.active .strike {
|
|
97
|
+
animation: glitchStrike 0.5s ease forwards;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@keyframes glitchStrike {
|
|
101
|
+
0% { width: 0; transform: translateY(-50%); }
|
|
102
|
+
30% { width: 110%; transform: translateY(-50%) translateX(-5%); }
|
|
103
|
+
50% { width: 95%; transform: translateY(-50%) translateX(3%); }
|
|
104
|
+
70% { width: 102%; transform: translateY(-50%) translateX(-2%); }
|
|
105
|
+
100% { width: 100%; transform: translateY(-50%); }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Colors */
|
|
109
|
+
.blood .strike { background: #ff0040; }
|
|
110
|
+
.cyber .strike { background: #00ffff; }
|
|
111
|
+
.acid .strike { background: #aaff00; }
|
|
112
|
+
.void .strike { background: #0a0a0a; }
|
|
113
|
+
|
|
114
|
+
/* Hover trigger */
|
|
115
|
+
.hover:hover .strike {
|
|
116
|
+
width: 100%;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.hover:hover .revealed {
|
|
120
|
+
opacity: 1;
|
|
121
|
+
transform: translateY(0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Animation on scroll */
|
|
125
|
+
.scroll .strike {
|
|
126
|
+
transition-delay: var(--delay, 0ms);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Double strike */
|
|
130
|
+
.double .strike::after {
|
|
131
|
+
content: '';
|
|
132
|
+
position: absolute;
|
|
133
|
+
left: 0;
|
|
134
|
+
top: 4px;
|
|
135
|
+
width: 100%;
|
|
136
|
+
height: 2px;
|
|
137
|
+
background: inherit;
|
|
138
|
+
opacity: 0.5;
|
|
139
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, HTMLAttributes, useEffect, useState, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface StrikeRevealProps extends HTMLAttributes<HTMLSpanElement> {
|
|
6
|
+
children: string;
|
|
7
|
+
revealText?: string;
|
|
8
|
+
variant?: 'permanent' | 'crossout' | 'redacted' | 'censored' | 'glitch';
|
|
9
|
+
color?: 'default' | 'blood' | 'cyber' | 'acid' | 'void';
|
|
10
|
+
trigger?: 'auto' | 'hover' | 'scroll' | 'click';
|
|
11
|
+
delay?: number;
|
|
12
|
+
double?: boolean;
|
|
13
|
+
strikeColor?: string;
|
|
14
|
+
onReveal?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const colorStyles = {
|
|
18
|
+
default: 'bg-current',
|
|
19
|
+
blood: 'bg-rose-500',
|
|
20
|
+
cyber: 'bg-cyan-400',
|
|
21
|
+
acid: 'bg-lime-400',
|
|
22
|
+
void: 'bg-[#0a0a0a]',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const StrikeReveal = forwardRef<HTMLSpanElement, StrikeRevealProps>(
|
|
26
|
+
(
|
|
27
|
+
{
|
|
28
|
+
children,
|
|
29
|
+
revealText,
|
|
30
|
+
variant = 'permanent',
|
|
31
|
+
color = 'default',
|
|
32
|
+
trigger = 'auto',
|
|
33
|
+
delay = 0,
|
|
34
|
+
double = false,
|
|
35
|
+
strikeColor,
|
|
36
|
+
onReveal,
|
|
37
|
+
className = '',
|
|
38
|
+
style,
|
|
39
|
+
...props
|
|
40
|
+
},
|
|
41
|
+
ref
|
|
42
|
+
) => {
|
|
43
|
+
const [isActive, setIsActive] = useState(false);
|
|
44
|
+
const containerRef = useRef<HTMLSpanElement>(null);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (trigger === 'auto') {
|
|
48
|
+
const timeout = setTimeout(() => {
|
|
49
|
+
setIsActive(true);
|
|
50
|
+
setTimeout(() => onReveal?.(), 700);
|
|
51
|
+
}, delay);
|
|
52
|
+
return () => clearTimeout(timeout);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (trigger === 'scroll') {
|
|
56
|
+
const observer = new IntersectionObserver(
|
|
57
|
+
([entry]) => {
|
|
58
|
+
if (entry.isIntersecting) {
|
|
59
|
+
setTimeout(() => {
|
|
60
|
+
setIsActive(true);
|
|
61
|
+
setTimeout(() => onReveal?.(), 700);
|
|
62
|
+
}, delay);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{ threshold: 0.5 }
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (containerRef.current) observer.observe(containerRef.current);
|
|
69
|
+
return () => observer.disconnect();
|
|
70
|
+
}
|
|
71
|
+
}, [trigger, delay, onReveal]);
|
|
72
|
+
|
|
73
|
+
const handleClick = () => {
|
|
74
|
+
if (trigger === 'click') {
|
|
75
|
+
setIsActive(true);
|
|
76
|
+
setTimeout(() => onReveal?.(), 700);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleMouseEnter = () => {
|
|
81
|
+
if (trigger === 'hover') setIsActive(true);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleMouseLeave = () => {
|
|
85
|
+
if (trigger === 'hover') setIsActive(false);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const strikeHeight = variant === 'redacted' ? 'h-[1em]' : variant === 'censored' ? 'h-full' : 'h-0.5';
|
|
89
|
+
const bgStyle = strikeColor ? { backgroundColor: strikeColor } : undefined;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<span
|
|
93
|
+
ref={(node) => {
|
|
94
|
+
(containerRef as React.MutableRefObject<HTMLSpanElement | null>).current = node;
|
|
95
|
+
if (typeof ref === 'function') ref(node);
|
|
96
|
+
else if (ref) ref.current = node;
|
|
97
|
+
}}
|
|
98
|
+
className={`relative inline-block ${className}`}
|
|
99
|
+
style={style}
|
|
100
|
+
onClick={handleClick}
|
|
101
|
+
onMouseEnter={handleMouseEnter}
|
|
102
|
+
onMouseLeave={handleMouseLeave}
|
|
103
|
+
{...props}
|
|
104
|
+
>
|
|
105
|
+
<span className={`relative inline ${isActive && variant === 'crossout' ? 'opacity-30' : ''} ${isActive && variant === 'redacted' ? 'text-transparent' : ''} transition-opacity duration-300`}>
|
|
106
|
+
{children}
|
|
107
|
+
<span
|
|
108
|
+
className={`absolute left-0 top-1/2 -translate-y-1/2 ${strikeHeight} ${colorStyles[color]} transition-all duration-400 ease-out ${
|
|
109
|
+
isActive ? 'w-full' : 'w-0'
|
|
110
|
+
}`}
|
|
111
|
+
style={bgStyle}
|
|
112
|
+
/>
|
|
113
|
+
{double && (
|
|
114
|
+
<span
|
|
115
|
+
className={`absolute left-0 top-1/2 translate-y-0.5 h-0.5 ${colorStyles[color]} opacity-50 transition-all duration-400 ease-out ${
|
|
116
|
+
isActive ? 'w-full' : 'w-0'
|
|
117
|
+
}`}
|
|
118
|
+
style={bgStyle}
|
|
119
|
+
/>
|
|
120
|
+
)}
|
|
121
|
+
</span>
|
|
122
|
+
{revealText && (
|
|
123
|
+
<span
|
|
124
|
+
className={`block mt-2 transition-all duration-400 ease-out ${
|
|
125
|
+
isActive ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-2'
|
|
126
|
+
}`}
|
|
127
|
+
style={{ transitionDelay: '300ms' }}
|
|
128
|
+
>
|
|
129
|
+
{revealText}
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
</span>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
StrikeReveal.displayName = 'StrikeReveal';
|
|
138
|
+
export default StrikeReveal;
|
|
@@ -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;
|