import Prism from 'prismjs'; import 'prismjs/components/prism-css'; import 'prismjs/components/prism-javascript'; import 'prismjs/components/prism-markup'; import { initOneko } from './oneko'; import { createEditor, setEditorTheme } from './editor'; import type { EditorView } from 'codemirror'; type Theme = 'light' | 'dark'; const THEME_STORAGE_KEY = 'theme'; const DARK_THEME_ATTRIBUTE = 'data-theme'; let editorInstance: EditorView | null = null; function detectAssetBase(): string { const currentScript = document.currentScript as HTMLScriptElement | null; const scriptSrc = currentScript?.src ?? document.querySelector('script[src*="bundle"]')?.src ?? ''; if (!scriptSrc) return ''; try { return new URL(scriptSrc, window.location.href).origin; } catch { return ''; } } function setAssetBase(): void { window.ASSET_BASE = detectAssetBase(); } function getSystemTheme(): Theme { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } function applyTheme(theme: Theme): void { if (theme === 'dark') { document.documentElement.setAttribute(DARK_THEME_ATTRIBUTE, 'dark'); } else { document.documentElement.removeAttribute(DARK_THEME_ATTRIBUTE); } } function getStoredTheme(): Theme | null { const theme = sessionStorage.getItem(THEME_STORAGE_KEY); return theme === 'dark' || theme === 'light' ? theme : null; } function initThemeToggle(): void { const toggleButton = document.getElementById('theme-toggle'); const initialTheme = getStoredTheme() ?? getSystemTheme(); applyTheme(initialTheme); if (!(toggleButton instanceof HTMLInputElement)) return; toggleButton.checked = initialTheme === 'dark'; toggleButton.addEventListener('change', () => { const nextTheme: Theme = toggleButton.checked ? 'dark' : 'light'; applyTheme(nextTheme); sessionStorage.setItem(THEME_STORAGE_KEY, nextTheme); if (editorInstance) { setEditorTheme(editorInstance, nextTheme); } }); } function sample(items: readonly T[]): T { return items[Math.floor(Math.random() * items.length)]; } function initFairyDust(): void { if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; const colors = ['#ff69b4', '#b19cd9', '#8b6f47', '#ff85c0', '#c4b5fd', '#d4a574'] as const; const shapes = ['❀', '✿', '✽', '✾', '✻', '❊', '❋', '✼'] as const; const particleLifetimeMs = 800; function createParticle(x: number, y: number): void { const particle = document.createElement('div'); particle.className = 'fairy-dust'; const offsetX = (Math.random() - 0.5) * 20; const offsetY = (Math.random() - 0.5) * 20; const rotationDeg = Math.random() * 360; const sizePx = Math.random() * 8 + 6; particle.textContent = sample(shapes); particle.style.left = `${x + offsetX}px`; particle.style.top = `${y + offsetY}px`; particle.style.fontSize = `${sizePx}px`; particle.style.color = sample(colors); particle.style.transform = `rotate(${rotationDeg}deg)`; document.body.appendChild(particle); window.setTimeout(() => particle.remove(), particleLifetimeMs); } // rAF throttle to avoid creating a particle per mouse event. let latestX = 0; let latestY = 0; let scheduled = false; document.addEventListener('mousemove', (event: MouseEvent) => { latestX = event.clientX; latestY = event.clientY; if (scheduled) return; scheduled = true; window.requestAnimationFrame(() => { scheduled = false; createParticle(latestX, latestY); }); }); } function initFileInputs(): void { const fileInputs = document.querySelectorAll('input[type="file"]'); fileInputs.forEach((input) => { const label = document.querySelector( `label.file-input-button[for="${input.id}"]` ); if (!label) return; const originalText = label.textContent || 'Browse Files'; input.addEventListener('change', () => { const files = input.files; if (!files || files.length === 0) { label.textContent = originalText; label.classList.remove('has-file'); return; } label.classList.add('has-file'); if (files.length === 1) { label.textContent = `✓ ${files[0].name}`; } else { label.textContent = `✓ ${files.length} files selected`; } }); }); } function init(): void { setAssetBase(); initThemeToggle(); initFairyDust(); initFileInputs(); Prism.highlightAll(); initOneko(); } document.addEventListener('DOMContentLoaded', init);