@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.
@@ -1,34 +1,13 @@
1
1
  /**
2
- * @fileoverview Crawl Component
3
- * @description Cinematic 3D text crawl animation component with starfield background, customizable intro text, title, and content sections. Optimized for performance and Lighthouse scores.
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
- * @example
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 ComponentType, type CSSProperties } from 'react';
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 performance optimizations.
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
- * ## Behavior
64
- * 1. Component initially shows a play button overlay with optional intro text
65
- * 2. User clicks play button to start the animation
66
- * 3. Starfield background and animated text crawl begin
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
- * ## Customization
70
- * CSS variables can be customized via theme or global styles:
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: ComponentType<CrawlProps>;
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAKL,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,aAAa,EAEnB,MAAM,OAAO,CAAC;AAOf,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,QAAA,MAAM,KAAK,EAAE,aAAa,CAAC,UAAU,CAsXpC,CAAC;AAEF,eAAe,KAAK,CAAC"}
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 animation component with starfield background, customizable intro text, title, and content sections. Optimized for performance and Lighthouse scores.
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
- * @example
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 { useEffect, useRef, useState, useMemo, isValidElement, } from 'react';
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 performance optimizations.
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
- * ## Behavior
49
- * 1. Component initially shows a play button overlay with optional intro text
50
- * 2. User clicks play button to start the animation
51
- * 3. Starfield background and animated text crawl begin
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
- * ## Customization
55
- * CSS variables can be customized via theme or global styles:
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
- // Normalize intro/title/content to ReactNodes. When strings are provided,
80
- // parse limited inline HTML and wrap paragraphs automatically.
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
- // Handle pause button click (resume)
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" }) }) })] })), isPaused && isAnimationActive && isHydrated && (_jsx("div", { className: "dndev-absolute dndev-inset-0 dndev-flex dndev-items-center dndev-justify-center", style: {
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;