portosaurus 0.14.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.
Potentially problematic release.
This version of portosaurus might be problematic. Click here for more details.
- package/.vscode/snippets.code-snippets +79 -0
- package/AGENTS.md +37 -0
- package/GG/config.js +233 -0
- package/GG/package.json +14 -0
- package/GG/static/.nojekyll +0 -0
- package/GG/static/docusaurus-snippet.css +3 -0
- package/GG/static/img/icon-bg.png +0 -0
- package/GG/static/img/icon-old.png +0 -0
- package/GG/static/img/icon.png +0 -0
- package/GG/static/img/project-blank.png +0 -0
- package/GG/static/img/social-card.jpeg +0 -0
- package/LICENSE +674 -0
- package/README.md +57 -0
- package/bin/portosaurus.js +136 -0
- package/package.json +36 -0
- package/src/config/iconMappings.js +329 -0
- package/src/config/metaTags.js +240 -0
- package/src/config/prism.js +179 -0
- package/src/config/sidebar.js +20 -0
- package/src/configLoader.js +99 -0
- package/src/index.js +79 -0
- package/src/pages/index.js +98 -0
- package/src/pages/notes.js +88 -0
- package/src/pages/tasks.js +251 -0
- package/src/theme/components/AboutSection/index.js +67 -0
- package/src/theme/components/AboutSection/styles.module.css +492 -0
- package/src/theme/components/ContactSection/index.js +87 -0
- package/src/theme/components/ContactSection/styles.module.css +327 -0
- package/src/theme/components/ExperienceSection/index.js +25 -0
- package/src/theme/components/ExperienceSection/styles.module.css +180 -0
- package/src/theme/components/HeroSection/index.js +63 -0
- package/src/theme/components/HeroSection/styles.module.css +471 -0
- package/src/theme/components/NoteIndex/index.js +119 -0
- package/src/theme/components/NoteIndex/styles.module.css +143 -0
- package/src/theme/components/ProjectsSection/index.js +529 -0
- package/src/theme/components/ProjectsSection/styles.module.css +830 -0
- package/src/theme/components/ScrollToTop/index.js +98 -0
- package/src/theme/components/ScrollToTop/styles.module.css +96 -0
- package/src/theme/components/SocialLinks/index.js +129 -0
- package/src/theme/components/SocialLinks/styles.module.css +55 -0
- package/src/theme/components/Tooltip/index.js +30 -0
- package/src/theme/components/Tooltip/styles.module.css +92 -0
- package/src/theme/css/bootstrap.css +6 -0
- package/src/theme/css/catppuccin.css +632 -0
- package/src/theme/css/custom.css +186 -0
- package/src/theme/css/tasks.css +868 -0
- package/src/theme/staticLink/.nojekyll +0 -0
- package/src/theme/staticLink/docusaurus-snippet.css +3 -0
- package/src/theme/staticLink/img/icon-bg.png +0 -0
- package/src/theme/staticLink/img/icon-old.png +0 -0
- package/src/theme/staticLink/img/icon.png +0 -0
- package/src/theme/staticLink/img/project-blank.png +0 -0
- package/src/theme/staticLink/img/social-card.jpeg +0 -0
- package/src/utils/HashNavigation.js +250 -0
- package/src/utils/appVersion.js +27 -0
- package/src/utils/cssUtils.js +99 -0
- package/src/utils/filterEnabledItems.js +21 -0
- package/src/utils/generateFavicon.js +256 -0
- package/src/utils/generateRobotsTxt.js +97 -0
- package/src/utils/iconExtractor.js +159 -0
- package/src/utils/imageDownloader.js +88 -0
- package/src/utils/imageProcessor.js +134 -0
- package/src/utils/linkShortner.js +0 -0
- package/src/utils/updateTitle.js +107 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { IoIosArrowUp } from 'react-icons/io';
|
|
3
|
+
import styles from './styles.module.css';
|
|
4
|
+
|
|
5
|
+
export default function ScrollToTop({ hideDelay = 1500 }) {
|
|
6
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
7
|
+
const [isHovering, setIsHovering] = useState(false);
|
|
8
|
+
const timeoutRef = useRef(null);
|
|
9
|
+
const lastScrollTopRef = useRef(0);
|
|
10
|
+
|
|
11
|
+
const startHideTimer = () => {
|
|
12
|
+
|
|
13
|
+
// Clear any existing timeout
|
|
14
|
+
if (timeoutRef.current) {
|
|
15
|
+
clearTimeout(timeoutRef.current);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Only start timer if not hovering
|
|
19
|
+
if (!isHovering) {
|
|
20
|
+
timeoutRef.current = setTimeout(() => {
|
|
21
|
+
setIsVisible(false);
|
|
22
|
+
}, hideDelay);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
|
|
28
|
+
const handleScroll = () => {
|
|
29
|
+
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
30
|
+
const isScrollingUp = scrollTop < lastScrollTopRef.current;
|
|
31
|
+
|
|
32
|
+
// Save the current scroll position
|
|
33
|
+
lastScrollTopRef.current = scrollTop;
|
|
34
|
+
|
|
35
|
+
// Show button when scrolling up past threshold
|
|
36
|
+
if (isScrollingUp && scrollTop > 300) {
|
|
37
|
+
|
|
38
|
+
setIsVisible(true);
|
|
39
|
+
startHideTimer();
|
|
40
|
+
} else {
|
|
41
|
+
|
|
42
|
+
setIsVisible(false);
|
|
43
|
+
|
|
44
|
+
if (timeoutRef.current) {
|
|
45
|
+
clearTimeout(timeoutRef.current);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Set up event listener
|
|
51
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
52
|
+
|
|
53
|
+
// Clean up
|
|
54
|
+
return () => {
|
|
55
|
+
window.removeEventListener('scroll', handleScroll);
|
|
56
|
+
if (timeoutRef.current) {
|
|
57
|
+
clearTimeout(timeoutRef.current);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}, [hideDelay, isHovering]);
|
|
61
|
+
|
|
62
|
+
const handleMouseEnter = () => {
|
|
63
|
+
|
|
64
|
+
setIsHovering(true);
|
|
65
|
+
|
|
66
|
+
if (timeoutRef.current) {
|
|
67
|
+
clearTimeout(timeoutRef.current);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleMouseLeave = () => {
|
|
72
|
+
|
|
73
|
+
setIsHovering(false);
|
|
74
|
+
startHideTimer();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const scrollToTop = () => {
|
|
78
|
+
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
79
|
+
|
|
80
|
+
window.scrollTo({
|
|
81
|
+
top: 0,
|
|
82
|
+
behavior: prefersReducedMotion ? 'auto' : 'smooth'
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<button
|
|
88
|
+
className={`${styles.scrollToTop} ${isVisible ? styles.visible : ''}`}
|
|
89
|
+
onClick={scrollToTop}
|
|
90
|
+
onMouseEnter={handleMouseEnter}
|
|
91
|
+
onMouseLeave={handleMouseLeave}
|
|
92
|
+
aria-label="Scroll to top"
|
|
93
|
+
title="Scroll to top"
|
|
94
|
+
>
|
|
95
|
+
<IoIosArrowUp />
|
|
96
|
+
</button>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
.scrollToTop {
|
|
2
|
+
position: fixed;
|
|
3
|
+
bottom: 30px;
|
|
4
|
+
right: 30px;
|
|
5
|
+
background-color: var(--ifm-color-primary);
|
|
6
|
+
color: var(--ifm-background-color);
|
|
7
|
+
width: 50px;
|
|
8
|
+
height: 50px;
|
|
9
|
+
border-radius: 50%;
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
|
15
|
+
border: none;
|
|
16
|
+
z-index: 100;
|
|
17
|
+
opacity: 0;
|
|
18
|
+
transform: translateY(20px) scale(0.9);
|
|
19
|
+
pointer-events: none;
|
|
20
|
+
transition: opacity 0.3s ease, transform 0.3s ease, background-color 0.3s ease;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.scrollToTop.visible {
|
|
24
|
+
opacity: 1;
|
|
25
|
+
transform: translateY(0) scale(1);
|
|
26
|
+
pointer-events: auto;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.scrollToTop:hover {
|
|
30
|
+
background-color: var(--ifm-color-primary-dark);
|
|
31
|
+
transform: translateY(-3px) scale(1.05);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.scrollToTop:active {
|
|
35
|
+
transform: translateY(-1px) scale(1.02);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.scrollToTop svg {
|
|
39
|
+
width: 28px;
|
|
40
|
+
height: 28px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.scrollToTop:hover svg {
|
|
44
|
+
transform: scale(1.1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Responsive adjustments */
|
|
48
|
+
@media (max-width: 768px) {
|
|
49
|
+
|
|
50
|
+
.scrollToTop {
|
|
51
|
+
width: 45px;
|
|
52
|
+
height: 45px;
|
|
53
|
+
bottom: 25px;
|
|
54
|
+
right: 25px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.scrollToTop svg {
|
|
58
|
+
width: 24px;
|
|
59
|
+
height: 24px;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@media (max-width: 480px) {
|
|
64
|
+
|
|
65
|
+
.scrollToTop {
|
|
66
|
+
width: 40px;
|
|
67
|
+
height: 40px;
|
|
68
|
+
bottom: 20px;
|
|
69
|
+
right: 50%;
|
|
70
|
+
transform: translateX(50%) translateY(20px) scale(0.9);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.scrollToTop.visible {
|
|
74
|
+
transform: translateX(50%) scale(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.scrollToTop:hover {
|
|
78
|
+
transform: translateX(50%) translateY(-3px) scale(1.05);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.scrollToTop:active {
|
|
82
|
+
transform: translateX(50%) translateY(-1px) scale(1.02);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.scrollToTop svg {
|
|
86
|
+
width: 22px;
|
|
87
|
+
height: 22px;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Accessibility preferences */
|
|
92
|
+
@media (prefers-reduced-motion: reduce) {
|
|
93
|
+
.scrollToTop {
|
|
94
|
+
transition: none;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
2
|
+
import styles from './styles.module.css';
|
|
3
|
+
import { FaQuestionCircle } from 'react-icons/fa';
|
|
4
|
+
|
|
5
|
+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
6
|
+
import useIsBrowser from '@docusaurus/useIsBrowser';
|
|
7
|
+
|
|
8
|
+
import Tooltip from '@site/src/components/Tooltip';
|
|
9
|
+
import { iconMap } from '@site/src/config/iconMappings';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// Default icon & icon
|
|
13
|
+
const DEFAULT_ICON = FaQuestionCircle;
|
|
14
|
+
const DEFAULT_COLOR = 'var(--ifm-color-primary)';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export default function SocialIcons({ showAll = false }) {
|
|
18
|
+
const { siteConfig } = useDocusaurusContext();
|
|
19
|
+
const { customFields } = siteConfig;
|
|
20
|
+
const isBrowser = useIsBrowser();
|
|
21
|
+
|
|
22
|
+
const [animationDelays, setAnimationDelays] = useState({});
|
|
23
|
+
|
|
24
|
+
const allSocialLinks = customFields.socialLinks.links || [];
|
|
25
|
+
|
|
26
|
+
// FIX: `to prevent unnecessary recalculations`
|
|
27
|
+
const socialLinks = useMemo(() => {
|
|
28
|
+
return showAll
|
|
29
|
+
? allSocialLinks
|
|
30
|
+
: allSocialLinks.filter(link => link.pin);
|
|
31
|
+
}, [allSocialLinks, showAll]);
|
|
32
|
+
|
|
33
|
+
// Calculate delays based on screen size
|
|
34
|
+
const calculateDelays = useCallback(() => {
|
|
35
|
+
if (!isBrowser) return {};
|
|
36
|
+
|
|
37
|
+
const isTablet = window.innerWidth <= 768;
|
|
38
|
+
const isMobile = window.innerWidth <= 480;
|
|
39
|
+
const delays = {};
|
|
40
|
+
|
|
41
|
+
const baseDelay = isMobile ? 0.7 : (isTablet ? 0.9 : 1.3);
|
|
42
|
+
const incrementDelay = 0.1;
|
|
43
|
+
|
|
44
|
+
socialLinks.forEach((_, index) => {
|
|
45
|
+
delays[index] = `${baseDelay + (index * incrementDelay)}s`;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return delays;
|
|
49
|
+
}, [isBrowser, socialLinks]);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!isBrowser) return;
|
|
53
|
+
|
|
54
|
+
// Set initial delays
|
|
55
|
+
setAnimationDelays(calculateDelays());
|
|
56
|
+
|
|
57
|
+
const handleResize = () => {
|
|
58
|
+
setAnimationDelays(calculateDelays());
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
window.addEventListener('resize', handleResize);
|
|
62
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
63
|
+
}, [isBrowser, calculateDelays]);
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
// Get icon component and color
|
|
67
|
+
const getIconDetails = (iconName) => {
|
|
68
|
+
|
|
69
|
+
if (!iconName) {
|
|
70
|
+
return {
|
|
71
|
+
icon: DEFAULT_ICON,
|
|
72
|
+
color: DEFAULT_COLOR
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const formattedIconName = iconName.toLowerCase();
|
|
77
|
+
const iconDetails = iconMap[formattedIconName];
|
|
78
|
+
|
|
79
|
+
if (!iconDetails) {
|
|
80
|
+
return {
|
|
81
|
+
icon: DEFAULT_ICON,
|
|
82
|
+
color: DEFAULT_COLOR
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
icon: iconDetails.icon,
|
|
88
|
+
color: iconDetails.color || DEFAULT_COLOR
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (socialLinks.length === 0) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className={styles.socialIcons}>
|
|
98
|
+
{
|
|
99
|
+
socialLinks.map((social, index) => {
|
|
100
|
+
const { icon: IconComponent, color: iconColor } = getIconDetails(social.icon);
|
|
101
|
+
const href = social.url || '#';
|
|
102
|
+
const displayColor = social.color || iconColor;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Tooltip
|
|
106
|
+
key={index}
|
|
107
|
+
content={social.desc || social.icon || 'Link'}
|
|
108
|
+
position="top"
|
|
109
|
+
color={displayColor}
|
|
110
|
+
>
|
|
111
|
+
<a
|
|
112
|
+
href={href}
|
|
113
|
+
target="_blank"
|
|
114
|
+
rel="noopener noreferrer"
|
|
115
|
+
className={styles.socialLink}
|
|
116
|
+
style={{
|
|
117
|
+
'--hover-color': displayColor,
|
|
118
|
+
animationDelay: animationDelays[index] || '0s'
|
|
119
|
+
}}
|
|
120
|
+
aria-label={social.icon || 'social link'}
|
|
121
|
+
>
|
|
122
|
+
<IconComponent size={24} />
|
|
123
|
+
</a>
|
|
124
|
+
</Tooltip>
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
/* Animation */
|
|
3
|
+
@keyframes fadeIn {
|
|
4
|
+
from { opacity: 0; }
|
|
5
|
+
to { opacity: 1; }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
.socialIcons {
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
gap: 18px;
|
|
13
|
+
height: 38px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.socialLink {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
color: var(--ifm-color-primary);
|
|
21
|
+
transition: color 0.3s, transform 0.2s;
|
|
22
|
+
position: relative;
|
|
23
|
+
animation: fadeIn 0.3s ease-out forwards;
|
|
24
|
+
opacity: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.socialLink:hover {
|
|
28
|
+
opacity: 0.9;
|
|
29
|
+
transform: translateY(-4px) scale(1.15);
|
|
30
|
+
text-decoration: none;
|
|
31
|
+
color: var(--hover-color, var(--ifm-color-primary-dark));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
/* Responsive */
|
|
36
|
+
@media (max-width: 768px) {
|
|
37
|
+
.socialIcons {
|
|
38
|
+
justify-content: center;
|
|
39
|
+
width: 100%;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.socialLink {
|
|
43
|
+
margin: 0 9px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Accessibility */
|
|
49
|
+
@media (prefers-reduced-motion: reduce) {
|
|
50
|
+
.socialLink {
|
|
51
|
+
animation: none !important;
|
|
52
|
+
opacity: 1;
|
|
53
|
+
transition: none;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import styles from './styles.module.css';
|
|
3
|
+
|
|
4
|
+
export default function Tooltip({ children, content, position = 'top', color }) {
|
|
5
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
6
|
+
|
|
7
|
+
const tooltipStyle = color ? { '--tooltip-color': color } : {};
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className={styles.tooltipContainer}
|
|
12
|
+
onMouseEnter={() => setIsVisible(true)}
|
|
13
|
+
onMouseLeave={() => setIsVisible(false)}
|
|
14
|
+
onFocus={() => setIsVisible(true)}
|
|
15
|
+
onBlur={() => setIsVisible(false)}
|
|
16
|
+
>
|
|
17
|
+
{children}
|
|
18
|
+
{isVisible && (
|
|
19
|
+
<div
|
|
20
|
+
className={`${styles.tooltip} ${styles[position]}`}
|
|
21
|
+
style={tooltipStyle}
|
|
22
|
+
role="tooltip"
|
|
23
|
+
>
|
|
24
|
+
{content}
|
|
25
|
+
<div className={styles.arrow} />
|
|
26
|
+
</div>
|
|
27
|
+
)}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
.tooltipContainer {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: inline-flex;
|
|
4
|
+
cursor: pointer;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.tooltip {
|
|
8
|
+
position: absolute;
|
|
9
|
+
background-color: var(--tooltip-color, var(--ifm-background-surface-color));
|
|
10
|
+
color: var(--ifm-font-color-base);
|
|
11
|
+
padding: 6px 10px;
|
|
12
|
+
border-radius: 4px;
|
|
13
|
+
font-size: 0.8rem;
|
|
14
|
+
font-weight: 400;
|
|
15
|
+
white-space: nowrap;
|
|
16
|
+
z-index: 10;
|
|
17
|
+
opacity: 0;
|
|
18
|
+
animation: tooltipFadeIn 0.15s ease-out forwards;
|
|
19
|
+
box-shadow: none;
|
|
20
|
+
pointer-events: none;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.arrow {
|
|
24
|
+
position: absolute;
|
|
25
|
+
width: 8px;
|
|
26
|
+
height: 8px;
|
|
27
|
+
background-color: inherit;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Positioning variations */
|
|
31
|
+
.top {
|
|
32
|
+
bottom: 100%;
|
|
33
|
+
left: 50%;
|
|
34
|
+
transform: translateX(-50%) translateY(-10px);
|
|
35
|
+
margin-bottom: 4px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.top .arrow {
|
|
39
|
+
top: 100%;
|
|
40
|
+
left: 50%;
|
|
41
|
+
transform: translateX(-50%) rotate(45deg);
|
|
42
|
+
margin-top: -4px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.bottom {
|
|
46
|
+
top: 100%;
|
|
47
|
+
left: 50%;
|
|
48
|
+
transform: translateX(-50%) translateY(6px);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.bottom .arrow {
|
|
52
|
+
bottom: 100%;
|
|
53
|
+
left: 50%;
|
|
54
|
+
transform: translateX(-50%) rotate(45deg);
|
|
55
|
+
margin-bottom: -4px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.left {
|
|
59
|
+
right: 100%;
|
|
60
|
+
top: 50%;
|
|
61
|
+
transform: translateY(-50%) translateX(-6px);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.left .arrow {
|
|
65
|
+
right: -4px;
|
|
66
|
+
top: 50%;
|
|
67
|
+
transform: translateY(-50%) rotate(45deg);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.right {
|
|
71
|
+
left: 100%;
|
|
72
|
+
top: 50%;
|
|
73
|
+
transform: translateY(-50%) translateX(6px);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.right .arrow {
|
|
77
|
+
left: -4px;
|
|
78
|
+
top: 50%;
|
|
79
|
+
transform: translateY(-50%) rotate(45deg);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@keyframes tooltipFadeIn {
|
|
83
|
+
from { opacity: 0; }
|
|
84
|
+
to { opacity: 1; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Accessibility */
|
|
88
|
+
@media (prefers-reduced-motion: reduce) {
|
|
89
|
+
.tooltip {
|
|
90
|
+
animation: none;
|
|
91
|
+
}
|
|
92
|
+
}
|