portosaurus 3.0.2 → 4.0.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/README.md +26 -126
- package/bin/portosaurus.mjs +8 -0
- package/package.json +6 -3
- package/src/assets/img/icon.png +0 -0
- package/src/assets/img/{icon.svg → svg/icon.svg} +35 -37
- package/src/assets/img/svg/project-blank.svg +140 -0
- package/src/assets/sample-resume.pdf +0 -0
- package/src/cli/build.mjs +2 -5
- package/src/cli/dev.mjs +27 -5
- package/src/cli/init.mjs +6 -12
- package/src/cli/schema.mjs +211 -0
- package/src/core/buildDocuConfig.mjs +305 -188
- package/src/core/constants.mjs +7 -1
- package/src/template/config.yml +150 -0
- package/src/template/notes/welcome.mdx +6 -0
- package/src/template/package.json +3 -3
- package/src/theme/MDXComponents.js +0 -1
- package/src/theme/components/AboutSection/index.js +32 -17
- package/src/theme/components/AboutSection/styles.module.css +151 -344
- package/src/theme/components/ContactSection/index.js +23 -14
- package/src/theme/components/ContactSection/styles.module.css +19 -8
- package/src/theme/components/ExperienceSection/index.js +12 -5
- package/src/theme/components/HeroSection/index.js +4 -3
- package/src/theme/components/HeroSection/styles.module.css +17 -16
- package/src/theme/components/NavArrow/index.js +114 -0
- package/src/theme/components/NavArrow/styles.module.css +107 -0
- package/src/theme/components/NoteIndex/index.js +66 -95
- package/src/theme/components/NoteIndex/styles.module.css +85 -89
- package/src/theme/components/Preview/components/FeedbackStates.js +3 -1
- package/src/theme/components/Preview/components/PreviewContent.js +91 -0
- package/src/theme/components/Preview/components/PreviewHeader.js +41 -33
- package/src/theme/components/Preview/components/Triggers/Pv.js +129 -72
- package/src/theme/components/Preview/components/ViewerWindow.js +198 -234
- package/src/theme/components/Preview/hooks/useAdaptiveSizing.js +115 -0
- package/src/theme/components/Preview/hooks/useDeepLinkHash.js +18 -23
- package/src/theme/components/Preview/hooks/useDockLayout.js +48 -8
- package/src/theme/components/Preview/hooks/useTouchZoom.js +118 -0
- package/src/theme/components/Preview/renderers/CodeRenderer.js +64 -25
- package/src/theme/components/Preview/state/index.js +70 -17
- package/src/theme/components/Preview/styles.module.css +181 -45
- package/src/theme/components/Preview/utils/index.js +11 -10
- package/src/theme/components/ProjectsSection/index.js +138 -148
- package/src/theme/components/ProjectsSection/styles.module.css +178 -112
- package/src/theme/components/SocialLinks/index.js +9 -7
- package/src/theme/components/Tooltip/index.js +31 -20
- package/src/theme/components/Tooltip/styles.module.css +101 -38
- package/src/theme/config/iconMappings.js +2 -0
- package/src/theme/css/custom.css +72 -0
- package/src/theme/hooks/useScrollReveal.js +30 -0
- package/src/theme/pages/index.js +7 -27
- package/src/theme/pages/notes.js +2 -2
- package/src/theme/pages/tasks.js +12 -11
- package/src/utils/cliUtils.mjs +23 -51
- package/src/utils/configUtils.mjs +95 -84
- package/src/utils/systemUtils.mjs +171 -0
- package/src/template/config.js +0 -68
- package/src/theme/components/ScrollToTop/index.js +0 -95
- package/src/theme/components/ScrollToTop/styles.module.css +0 -97
- package/src/theme/config/metaTags.js +0 -21
- /package/src/template/{.nojekyll → static/.nojekyll} +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
|
2
3
|
import { iconMap } from "../../config/iconMappings";
|
|
3
4
|
import { FaQuestionCircle } from "react-icons/fa";
|
|
5
|
+
import useScrollReveal from "../../hooks/useScrollReveal";
|
|
4
6
|
import styles from "./styles.module.css";
|
|
5
7
|
|
|
6
8
|
const sortEmail = (links) => {
|
|
@@ -22,27 +24,34 @@ const sortEmail = (links) => {
|
|
|
22
24
|
});
|
|
23
25
|
};
|
|
24
26
|
|
|
25
|
-
export default function ContactSection({ id, className
|
|
27
|
+
export default function ContactSection({ id, className }) {
|
|
26
28
|
const { siteConfig } = useDocusaurusContext();
|
|
27
29
|
const { customFields } = siteConfig;
|
|
28
|
-
|
|
30
|
+
const socialLinksConfig = customFields.socialLinks || {};
|
|
31
|
+
|
|
32
|
+
if (socialLinksConfig.enable === false) return null;
|
|
33
|
+
|
|
34
|
+
let socialLinks = socialLinksConfig.links || [];
|
|
35
|
+
|
|
36
|
+
const displayHeading = socialLinksConfig.heading;
|
|
37
|
+
const displaySubheading = socialLinksConfig.subheading;
|
|
38
|
+
|
|
39
|
+
const [sectionRef, isVisible] = useScrollReveal();
|
|
29
40
|
|
|
30
41
|
socialLinks = sortEmail(socialLinks);
|
|
31
42
|
|
|
32
43
|
return (
|
|
33
44
|
<div
|
|
34
45
|
id={id}
|
|
35
|
-
|
|
46
|
+
ref={sectionRef}
|
|
47
|
+
className={`${styles.contactSection} ${isVisible ? "is-visible" : ""} ${className || ""}`}
|
|
36
48
|
role="region"
|
|
37
49
|
aria-label="Contact section"
|
|
38
50
|
>
|
|
39
51
|
<div className={styles.contactContainer}>
|
|
40
52
|
<div className={styles.contactHeader}>
|
|
41
|
-
<h2 className={styles.contactTitle}>{
|
|
42
|
-
<p className={styles.contactSubtitle}>
|
|
43
|
-
{subtitle ||
|
|
44
|
-
"Feel free to reach out for collaborations, questions, or just to say hello!"}
|
|
45
|
-
</p>
|
|
53
|
+
<h2 className={styles.contactTitle}>{displayHeading}</h2>
|
|
54
|
+
<p className={styles.contactSubtitle}>{displaySubheading}</p>
|
|
46
55
|
</div>
|
|
47
56
|
|
|
48
57
|
{/* SocialCard */}
|
|
@@ -53,13 +62,13 @@ export default function ContactSection({ id, className, title, subtitle }) {
|
|
|
53
62
|
aria-label="Social media and contact links"
|
|
54
63
|
>
|
|
55
64
|
{socialLinks.map((social, index) => {
|
|
56
|
-
const
|
|
65
|
+
const iconKey = (social.icon || social.name || "").toLowerCase();
|
|
66
|
+
const iconData = iconMap[iconKey] || {};
|
|
57
67
|
|
|
58
|
-
const name = social.name
|
|
68
|
+
const name = social.name;
|
|
59
69
|
const Icon = iconData.icon || FaQuestionCircle;
|
|
60
70
|
const iconColor = iconData.color || "var(--ifm-color-primary)";
|
|
61
|
-
const
|
|
62
|
-
name === "?" ? "" : social.desc || `Connect with me on ${name}`;
|
|
71
|
+
const desc = social.desc || `Connect with me on ${name}`;
|
|
63
72
|
const url = social.url;
|
|
64
73
|
|
|
65
74
|
return (
|
|
@@ -73,7 +82,7 @@ export default function ContactSection({ id, className, title, subtitle }) {
|
|
|
73
82
|
"--card-index": index,
|
|
74
83
|
"--icon-hover-color": iconColor,
|
|
75
84
|
}}
|
|
76
|
-
aria-label={`Connect with me on ${name}: ${
|
|
85
|
+
aria-label={`Connect with me on ${name}: ${desc}`}
|
|
77
86
|
role="listitem"
|
|
78
87
|
>
|
|
79
88
|
{Icon && (
|
|
@@ -82,7 +91,7 @@ export default function ContactSection({ id, className, title, subtitle }) {
|
|
|
82
91
|
</div>
|
|
83
92
|
)}
|
|
84
93
|
<h3 className={styles.socialTitle}>{name}</h3>
|
|
85
|
-
<p className={styles.socialDesc}>{
|
|
94
|
+
<p className={styles.socialDesc}>{desc}</p>
|
|
86
95
|
</a>
|
|
87
96
|
);
|
|
88
97
|
})}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/* Animations */
|
|
2
|
-
@keyframes
|
|
2
|
+
@keyframes fadeIn {
|
|
3
3
|
from {
|
|
4
4
|
opacity: 0;
|
|
5
|
-
transform: translateY(
|
|
5
|
+
transform: translateY(30px);
|
|
6
6
|
}
|
|
7
7
|
to {
|
|
8
8
|
opacity: 1;
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
@keyframes
|
|
13
|
+
@keyframes slideUp {
|
|
14
14
|
from {
|
|
15
15
|
opacity: 0;
|
|
16
|
-
transform: translateY(
|
|
16
|
+
transform: translateY(30px);
|
|
17
17
|
}
|
|
18
18
|
to {
|
|
19
19
|
opacity: 1;
|
|
@@ -55,17 +55,25 @@
|
|
|
55
55
|
font-weight: 600;
|
|
56
56
|
color: var(--ifm-color-primary);
|
|
57
57
|
margin-bottom: 0.6rem;
|
|
58
|
-
|
|
58
|
+
opacity: 0;
|
|
59
59
|
position: relative;
|
|
60
60
|
display: inline-block;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
:global(.is-visible) .contactTitle {
|
|
64
|
+
animation: slideUp 0.4s ease-out forwards;
|
|
65
|
+
}
|
|
66
|
+
|
|
63
67
|
.contactSubtitle {
|
|
64
68
|
font-size: 0.95rem;
|
|
65
69
|
color: var(--ifm-font-color-tertiary);
|
|
66
70
|
max-width: 600px;
|
|
67
71
|
margin: 0.05rem auto 0;
|
|
68
|
-
|
|
72
|
+
opacity: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
:global(.is-visible) .contactSubtitle {
|
|
76
|
+
animation: slideUp 0.4s ease-out 0.1s forwards;
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
/* SocialCard */
|
|
@@ -99,8 +107,6 @@
|
|
|
99
107
|
transform 0.3s ease,
|
|
100
108
|
box-shadow 0.3s ease,
|
|
101
109
|
background-color 0.3s ease;
|
|
102
|
-
animation: fadeIn 0.5s ease-out forwards;
|
|
103
|
-
animation-delay: calc(0.1s + (var(--card-index, 0) * 0.1s));
|
|
104
110
|
box-shadow: var(--ifm-global-shadow-lw);
|
|
105
111
|
text-decoration: none;
|
|
106
112
|
display: flex;
|
|
@@ -114,6 +120,11 @@
|
|
|
114
120
|
overflow: hidden;
|
|
115
121
|
}
|
|
116
122
|
|
|
123
|
+
:global(.is-visible) .socialCard {
|
|
124
|
+
animation: fadeIn 0.8s ease-out forwards;
|
|
125
|
+
animation-delay: calc(0.3s + (var(--card-index, 0) * 0.1s));
|
|
126
|
+
}
|
|
127
|
+
|
|
117
128
|
.socialCard:hover {
|
|
118
129
|
transform: translateY(-5px);
|
|
119
130
|
box-shadow: var(--ifm-global-shadow-md);
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
|
1
2
|
import styles from "./styles.module.css";
|
|
2
3
|
|
|
3
|
-
export default function ExperienceSection({ id, className
|
|
4
|
+
export default function ExperienceSection({ id, className }) {
|
|
5
|
+
const { siteConfig } = useDocusaurusContext();
|
|
6
|
+
const experience = siteConfig.customFields?.experience || {};
|
|
7
|
+
|
|
8
|
+
if (experience.enable === false) return null;
|
|
9
|
+
|
|
10
|
+
const displayHeading = experience.heading;
|
|
11
|
+
const displaySubheading = experience.subheading;
|
|
12
|
+
|
|
4
13
|
return (
|
|
5
14
|
<div
|
|
6
15
|
id={id}
|
|
@@ -10,10 +19,8 @@ export default function ExperienceSection({ id, className, title, subtitle }) {
|
|
|
10
19
|
>
|
|
11
20
|
<div className={styles.experienceContainer}>
|
|
12
21
|
<div className={styles.experienceHeader}>
|
|
13
|
-
<h2 className={styles.experienceTitle}>{
|
|
14
|
-
<p className={styles.experienceSubtitle}>
|
|
15
|
-
{subtitle || "My professional journey and work experience"}
|
|
16
|
-
</p>
|
|
22
|
+
<h2 className={styles.experienceTitle}>{displayHeading}</h2>
|
|
23
|
+
<p className={styles.experienceSubtitle}>{displaySubheading}</p>
|
|
17
24
|
</div>
|
|
18
25
|
|
|
19
26
|
<div className={styles.noticeWrapper}>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
|
2
2
|
import SocialLinks from "../SocialLinks/index.js";
|
|
3
|
+
import { FaChevronDown } from "react-icons/fa";
|
|
3
4
|
import styles from "./styles.module.css";
|
|
4
5
|
|
|
5
6
|
export default function HeroSection({ id, className }) {
|
|
@@ -10,7 +11,7 @@ export default function HeroSection({ id, className }) {
|
|
|
10
11
|
const title = customFields.heroSection.title;
|
|
11
12
|
const subtitle = customFields.heroSection.subtitle;
|
|
12
13
|
const profession = customFields.heroSection.profession;
|
|
13
|
-
const
|
|
14
|
+
const desc = customFields.heroSection.desc;
|
|
14
15
|
const profilePic = customFields.heroSection.profilePic;
|
|
15
16
|
const learnMoreButtonText = customFields.heroSection.learnMoreButtonTxt;
|
|
16
17
|
|
|
@@ -33,7 +34,7 @@ export default function HeroSection({ id, className }) {
|
|
|
33
34
|
<h2 className={styles.profession}>{profession}</h2>
|
|
34
35
|
<span className={styles.subtitle}>.</span>
|
|
35
36
|
</div>
|
|
36
|
-
<p className={styles.description}>{
|
|
37
|
+
<p className={styles.description}>{desc}</p>
|
|
37
38
|
<div className={styles.actionRow}>
|
|
38
39
|
<div className={styles.cta}>
|
|
39
40
|
<a
|
|
@@ -44,7 +45,7 @@ export default function HeroSection({ id, className }) {
|
|
|
44
45
|
{learnMoreButtonText}
|
|
45
46
|
</a>
|
|
46
47
|
</div>
|
|
47
|
-
<SocialLinks />
|
|
48
|
+
<SocialLinks links={customFields.heroSection.social} />
|
|
48
49
|
</div>
|
|
49
50
|
</div>
|
|
50
51
|
<div className={styles.rightSection}>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
@keyframes slideUp {
|
|
12
12
|
from {
|
|
13
13
|
opacity: 0;
|
|
14
|
-
transform: translateY(
|
|
14
|
+
transform: translateY(30px);
|
|
15
15
|
}
|
|
16
16
|
to {
|
|
17
17
|
opacity: 1;
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
@keyframes slideInLeft {
|
|
23
23
|
from {
|
|
24
24
|
opacity: 0;
|
|
25
|
-
transform: translateX(-
|
|
25
|
+
transform: translateX(-40px);
|
|
26
26
|
}
|
|
27
27
|
to {
|
|
28
28
|
opacity: 1;
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
@keyframes slideInRight {
|
|
34
34
|
from {
|
|
35
35
|
opacity: 0;
|
|
36
|
-
transform: translateX(
|
|
36
|
+
transform: translateX(40px);
|
|
37
37
|
}
|
|
38
38
|
to {
|
|
39
39
|
opacity: 1;
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
@keyframes
|
|
44
|
+
@keyframes scaleIn {
|
|
45
45
|
from {
|
|
46
46
|
opacity: 0;
|
|
47
|
-
transform: scale(0.
|
|
47
|
+
transform: scale(0.95);
|
|
48
48
|
}
|
|
49
49
|
to {
|
|
50
50
|
opacity: 1;
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
|
|
55
55
|
/* Base layout */
|
|
56
56
|
.hero {
|
|
57
|
+
position: relative;
|
|
57
58
|
display: flex;
|
|
58
59
|
justify-content: center;
|
|
59
60
|
align-items: center;
|
|
@@ -89,7 +90,7 @@
|
|
|
89
90
|
color: var(--ifm-font-color-tertiary);
|
|
90
91
|
font-family: var(--ifm-font-family-base);
|
|
91
92
|
margin-bottom: 0.2rem;
|
|
92
|
-
animation: slideUp 0.
|
|
93
|
+
animation: slideUp 0.8s ease-out 0.4s both;
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
.title {
|
|
@@ -98,7 +99,7 @@
|
|
|
98
99
|
margin: 0.2rem 0;
|
|
99
100
|
color: var(--ifm-color-primary);
|
|
100
101
|
font-family: var(--ifm-font-family-base);
|
|
101
|
-
animation: slideUp 0.
|
|
102
|
+
animation: slideUp 0.8s ease-out 0.6s both;
|
|
102
103
|
transition: all 0.3s ease;
|
|
103
104
|
position: relative;
|
|
104
105
|
display: inline-block;
|
|
@@ -132,7 +133,7 @@
|
|
|
132
133
|
font-family: var(--ifm-font-family-base);
|
|
133
134
|
margin-left: 0.1rem;
|
|
134
135
|
margin-right: 0.5rem;
|
|
135
|
-
animation: slideUp 0.
|
|
136
|
+
animation: slideUp 0.8s ease-out 0.6s both;
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
/* Subtitle section */
|
|
@@ -140,7 +141,7 @@
|
|
|
140
141
|
display: flex;
|
|
141
142
|
align-items: baseline;
|
|
142
143
|
flex-wrap: wrap;
|
|
143
|
-
animation: slideUp 0.
|
|
144
|
+
animation: slideUp 0.8s ease-out 0.8s both;
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
.subtitle {
|
|
@@ -189,7 +190,7 @@
|
|
|
189
190
|
max-width: 600px;
|
|
190
191
|
font-family: var(--ifm-font-family-base);
|
|
191
192
|
text-align: left;
|
|
192
|
-
animation: slideUp 0.
|
|
193
|
+
animation: slideUp 0.8s ease-out 1s both;
|
|
193
194
|
}
|
|
194
195
|
|
|
195
196
|
/* Action row styling */
|
|
@@ -197,7 +198,7 @@
|
|
|
197
198
|
display: flex;
|
|
198
199
|
align-items: center;
|
|
199
200
|
margin-top: 2.5rem;
|
|
200
|
-
animation: slideUp 0.
|
|
201
|
+
animation: slideUp 0.8s ease-out 1.2s both;
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
.cta {
|
|
@@ -255,20 +256,20 @@
|
|
|
255
256
|
height: 270px;
|
|
256
257
|
border-radius: 20%;
|
|
257
258
|
object-fit: cover;
|
|
258
|
-
box-shadow:
|
|
259
|
+
box-shadow: none;
|
|
259
260
|
margin-top: -30px;
|
|
260
261
|
margin-right: 2rem;
|
|
261
262
|
transition:
|
|
262
263
|
transform 0.5s ease,
|
|
263
264
|
box-shadow 0.5s ease;
|
|
264
|
-
animation:
|
|
265
|
+
animation: scaleIn 0.8s ease-out both;
|
|
265
266
|
will-change: transform;
|
|
266
267
|
position: relative;
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
.profilePic:hover {
|
|
270
271
|
transform: scale(1.04) translateY(-8px);
|
|
271
|
-
box-shadow: 0
|
|
272
|
+
box-shadow: 0 20px 40px var(--ifm-shadow-color);
|
|
272
273
|
}
|
|
273
274
|
|
|
274
275
|
/* Responsive styles for sections */
|
|
@@ -311,7 +312,7 @@
|
|
|
311
312
|
margin-top: -20px;
|
|
312
313
|
margin-bottom: 1.2rem;
|
|
313
314
|
margin-right: 0;
|
|
314
|
-
box-shadow:
|
|
315
|
+
box-shadow: none;
|
|
315
316
|
}
|
|
316
317
|
|
|
317
318
|
.profilePic:hover {
|
|
@@ -418,7 +419,7 @@
|
|
|
418
419
|
height: 160px;
|
|
419
420
|
margin-top: -25px;
|
|
420
421
|
margin-bottom: 1.5rem;
|
|
421
|
-
box-shadow:
|
|
422
|
+
box-shadow: none;
|
|
422
423
|
}
|
|
423
424
|
|
|
424
425
|
.title {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { FaChevronDown, FaChevronUp } from "react-icons/fa";
|
|
3
|
+
import Tooltip from "../Tooltip";
|
|
4
|
+
import styles from "./styles.module.css";
|
|
5
|
+
|
|
6
|
+
export default function NavArrow() {
|
|
7
|
+
const [direction, setDirection] = useState("down");
|
|
8
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
9
|
+
const [isScrolling, setIsScrolling] = useState(false);
|
|
10
|
+
const scrollTimeoutRef = useRef(null);
|
|
11
|
+
|
|
12
|
+
const getSections = () => {
|
|
13
|
+
// Dynamically find all main sections with IDs
|
|
14
|
+
const main = document.querySelector("main");
|
|
15
|
+
if (!main) return [];
|
|
16
|
+
|
|
17
|
+
return Array.from(main.querySelectorAll(":scope > [id]"))
|
|
18
|
+
.map((el) => el.id)
|
|
19
|
+
.filter((id) => id !== "nav-arrow"); // Exclude itself if it had an ID
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const handleScroll = () => {
|
|
24
|
+
const scrollTop =
|
|
25
|
+
window.pageYOffset || document.documentElement.scrollTop;
|
|
26
|
+
const windowHeight = window.innerHeight;
|
|
27
|
+
const fullHeight = document.documentElement.scrollHeight;
|
|
28
|
+
|
|
29
|
+
// Show after initial scroll
|
|
30
|
+
setIsVisible(scrollTop > 100);
|
|
31
|
+
|
|
32
|
+
// Point UP only if we are at the very bottom
|
|
33
|
+
if (scrollTop + windowHeight >= fullHeight - 50) {
|
|
34
|
+
setDirection("up");
|
|
35
|
+
} else {
|
|
36
|
+
setDirection("down");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setIsScrolling(true);
|
|
40
|
+
if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);
|
|
41
|
+
scrollTimeoutRef.current = setTimeout(() => setIsScrolling(false), 800);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
45
|
+
handleScroll();
|
|
46
|
+
|
|
47
|
+
return () => {
|
|
48
|
+
window.removeEventListener("scroll", handleScroll);
|
|
49
|
+
if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const handleClick = () => {
|
|
54
|
+
if (direction === "up") {
|
|
55
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
56
|
+
} else {
|
|
57
|
+
const sections = getSections();
|
|
58
|
+
const windowTop =
|
|
59
|
+
window.pageYOffset || document.documentElement.scrollTop;
|
|
60
|
+
|
|
61
|
+
let nextSectionId = null;
|
|
62
|
+
|
|
63
|
+
// Find the first section whose top is below the current scroll position + small offset
|
|
64
|
+
for (const id of sections) {
|
|
65
|
+
const element = document.getElementById(id);
|
|
66
|
+
if (element) {
|
|
67
|
+
const rect = element.getBoundingClientRect();
|
|
68
|
+
const absoluteTop = rect.top + windowTop;
|
|
69
|
+
|
|
70
|
+
if (absoluteTop > windowTop + 100) {
|
|
71
|
+
nextSectionId = id;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (nextSectionId) {
|
|
78
|
+
const element = document.getElementById(nextSectionId);
|
|
79
|
+
element.scrollIntoView({ behavior: "smooth" });
|
|
80
|
+
} else {
|
|
81
|
+
// If no more sections, just go to bottom
|
|
82
|
+
window.scrollTo({
|
|
83
|
+
top: document.documentElement.scrollHeight,
|
|
84
|
+
behavior: "smooth",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<button
|
|
92
|
+
className={`${styles.navArrow} ${isVisible ? styles.visible : ""} ${isScrolling ? styles.scrolling : ""}`}
|
|
93
|
+
onClick={handleClick}
|
|
94
|
+
aria-label={
|
|
95
|
+
direction === "down" ? "Scroll to next section" : "Scroll to top"
|
|
96
|
+
}
|
|
97
|
+
>
|
|
98
|
+
<Tooltip
|
|
99
|
+
msg={direction === "down" ? "Next Section" : "Back to Top"}
|
|
100
|
+
position="top"
|
|
101
|
+
gap={25}
|
|
102
|
+
underline={false}
|
|
103
|
+
>
|
|
104
|
+
<div className={`${styles.iconWrapper} ${styles[direction]}`}>
|
|
105
|
+
{direction === "down" ? (
|
|
106
|
+
<FaChevronDown className={styles.chevron} />
|
|
107
|
+
) : (
|
|
108
|
+
<FaChevronUp className={styles.chevron} />
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</Tooltip>
|
|
112
|
+
</button>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--nav-arrow-size: 50px;
|
|
3
|
+
--nav-arrow-bottom: 2.5rem;
|
|
4
|
+
--nav-arrow-mobile-size: 46px;
|
|
5
|
+
--nav-arrow-mobile-bottom: 1.5rem;
|
|
6
|
+
--nav-arrow-bounce-y: -8px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.navArrow {
|
|
10
|
+
position: fixed;
|
|
11
|
+
bottom: var(--nav-arrow-bottom);
|
|
12
|
+
left: 50%;
|
|
13
|
+
transform: translateX(-50%) translateY(100px);
|
|
14
|
+
width: var(--nav-arrow-size);
|
|
15
|
+
height: var(--nav-arrow-size);
|
|
16
|
+
border-radius: 50%;
|
|
17
|
+
background-color: var(--ifm-color-primary);
|
|
18
|
+
color: var(--ifm-color-black);
|
|
19
|
+
border: none;
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
z-index: var(--ifm-z-index-fixed, 1000);
|
|
25
|
+
box-shadow: var(--ifm-global-shadow-lw);
|
|
26
|
+
transition:
|
|
27
|
+
transform var(--ifm-transition-slow) cubic-bezier(0.175, 0.885, 0.32, 1.275),
|
|
28
|
+
opacity var(--ifm-transition-slow) ease,
|
|
29
|
+
background-color var(--ifm-transition-fast) ease;
|
|
30
|
+
opacity: 0;
|
|
31
|
+
pointer-events: none;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.navArrow.visible {
|
|
35
|
+
transform: translateX(-50%) translateY(0);
|
|
36
|
+
opacity: 1;
|
|
37
|
+
pointer-events: auto;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.navArrow:hover {
|
|
41
|
+
background-color: var(--ifm-color-primary-darker);
|
|
42
|
+
transform: translateX(-50%) translateY(-5px) scale(1.1);
|
|
43
|
+
box-shadow: var(--ifm-global-shadow-md);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.navArrow:active {
|
|
47
|
+
transform: translateX(-50%) translateY(-2px) scale(0.95);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.iconWrapper {
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
justify-content: center;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.down {
|
|
57
|
+
animation: bounce 2s infinite;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.chevron {
|
|
61
|
+
font-size: 1.4rem;
|
|
62
|
+
color: inherit;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@keyframes bounce {
|
|
66
|
+
0%,
|
|
67
|
+
20%,
|
|
68
|
+
50%,
|
|
69
|
+
80%,
|
|
70
|
+
100% {
|
|
71
|
+
transform: translateY(0);
|
|
72
|
+
}
|
|
73
|
+
40% {
|
|
74
|
+
transform: translateY(var(--nav-arrow-bounce-y));
|
|
75
|
+
}
|
|
76
|
+
60% {
|
|
77
|
+
transform: translateY(calc(var(--nav-arrow-bounce-y) / 2));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Subtly fade during manual scroll */
|
|
82
|
+
.navArrow.scrolling {
|
|
83
|
+
opacity: 0.7;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@media (max-width: 768px) {
|
|
87
|
+
.navArrow {
|
|
88
|
+
bottom: var(--nav-arrow-mobile-bottom);
|
|
89
|
+
width: var(--nav-arrow-mobile-size);
|
|
90
|
+
height: var(--nav-arrow-mobile-size);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.chevron {
|
|
94
|
+
font-size: 1.2rem;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Reduced motion support */
|
|
99
|
+
@media (prefers-reduced-motion: reduce) {
|
|
100
|
+
.navArrow {
|
|
101
|
+
transition: opacity var(--ifm-transition-fast) ease;
|
|
102
|
+
animation: none !important;
|
|
103
|
+
}
|
|
104
|
+
.navArrow.visible {
|
|
105
|
+
transform: translateX(-50%) translateY(0);
|
|
106
|
+
}
|
|
107
|
+
}
|