@donotdev/adv-comps 0.0.2 → 0.0.4
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/dist/components/Crawl/Crawl.d.ts +13 -64
- package/dist/components/Crawl/Crawl.d.ts.map +1 -1
- package/dist/components/Crawl/Crawl.js +18 -220
- package/dist/components/Crawl/CrawlContent.d.ts +32 -0
- package/dist/components/Crawl/CrawlContent.d.ts.map +1 -0
- package/dist/components/Crawl/CrawlContent.js +118 -0
- package/dist/components/Roadmap/Roadmap.css +248 -0
- package/dist/components/Roadmap/Roadmap.d.ts +15 -0
- package/dist/components/Roadmap/Roadmap.d.ts.map +1 -0
- package/dist/components/Roadmap/Roadmap.js +16 -0
- package/dist/components/Roadmap/RoadmapContent.d.ts +28 -0
- package/dist/components/Roadmap/RoadmapContent.d.ts.map +1 -0
- package/dist/components/Roadmap/RoadmapContent.js +47 -0
- package/dist/components/Roadmap/index.d.ts +3 -0
- package/dist/components/Roadmap/index.d.ts.map +1 -0
- package/dist/components/Roadmap/index.js +2 -0
- package/dist/components/StackedCards/StackedCards.css +10 -18
- package/dist/components/StackedCards/StackedCardsContent.d.ts +8 -0
- package/dist/components/StackedCards/StackedCardsContent.d.ts.map +1 -1
- package/dist/components/StackedCards/StackedCardsContent.js +10 -21
- package/dist/index.css +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/package.json +2 -2
|
@@ -1,34 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Crawl Component
|
|
3
|
-
* @description Cinematic 3D text crawl
|
|
2
|
+
* @fileoverview Crawl Component - Thin wrapper with lazy loading
|
|
3
|
+
* @description Cinematic 3D text crawl. Intro renders immediately (eager),
|
|
4
|
+
* heavy 3D animation loads lazily after user clicks play.
|
|
4
5
|
*
|
|
5
|
-
* @
|
|
6
|
-
* ```tsx
|
|
7
|
-
* <Crawl
|
|
8
|
-
* intro="Lorem ipsum dolor sit amet"
|
|
9
|
-
* title="LOREM IPSUM"
|
|
10
|
-
* content={[
|
|
11
|
-
* "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
|
12
|
-
* "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
|
13
|
-
* ]}
|
|
14
|
-
* />
|
|
15
|
-
* ```
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```tsx
|
|
19
|
-
* <Crawl
|
|
20
|
-
* title="Lorem Ipsum"
|
|
21
|
-
* content="First paragraph.\n\nSecond paragraph with <strong>bold</strong> text."
|
|
22
|
-
* duration={30}
|
|
23
|
-
* contentHeight="200vh"
|
|
24
|
-
* />
|
|
25
|
-
* ```
|
|
26
|
-
*
|
|
27
|
-
* @version 0.0.1
|
|
6
|
+
* @version 0.0.2
|
|
28
7
|
* @since 0.0.1
|
|
29
8
|
* @author AMBROISE PARK Consulting
|
|
30
9
|
*/
|
|
31
|
-
import { type ReactNode, type
|
|
10
|
+
import { type ReactNode, type CSSProperties } from 'react';
|
|
32
11
|
export interface CrawlProps {
|
|
33
12
|
/** Optional intro text displayed with play button overlay. Accepts ReactNode or string. */
|
|
34
13
|
intro?: ReactNode | string;
|
|
@@ -50,46 +29,16 @@ export interface CrawlProps {
|
|
|
50
29
|
'aria-label'?: string;
|
|
51
30
|
}
|
|
52
31
|
/**
|
|
53
|
-
* Cinematic 3D text crawl component with
|
|
54
|
-
*
|
|
55
|
-
* ## Features
|
|
56
|
-
* - **Lighthouse-optimized**: Intro content immediately visible for LCP measurement
|
|
57
|
-
* - **User-controlled**: Play button overlay for optional animation experience
|
|
58
|
-
* - **Zero memory usage**: Animation only starts when user clicks play
|
|
59
|
-
* - **Responsive**: Title wraps on mobile/tablet, stays on one line on desktop
|
|
60
|
-
* - **Accessible**: Respects `prefers-reduced-motion` preference
|
|
61
|
-
* - **SSR-safe**: Handles server-side rendering gracefully
|
|
32
|
+
* Cinematic 3D text crawl component with lazy loading.
|
|
62
33
|
*
|
|
63
|
-
* ##
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* 4. Animation loops infinitely until user navigates away
|
|
34
|
+
* ## Performance
|
|
35
|
+
* - Intro overlay renders IMMEDIATELY (LCP-optimized)
|
|
36
|
+
* - Heavy 3D animation lazy loads after user clicks play
|
|
37
|
+
* - Zero initial bundle impact for animation code
|
|
68
38
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* - `--crawl-title-size`: Title font size (responsive: 3rem → 4.5rem → 8rem)
|
|
72
|
-
* - `--crawl-body-size`: Content font size (responsive: 1.6rem → 2.5rem → 3rem)
|
|
73
|
-
* - `--crawl-text-color`: Text color (default: #ffc909)
|
|
74
|
-
* - `--crawl-intro-color`: Intro text color (default: #4a9eff)
|
|
75
|
-
* - `--crawl-perspective`: 3D perspective distance (default: 600px)
|
|
76
|
-
* - `--crawl-tilt`: 3D tilt angle (default: 40deg)
|
|
77
|
-
*
|
|
78
|
-
* ## Content Parsing
|
|
79
|
-
* When strings are provided for `intro`, `title`, or `content`:
|
|
80
|
-
* - Blank lines (`\n\n`) split content into paragraphs
|
|
81
|
-
* - Limited HTML tags are parsed: `u`, `br`, `i`, `em`, `strong`, `b`
|
|
82
|
-
* - All other HTML is escaped for security
|
|
83
|
-
*
|
|
84
|
-
* @example
|
|
85
|
-
* ```tsx
|
|
86
|
-
* <Crawl
|
|
87
|
-
* intro="Lorem ipsum dolor sit amet"
|
|
88
|
-
* title="Lorem Ipsum"
|
|
89
|
-
* content="First paragraph.\n\nSecond paragraph with <strong>bold</strong> text."
|
|
90
|
-
* />
|
|
91
|
-
* ```
|
|
39
|
+
* @version 0.0.2
|
|
40
|
+
* @since 0.0.1
|
|
92
41
|
*/
|
|
93
|
-
declare const Crawl:
|
|
42
|
+
declare const Crawl: ({ content, intro, title, duration, contentHeight, tiltAngle, className, style, "aria-label": ariaLabel, }: CrawlProps) => import("react/jsx-runtime").JSX.Element;
|
|
94
43
|
export default Crawl;
|
|
95
44
|
//# sourceMappingURL=Crawl.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Crawl.d.ts","sourceRoot":"","sources":["../../../src/components/Crawl/Crawl.tsx"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"Crawl.d.ts","sourceRoot":"","sources":["../../../src/components/Crawl/Crawl.tsx"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,OAAO,EAKL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,OAAO,CAAC;AASf,MAAM,WAAW,UAAU;IACzB,2FAA2F;IAC3F,KAAK,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAC3B,sFAAsF;IACtF,KAAK,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAC3B,oFAAoF;IACpF,OAAO,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,EAAE,CAAC;IACvC,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,mCAAmC;IACnC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;GAUG;AACH,QAAA,MAAM,KAAK,GAAI,2GAUZ,UAAU,4CA+GZ,CAAC;AAEF,eAAe,KAAK,CAAC"}
|
|
@@ -1,95 +1,33 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// packages/adv-comps/src/components/Crawl/Crawl.tsx
|
|
3
3
|
/**
|
|
4
|
-
* @fileoverview Crawl Component
|
|
5
|
-
* @description Cinematic 3D text crawl
|
|
4
|
+
* @fileoverview Crawl Component - Thin wrapper with lazy loading
|
|
5
|
+
* @description Cinematic 3D text crawl. Intro renders immediately (eager),
|
|
6
|
+
* heavy 3D animation loads lazily after user clicks play.
|
|
6
7
|
*
|
|
7
|
-
* @
|
|
8
|
-
* ```tsx
|
|
9
|
-
* <Crawl
|
|
10
|
-
* intro="Lorem ipsum dolor sit amet"
|
|
11
|
-
* title="LOREM IPSUM"
|
|
12
|
-
* content={[
|
|
13
|
-
* "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
|
14
|
-
* "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
|
15
|
-
* ]}
|
|
16
|
-
* />
|
|
17
|
-
* ```
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* ```tsx
|
|
21
|
-
* <Crawl
|
|
22
|
-
* title="Lorem Ipsum"
|
|
23
|
-
* content="First paragraph.\n\nSecond paragraph with <strong>bold</strong> text."
|
|
24
|
-
* duration={30}
|
|
25
|
-
* contentHeight="200vh"
|
|
26
|
-
* />
|
|
27
|
-
* ```
|
|
28
|
-
*
|
|
29
|
-
* @version 0.0.1
|
|
8
|
+
* @version 0.0.2
|
|
30
9
|
* @since 0.0.1
|
|
31
10
|
* @author AMBROISE PARK Consulting
|
|
32
11
|
*/
|
|
33
|
-
import {
|
|
12
|
+
import { useState, useMemo, lazy, Suspense, } from 'react';
|
|
34
13
|
import { cn } from '@donotdev/components';
|
|
35
|
-
import { isClient } from '@donotdev/core';
|
|
36
14
|
import { parseHtml } from '../../utils/parseHtml';
|
|
15
|
+
// Lazy load heavy 3D animation content
|
|
16
|
+
const CrawlContent = lazy(() => import('./CrawlContent'));
|
|
37
17
|
/**
|
|
38
|
-
* Cinematic 3D text crawl component with
|
|
39
|
-
*
|
|
40
|
-
* ## Features
|
|
41
|
-
* - **Lighthouse-optimized**: Intro content immediately visible for LCP measurement
|
|
42
|
-
* - **User-controlled**: Play button overlay for optional animation experience
|
|
43
|
-
* - **Zero memory usage**: Animation only starts when user clicks play
|
|
44
|
-
* - **Responsive**: Title wraps on mobile/tablet, stays on one line on desktop
|
|
45
|
-
* - **Accessible**: Respects `prefers-reduced-motion` preference
|
|
46
|
-
* - **SSR-safe**: Handles server-side rendering gracefully
|
|
18
|
+
* Cinematic 3D text crawl component with lazy loading.
|
|
47
19
|
*
|
|
48
|
-
* ##
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* 4. Animation loops infinitely until user navigates away
|
|
20
|
+
* ## Performance
|
|
21
|
+
* - Intro overlay renders IMMEDIATELY (LCP-optimized)
|
|
22
|
+
* - Heavy 3D animation lazy loads after user clicks play
|
|
23
|
+
* - Zero initial bundle impact for animation code
|
|
53
24
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* - `--crawl-title-size`: Title font size (responsive: 3rem → 4.5rem → 8rem)
|
|
57
|
-
* - `--crawl-body-size`: Content font size (responsive: 1.6rem → 2.5rem → 3rem)
|
|
58
|
-
* - `--crawl-text-color`: Text color (default: #ffc909)
|
|
59
|
-
* - `--crawl-intro-color`: Intro text color (default: #4a9eff)
|
|
60
|
-
* - `--crawl-perspective`: 3D perspective distance (default: 600px)
|
|
61
|
-
* - `--crawl-tilt`: 3D tilt angle (default: 40deg)
|
|
62
|
-
*
|
|
63
|
-
* ## Content Parsing
|
|
64
|
-
* When strings are provided for `intro`, `title`, or `content`:
|
|
65
|
-
* - Blank lines (`\n\n`) split content into paragraphs
|
|
66
|
-
* - Limited HTML tags are parsed: `u`, `br`, `i`, `em`, `strong`, `b`
|
|
67
|
-
* - All other HTML is escaped for security
|
|
68
|
-
*
|
|
69
|
-
* @example
|
|
70
|
-
* ```tsx
|
|
71
|
-
* <Crawl
|
|
72
|
-
* intro="Lorem ipsum dolor sit amet"
|
|
73
|
-
* title="Lorem Ipsum"
|
|
74
|
-
* content="First paragraph.\n\nSecond paragraph with <strong>bold</strong> text."
|
|
75
|
-
* />
|
|
76
|
-
* ```
|
|
25
|
+
* @version 0.0.2
|
|
26
|
+
* @since 0.0.1
|
|
77
27
|
*/
|
|
78
28
|
const Crawl = ({ content, intro, title, duration = 26, contentHeight = '150vh', tiltAngle, className, style, 'aria-label': ariaLabel, }) => {
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
const renderStringAsParagraphs = (value) => {
|
|
82
|
-
if (!value || typeof value !== 'string')
|
|
83
|
-
return null;
|
|
84
|
-
// Split by blank lines to form paragraphs
|
|
85
|
-
const blocks = value
|
|
86
|
-
.split(/\n\s*\n/) // blank line delimiter
|
|
87
|
-
.map((block) => block.trim())
|
|
88
|
-
.filter((block) => block.length > 0);
|
|
89
|
-
return blocks.map((block, idx) => (_jsx("p", { children: parseHtml(block, {
|
|
90
|
-
allowedTags: ['u', 'br', 'i', 'em', 'strong', 'b'],
|
|
91
|
-
}) }, `p-${idx}`)));
|
|
92
|
-
};
|
|
29
|
+
const [showPlayOverlay, setShowPlayOverlay] = useState(true);
|
|
30
|
+
// Parse intro immediately (lightweight)
|
|
93
31
|
const resolvedIntro = useMemo(() => {
|
|
94
32
|
if (intro == null)
|
|
95
33
|
return null;
|
|
@@ -97,110 +35,10 @@ const Crawl = ({ content, intro, title, duration = 26, contentHeight = '150vh',
|
|
|
97
35
|
allowedTags: ['u', 'br', 'i', 'em', 'strong', 'b'],
|
|
98
36
|
}) })) : (intro);
|
|
99
37
|
}, [intro]);
|
|
100
|
-
const resolvedTitle = useMemo(() => {
|
|
101
|
-
if (!title)
|
|
102
|
-
return null;
|
|
103
|
-
return typeof title === 'string' ? (_jsx("h2", { className: "dndev-crawl-title", children: parseHtml(title, {
|
|
104
|
-
allowedTags: ['u', 'br', 'i', 'em', 'strong', 'b'],
|
|
105
|
-
}) })) : (title);
|
|
106
|
-
}, [title]);
|
|
107
|
-
const resolvedContent = useMemo(() => {
|
|
108
|
-
if (isValidElement(content))
|
|
109
|
-
return content;
|
|
110
|
-
if (Array.isArray(content)) {
|
|
111
|
-
// string[] → each item becomes a paragraph
|
|
112
|
-
const items = content
|
|
113
|
-
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
114
|
-
.filter((s) => s && s.length > 0);
|
|
115
|
-
return items.map((item, idx) => (_jsx("p", { children: parseHtml(item, {
|
|
116
|
-
allowedTags: ['u', 'br', 'i', 'em', 'strong', 'b'],
|
|
117
|
-
}) }, `c-${idx}`)));
|
|
118
|
-
}
|
|
119
|
-
if (typeof content === 'string') {
|
|
120
|
-
return renderStringAsParagraphs(content);
|
|
121
|
-
}
|
|
122
|
-
return content;
|
|
123
|
-
}, [content]);
|
|
124
|
-
const textRef = useRef(null);
|
|
125
|
-
const containerRef = useRef(null);
|
|
126
|
-
const [showPlayOverlay, setShowPlayOverlay] = useState(false);
|
|
127
|
-
const [isAnimationActive, setIsAnimationActive] = useState(false);
|
|
128
|
-
const [isPaused, setIsPaused] = useState(false);
|
|
129
|
-
const [isHydrated, setIsHydrated] = useState(false);
|
|
130
|
-
// SSR-safe hydration
|
|
131
|
-
useEffect(() => {
|
|
132
|
-
setIsHydrated(true);
|
|
133
|
-
// Always show play overlay (LCP) - intro is optional
|
|
134
|
-
setShowPlayOverlay(true);
|
|
135
|
-
}, []);
|
|
136
|
-
// Handle play button click
|
|
137
38
|
const handlePlayClick = () => {
|
|
138
39
|
setShowPlayOverlay(false);
|
|
139
|
-
setIsAnimationActive(true);
|
|
140
|
-
setIsPaused(false);
|
|
141
|
-
};
|
|
142
|
-
// Handle container click (pause when animation is active)
|
|
143
|
-
const handleContainerClick = (e) => {
|
|
144
|
-
if (isAnimationActive && !isPaused && !showPlayOverlay) {
|
|
145
|
-
setIsPaused(true);
|
|
146
|
-
}
|
|
147
40
|
};
|
|
148
|
-
|
|
149
|
-
const handlePauseClick = (e) => {
|
|
150
|
-
e.stopPropagation();
|
|
151
|
-
setIsPaused(false);
|
|
152
|
-
};
|
|
153
|
-
// Animation style - only active when user requests it
|
|
154
|
-
const animationStyle = useMemo(() => {
|
|
155
|
-
const style = {
|
|
156
|
-
'--crawl-content-height': contentHeight,
|
|
157
|
-
animationName: isAnimationActive ? 'dndev-crawl-animation' : 'none',
|
|
158
|
-
animationDuration: `${duration}s`,
|
|
159
|
-
animationTimingFunction: 'linear',
|
|
160
|
-
animationIterationCount: 'infinite',
|
|
161
|
-
animationFillMode: 'forwards',
|
|
162
|
-
animationPlayState: isPaused ? 'paused' : 'running',
|
|
163
|
-
};
|
|
164
|
-
// Only override CSS default if tiltAngle is explicitly provided
|
|
165
|
-
if (tiltAngle !== undefined) {
|
|
166
|
-
style['--crawl-tilt'] = `${tiltAngle}deg`;
|
|
167
|
-
}
|
|
168
|
-
return style;
|
|
169
|
-
}, [isAnimationActive, isPaused, duration, contentHeight, tiltAngle]);
|
|
170
|
-
// Handle reduced motion preference
|
|
171
|
-
useEffect(() => {
|
|
172
|
-
if (!isHydrated || !isClient() || !textRef.current)
|
|
173
|
-
return;
|
|
174
|
-
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
175
|
-
const handleChange = () => {
|
|
176
|
-
if (mediaQuery.matches && textRef.current) {
|
|
177
|
-
textRef.current.classList.add('dndev-crawl-reduced-motion');
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
handleChange(); // Check initial state
|
|
181
|
-
mediaQuery.addEventListener('change', handleChange);
|
|
182
|
-
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
183
|
-
}, [isHydrated]);
|
|
184
|
-
// Intersection Observer - auto-pause when scrolled out of view
|
|
185
|
-
useEffect(() => {
|
|
186
|
-
if (!isHydrated ||
|
|
187
|
-
!isClient() ||
|
|
188
|
-
!isAnimationActive ||
|
|
189
|
-
!containerRef.current)
|
|
190
|
-
return;
|
|
191
|
-
const observer = new IntersectionObserver((entries) => {
|
|
192
|
-
const isVisible = entries[0]?.isIntersecting ?? true;
|
|
193
|
-
if (!isVisible) {
|
|
194
|
-
setIsPaused(true);
|
|
195
|
-
}
|
|
196
|
-
}, { threshold: 0.1 });
|
|
197
|
-
observer.observe(containerRef.current);
|
|
198
|
-
return () => observer.disconnect();
|
|
199
|
-
}, [isHydrated, isAnimationActive]);
|
|
200
|
-
return (_jsxs("div", { ref: containerRef, className: cn('dndev-crawl-container', 'dndev-relative dndev-overflow-hidden', className), style: { ...style, height: 'calc(100dvh - var(--header-height))' }, onClick: handleContainerClick, "aria-label": ariaLabel || 'Animated text crawl', role: "region", "aria-live": "polite", children: [!showPlayOverlay && (_jsx("div", { className: "dndev-crawl-3d-container", children: _jsx("div", { ref: textRef, className: "dndev-crawl-text", style: {
|
|
201
|
-
...animationStyle,
|
|
202
|
-
willChange: isAnimationActive && !isPaused ? 'transform' : 'auto',
|
|
203
|
-
}, children: _jsxs("div", { className: "dndev-crawl-content", children: [resolvedTitle, resolvedContent] }) }) })), showPlayOverlay && isHydrated && (_jsxs("div", { className: "dndev-absolute dndev-inset-0 dndev-flex dndev-flex-col dndev-items-center dndev-justify-center", style: {
|
|
41
|
+
return (_jsxs("div", { className: cn('dndev-crawl-container', 'dndev-relative dndev-overflow-hidden', className), style: { ...style, height: 'calc(100dvh - var(--header-height))' }, "aria-label": ariaLabel || 'Animated text crawl', role: "region", "aria-live": "polite", children: [showPlayOverlay && (_jsxs("div", { className: "dndev-absolute dndev-inset-0 dndev-flex dndev-flex-col dndev-items-center dndev-justify-center", style: {
|
|
204
42
|
zIndex: 30,
|
|
205
43
|
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
|
206
44
|
backdropFilter: 'blur(4px)',
|
|
@@ -219,7 +57,6 @@ const Crawl = ({ content, intro, title, duration = 26, contentHeight = '150vh',
|
|
|
219
57
|
backdropFilter: 'blur(4px)',
|
|
220
58
|
cursor: 'pointer',
|
|
221
59
|
transition: 'all var(--dur-normal) var(--ease-in-out)',
|
|
222
|
-
transformOrigin: 'center',
|
|
223
60
|
}, onMouseEnter: (e) => {
|
|
224
61
|
e.currentTarget.style.backgroundColor =
|
|
225
62
|
'rgba(255, 255, 255, 0.3)';
|
|
@@ -230,45 +67,6 @@ const Crawl = ({ content, intro, title, duration = 26, contentHeight = '150vh',
|
|
|
230
67
|
'rgba(255, 255, 255, 0.2)';
|
|
231
68
|
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.4)';
|
|
232
69
|
e.currentTarget.style.transform = 'scale(1)';
|
|
233
|
-
}, "aria-label": "Start animated introduction", children: _jsx("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "white", style: { marginInlineStart: '4px' }, children: _jsx("path", { d: "M8 5v14l11-7z" }) }) })] })),
|
|
234
|
-
zIndex: 30,
|
|
235
|
-
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
236
|
-
backdropFilter: 'blur(2px)',
|
|
237
|
-
}, onClick: handlePauseClick, children: _jsx("button", { style: {
|
|
238
|
-
width: '96px',
|
|
239
|
-
height: '96px',
|
|
240
|
-
borderRadius: '50%',
|
|
241
|
-
display: 'flex',
|
|
242
|
-
alignItems: 'center',
|
|
243
|
-
justifyContent: 'center',
|
|
244
|
-
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
|
245
|
-
border: '4px solid rgba(255, 255, 255, 0.4)',
|
|
246
|
-
backdropFilter: 'blur(4px)',
|
|
247
|
-
cursor: 'pointer',
|
|
248
|
-
transition: 'all var(--dur-normal) var(--ease-in-out)',
|
|
249
|
-
transformOrigin: 'center',
|
|
250
|
-
}, onMouseEnter: (e) => {
|
|
251
|
-
e.currentTarget.style.backgroundColor =
|
|
252
|
-
'rgba(255, 255, 255, 0.3)';
|
|
253
|
-
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.6)';
|
|
254
|
-
e.currentTarget.style.transform = 'scale(1.1)';
|
|
255
|
-
}, onMouseLeave: (e) => {
|
|
256
|
-
e.currentTarget.style.backgroundColor =
|
|
257
|
-
'rgba(255, 255, 255, 0.2)';
|
|
258
|
-
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.4)';
|
|
259
|
-
e.currentTarget.style.transform = 'scale(1)';
|
|
260
|
-
}, "aria-label": "Resume animation", children: _jsx("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "white", children: _jsx("path", { d: "M6 4h4v16H6V4zm8 0h4v16h-4V4z" }) }) }) })), isAnimationActive && (_jsxs("div", { className: "dndev-crawl-stars", children: [_jsx("div", { className: "dndev-crawl-stars-layer-1" }), _jsx("div", { className: "dndev-crawl-stars-layer-2" }), _jsx("div", { className: "dndev-crawl-stars-layer-3" })] })), !isHydrated && (_jsxs("div", { className: "dndev-absolute dndev-inset-0 dndev-flex dndev-flex-col dndev-items-center dndev-justify-center", style: { zIndex: 20, backgroundColor: 'rgba(0, 0, 0, 0.5)' }, children: [resolvedIntro && (_jsx("div", { className: "dndev-w-full", style: {
|
|
261
|
-
paddingInline: 'var(--content-padding)',
|
|
262
|
-
marginBottom: '3rem',
|
|
263
|
-
}, children: _jsx("div", { className: "dndev-crawl-intro-content", children: resolvedIntro }) })), _jsx("div", { style: {
|
|
264
|
-
width: '96px',
|
|
265
|
-
height: '96px',
|
|
266
|
-
borderRadius: '50%',
|
|
267
|
-
display: 'flex',
|
|
268
|
-
alignItems: 'center',
|
|
269
|
-
justifyContent: 'center',
|
|
270
|
-
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
|
271
|
-
border: '4px solid rgba(255, 255, 255, 0.4)',
|
|
272
|
-
}, children: _jsx("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "white", style: { marginInlineStart: '4px' }, children: _jsx("path", { d: "M8 5v14l11-7z" }) }) })] }))] }));
|
|
70
|
+
}, "aria-label": "Start animated introduction", children: _jsx("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "white", style: { marginInlineStart: '4px' }, children: _jsx("path", { d: "M8 5v14l11-7z" }) }) })] })), !showPlayOverlay && (_jsx(Suspense, { fallback: null, children: _jsx(CrawlContent, { title: title, content: content, duration: duration, contentHeight: contentHeight, tiltAngle: tiltAngle }) }))] }));
|
|
273
71
|
};
|
|
274
72
|
export default Crawl;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CrawlContent - Heavy 3D animation content (lazy loaded)
|
|
3
|
+
* @description Contains the 3D perspective animation, starfield, and text crawl.
|
|
4
|
+
* Lazy loaded after user clicks play to keep initial bundle small.
|
|
5
|
+
*
|
|
6
|
+
* @version 0.0.1
|
|
7
|
+
* @since 0.0.1
|
|
8
|
+
* @author AMBROISE PARK Consulting
|
|
9
|
+
*/
|
|
10
|
+
import { type ReactNode } from 'react';
|
|
11
|
+
export interface CrawlContentProps {
|
|
12
|
+
/** Optional title rendered at the start of the crawl */
|
|
13
|
+
title?: ReactNode | string;
|
|
14
|
+
/** Main crawl content */
|
|
15
|
+
content: ReactNode | string | string[];
|
|
16
|
+
/** Duration of the crawl animation in seconds */
|
|
17
|
+
duration?: number;
|
|
18
|
+
/** Height of the content area in viewport units */
|
|
19
|
+
contentHeight?: string;
|
|
20
|
+
/** Tilt angle for the 3D perspective effect */
|
|
21
|
+
tiltAngle?: number;
|
|
22
|
+
/** Callback when animation is paused */
|
|
23
|
+
onPause?: () => void;
|
|
24
|
+
/** Callback when animation resumes */
|
|
25
|
+
onResume?: () => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Heavy 3D crawl animation content - lazy loaded
|
|
29
|
+
*/
|
|
30
|
+
declare const CrawlContent: ({ content, title, duration, contentHeight, tiltAngle, onPause, onResume, }: CrawlContentProps) => import("react/jsx-runtime").JSX.Element;
|
|
31
|
+
export default CrawlContent;
|
|
32
|
+
//# sourceMappingURL=CrawlContent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CrawlContent.d.ts","sourceRoot":"","sources":["../../../src/components/Crawl/CrawlContent.tsx"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,OAAO,EAKL,KAAK,SAAS,EAGf,MAAM,OAAO,CAAC;AAMf,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,KAAK,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAC3B,yBAAyB;IACzB,OAAO,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,EAAE,CAAC;IACvC,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB;AAED;;GAEG;AACH,QAAA,MAAM,YAAY,GAAI,4EAQnB,iBAAiB,4CAgKnB,CAAC;AAEF,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
// packages/adv-comps/src/components/Crawl/CrawlContent.tsx
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview CrawlContent - Heavy 3D animation content (lazy loaded)
|
|
5
|
+
* @description Contains the 3D perspective animation, starfield, and text crawl.
|
|
6
|
+
* Lazy loaded after user clicks play to keep initial bundle small.
|
|
7
|
+
*
|
|
8
|
+
* @version 0.0.1
|
|
9
|
+
* @since 0.0.1
|
|
10
|
+
* @author AMBROISE PARK Consulting
|
|
11
|
+
*/
|
|
12
|
+
import { useEffect, useRef, useState, useMemo, isValidElement, } from 'react';
|
|
13
|
+
import { isClient } from '@donotdev/core';
|
|
14
|
+
import { parseHtml } from '../../utils/parseHtml';
|
|
15
|
+
/**
|
|
16
|
+
* Heavy 3D crawl animation content - lazy loaded
|
|
17
|
+
*/
|
|
18
|
+
const CrawlContent = ({ content, title, duration = 26, contentHeight = '150vh', tiltAngle, onPause, onResume, }) => {
|
|
19
|
+
const textRef = useRef(null);
|
|
20
|
+
const [isPaused, setIsPaused] = useState(false);
|
|
21
|
+
// Normalize title/content to ReactNodes
|
|
22
|
+
const renderStringAsParagraphs = (value) => {
|
|
23
|
+
if (!value || typeof value !== 'string')
|
|
24
|
+
return null;
|
|
25
|
+
const blocks = value
|
|
26
|
+
.split(/\n\s*\n/)
|
|
27
|
+
.map((block) => block.trim())
|
|
28
|
+
.filter((block) => block.length > 0);
|
|
29
|
+
return blocks.map((block, idx) => (_jsx("p", { children: parseHtml(block, {
|
|
30
|
+
allowedTags: ['u', 'br', 'i', 'em', 'strong', 'b'],
|
|
31
|
+
}) }, `p-${idx}`)));
|
|
32
|
+
};
|
|
33
|
+
const resolvedTitle = useMemo(() => {
|
|
34
|
+
if (!title)
|
|
35
|
+
return null;
|
|
36
|
+
return typeof title === 'string' ? (_jsx("h2", { className: "dndev-crawl-title", children: parseHtml(title, {
|
|
37
|
+
allowedTags: ['u', 'br', 'i', 'em', 'strong', 'b'],
|
|
38
|
+
}) })) : (title);
|
|
39
|
+
}, [title]);
|
|
40
|
+
const resolvedContent = useMemo(() => {
|
|
41
|
+
if (isValidElement(content))
|
|
42
|
+
return content;
|
|
43
|
+
if (Array.isArray(content)) {
|
|
44
|
+
const items = content
|
|
45
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
46
|
+
.filter((s) => s && s.length > 0);
|
|
47
|
+
return items.map((item, idx) => (_jsx("p", { children: parseHtml(item, {
|
|
48
|
+
allowedTags: ['u', 'br', 'i', 'em', 'strong', 'b'],
|
|
49
|
+
}) }, `c-${idx}`)));
|
|
50
|
+
}
|
|
51
|
+
if (typeof content === 'string') {
|
|
52
|
+
return renderStringAsParagraphs(content);
|
|
53
|
+
}
|
|
54
|
+
return content;
|
|
55
|
+
}, [content]);
|
|
56
|
+
// Animation style
|
|
57
|
+
const animationStyle = useMemo(() => {
|
|
58
|
+
const style = {
|
|
59
|
+
'--crawl-content-height': contentHeight,
|
|
60
|
+
animationName: 'dndev-crawl-animation',
|
|
61
|
+
animationDuration: `${duration}s`,
|
|
62
|
+
animationTimingFunction: 'linear',
|
|
63
|
+
animationIterationCount: 'infinite',
|
|
64
|
+
animationFillMode: 'forwards',
|
|
65
|
+
animationPlayState: isPaused ? 'paused' : 'running',
|
|
66
|
+
};
|
|
67
|
+
if (tiltAngle !== undefined) {
|
|
68
|
+
style['--crawl-tilt'] = `${tiltAngle}deg`;
|
|
69
|
+
}
|
|
70
|
+
return style;
|
|
71
|
+
}, [isPaused, duration, contentHeight, tiltAngle]);
|
|
72
|
+
// Handle reduced motion preference
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!isClient() || !textRef.current)
|
|
75
|
+
return;
|
|
76
|
+
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
77
|
+
const handleChange = () => {
|
|
78
|
+
if (mediaQuery.matches && textRef.current) {
|
|
79
|
+
textRef.current.classList.add('dndev-crawl-reduced-motion');
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
handleChange();
|
|
83
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
84
|
+
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
85
|
+
}, []);
|
|
86
|
+
// Handle click to pause
|
|
87
|
+
const handleClick = () => {
|
|
88
|
+
if (isPaused) {
|
|
89
|
+
setIsPaused(false);
|
|
90
|
+
onResume?.();
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
setIsPaused(true);
|
|
94
|
+
onPause?.();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "dndev-crawl-3d-container", onClick: handleClick, children: _jsx("div", { ref: textRef, className: "dndev-crawl-text", style: {
|
|
98
|
+
...animationStyle,
|
|
99
|
+
willChange: !isPaused ? 'transform' : 'auto',
|
|
100
|
+
}, children: _jsxs("div", { className: "dndev-crawl-content", children: [resolvedTitle, resolvedContent] }) }) }), isPaused && (_jsx("div", { className: "dndev-absolute dndev-inset-0 dndev-flex dndev-items-center dndev-justify-center", style: {
|
|
101
|
+
zIndex: 30,
|
|
102
|
+
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
103
|
+
backdropFilter: 'blur(2px)',
|
|
104
|
+
}, onClick: handleClick, children: _jsx("button", { style: {
|
|
105
|
+
width: '96px',
|
|
106
|
+
height: '96px',
|
|
107
|
+
borderRadius: '50%',
|
|
108
|
+
display: 'flex',
|
|
109
|
+
alignItems: 'center',
|
|
110
|
+
justifyContent: 'center',
|
|
111
|
+
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
|
112
|
+
border: '4px solid rgba(255, 255, 255, 0.4)',
|
|
113
|
+
backdropFilter: 'blur(4px)',
|
|
114
|
+
cursor: 'pointer',
|
|
115
|
+
transition: 'all var(--dur-normal) var(--ease-in-out)',
|
|
116
|
+
}, "aria-label": "Resume animation", children: _jsx("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "white", children: _jsx("path", { d: "M6 4h4v16H6V4zm8 0h4v16h-4V4z" }) }) }) })), _jsxs("div", { className: "dndev-crawl-stars", children: [_jsx("div", { className: "dndev-crawl-stars-layer-1" }), _jsx("div", { className: "dndev-crawl-stars-layer-2" }), _jsx("div", { className: "dndev-crawl-stars-layer-3" })] })] }));
|
|
117
|
+
};
|
|
118
|
+
export default CrawlContent;
|