@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,113 @@
1
+ /* scattered-nav - Nav éclatée/fragments positionnés aléatoirement */
2
+ .nav {
3
+ position: fixed;
4
+ top: 0;
5
+ left: 0;
6
+ right: 0;
7
+ z-index: 1000;
8
+ height: 60px;
9
+ background: var(--chaos-void, #0a0a0a);
10
+ border-bottom: 1px solid var(--chaos-blood, #ff0040);
11
+ display: flex;
12
+ align-items: center;
13
+ padding: 0 1rem;
14
+ }
15
+
16
+ .item {
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ padding: 0 1rem;
21
+ height: 100%;
22
+ font-size: 0.7rem;
23
+ letter-spacing: 0.1em;
24
+ text-transform: uppercase;
25
+ font-family: var(--chaos-font-mono, 'Space Mono', monospace);
26
+ border-right: 1px solid rgba(255, 255, 255, 0.1);
27
+ transition: all 0.2s;
28
+ color: var(--chaos-bone-dark, #888);
29
+ text-decoration: none;
30
+ }
31
+
32
+ .item:hover {
33
+ background: var(--chaos-blood, #ff0040);
34
+ color: var(--chaos-void, #0a0a0a);
35
+ }
36
+
37
+ .scattered1 { transform: translateY(3px); }
38
+ .scattered2 { transform: rotate(-2deg); border-left: 1px solid var(--chaos-blood, #ff0040); }
39
+ .scattered3 { transform: translateY(-2px) rotate(1deg); }
40
+ .scattered4 { transform: translateX(-3px); }
41
+ .scattered5 { transform: rotate(1.5deg) translateY(1px); }
42
+
43
+ .logo {
44
+ background: var(--chaos-blood, #ff0040);
45
+ color: var(--chaos-void, #0a0a0a);
46
+ font-family: var(--chaos-font-display, 'Bebas Neue', sans-serif);
47
+ font-size: 1.3rem;
48
+ padding: 0 1.5rem;
49
+ }
50
+
51
+ .glitch { position: relative; }
52
+
53
+ .glitch::before {
54
+ content: attr(data-text);
55
+ position: absolute;
56
+ left: 2px;
57
+ top: 0;
58
+ color: var(--chaos-bone, #e8e8e8);
59
+ opacity: 0.5;
60
+ animation: navGlitch 0.3s infinite;
61
+ }
62
+
63
+ @keyframes navGlitch {
64
+ 0%, 100% { clip-path: inset(0 0 80% 0); }
65
+ 25% { clip-path: inset(20% 0 60% 0); }
66
+ 50% { clip-path: inset(40% 0 40% 0); }
67
+ 75% { clip-path: inset(60% 0 20% 0); }
68
+ }
69
+
70
+ .corrupt {
71
+ font-family: var(--chaos-font-mono, monospace);
72
+ color: var(--chaos-blood, #ff0040);
73
+ font-size: 0.6rem;
74
+ opacity: 0.5;
75
+ animation: flickerText 3s infinite;
76
+ }
77
+
78
+ @keyframes flickerText {
79
+ 0%, 90%, 100% { opacity: 0.5; }
80
+ 92%, 94%, 96% { opacity: 0; }
81
+ }
82
+
83
+ .status {
84
+ margin-left: auto;
85
+ gap: 0.5rem;
86
+ font-size: 0.6rem;
87
+ background: rgba(0, 0, 0, 0.3);
88
+ padding: 0 1.5rem;
89
+ }
90
+
91
+ .statusDot {
92
+ width: 6px;
93
+ height: 6px;
94
+ background: var(--chaos-blood, #ff0040);
95
+ border-radius: 50%;
96
+ animation: pulse 2s ease-in-out infinite;
97
+ display: inline-block;
98
+ margin-right: 0.5rem;
99
+ }
100
+
101
+ @keyframes pulse {
102
+ 0%, 100% { opacity: 1; transform: scale(1); }
103
+ 50% { opacity: 0.5; transform: scale(0.8); }
104
+ }
105
+
106
+ .blink {
107
+ animation: blink 1s step-start infinite;
108
+ color: var(--chaos-blood, #ff0040);
109
+ }
110
+
111
+ @keyframes blink {
112
+ 50% { opacity: 0; }
113
+ }
@@ -0,0 +1,58 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, useEffect, useState } from 'react';
4
+ import styles from './scroll-indicator.module.css';
5
+
6
+ export interface ScrollIndicatorProps extends HTMLAttributes<HTMLDivElement> {
7
+ text?: string;
8
+ showArrow?: boolean;
9
+ showPercentage?: boolean;
10
+ variant?: 'default' | 'blood' | 'minimal';
11
+ position?: 'right' | 'left';
12
+ trackHeight?: number;
13
+ }
14
+
15
+ export const ScrollIndicator = forwardRef<HTMLDivElement, ScrollIndicatorProps>(
16
+ ({ text = 'SCROLL', showArrow = false, showPercentage = false, variant = 'default', position = 'right', trackHeight = 100, className, ...props }, ref) => {
17
+ const [scrollProgress, setScrollProgress] = useState(0);
18
+
19
+ useEffect(() => {
20
+ const handleScroll = () => {
21
+ const scrollTop = window.scrollY;
22
+ const docHeight = document.documentElement.scrollHeight - window.innerHeight;
23
+ const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
24
+ setScrollProgress(Math.min(100, Math.max(0, progress)));
25
+ };
26
+
27
+ window.addEventListener('scroll', handleScroll, { passive: true });
28
+ handleScroll();
29
+ return () => window.removeEventListener('scroll', handleScroll);
30
+ }, []);
31
+
32
+ const thumbTop = (scrollProgress / 100) * (trackHeight - 20);
33
+
34
+ const containerClasses = [
35
+ styles.container,
36
+ variant === 'blood' && styles.variantBlood,
37
+ variant === 'minimal' && styles.variantMinimal,
38
+ position === 'left' && styles.positionLeft,
39
+ className,
40
+ ].filter(Boolean).join(' ');
41
+
42
+ return (
43
+ <div ref={ref} className={containerClasses} {...props}>
44
+ <div className={styles.inner}>
45
+ {showArrow && <span className={styles.arrow}>↓</span>}
46
+ <div className={styles.track} style={{ height: trackHeight }}>
47
+ <div className={styles.thumb} style={{ top: thumbTop }} />
48
+ </div>
49
+ {text && <span className={styles.text}>{text}</span>}
50
+ {showPercentage && <span className={styles.percentage}>{Math.round(scrollProgress)}%</span>}
51
+ </div>
52
+ </div>
53
+ );
54
+ }
55
+ );
56
+
57
+ ScrollIndicator.displayName = 'ScrollIndicator';
58
+ export default ScrollIndicator;
@@ -0,0 +1,82 @@
1
+ /* scroll-indicator - Indicateur de scroll vertical avec texte */
2
+ .container {
3
+ position: fixed;
4
+ right: 0;
5
+ top: 0;
6
+ bottom: 0;
7
+ width: 60px;
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ z-index: 100;
12
+ }
13
+
14
+ .inner {
15
+ display: flex;
16
+ flex-direction: column;
17
+ align-items: center;
18
+ gap: 1rem;
19
+ }
20
+
21
+ .track {
22
+ width: 2px;
23
+ height: 100px;
24
+ background: var(--chaos-iron, #2a2820);
25
+ position: relative;
26
+ border-radius: 1px;
27
+ }
28
+
29
+ .thumb {
30
+ width: 6px;
31
+ height: 20px;
32
+ background: var(--chaos-gold, #c9a227);
33
+ position: absolute;
34
+ left: -2px;
35
+ top: 0;
36
+ border-radius: 3px;
37
+ transition: top 0.3s ease-out;
38
+ box-shadow: 0 0 10px rgba(201, 162, 39, 0.3);
39
+ }
40
+
41
+ .text {
42
+ writing-mode: vertical-rl;
43
+ text-orientation: mixed;
44
+ font-family: var(--chaos-font-serif, 'Cinzel', serif);
45
+ font-size: 0.5rem;
46
+ letter-spacing: 0.4em;
47
+ color: var(--chaos-bone-dark, #9a8b6f);
48
+ transform: rotate(180deg);
49
+ text-transform: uppercase;
50
+ }
51
+
52
+ .arrow {
53
+ animation: bounce 2s ease-in-out infinite;
54
+ color: var(--chaos-gold, #c9a227);
55
+ font-size: 0.8rem;
56
+ }
57
+
58
+ @keyframes bounce {
59
+ 0%, 100% { transform: translateY(0); opacity: 1; }
60
+ 50% { transform: translateY(5px); opacity: 0.5; }
61
+ }
62
+
63
+ .variantBlood .track { background: rgba(255, 0, 64, 0.2); }
64
+ .variantBlood .thumb { background: var(--chaos-blood, #ff0040); box-shadow: 0 0 10px rgba(255, 0, 64, 0.5); }
65
+ .variantBlood .text { color: var(--chaos-bone-dark, #666); }
66
+ .variantBlood .arrow { color: var(--chaos-blood, #ff0040); }
67
+
68
+ .variantMinimal .track { width: 1px; height: 60px; }
69
+ .variantMinimal .thumb { width: 3px; height: 10px; left: -1px; }
70
+ .variantMinimal .text { display: none; }
71
+
72
+ .positionLeft { left: 0; right: auto; }
73
+ .positionLeft .text { transform: rotate(0deg); }
74
+
75
+ .percentage {
76
+ font-family: var(--chaos-font-mono, monospace);
77
+ font-size: 0.6rem;
78
+ color: var(--chaos-gold, #c9a227);
79
+ margin-top: 0.5rem;
80
+ }
81
+
82
+ .variantBlood .percentage { color: var(--chaos-blood, #ff0040); }
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, ReactNode } from 'react';
4
+ import styles from './vertical-nav.module.css';
5
+
6
+ export interface VerticalNavItemProps {
7
+ href?: string;
8
+ glyph: string;
9
+ label?: string;
10
+ active?: boolean;
11
+ onClick?: () => void;
12
+ }
13
+
14
+ export interface VerticalNavProps extends HTMLAttributes<HTMLElement> {
15
+ items?: VerticalNavItemProps[];
16
+ runeTop?: string;
17
+ runeBottom?: string;
18
+ variant?: 'default' | 'dark';
19
+ size?: 'default' | 'compact';
20
+ children?: ReactNode;
21
+ }
22
+
23
+ export const VerticalNavItem = forwardRef<HTMLAnchorElement, VerticalNavItemProps>(
24
+ ({ href, glyph, label, active, onClick, ...props }, ref) => {
25
+ const classes = [styles.glyph, active && styles.glyphActive].filter(Boolean).join(' ');
26
+
27
+ if (href) {
28
+ return <a ref={ref} href={href} className={classes} data-label={label} onClick={onClick} {...props}>{glyph}</a>;
29
+ }
30
+ return <button className={classes} data-label={label} onClick={onClick} {...props}>{glyph}</button>;
31
+ }
32
+ );
33
+
34
+ VerticalNavItem.displayName = 'VerticalNavItem';
35
+
36
+ export const VerticalNav = forwardRef<HTMLElement, VerticalNavProps>(
37
+ ({ items, runeTop = 'ᛟ', runeBottom = 'ᛞ', variant = 'default', size = 'default', children, className, ...props }, ref) => {
38
+ const classes = [
39
+ styles.nav,
40
+ variant === 'dark' && styles.variantDark,
41
+ size === 'compact' && styles.sizeCompact,
42
+ className,
43
+ ].filter(Boolean).join(' ');
44
+
45
+ return (
46
+ <nav ref={ref} className={classes} {...props}>
47
+ <div className={styles.rune}>{runeTop}</div>
48
+ <div className={styles.items}>
49
+ {items?.map((item, i) => <VerticalNavItem key={i} {...item} />)}
50
+ {children}
51
+ </div>
52
+ <div className={styles.rune}>{runeBottom}</div>
53
+ </nav>
54
+ );
55
+ }
56
+ );
57
+
58
+ VerticalNav.displayName = 'VerticalNav';
59
+ export default VerticalNav;
@@ -0,0 +1,98 @@
1
+ /* vertical-nav - Nav latérale avec glyphes/runes */
2
+ .nav {
3
+ position: fixed;
4
+ left: 0;
5
+ top: 0;
6
+ bottom: 0;
7
+ width: 80px;
8
+ background: linear-gradient(to right, var(--chaos-obsidian, #121210) 0%, transparent 100%);
9
+ border-right: 1px solid var(--chaos-iron, #2a2820);
10
+ display: flex;
11
+ flex-direction: column;
12
+ align-items: center;
13
+ justify-content: space-between;
14
+ padding: 2rem 0;
15
+ z-index: 100;
16
+ }
17
+
18
+ .rune {
19
+ font-size: 1.5rem;
20
+ color: var(--chaos-gold-dark, #8b7017);
21
+ opacity: 0.5;
22
+ animation: runeGlow 4s ease-in-out infinite;
23
+ }
24
+
25
+ @keyframes runeGlow {
26
+ 0%, 100% { opacity: 0.3; text-shadow: none; }
27
+ 50% { opacity: 0.8; text-shadow: 0 0 10px var(--chaos-gold, #c9a227); }
28
+ }
29
+
30
+ .items {
31
+ display: flex;
32
+ flex-direction: column;
33
+ gap: 3rem;
34
+ }
35
+
36
+ .glyph {
37
+ width: 40px;
38
+ height: 40px;
39
+ display: flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ font-family: var(--chaos-font-display, 'Cinzel Decorative', serif);
43
+ font-size: 1rem;
44
+ color: var(--chaos-bone-dark, #9a8b6f);
45
+ text-decoration: none;
46
+ border: 1px solid var(--chaos-iron, #2a2820);
47
+ position: relative;
48
+ transition: all 0.4s;
49
+ }
50
+
51
+ .glyph::before {
52
+ content: attr(data-label);
53
+ position: absolute;
54
+ left: 60px;
55
+ font-family: var(--chaos-font-serif, 'Cinzel', serif);
56
+ font-size: 0.6rem;
57
+ letter-spacing: 0.3em;
58
+ color: var(--chaos-bone-dark, #9a8b6f);
59
+ opacity: 0;
60
+ transform: translateX(-10px);
61
+ transition: all 0.3s;
62
+ white-space: nowrap;
63
+ text-transform: uppercase;
64
+ }
65
+
66
+ .glyph:hover {
67
+ color: var(--chaos-gold, #c9a227);
68
+ border-color: var(--chaos-gold, #c9a227);
69
+ background: rgba(201, 162, 39, 0.1);
70
+ }
71
+
72
+ .glyph:hover::before {
73
+ opacity: 1;
74
+ transform: translateX(0);
75
+ }
76
+
77
+ .glyphActive {
78
+ color: var(--chaos-gold, #c9a227);
79
+ border-color: var(--chaos-gold, #c9a227);
80
+ box-shadow: 0 0 15px rgba(201, 162, 39, 0.3);
81
+ }
82
+
83
+ .variantDark {
84
+ background: linear-gradient(to right, var(--chaos-void, #0a0a0a) 0%, transparent 100%);
85
+ border-right-color: var(--chaos-blood, #ff0040);
86
+ }
87
+
88
+ .variantDark .rune { color: var(--chaos-blood, #ff0040); }
89
+ .variantDark .glyph { border-color: rgba(255, 0, 64, 0.3); }
90
+ .variantDark .glyph:hover {
91
+ color: var(--chaos-blood, #ff0040);
92
+ border-color: var(--chaos-blood, #ff0040);
93
+ background: rgba(255, 0, 64, 0.1);
94
+ }
95
+
96
+ .sizeCompact { width: 50px; padding: 1rem 0; }
97
+ .sizeCompact .glyph { width: 30px; height: 30px; font-size: 0.8rem; }
98
+ .sizeCompact .rune { font-size: 1rem; }
@@ -0,0 +1,173 @@
1
+ .container {
2
+ display: block;
3
+ font-family: 'Courier New', 'Monaco', 'Consolas', monospace;
4
+ white-space: pre;
5
+ line-height: 1.2;
6
+ letter-spacing: 0;
7
+ overflow-x: auto;
8
+ }
9
+
10
+ /* Size variants */
11
+ .xs { font-size: 0.5rem; }
12
+ .sm { font-size: 0.65rem; }
13
+ .md { font-size: 0.8rem; }
14
+ .lg { font-size: 1rem; }
15
+ .xl { font-size: 1.25rem; }
16
+
17
+ /* Color variants */
18
+ .blood {
19
+ color: #ff0040;
20
+ text-shadow: 0 0 5px #ff004080;
21
+ }
22
+
23
+ .cyber {
24
+ color: #00ffff;
25
+ text-shadow: 0 0 5px #00ffff80;
26
+ }
27
+
28
+ .matrix {
29
+ color: #00ff00;
30
+ text-shadow: 0 0 5px #00ff0080;
31
+ }
32
+
33
+ .amber {
34
+ color: #ffaa00;
35
+ text-shadow: 0 0 5px #ffaa0080;
36
+ }
37
+
38
+ .ghost {
39
+ color: #666;
40
+ opacity: 0.7;
41
+ }
42
+
43
+ .gradient {
44
+ background: linear-gradient(180deg, #ff0040 0%, #00ffff 50%, #00ff00 100%);
45
+ -webkit-background-clip: text;
46
+ -webkit-text-fill-color: transparent;
47
+ background-clip: text;
48
+ }
49
+
50
+ /* Animation variants */
51
+ .typing {
52
+ overflow: hidden;
53
+ white-space: nowrap;
54
+ animation: typeIn 2s steps(40, end) forwards;
55
+ }
56
+
57
+ @keyframes typeIn {
58
+ from { max-width: 0; }
59
+ to { max-width: 100%; }
60
+ }
61
+
62
+ .reveal .line {
63
+ opacity: 0;
64
+ animation: lineReveal 0.5s forwards;
65
+ }
66
+
67
+ @keyframes lineReveal {
68
+ from { opacity: 0; transform: translateX(-10px); }
69
+ to { opacity: 1; transform: translateX(0); }
70
+ }
71
+
72
+ .glitch {
73
+ position: relative;
74
+ }
75
+
76
+ .glitch::before,
77
+ .glitch::after {
78
+ content: attr(data-text);
79
+ position: absolute;
80
+ top: 0;
81
+ left: 0;
82
+ width: 100%;
83
+ height: 100%;
84
+ pointer-events: none;
85
+ }
86
+
87
+ .glitch::before {
88
+ color: #ff0040;
89
+ animation: asciiGlitch1 3s infinite;
90
+ clip-path: polygon(0 0, 100% 0, 100% 45%, 0 45%);
91
+ }
92
+
93
+ .glitch::after {
94
+ color: #00ffff;
95
+ animation: asciiGlitch2 3s infinite;
96
+ clip-path: polygon(0 55%, 100% 55%, 100% 100%, 0 100%);
97
+ }
98
+
99
+ @keyframes asciiGlitch1 {
100
+ 0%, 90%, 100% { transform: translate(0); opacity: 0; }
101
+ 92% { transform: translate(-2px, 0); opacity: 0.8; }
102
+ 94% { transform: translate(2px, 0); opacity: 0.8; }
103
+ }
104
+
105
+ @keyframes asciiGlitch2 {
106
+ 0%, 90%, 100% { transform: translate(0); opacity: 0; }
107
+ 93% { transform: translate(2px, 0); opacity: 0.8; }
108
+ 95% { transform: translate(-2px, 0); opacity: 0.8; }
109
+ }
110
+
111
+ /* Scanline effect */
112
+ .scanlines {
113
+ position: relative;
114
+ }
115
+
116
+ .scanlines::after {
117
+ content: '';
118
+ position: absolute;
119
+ top: 0;
120
+ left: 0;
121
+ right: 0;
122
+ bottom: 0;
123
+ background: repeating-linear-gradient(
124
+ 0deg,
125
+ transparent,
126
+ transparent 2px,
127
+ rgba(0, 0, 0, 0.1) 2px,
128
+ rgba(0, 0, 0, 0.1) 4px
129
+ );
130
+ pointer-events: none;
131
+ }
132
+
133
+ /* Border styles */
134
+ .bordered {
135
+ border: 1px solid currentColor;
136
+ padding: 1rem;
137
+ position: relative;
138
+ }
139
+
140
+ .bordered::before {
141
+ content: '┌' attr(data-title) '┐';
142
+ position: absolute;
143
+ top: -0.6em;
144
+ left: 1rem;
145
+ background: inherit;
146
+ padding: 0 0.5rem;
147
+ font-size: 0.8em;
148
+ }
149
+
150
+ /* Flicker effect */
151
+ .flicker {
152
+ animation: asciiFlicker 0.15s infinite;
153
+ }
154
+
155
+ @keyframes asciiFlicker {
156
+ 0%, 100% { opacity: 1; }
157
+ 50% { opacity: 0.8; }
158
+ }
159
+
160
+ /* Pulse effect */
161
+ .pulse {
162
+ animation: asciiPulse 2s ease-in-out infinite;
163
+ }
164
+
165
+ @keyframes asciiPulse {
166
+ 0%, 100% { opacity: 0.6; }
167
+ 50% { opacity: 1; }
168
+ }
169
+
170
+ /* Line container for reveal animation */
171
+ .line {
172
+ display: block;
173
+ }
@@ -0,0 +1,116 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, HTMLAttributes, useEffect, useState } from 'react';
4
+ import styles from './ascii-art.module.css';
5
+
6
+ export interface AsciiArtProps extends HTMLAttributes<HTMLPreElement> {
7
+ /** ASCII art content (multi-line string) */
8
+ children: string;
9
+ /** Size variant */
10
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
11
+ /** Color variant */
12
+ variant?: 'default' | 'blood' | 'cyber' | 'matrix' | 'amber' | 'ghost' | 'gradient';
13
+ /** Animation effect */
14
+ animation?: 'none' | 'typing' | 'reveal' | 'glitch' | 'flicker' | 'pulse';
15
+ /** Show scanlines overlay */
16
+ scanlines?: boolean;
17
+ /** Show border */
18
+ bordered?: boolean;
19
+ /** Border title */
20
+ title?: string;
21
+ /** Delay between lines for reveal animation (ms) */
22
+ revealDelay?: number;
23
+ /** Custom color */
24
+ color?: string;
25
+ }
26
+
27
+ export const AsciiArt = forwardRef<HTMLPreElement, AsciiArtProps>(
28
+ (
29
+ {
30
+ children,
31
+ size = 'md',
32
+ variant = 'default',
33
+ animation = 'none',
34
+ scanlines = false,
35
+ bordered = false,
36
+ title,
37
+ revealDelay = 100,
38
+ color,
39
+ className,
40
+ style,
41
+ ...props
42
+ },
43
+ ref
44
+ ) => {
45
+ const [revealedLines, setRevealedLines] = useState<number>(0);
46
+ const lines = children.split('\n');
47
+
48
+ useEffect(() => {
49
+ if (animation === 'reveal') {
50
+ setRevealedLines(0);
51
+ let lineIndex = 0;
52
+
53
+ const interval = setInterval(() => {
54
+ if (lineIndex < lines.length) {
55
+ setRevealedLines(lineIndex + 1);
56
+ lineIndex++;
57
+ } else {
58
+ clearInterval(interval);
59
+ }
60
+ }, revealDelay);
61
+
62
+ return () => clearInterval(interval);
63
+ }
64
+ }, [animation, lines.length, revealDelay, children]);
65
+
66
+ const containerClasses = [
67
+ styles.container,
68
+ styles[size],
69
+ variant !== 'default' && styles[variant],
70
+ animation !== 'none' && styles[animation],
71
+ scanlines && styles.scanlines,
72
+ bordered && styles.bordered,
73
+ className
74
+ ].filter(Boolean).join(' ');
75
+
76
+ const renderContent = () => {
77
+ if (animation === 'reveal') {
78
+ return lines.map((line, i) => (
79
+ <span
80
+ key={i}
81
+ className={styles.line}
82
+ style={{
83
+ opacity: i < revealedLines ? 1 : 0,
84
+ animationDelay: `${i * revealDelay}ms`,
85
+ transform: i < revealedLines ? 'translateX(0)' : 'translateX(-10px)',
86
+ transition: 'opacity 0.3s ease, transform 0.3s ease',
87
+ }}
88
+ >
89
+ {line}
90
+ {'\n'}
91
+ </span>
92
+ ));
93
+ }
94
+ return children;
95
+ };
96
+
97
+ return (
98
+ <pre
99
+ ref={ref}
100
+ className={containerClasses}
101
+ style={{
102
+ color: color,
103
+ ...style
104
+ }}
105
+ data-text={animation === 'glitch' ? children : undefined}
106
+ data-title={bordered ? title : undefined}
107
+ {...props}
108
+ >
109
+ {renderContent()}
110
+ </pre>
111
+ );
112
+ }
113
+ );
114
+
115
+ AsciiArt.displayName = 'AsciiArt';
116
+ export default AsciiArt;