@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,155 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, HTMLAttributes, useEffect, useState, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface CountdownDisplayProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
6
|
+
target: Date | number;
|
|
7
|
+
format?: 'full' | 'hms' | 'ms' | 'dhms';
|
|
8
|
+
size?: 'sm' | 'md' | 'lg';
|
|
9
|
+
variant?: 'default' | 'minimal' | 'neon' | 'brutal' | 'glitch';
|
|
10
|
+
accentColor?: string;
|
|
11
|
+
showLabels?: boolean;
|
|
12
|
+
compact?: boolean;
|
|
13
|
+
flip?: boolean;
|
|
14
|
+
urgentThreshold?: number;
|
|
15
|
+
onComplete?: () => void;
|
|
16
|
+
labels?: { days?: string; hours?: string; minutes?: string; seconds?: string; };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface TimeLeft { days: number; hours: number; minutes: number; seconds: number; }
|
|
20
|
+
|
|
21
|
+
const sizeClasses = {
|
|
22
|
+
sm: { value: 'text-3xl md:text-5xl', separator: 'text-xl md:text-3xl', label: 'text-[0.5rem]' },
|
|
23
|
+
md: { value: 'text-5xl md:text-7xl', separator: 'text-3xl md:text-5xl', label: 'text-[0.6rem]' },
|
|
24
|
+
lg: { value: 'text-7xl md:text-9xl', separator: 'text-5xl md:text-7xl', label: 'text-[0.7rem]' },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const CountdownDisplay = forwardRef<HTMLDivElement, CountdownDisplayProps>(
|
|
28
|
+
(
|
|
29
|
+
{
|
|
30
|
+
target,
|
|
31
|
+
format = 'hms',
|
|
32
|
+
size = 'md',
|
|
33
|
+
variant = 'default',
|
|
34
|
+
accentColor = '#ff0040',
|
|
35
|
+
showLabels = true,
|
|
36
|
+
compact = false,
|
|
37
|
+
flip = false,
|
|
38
|
+
urgentThreshold = 60,
|
|
39
|
+
onComplete,
|
|
40
|
+
labels = {},
|
|
41
|
+
className = '',
|
|
42
|
+
...props
|
|
43
|
+
},
|
|
44
|
+
ref
|
|
45
|
+
) => {
|
|
46
|
+
const [timeLeft, setTimeLeft] = useState<TimeLeft>({ days: 0, hours: 0, minutes: 0, seconds: 0 });
|
|
47
|
+
const [isUrgent, setIsUrgent] = useState(false);
|
|
48
|
+
const completedRef = useRef(false);
|
|
49
|
+
|
|
50
|
+
const defaultLabels = {
|
|
51
|
+
days: labels.days || 'DAYS', hours: labels.hours || 'HOURS',
|
|
52
|
+
minutes: labels.minutes || 'MIN', seconds: labels.seconds || 'SEC',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const calculateTimeLeft = (): TimeLeft => {
|
|
57
|
+
let totalSeconds: number;
|
|
58
|
+
if (target instanceof Date) {
|
|
59
|
+
totalSeconds = Math.max(0, Math.floor((target.getTime() - Date.now()) / 1000));
|
|
60
|
+
} else {
|
|
61
|
+
totalSeconds = Math.max(0, target);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
days: Math.floor(totalSeconds / 86400),
|
|
65
|
+
hours: Math.floor((totalSeconds % 86400) / 3600),
|
|
66
|
+
minutes: Math.floor((totalSeconds % 3600) / 60),
|
|
67
|
+
seconds: totalSeconds % 60,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const tick = () => {
|
|
72
|
+
const newTime = calculateTimeLeft();
|
|
73
|
+
const totalSeconds = newTime.days * 86400 + newTime.hours * 3600 + newTime.minutes * 60 + newTime.seconds;
|
|
74
|
+
setTimeLeft(newTime);
|
|
75
|
+
setIsUrgent(totalSeconds <= urgentThreshold && totalSeconds > 0);
|
|
76
|
+
if (totalSeconds === 0 && !completedRef.current) {
|
|
77
|
+
completedRef.current = true;
|
|
78
|
+
onComplete?.();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
tick();
|
|
83
|
+
const interval = setInterval(tick, 1000);
|
|
84
|
+
return () => clearInterval(interval);
|
|
85
|
+
}, [target, urgentThreshold, onComplete]);
|
|
86
|
+
|
|
87
|
+
const pad = (num: number) => String(num).padStart(2, '0');
|
|
88
|
+
const { value: valueClass, separator: sepClass, label: labelClass } = sizeClasses[size];
|
|
89
|
+
|
|
90
|
+
const getValueStyle = () => {
|
|
91
|
+
switch (variant) {
|
|
92
|
+
case 'neon':
|
|
93
|
+
return { color: accentColor, textShadow: `0 0 10px ${accentColor}, 0 0 20px ${accentColor}, 0 0 40px ${accentColor}` };
|
|
94
|
+
case 'brutal':
|
|
95
|
+
return { background: accentColor, color: '#0a0a0a', padding: '0 0.25em', WebkitTextFillColor: '#0a0a0a' };
|
|
96
|
+
case 'minimal':
|
|
97
|
+
return { color: '#fafafa' };
|
|
98
|
+
default:
|
|
99
|
+
return { background: 'linear-gradient(180deg, #fafafa 0%, #888 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' };
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const renderBlock = (value: number, label: string) => (
|
|
104
|
+
<div className="flex flex-col items-center relative">
|
|
105
|
+
<span
|
|
106
|
+
className={`${valueClass} font-extrabold leading-none tracking-tight ${isUrgent ? 'animate-pulse' : ''}`}
|
|
107
|
+
style={getValueStyle()}
|
|
108
|
+
>
|
|
109
|
+
{pad(value)}
|
|
110
|
+
</span>
|
|
111
|
+
{showLabels && (
|
|
112
|
+
<span className={`${labelClass} tracking-[0.3em] text-gray-600 mt-2 uppercase font-normal`}>
|
|
113
|
+
{label}
|
|
114
|
+
</span>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const renderSeparator = (key: number) => (
|
|
120
|
+
<span
|
|
121
|
+
key={`sep-${key}`}
|
|
122
|
+
className={`${sepClass} font-extrabold text-gray-700 animate-pulse self-start mt-2`}
|
|
123
|
+
style={variant === 'neon' ? { color: accentColor, textShadow: `0 0 10px ${accentColor}` } : undefined}
|
|
124
|
+
>
|
|
125
|
+
:
|
|
126
|
+
</span>
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const blocks = [];
|
|
130
|
+
if (format === 'dhms' || format === 'full') {
|
|
131
|
+
blocks.push(<div key="days">{renderBlock(timeLeft.days, defaultLabels.days)}</div>);
|
|
132
|
+
blocks.push(renderSeparator(1));
|
|
133
|
+
}
|
|
134
|
+
if (format !== 'ms') {
|
|
135
|
+
blocks.push(<div key="hours">{renderBlock(timeLeft.hours, defaultLabels.hours)}</div>);
|
|
136
|
+
blocks.push(renderSeparator(2));
|
|
137
|
+
}
|
|
138
|
+
blocks.push(<div key="minutes">{renderBlock(timeLeft.minutes, defaultLabels.minutes)}</div>);
|
|
139
|
+
blocks.push(renderSeparator(3));
|
|
140
|
+
blocks.push(<div key="seconds">{renderBlock(timeLeft.seconds, defaultLabels.seconds)}</div>);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div
|
|
144
|
+
ref={ref}
|
|
145
|
+
className={`flex items-center justify-center ${compact ? 'gap-2' : 'gap-4 md:gap-8'} font-sans ${className}`}
|
|
146
|
+
{...props}
|
|
147
|
+
>
|
|
148
|
+
{blocks}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
CountdownDisplay.displayName = 'CountdownDisplay';
|
|
155
|
+
export default CountdownDisplay;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: inline-block;
|
|
4
|
+
font-weight: 800;
|
|
5
|
+
line-height: 1;
|
|
6
|
+
letter-spacing: -0.02em;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.layer {
|
|
10
|
+
display: block;
|
|
11
|
+
position: relative;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.layer:not(:first-child) {
|
|
15
|
+
position: absolute;
|
|
16
|
+
top: 0;
|
|
17
|
+
left: 0;
|
|
18
|
+
pointer-events: none;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* Base layer styling */
|
|
22
|
+
.base {
|
|
23
|
+
background: linear-gradient(180deg, #fafafa 0%, #888 100%);
|
|
24
|
+
-webkit-background-clip: text;
|
|
25
|
+
-webkit-text-fill-color: transparent;
|
|
26
|
+
background-clip: text;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Shadow layers */
|
|
30
|
+
.shadow1 {
|
|
31
|
+
transform: translate(4px, 4px);
|
|
32
|
+
z-index: -1;
|
|
33
|
+
opacity: 0.5;
|
|
34
|
+
background: linear-gradient(180deg, #ff0040 0%, #ff004080 100%);
|
|
35
|
+
-webkit-background-clip: text;
|
|
36
|
+
-webkit-text-fill-color: transparent;
|
|
37
|
+
background-clip: text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.shadow2 {
|
|
41
|
+
transform: translate(8px, 8px);
|
|
42
|
+
z-index: -2;
|
|
43
|
+
opacity: 0.3;
|
|
44
|
+
background: linear-gradient(180deg, #ff0040 0%, #ff004040 100%);
|
|
45
|
+
-webkit-background-clip: text;
|
|
46
|
+
-webkit-text-fill-color: transparent;
|
|
47
|
+
background-clip: text;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.shadow3 {
|
|
51
|
+
transform: translate(12px, 12px);
|
|
52
|
+
z-index: -3;
|
|
53
|
+
opacity: 0.15;
|
|
54
|
+
background: linear-gradient(180deg, #ff0040 0%, #ff004020 100%);
|
|
55
|
+
-webkit-background-clip: text;
|
|
56
|
+
-webkit-text-fill-color: transparent;
|
|
57
|
+
background-clip: text;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Variants */
|
|
61
|
+
.blood .shadow1,
|
|
62
|
+
.blood .shadow2,
|
|
63
|
+
.blood .shadow3 {
|
|
64
|
+
background: linear-gradient(180deg, #ff0040 0%, transparent 100%);
|
|
65
|
+
-webkit-background-clip: text;
|
|
66
|
+
background-clip: text;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.cyber .shadow1 { background: linear-gradient(180deg, #00ffff 0%, transparent 100%); -webkit-background-clip: text; background-clip: text; }
|
|
70
|
+
.cyber .shadow2 { background: linear-gradient(180deg, #ff00ff 0%, transparent 100%); -webkit-background-clip: text; background-clip: text; }
|
|
71
|
+
.cyber .shadow3 { background: linear-gradient(180deg, #00ff00 0%, transparent 100%); -webkit-background-clip: text; background-clip: text; }
|
|
72
|
+
|
|
73
|
+
.mono .base,
|
|
74
|
+
.mono .shadow1,
|
|
75
|
+
.mono .shadow2,
|
|
76
|
+
.mono .shadow3 {
|
|
77
|
+
background: none;
|
|
78
|
+
-webkit-text-fill-color: currentColor;
|
|
79
|
+
}
|
|
80
|
+
.mono .shadow1 { color: #666; }
|
|
81
|
+
.mono .shadow2 { color: #444; }
|
|
82
|
+
.mono .shadow3 { color: #222; }
|
|
83
|
+
|
|
84
|
+
.neon .base {
|
|
85
|
+
color: #fff;
|
|
86
|
+
-webkit-text-fill-color: #fff;
|
|
87
|
+
text-shadow: 0 0 10px #ff0040, 0 0 20px #ff0040, 0 0 40px #ff0040;
|
|
88
|
+
}
|
|
89
|
+
.neon .shadow1,
|
|
90
|
+
.neon .shadow2,
|
|
91
|
+
.neon .shadow3 {
|
|
92
|
+
opacity: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Animated variant */
|
|
96
|
+
.animated .shadow1 {
|
|
97
|
+
animation: layerFloat1 3s ease-in-out infinite;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.animated .shadow2 {
|
|
101
|
+
animation: layerFloat2 4s ease-in-out infinite;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.animated .shadow3 {
|
|
105
|
+
animation: layerFloat3 5s ease-in-out infinite;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@keyframes layerFloat1 {
|
|
109
|
+
0%, 100% { transform: translate(4px, 4px); }
|
|
110
|
+
50% { transform: translate(6px, 6px); }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@keyframes layerFloat2 {
|
|
114
|
+
0%, 100% { transform: translate(8px, 8px); }
|
|
115
|
+
50% { transform: translate(12px, 10px); }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@keyframes layerFloat3 {
|
|
119
|
+
0%, 100% { transform: translate(12px, 12px); }
|
|
120
|
+
50% { transform: translate(16px, 14px); }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Hover effect */
|
|
124
|
+
.hover:hover .shadow1 {
|
|
125
|
+
transform: translate(8px, 8px);
|
|
126
|
+
transition: transform 0.3s ease;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.hover:hover .shadow2 {
|
|
130
|
+
transform: translate(16px, 16px);
|
|
131
|
+
transition: transform 0.3s ease;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.hover:hover .shadow3 {
|
|
135
|
+
transform: translate(24px, 24px);
|
|
136
|
+
transition: transform 0.3s ease;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Sizes */
|
|
140
|
+
.sm { font-size: clamp(2rem, 8vw, 4rem); }
|
|
141
|
+
.md { font-size: clamp(4rem, 15vw, 8rem); }
|
|
142
|
+
.lg { font-size: clamp(6rem, 20vw, 12rem); }
|
|
143
|
+
.xl { font-size: clamp(8rem, 25vw, 18rem); }
|
|
144
|
+
|
|
145
|
+
/* Offset directions */
|
|
146
|
+
.diagonal .shadow1 { transform: translate(4px, 4px); }
|
|
147
|
+
.diagonal .shadow2 { transform: translate(8px, 8px); }
|
|
148
|
+
.diagonal .shadow3 { transform: translate(12px, 12px); }
|
|
149
|
+
|
|
150
|
+
.horizontal .shadow1 { transform: translateX(4px); }
|
|
151
|
+
.horizontal .shadow2 { transform: translateX(8px); }
|
|
152
|
+
.horizontal .shadow3 { transform: translateX(12px); }
|
|
153
|
+
|
|
154
|
+
.vertical .shadow1 { transform: translateY(4px); }
|
|
155
|
+
.vertical .shadow2 { transform: translateY(8px); }
|
|
156
|
+
.vertical .shadow3 { transform: translateY(12px); }
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, HTMLAttributes } from 'react';
|
|
4
|
+
import styles from './giant-layers.module.css';
|
|
5
|
+
|
|
6
|
+
export interface GiantLayersProps extends HTMLAttributes<HTMLSpanElement> {
|
|
7
|
+
/** Text to display */
|
|
8
|
+
children: string;
|
|
9
|
+
/** Number of shadow layers (1-3) */
|
|
10
|
+
layers?: 1 | 2 | 3;
|
|
11
|
+
/** Size preset */
|
|
12
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
13
|
+
/** Visual variant */
|
|
14
|
+
variant?: 'blood' | 'cyber' | 'mono' | 'neon';
|
|
15
|
+
/** Shadow offset direction */
|
|
16
|
+
direction?: 'diagonal' | 'horizontal' | 'vertical';
|
|
17
|
+
/** Animate layers */
|
|
18
|
+
animated?: boolean;
|
|
19
|
+
/** Expand on hover */
|
|
20
|
+
hover?: boolean;
|
|
21
|
+
/** Custom layer colors */
|
|
22
|
+
layerColors?: [string, string?, string?];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const GiantLayers = forwardRef<HTMLSpanElement, GiantLayersProps>(
|
|
26
|
+
(
|
|
27
|
+
{
|
|
28
|
+
children,
|
|
29
|
+
layers = 3,
|
|
30
|
+
size = 'lg',
|
|
31
|
+
variant = 'blood',
|
|
32
|
+
direction = 'diagonal',
|
|
33
|
+
animated = false,
|
|
34
|
+
hover = false,
|
|
35
|
+
layerColors,
|
|
36
|
+
className,
|
|
37
|
+
style,
|
|
38
|
+
...props
|
|
39
|
+
},
|
|
40
|
+
ref
|
|
41
|
+
) => {
|
|
42
|
+
const containerClasses = [
|
|
43
|
+
styles.container,
|
|
44
|
+
styles[size],
|
|
45
|
+
styles[variant],
|
|
46
|
+
styles[direction],
|
|
47
|
+
animated && styles.animated,
|
|
48
|
+
hover && styles.hover,
|
|
49
|
+
className
|
|
50
|
+
].filter(Boolean).join(' ');
|
|
51
|
+
|
|
52
|
+
const getLayerStyle = (index: number) => {
|
|
53
|
+
if (layerColors && layerColors[index]) {
|
|
54
|
+
return {
|
|
55
|
+
background: `linear-gradient(180deg, ${layerColors[index]} 0%, transparent 100%)`,
|
|
56
|
+
WebkitBackgroundClip: 'text',
|
|
57
|
+
WebkitTextFillColor: 'transparent',
|
|
58
|
+
backgroundClip: 'text',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<span
|
|
66
|
+
ref={ref}
|
|
67
|
+
className={containerClasses}
|
|
68
|
+
style={style}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{/* Shadow layers */}
|
|
72
|
+
{layers >= 3 && (
|
|
73
|
+
<span className={`${styles.layer} ${styles.shadow3}`} style={getLayerStyle(2)} aria-hidden>
|
|
74
|
+
{children}
|
|
75
|
+
</span>
|
|
76
|
+
)}
|
|
77
|
+
{layers >= 2 && (
|
|
78
|
+
<span className={`${styles.layer} ${styles.shadow2}`} style={getLayerStyle(1)} aria-hidden>
|
|
79
|
+
{children}
|
|
80
|
+
</span>
|
|
81
|
+
)}
|
|
82
|
+
{layers >= 1 && (
|
|
83
|
+
<span className={`${styles.layer} ${styles.shadow1}`} style={getLayerStyle(0)} aria-hidden>
|
|
84
|
+
{children}
|
|
85
|
+
</span>
|
|
86
|
+
)}
|
|
87
|
+
{/* Base layer */}
|
|
88
|
+
<span className={`${styles.layer} ${styles.base}`}>
|
|
89
|
+
{children}
|
|
90
|
+
</span>
|
|
91
|
+
</span>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
GiantLayers.displayName = 'GiantLayers';
|
|
97
|
+
export default GiantLayers;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, HTMLAttributes } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface GiantLayersProps extends HTMLAttributes<HTMLSpanElement> {
|
|
6
|
+
children: string;
|
|
7
|
+
layers?: 1 | 2 | 3;
|
|
8
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
9
|
+
variant?: 'blood' | 'cyber' | 'mono' | 'neon';
|
|
10
|
+
direction?: 'diagonal' | 'horizontal' | 'vertical';
|
|
11
|
+
animated?: boolean;
|
|
12
|
+
hover?: boolean;
|
|
13
|
+
layerColors?: [string, string?, string?];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sizeClasses = {
|
|
17
|
+
sm: 'text-4xl md:text-6xl',
|
|
18
|
+
md: 'text-6xl md:text-8xl',
|
|
19
|
+
lg: 'text-7xl md:text-9xl',
|
|
20
|
+
xl: 'text-8xl md:text-[12rem]',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const variantStyles = {
|
|
24
|
+
blood: {
|
|
25
|
+
base: 'bg-gradient-to-b from-white to-gray-500 bg-clip-text text-transparent',
|
|
26
|
+
shadow: 'text-rose-500',
|
|
27
|
+
},
|
|
28
|
+
cyber: {
|
|
29
|
+
base: 'bg-gradient-to-b from-white to-gray-500 bg-clip-text text-transparent',
|
|
30
|
+
shadow: 'text-cyan-400',
|
|
31
|
+
},
|
|
32
|
+
mono: {
|
|
33
|
+
base: 'text-white',
|
|
34
|
+
shadow: 'text-gray-600',
|
|
35
|
+
},
|
|
36
|
+
neon: {
|
|
37
|
+
base: 'text-white drop-shadow-[0_0_10px_#ff0040] drop-shadow-[0_0_20px_#ff0040]',
|
|
38
|
+
shadow: 'text-transparent',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const getOffset = (direction: string, layer: number) => {
|
|
43
|
+
const px = (layer + 1) * 4;
|
|
44
|
+
switch (direction) {
|
|
45
|
+
case 'horizontal': return { transform: `translateX(${px}px)` };
|
|
46
|
+
case 'vertical': return { transform: `translateY(${px}px)` };
|
|
47
|
+
default: return { transform: `translate(${px}px, ${px}px)` };
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const GiantLayers = forwardRef<HTMLSpanElement, GiantLayersProps>(
|
|
52
|
+
(
|
|
53
|
+
{
|
|
54
|
+
children,
|
|
55
|
+
layers = 3,
|
|
56
|
+
size = 'lg',
|
|
57
|
+
variant = 'blood',
|
|
58
|
+
direction = 'diagonal',
|
|
59
|
+
animated = false,
|
|
60
|
+
hover = false,
|
|
61
|
+
layerColors,
|
|
62
|
+
className = '',
|
|
63
|
+
...props
|
|
64
|
+
},
|
|
65
|
+
ref
|
|
66
|
+
) => {
|
|
67
|
+
const { base, shadow } = variantStyles[variant];
|
|
68
|
+
const opacities = [0.5, 0.3, 0.15];
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<span
|
|
72
|
+
ref={ref}
|
|
73
|
+
className={`relative inline-block font-extrabold leading-none tracking-tight ${sizeClasses[size]} ${
|
|
74
|
+
hover ? 'group' : ''
|
|
75
|
+
} ${className}`}
|
|
76
|
+
{...props}
|
|
77
|
+
>
|
|
78
|
+
{/* Shadow layers */}
|
|
79
|
+
{[...Array(layers)].map((_, i) => {
|
|
80
|
+
const layerIndex = layers - 1 - i;
|
|
81
|
+
const colorStyle = layerColors?.[layerIndex]
|
|
82
|
+
? { color: layerColors[layerIndex] }
|
|
83
|
+
: undefined;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<span
|
|
87
|
+
key={i}
|
|
88
|
+
className={`absolute top-0 left-0 pointer-events-none ${shadow} ${
|
|
89
|
+
animated ? 'animate-pulse' : ''
|
|
90
|
+
} ${hover ? 'transition-transform duration-300 group-hover:translate-x-6 group-hover:translate-y-6' : ''}`}
|
|
91
|
+
style={{
|
|
92
|
+
...getOffset(direction, layerIndex),
|
|
93
|
+
opacity: opacities[layerIndex],
|
|
94
|
+
zIndex: -(layerIndex + 1),
|
|
95
|
+
...colorStyle,
|
|
96
|
+
}}
|
|
97
|
+
aria-hidden
|
|
98
|
+
>
|
|
99
|
+
{children}
|
|
100
|
+
</span>
|
|
101
|
+
);
|
|
102
|
+
})}
|
|
103
|
+
{/* Base layer */}
|
|
104
|
+
<span className={`relative ${base}`}>{children}</span>
|
|
105
|
+
</span>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
GiantLayers.displayName = 'GiantLayers';
|
|
111
|
+
export default GiantLayers;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, HTMLAttributes, useEffect, useState, useRef } from 'react';
|
|
4
|
+
import styles from './reveal-text.module.css';
|
|
5
|
+
|
|
6
|
+
export interface RevealTextProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** Text to reveal */
|
|
8
|
+
children: string;
|
|
9
|
+
/** Split mode */
|
|
10
|
+
splitBy?: 'word' | 'char' | 'line';
|
|
11
|
+
/** Reveal direction */
|
|
12
|
+
direction?: 'fromBottom' | 'fromTop' | 'fromLeft' | 'fromRight';
|
|
13
|
+
/** Additional effect */
|
|
14
|
+
effect?: 'none' | 'blur' | 'scale' | 'rotate';
|
|
15
|
+
/** Stagger delay between elements in ms */
|
|
16
|
+
stagger?: number;
|
|
17
|
+
/** Animation speed */
|
|
18
|
+
speed?: 'fast' | 'normal' | 'slow';
|
|
19
|
+
/** Trigger threshold (0-1) */
|
|
20
|
+
threshold?: number;
|
|
21
|
+
/** Only animate once */
|
|
22
|
+
once?: boolean;
|
|
23
|
+
/** Show highlight underline */
|
|
24
|
+
highlight?: boolean;
|
|
25
|
+
/** Highlight color */
|
|
26
|
+
highlightColor?: string;
|
|
27
|
+
/** Trigger immediately without scroll */
|
|
28
|
+
immediate?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const RevealText = forwardRef<HTMLDivElement, RevealTextProps>(
|
|
32
|
+
(
|
|
33
|
+
{
|
|
34
|
+
children,
|
|
35
|
+
splitBy = 'word',
|
|
36
|
+
direction = 'fromBottom',
|
|
37
|
+
effect = 'none',
|
|
38
|
+
stagger = 50,
|
|
39
|
+
speed = 'normal',
|
|
40
|
+
threshold = 0.2,
|
|
41
|
+
once = true,
|
|
42
|
+
highlight = false,
|
|
43
|
+
highlightColor = '#ff0040',
|
|
44
|
+
immediate = false,
|
|
45
|
+
className,
|
|
46
|
+
style,
|
|
47
|
+
...props
|
|
48
|
+
},
|
|
49
|
+
ref
|
|
50
|
+
) => {
|
|
51
|
+
const [visibleIndices, setVisibleIndices] = useState<Set<number>>(new Set());
|
|
52
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
53
|
+
const hasAnimated = useRef(false);
|
|
54
|
+
|
|
55
|
+
const elements = splitBy === 'line'
|
|
56
|
+
? children.split('\n')
|
|
57
|
+
: splitBy === 'char'
|
|
58
|
+
? children.split('')
|
|
59
|
+
: children.split(' ');
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (immediate) {
|
|
63
|
+
elements.forEach((_, i) => {
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
setVisibleIndices(prev => new Set(prev).add(i));
|
|
66
|
+
}, i * stagger);
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const observer = new IntersectionObserver(
|
|
72
|
+
([entry]) => {
|
|
73
|
+
if (entry.isIntersecting && (!once || !hasAnimated.current)) {
|
|
74
|
+
hasAnimated.current = true;
|
|
75
|
+
elements.forEach((_, i) => {
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
setVisibleIndices(prev => new Set(prev).add(i));
|
|
78
|
+
}, i * stagger);
|
|
79
|
+
});
|
|
80
|
+
} else if (!entry.isIntersecting && !once) {
|
|
81
|
+
setVisibleIndices(new Set());
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{ threshold }
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (containerRef.current) {
|
|
88
|
+
observer.observe(containerRef.current);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return () => observer.disconnect();
|
|
92
|
+
}, [elements, stagger, threshold, once, immediate]);
|
|
93
|
+
|
|
94
|
+
const containerClasses = [
|
|
95
|
+
styles.container,
|
|
96
|
+
styles[speed],
|
|
97
|
+
className
|
|
98
|
+
].filter(Boolean).join(' ');
|
|
99
|
+
|
|
100
|
+
const renderWord = (word: string, index: number) => {
|
|
101
|
+
const isVisible = visibleIndices.has(index);
|
|
102
|
+
const wordClasses = [
|
|
103
|
+
styles.word,
|
|
104
|
+
styles[direction],
|
|
105
|
+
effect !== 'none' && styles[effect],
|
|
106
|
+
isVisible && styles.visible,
|
|
107
|
+
highlight && styles.highlight
|
|
108
|
+
].filter(Boolean).join(' ');
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<span
|
|
112
|
+
key={index}
|
|
113
|
+
className={wordClasses}
|
|
114
|
+
style={{
|
|
115
|
+
transitionDelay: `${index * stagger}ms`,
|
|
116
|
+
'--highlight-color': highlightColor
|
|
117
|
+
} as React.CSSProperties}
|
|
118
|
+
>
|
|
119
|
+
<span className={styles.wordInner}>{word}</span>
|
|
120
|
+
</span>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const renderChar = (char: string, index: number) => {
|
|
125
|
+
const isVisible = visibleIndices.has(index);
|
|
126
|
+
return (
|
|
127
|
+
<span
|
|
128
|
+
key={index}
|
|
129
|
+
className={`${styles.char} ${isVisible ? styles.visible : ''}`}
|
|
130
|
+
style={{ transitionDelay: `${index * stagger}ms` }}
|
|
131
|
+
>
|
|
132
|
+
{char === ' ' ? '\u00A0' : char}
|
|
133
|
+
</span>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const renderLine = (line: string, index: number) => {
|
|
138
|
+
const isVisible = visibleIndices.has(index);
|
|
139
|
+
const lineClasses = [
|
|
140
|
+
styles.line,
|
|
141
|
+
styles[direction],
|
|
142
|
+
isVisible && styles.visible
|
|
143
|
+
].filter(Boolean).join(' ');
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<span
|
|
147
|
+
key={index}
|
|
148
|
+
className={lineClasses}
|
|
149
|
+
style={{ transitionDelay: `${index * stagger}ms` }}
|
|
150
|
+
>
|
|
151
|
+
<span className={styles.lineInner}>{line}</span>
|
|
152
|
+
</span>
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div
|
|
158
|
+
ref={(node) => {
|
|
159
|
+
(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
160
|
+
if (typeof ref === 'function') ref(node);
|
|
161
|
+
else if (ref) ref.current = node;
|
|
162
|
+
}}
|
|
163
|
+
className={containerClasses}
|
|
164
|
+
style={style}
|
|
165
|
+
{...props}
|
|
166
|
+
>
|
|
167
|
+
{elements.map((el, i) =>
|
|
168
|
+
splitBy === 'line'
|
|
169
|
+
? renderLine(el, i)
|
|
170
|
+
: splitBy === 'char'
|
|
171
|
+
? renderChar(el, i)
|
|
172
|
+
: renderWord(el, i)
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
RevealText.displayName = 'RevealText';
|
|
180
|
+
export default RevealText;
|