--- /dev/null
+// 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);
+ });
+})();
-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';
--- /dev/null
+// 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 };
+})();
--- /dev/null
+// 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();
+})();
--- /dev/null
+// 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,
+ };
+})();