From: Danilo M. Date: Sun, 5 Apr 2026 06:40:09 +0000 (+0200) Subject: feat: add JavaScript modules (theme toggle, matrix rain, progress tracking, copy... X-Git-Url: https://git.danix.xyz/?a=commitdiff_plain;h=dcf54cad8529526fd7f8d9d4b84b63ccb3fa9630;p=danix2-hugo-theme.git feat: add JavaScript modules (theme toggle, matrix rain, progress tracking, copy-to-clipboard) Implement all 4 JavaScript modules: - theme-toggle.js: Theme switching with localStorage persistence - matrix-rain.js: Animated matrix-style rain effect on canvas - progress-bar.js: Reading progress tracking during scroll - copy-code.js: Copy-to-clipboard functionality for code blocks Co-Authored-By: Claude Haiku 4.5 --- diff --git a/assets/js/copy-code.js b/assets/js/copy-code.js new file mode 100644 index 0000000..a18bf6c --- /dev/null +++ b/assets/js/copy-code.js @@ -0,0 +1,42 @@ +// copy-code.js +(function() { + // Add copy button to all code blocks + const codeBlocks = document.querySelectorAll('pre, .highlight'); + + codeBlocks.forEach(block => { + // Create copy button + const btn = document.createElement('button'); + btn.className = 'code-copy-btn'; + btn.textContent = 'copy'; + btn.type = 'button'; + btn.setAttribute('aria-label', 'Copy code'); + + // Get code text + const code = block.querySelector('code'); + const text = code ? code.textContent : block.textContent; + + // Copy on click + btn.addEventListener('click', async function() { + try { + await navigator.clipboard.writeText(text); + + // Show feedback + const originalText = btn.textContent; + btn.textContent = 'copied!'; + btn.classList.add('copied'); + + setTimeout(() => { + btn.textContent = originalText; + btn.classList.remove('copied'); + }, 2000); + } catch (err) { + console.error('Failed to copy:', err); + btn.textContent = 'error'; + } + }); + + // Add button to block + block.style.position = 'relative'; + block.appendChild(btn); + }); +})(); diff --git a/assets/js/main.js b/assets/js/main.js index e2aac52..5b5848c 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1 +1,5 @@ -console.log('This site was generated by Hugo.'); +// main.js +import './theme-toggle.js'; +import './matrix-rain.js'; +import './progress-bar.js'; +import './copy-code.js'; diff --git a/assets/js/matrix-rain.js b/assets/js/matrix-rain.js new file mode 100644 index 0000000..479231f --- /dev/null +++ b/assets/js/matrix-rain.js @@ -0,0 +1,65 @@ +// matrix-rain.js +(function () { + const canvas = document.getElementById('matrix-canvas'); + if (!canvas) return; + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + + const ctx = canvas.getContext('2d'); + const CHARS = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789ABCDEF<>/\\|{}[]$#@!'; + const FS = 14; // font size / column width in px + let cols, drops, raf; + + function init() { + canvas.width = canvas.offsetWidth; + canvas.height = canvas.offsetHeight; + cols = Math.floor(canvas.width / FS) + 1; + drops = Array.from({ length: cols }, () => Math.random() * -(canvas.height / FS)); + } + + function tick() { + const light = document.documentElement.classList.contains('theme-light'); + // Fade trail: near-transparent fill each frame + ctx.fillStyle = light ? 'rgba(240,244,248,0.07)' : 'rgba(6,11,16,0.055)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.font = `${FS}px "JetBrains Mono", monospace`; + + for (let i = 0; i < cols; i++) { + const char = CHARS[Math.floor(Math.random() * CHARS.length)]; + // 4% chance of bright "head" char, otherwise use accent color + ctx.fillStyle = Math.random() > 0.96 + ? (light ? '#008f5a' : '#00ff88') // bright green head + : (light ? '#7c3aed' : '#a855f7'); // purple trail + ctx.fillText(char, i * FS, drops[i] * FS); + + if (drops[i] * FS > canvas.height && Math.random() > 0.975) { + drops[i] = Math.random() * -20; // reset column randomly + } + drops[i] += 0.5; // slow fall speed + } + raf = requestAnimationFrame(tick); + } + + // Listen for theme changes and reinit + window.addEventListener('theme-changed', function() { + // Matrix rain auto-colors based on theme-light class + }, { passive: true }); + + init(); + window.addEventListener('resize', () => { + cancelAnimationFrame(raf); + init(); + tick(); + }, { passive: true }); + + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + cancelAnimationFrame(raf); + } else { + tick(); + } + }); + + tick(); + + window.MatrixRain = { init, tick }; +})(); diff --git a/assets/js/progress-bar.js b/assets/js/progress-bar.js new file mode 100644 index 0000000..bc8b70a --- /dev/null +++ b/assets/js/progress-bar.js @@ -0,0 +1,38 @@ +// progress-bar.js +(function() { + const progressBar = document.querySelector('.reading-progress'); + if (!progressBar) return; + + // Only enable on pages with substantial content + const mainContent = document.querySelector('main'); + if (!mainContent) return; + + function updateProgress() { + // Calculate scroll percentage + const windowHeight = window.innerHeight; + const docHeight = document.documentElement.scrollHeight - windowHeight; + const scrolled = window.scrollY; + const percent = docHeight > 0 ? (scrolled / docHeight) * 100 : 0; + + progressBar.style.width = percent + '%'; + } + + // Mark body as scrollable if there's significant content + const contentHeight = mainContent.offsetHeight; + if (contentHeight > window.innerHeight * 1.5) { + document.body.classList.add('scrollable'); + } + + // Use requestAnimationFrame for smooth updates + let ticking = false; + window.addEventListener('scroll', function() { + if (!ticking) { + requestAnimationFrame(updateProgress); + ticking = true; + setTimeout(() => { ticking = false; }, 100); + } + }, { passive: true }); + + // Initial update + updateProgress(); +})(); diff --git a/assets/js/theme-toggle.js b/assets/js/theme-toggle.js new file mode 100644 index 0000000..9f0fd5a --- /dev/null +++ b/assets/js/theme-toggle.js @@ -0,0 +1,83 @@ +// theme-toggle.js +(function() { + const STORAGE_KEY = 'danix-theme'; + const DARK_CLASS = 'theme-dark'; + const LIGHT_CLASS = 'theme-light'; + + // Initialize theme on page load + function init() { + const saved = localStorage.getItem(STORAGE_KEY); + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const isDark = saved === null ? prefersDark : saved === 'dark'; + + applyTheme(isDark ? 'dark' : 'light'); + } + + // Apply theme to document + function applyTheme(theme) { + const html = document.documentElement; + + html.classList.remove(DARK_CLASS, LIGHT_CLASS); + + if (theme === 'dark') { + html.classList.remove(LIGHT_CLASS); + localStorage.setItem(STORAGE_KEY, 'dark'); + } else { + html.classList.add(LIGHT_CLASS); + localStorage.setItem(STORAGE_KEY, 'light'); + } + } + + // Get current theme + function getCurrentTheme() { + return document.documentElement.classList.contains(LIGHT_CLASS) ? 'light' : 'dark'; + } + + // Toggle theme + function toggleTheme() { + const current = getCurrentTheme(); + const next = current === 'dark' ? 'light' : 'dark'; + applyTheme(next); + + // Dispatch custom event for other scripts to listen + window.dispatchEvent(new CustomEvent('theme-changed', { detail: { theme: next } })); + } + + // Setup toggle button + function setupToggleButton() { + const btn = document.getElementById('theme-toggle-btn'); + if (btn) { + btn.addEventListener('click', toggleTheme); + updateToggleButtonLabel(); + + // Listen for theme changes to update button label + window.addEventListener('theme-changed', updateToggleButtonLabel); + } + } + + function updateToggleButtonLabel() { + const btn = document.getElementById('theme-toggle-btn'); + if (btn) { + const current = getCurrentTheme(); + btn.textContent = current === 'dark' ? '☀️ light' : '🌙 dark'; + } + } + + // Initialize on DOMContentLoaded + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + init(); + setupToggleButton(); + }); + } else { + init(); + setupToggleButton(); + } + + // Expose to global scope for testing + window.ThemeToggle = { + toggle: toggleTheme, + set: applyTheme, + get: getCurrentTheme, + }; +})();