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.

Files changed (64) hide show
  1. package/.vscode/snippets.code-snippets +79 -0
  2. package/AGENTS.md +37 -0
  3. package/GG/config.js +233 -0
  4. package/GG/package.json +14 -0
  5. package/GG/static/.nojekyll +0 -0
  6. package/GG/static/docusaurus-snippet.css +3 -0
  7. package/GG/static/img/icon-bg.png +0 -0
  8. package/GG/static/img/icon-old.png +0 -0
  9. package/GG/static/img/icon.png +0 -0
  10. package/GG/static/img/project-blank.png +0 -0
  11. package/GG/static/img/social-card.jpeg +0 -0
  12. package/LICENSE +674 -0
  13. package/README.md +57 -0
  14. package/bin/portosaurus.js +136 -0
  15. package/package.json +36 -0
  16. package/src/config/iconMappings.js +329 -0
  17. package/src/config/metaTags.js +240 -0
  18. package/src/config/prism.js +179 -0
  19. package/src/config/sidebar.js +20 -0
  20. package/src/configLoader.js +99 -0
  21. package/src/index.js +79 -0
  22. package/src/pages/index.js +98 -0
  23. package/src/pages/notes.js +88 -0
  24. package/src/pages/tasks.js +251 -0
  25. package/src/theme/components/AboutSection/index.js +67 -0
  26. package/src/theme/components/AboutSection/styles.module.css +492 -0
  27. package/src/theme/components/ContactSection/index.js +87 -0
  28. package/src/theme/components/ContactSection/styles.module.css +327 -0
  29. package/src/theme/components/ExperienceSection/index.js +25 -0
  30. package/src/theme/components/ExperienceSection/styles.module.css +180 -0
  31. package/src/theme/components/HeroSection/index.js +63 -0
  32. package/src/theme/components/HeroSection/styles.module.css +471 -0
  33. package/src/theme/components/NoteIndex/index.js +119 -0
  34. package/src/theme/components/NoteIndex/styles.module.css +143 -0
  35. package/src/theme/components/ProjectsSection/index.js +529 -0
  36. package/src/theme/components/ProjectsSection/styles.module.css +830 -0
  37. package/src/theme/components/ScrollToTop/index.js +98 -0
  38. package/src/theme/components/ScrollToTop/styles.module.css +96 -0
  39. package/src/theme/components/SocialLinks/index.js +129 -0
  40. package/src/theme/components/SocialLinks/styles.module.css +55 -0
  41. package/src/theme/components/Tooltip/index.js +30 -0
  42. package/src/theme/components/Tooltip/styles.module.css +92 -0
  43. package/src/theme/css/bootstrap.css +6 -0
  44. package/src/theme/css/catppuccin.css +632 -0
  45. package/src/theme/css/custom.css +186 -0
  46. package/src/theme/css/tasks.css +868 -0
  47. package/src/theme/staticLink/.nojekyll +0 -0
  48. package/src/theme/staticLink/docusaurus-snippet.css +3 -0
  49. package/src/theme/staticLink/img/icon-bg.png +0 -0
  50. package/src/theme/staticLink/img/icon-old.png +0 -0
  51. package/src/theme/staticLink/img/icon.png +0 -0
  52. package/src/theme/staticLink/img/project-blank.png +0 -0
  53. package/src/theme/staticLink/img/social-card.jpeg +0 -0
  54. package/src/utils/HashNavigation.js +250 -0
  55. package/src/utils/appVersion.js +27 -0
  56. package/src/utils/cssUtils.js +99 -0
  57. package/src/utils/filterEnabledItems.js +21 -0
  58. package/src/utils/generateFavicon.js +256 -0
  59. package/src/utils/generateRobotsTxt.js +97 -0
  60. package/src/utils/iconExtractor.js +159 -0
  61. package/src/utils/imageDownloader.js +88 -0
  62. package/src/utils/imageProcessor.js +134 -0
  63. package/src/utils/linkShortner.js +0 -0
  64. package/src/utils/updateTitle.js +107 -0
@@ -0,0 +1,97 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+
5
+ function formatRules(rules) {
6
+ if (!Array.isArray(rules) || !rules.length) return '';
7
+
8
+ let content = '';
9
+
10
+ rules.forEach(rule => {
11
+
12
+ // Default userAgent to '*' if not specified
13
+ const userAgent = rule.userAgent || '*';
14
+
15
+ content += `User-agent: ${userAgent}\n`;
16
+
17
+ // Check if root path is explicitly disallowed
18
+ const disallowsRoot = Array.isArray(rule.disallow) &&
19
+ rule.disallow.some(path => path === '/' || path === '/*');
20
+
21
+ // Process allow directives
22
+ if (Array.isArray(rule.allow) && rule.allow.length > 0) {
23
+
24
+ // Use explicitly defined allow rules
25
+ for (const allowPath of rule.allow) {
26
+ content += `Allow: ${allowPath}\n`;
27
+ }
28
+
29
+ } else if (!disallowsRoot) {
30
+ content += `Allow: /\n`;
31
+ }
32
+
33
+ // Process disallow directives
34
+ if (Array.isArray(rule.disallow)) {
35
+ for (const disallowPath of rule.disallow) {
36
+ content += `Disallow: ${disallowPath}\n`;
37
+ }
38
+ }
39
+
40
+ content += '\n';
41
+ });
42
+
43
+ return content;
44
+ }
45
+
46
+
47
+ module.exports = function() {
48
+ return {
49
+ name: 'generate-robots-txt',
50
+
51
+ async postBuild({ outDir, siteConfig }) {
52
+ const robotsConfig = siteConfig.customFields?.robotsTxt;
53
+
54
+ // Early return if robots.txt is disabled
55
+ if (!robotsConfig || robotsConfig.enable === false) {
56
+ console.log('ℹ️ robots.txt generation is disabled in config');
57
+ return;
58
+ }
59
+
60
+ try {
61
+ const robotsTxtPath = path.join(outDir, 'robots.txt');
62
+ let content = '';
63
+
64
+ // Process the rules structure
65
+ if (robotsConfig.rules) {
66
+ content += formatRules(robotsConfig.rules);
67
+ }
68
+
69
+ // Always include sitemap reference
70
+ content += `Sitemap: ${siteConfig.url}/sitemap.xml\n`;
71
+
72
+ if (Array.isArray(robotsConfig.customLines) && robotsConfig.customLines.length) {
73
+
74
+ const filteredCustomLines = robotsConfig.customLines.filter(
75
+ line => !line.trim().toLowerCase().startsWith('sitemap:')
76
+ );
77
+
78
+ if (filteredCustomLines.length > 0) {
79
+ content += filteredCustomLines.join('\n') + '\n';
80
+ }
81
+ }
82
+
83
+ // Only write file if we have content
84
+ if (content) {
85
+ fs.writeFileSync(robotsTxtPath, content, 'utf8');
86
+ console.log('✅ robots.txt generated successfully');
87
+
88
+ } else {
89
+ console.warn('⚠️ Warning: No robots.txt rules defined in config');
90
+ }
91
+
92
+ } catch (error) {
93
+ console.error('❌ Error generating robots.txt:', error);
94
+ }
95
+ }
96
+ };
97
+ };
@@ -0,0 +1,159 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const React = require('react');
4
+ const ReactDOMServer = require('react-dom/server');
5
+ const { iconMap } = require('../config/iconMappings');
6
+
7
+ /// AI Generated
8
+
9
+ /**
10
+ * Dynamically requires a React Icon
11
+ * @param {string} iconPath - Path in format "package/IconName" (e.g., "ai/AiFillAlert")
12
+ * @returns {Object|null} The icon component or null if not found
13
+ */
14
+ function requireDynamicIcon(iconPath) {
15
+ try {
16
+ const [packageName, iconName] = iconPath.split('/');
17
+
18
+ if (packageName && iconName) {
19
+ const iconPackage = require(`react-icons/${packageName}`);
20
+ return iconPackage[iconName];
21
+ }
22
+ return null;
23
+ } catch (error) {
24
+ console.error(`Failed to import icon ${iconPath}:`, error.message);
25
+ return null;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Cleans SVG string to create a properly formatted standalone SVG file
31
+ * @param {string} svgString - The SVG string from ReactDOMServer
32
+ * @param {string} color - The color to apply to the SVG
33
+ * @returns {string} Clean SVG content
34
+ */
35
+ function cleanSvgString(svgString, color) {
36
+
37
+ // Remove React-specific attributes and add proper XML declaration
38
+ let cleanedSvg = svgString
39
+ .replace(/<!--.*?-->/g, '') // Remove comments
40
+ .replace(/stroke="currentColor"/g, `stroke="${color}"`)
41
+ .replace(/fill="currentColor"/g, `fill="${color}"`);
42
+
43
+ // Add XML declaration if not present
44
+ if (!cleanedSvg.includes('<?xml')) {
45
+ cleanedSvg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n${cleanedSvg}`;
46
+ }
47
+
48
+ // Add viewBox if not present
49
+ if (!cleanedSvg.includes('viewBox')) {
50
+ cleanedSvg = cleanedSvg.replace('<svg', '<svg viewBox="0 0 24 24"');
51
+ }
52
+
53
+ return cleanedSvg;
54
+ }
55
+
56
+ /**
57
+ * Extracts SVG content from a React Icon component and saves it to a file
58
+ *
59
+ * @param {React.ComponentType|string} icon - Icon component, name from iconMap, or path like "ai/AiFillAlert"
60
+ * @param {string} outputPath - Path to save the SVG file, defaults to static/img/svg if not provided
61
+ * @param {object} options - Additional options
62
+ * @param {string} options.color - Icon color (default: 'white')
63
+ * @param {number} options.size - Icon size (default: 24)
64
+ * @param {string} options.filename - Override filename (default: icon-<name>.svg)
65
+ * @param {string} options.iconName - Specify icon name instead of using component name
66
+ * @returns {string} The path to the saved SVG file
67
+ */
68
+ function extractSvg(icon, outputPath = null, options = {}) {
69
+
70
+ // Default output path
71
+ if (!outputPath) {
72
+ outputPath = path.join(path.resolve(__dirname, '../..'), 'static', 'img', 'svg');
73
+ }
74
+
75
+ // Ensure the base output directory exists
76
+ const baseOutputDir = typeof outputPath === 'string' && !path.extname(outputPath)
77
+ ? outputPath
78
+ : path.dirname(outputPath);
79
+
80
+ if (!fs.existsSync(baseOutputDir)) {
81
+ fs.mkdirSync(baseOutputDir, { recursive: true });
82
+ console.log(`[INFO] Created directory: ${baseOutputDir}`);
83
+ }
84
+
85
+ // Resolve icon component
86
+ let IconComponent = icon;
87
+ let iconIdentifier = null;
88
+
89
+ if (typeof icon === 'string') {
90
+
91
+ if (icon.includes('/')) {
92
+
93
+ // ("ai/AiFillAlert")
94
+ IconComponent = requireDynamicIcon(icon);
95
+ iconIdentifier = icon.split('/')[1];
96
+
97
+ } else {
98
+
99
+ // Icon from the iconMap
100
+ IconComponent = iconMap[icon.toLowerCase()]?.icon;
101
+ iconIdentifier = icon;
102
+ }
103
+
104
+ if (!IconComponent) {
105
+ throw new Error(`Icon "${icon}" could not be resolved`);
106
+ }
107
+
108
+ // Use identifier if iconName not specified
109
+ if (!options.iconName && iconIdentifier) {
110
+ options.iconName = iconIdentifier;
111
+ }
112
+ }
113
+
114
+ const { color = 'white', size = 24, filename = null, iconName = null } = options;
115
+
116
+ // Determine final output path
117
+ const pathStats = fs.existsSync(outputPath) ? fs.statSync(outputPath) : null;
118
+ const isDirectory = pathStats ? pathStats.isDirectory() : false;
119
+
120
+ if (isDirectory || filename) {
121
+ const baseDir = isDirectory ? outputPath : path.dirname(outputPath);
122
+
123
+ // Create directory if needed
124
+ if (!fs.existsSync(baseDir)) {
125
+ fs.mkdirSync(baseDir, { recursive: true });
126
+ }
127
+
128
+ // Generate filename if not provided
129
+ const componentName = iconName || IconComponent.name;
130
+ const finalFilename = filename || `icon-${componentName}.svg`;
131
+ outputPath = path.join(baseDir, finalFilename);
132
+ }
133
+
134
+ // Create and render the icon
135
+ const element = React.createElement(IconComponent, {
136
+ color,
137
+ size,
138
+ style: { color }
139
+ });
140
+
141
+ // Render the React element to an HTML string and clean it
142
+ const svgString = ReactDOMServer.renderToStaticMarkup(element);
143
+ const cleanedSvg = cleanSvgString(svgString, color);
144
+
145
+ // Ensure final output path exists
146
+ const finalDir = path.dirname(outputPath);
147
+
148
+ if (!fs.existsSync(finalDir)) {
149
+ fs.mkdirSync(finalDir, { recursive: true });
150
+ }
151
+
152
+ // Write to file
153
+ fs.writeFileSync(outputPath, cleanedSvg);
154
+ console.log(`[INFO] Generated SVG icon: ${outputPath}`);
155
+
156
+ return outputPath;
157
+ }
158
+
159
+ module.exports = { extractSvg, iconMap };
@@ -0,0 +1,88 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const https = require('https');
4
+
5
+
6
+ // AI GENERATED - Partially
7
+
8
+ /**
9
+ * Downloads or copies an image from a URL or local file path to a specified directory
10
+ *
11
+ * @param {string} source - The URL or local file path of the image
12
+ * @param {string} outputDir - The directory where the image should be saved
13
+ * @param {string} [filename='temp_image.png'] - The filename to use for the saved image
14
+ * @returns {Promise<string>} - A promise that resolves with the path to the saved image
15
+ */
16
+ async function downloadImage(source, outputDir, filename = 'temp_image.png') {
17
+
18
+ const tmpImagePath = path.resolve(outputDir, filename);
19
+
20
+ // Check if the source is a local file path or a URL
21
+ const isUrl = source.startsWith('http://') || source.startsWith('https://');
22
+
23
+ if (isUrl) {
24
+ console.log(`[INFO] Downloading remote image from: ${source}`);
25
+
26
+ return new Promise((resolve, reject) => {
27
+ const file = fs.createWriteStream(tmpImagePath);
28
+
29
+ https.get(source, (response) => {
30
+ if (response.statusCode !== 200) {
31
+ reject(new Error(`Failed to download image, status code: ${response.statusCode}`));
32
+ return;
33
+ }
34
+
35
+ response.pipe(file);
36
+
37
+ file.on('finish', () => {
38
+ file.close();
39
+ resolve(tmpImagePath);
40
+ });
41
+
42
+ file.on('error', (err) => {
43
+ fs.unlink(tmpImagePath, () => {});
44
+ reject(err);
45
+ });
46
+ }).on('error', (err) => {
47
+ fs.unlink(tmpImagePath, () => {});
48
+ reject(err);
49
+ });
50
+ });
51
+ } else {
52
+
53
+ // local file path
54
+ console.log(`[INFO] Copying local image from: ${source}`);
55
+
56
+ return new Promise((resolve, reject) => {
57
+
58
+ // Check if source file exists
59
+ if (!fs.existsSync(source)) {
60
+ reject(new Error(`Source file does not exist: ${source}`));
61
+ return;
62
+ }
63
+
64
+ // Create a read stream from the source file
65
+ const readStream = fs.createReadStream(source);
66
+ const writeStream = fs.createWriteStream(tmpImagePath);
67
+
68
+ // Pipe the read stream to the write stream
69
+ readStream.pipe(writeStream);
70
+
71
+ writeStream.on('finish', () => {
72
+ resolve(tmpImagePath);
73
+ });
74
+
75
+ writeStream.on('error', (err) => {
76
+ fs.unlink(tmpImagePath, () => {});
77
+ reject(err);
78
+ });
79
+
80
+ readStream.on('error', (err) => {
81
+ fs.unlink(tmpImagePath, () => {});
82
+ reject(err);
83
+ });
84
+ });
85
+ }
86
+ }
87
+
88
+ module.exports = { downloadImage };
@@ -0,0 +1,134 @@
1
+ const sharp = require('sharp');
2
+
3
+ // AI GENERATED
4
+
5
+ /**
6
+ * Reshapes an image to various predefined shapes
7
+ *
8
+ * @param {string} imagePath - Path to the input image
9
+ * @param {string} outputPath - Path to save the processed image
10
+ * @param {string} [shape='circle'] - Shape to apply ('circle', 'square', 'rounded', 'hexagon', 'shield')
11
+ * @param {number} [roundedCornerRadius=50] - Corner radius for rounded shape (percentage)
12
+ * @returns {Promise<string>} - A promise that resolves with the path to the processed image
13
+ */
14
+ async function reshapeImage(imagePath, outputPath, shape = 'circle', roundedCornerRadius = 50) {
15
+ console.log(`[INFO] Processing image to ${shape} shape...`);
16
+
17
+ try {
18
+ // Get image dimensions
19
+ const metadata = await sharp(imagePath).metadata();
20
+ const size = Math.min(metadata.width, metadata.height);
21
+
22
+ let shapeMask;
23
+
24
+ switch (shape.toLowerCase()) {
25
+ case 'circle':
26
+ shapeMask = Buffer.from(
27
+ `<svg><circle cx="${size/2}" cy="${size/2}" r="${size/2}" fill="white"/></svg>`
28
+ );
29
+ break;
30
+
31
+ case 'square':
32
+ // No mask needed for square, just resize to equal width/height
33
+ await sharp(imagePath)
34
+ .resize(size, size, { fit: 'cover' })
35
+ .toFile(outputPath);
36
+ return outputPath;
37
+
38
+ case 'rounded':
39
+ // Convert percentage to actual pixels (e.g., 50% becomes size/4)
40
+ const radius = Math.min(100, Math.max(0, roundedCornerRadius)) * size / 200;
41
+ shapeMask = Buffer.from(
42
+ `<svg><rect x="0" y="0" width="${size}" height="${size}" rx="${radius}" ry="${radius}" fill="white"/></svg>`
43
+ );
44
+ break;
45
+
46
+ case 'hexagon':
47
+ // Calculate hexagon points
48
+ const centerX = size / 2;
49
+ const centerY = size / 2;
50
+ const hexRadius = size / 2;
51
+ let points = '';
52
+ for (let i = 0; i < 6; i++) {
53
+ const angleDeg = 60 * i - 30;
54
+ const angleRad = Math.PI / 180 * angleDeg;
55
+ const x = centerX + hexRadius * Math.cos(angleRad);
56
+ const y = centerY + hexRadius * Math.sin(angleRad);
57
+ points += `${x},${y} `;
58
+ }
59
+ shapeMask = Buffer.from(
60
+ `<svg><polygon points="${points}" fill="white"/></svg>`
61
+ );
62
+ break;
63
+
64
+ case 'shield':
65
+ const shieldWidth = size;
66
+ const shieldHeight = size;
67
+ const bottomCurve = size / 3;
68
+ shapeMask = Buffer.from(
69
+ `<svg>
70
+ <path d="M ${shieldWidth/2},0
71
+ L ${shieldWidth},${shieldHeight/3}
72
+ C ${shieldWidth},${shieldHeight-bottomCurve} ${shieldWidth/2},${shieldHeight} ${shieldWidth/2},${shieldHeight}
73
+ C ${shieldWidth/2},${shieldHeight} 0,${shieldHeight-bottomCurve} 0,${shieldHeight/3}
74
+ L ${shieldWidth/2},0 Z"
75
+ fill="white"/>
76
+ </svg>`
77
+ );
78
+ break;
79
+
80
+ default:
81
+ // Default to circle if shape is not recognized
82
+ console.warn(`Shape "${shape}" not recognized, defaulting to circle`);
83
+ shapeMask = Buffer.from(
84
+ `<svg><circle cx="${size/2}" cy="${size/2}" r="${size/2}" fill="white"/></svg>`
85
+ );
86
+ }
87
+
88
+ // Apply the mask and save the final image
89
+ await sharp(imagePath)
90
+ .resize(size, size, { fit: 'cover' })
91
+ .composite([
92
+ {
93
+ input: shapeMask,
94
+ blend: 'dest-in'
95
+ }
96
+ ])
97
+ .png()
98
+ .toFile(outputPath);
99
+
100
+ return outputPath;
101
+ } catch (error) {
102
+ console.error(`Error reshaping image to ${shape}:`, error);
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Resizes an image while maintaining aspect ratio
109
+ *
110
+ * @param {string} imagePath - Path to the input image
111
+ * @param {string} outputPath - Path to save the processed image
112
+ * @param {number} maxWidth - Maximum width of the output image
113
+ * @param {number} maxHeight - Maximum height of the output image
114
+ * @returns {Promise<string>} - A promise that resolves with the path to the processed image
115
+ */
116
+ async function resizeImage(imagePath, outputPath, maxWidth, maxHeight) {
117
+ console.log(`Resizing image to max ${maxWidth}x${maxHeight}...`);
118
+
119
+ await sharp(imagePath)
120
+ .resize(maxWidth, maxHeight, {
121
+ fit: 'inside',
122
+ withoutEnlargement: true
123
+ })
124
+ .toFile(outputPath);
125
+
126
+ return outputPath;
127
+ }
128
+
129
+
130
+
131
+ module.exports = {
132
+ reshapeImage,
133
+ resizeImage,
134
+ };
File without changes
@@ -0,0 +1,107 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useLocation } from '@docusaurus/router';
3
+ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
4
+
5
+ // AI Generated
6
+
7
+ /**
8
+ * UpdateTitle component that changes the page title based on the section in view
9
+ * @param {Object} props - Component props
10
+ * @param {Object} props.sections - Map of section IDs to their corresponding titles
11
+ * @param {string} props.defaultTitle - Default title to use when no section is prominently visible
12
+ * @param {boolean} props.enabled - Whether the dynamic title functionality is enabled
13
+ * @returns {null} This component doesn't render anything visible
14
+ */
15
+
16
+
17
+ export default function UpdateTitle({
18
+ sections = {},
19
+ defaultTitle = null,
20
+ enabled = true
21
+ }) {
22
+ const location = useLocation();
23
+ const { siteConfig } = useDocusaurusContext();
24
+ const [currentTitle, setCurrentTitle] = useState(null);
25
+
26
+ // Use the provided default title or fall back to site title
27
+ const effectiveDefaultTitle = defaultTitle || siteConfig.title;
28
+
29
+ useEffect(() => {
30
+
31
+ // Only run if enabled
32
+ if (!enabled) return;
33
+
34
+ // Use provided sections or default to empty object
35
+ const sectionTitles = Object.keys(sections).length > 0
36
+ ? sections
37
+ : {};
38
+
39
+ const updateTitle = () => {
40
+ // Get all sections we want to track
41
+ const sectionsToTrack = Object.keys(sectionTitles)
42
+ .map(id => document.getElementById(id))
43
+ .filter(Boolean);
44
+
45
+ if (sectionsToTrack.length === 0) {
46
+ // No sections found, use default title
47
+ setCurrentTitle(effectiveDefaultTitle);
48
+ return;
49
+ }
50
+
51
+ // Calculate which section is most visible
52
+ const viewportHeight = window.innerHeight;
53
+ let maxVisibleSection = null;
54
+ let maxVisibleArea = 0;
55
+
56
+ sectionsToTrack.forEach(section => {
57
+ const rect = section.getBoundingClientRect();
58
+ const visibleTop = Math.max(0, rect.top);
59
+ const visibleBottom = Math.min(viewportHeight, rect.bottom);
60
+ const visibleArea = Math.max(0, visibleBottom - visibleTop);
61
+
62
+ if (visibleArea > maxVisibleArea) {
63
+ maxVisibleArea = visibleArea;
64
+ maxVisibleSection = section.id;
65
+ }
66
+ });
67
+
68
+ // Update title state based on visible section
69
+ if (maxVisibleSection && sectionTitles[maxVisibleSection]) {
70
+ setCurrentTitle(sectionTitles[maxVisibleSection]);
71
+ } else {
72
+ setCurrentTitle(effectiveDefaultTitle);
73
+ }
74
+ };
75
+
76
+ // Add scroll event listener with throttling
77
+ let isScrolling = false;
78
+ const handleScroll = () => {
79
+ if (!isScrolling) {
80
+ window.requestAnimationFrame(() => {
81
+ updateTitle();
82
+ isScrolling = false;
83
+ });
84
+ isScrolling = true;
85
+ }
86
+ };
87
+
88
+ window.addEventListener('scroll', handleScroll);
89
+
90
+ // Initial call
91
+ updateTitle();
92
+
93
+ // Clean up
94
+ return () => {
95
+ window.removeEventListener('scroll', handleScroll);
96
+ };
97
+ }, [location.pathname, sections, effectiveDefaultTitle, enabled]);
98
+
99
+ useEffect(() => {
100
+ if (currentTitle) {
101
+ document.title = currentTitle;
102
+ }
103
+ }, [currentTitle]);
104
+
105
+ // Component doesn't render anything visible
106
+ return null;
107
+ }