website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
assets/client.js
| 60e980e | 1 | // All client-side JS the site ships, in one plain vanilla file — no |
| 60e980e | 2 | // bundler needed for two small, independent event listeners. |
| 60e980e | 3 | |
| ada6e53 | 4 | // Wires up the theme button rendered as static markup in site-header.njk |
| ada6e53 | 5 | // (no web component, no framework — a stateless click-to-toggle button |
| ada6e53 | 6 | // doesn't need one). Loaded with `defer` so this runs after the DOM is |
| ada6e53 | 7 | // parsed and #theme-change already exists. |
| ada6e53 | 8 | document.getElementById("theme-change").addEventListener("click", () => { |
| ada6e53 | 9 | const root = document.documentElement; |
| ada6e53 | 10 | // data-theme is tri-state: absent means "follow the OS preference" (see |
| ada6e53 | 11 | // `color-scheme: light dark` in shared.css). Resolve against that OS |
| ada6e53 | 12 | // preference when no explicit value is set yet, so the first click |
| ada6e53 | 13 | // always flips to the opposite of what's *currently shown* rather than |
| ada6e53 | 14 | // assuming light. |
| ada6e53 | 15 | const currentTheme = root.getAttribute("data-theme"); |
| ada6e53 | 16 | const isDark = currentTheme ? currentTheme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches; |
| ada6e53 | 17 | const next = isDark ? "light" : "dark"; |
| ada6e53 | 18 | root.setAttribute("data-theme", next); |
| ada6e53 | 19 | localStorage.setItem("theme", next); |
| ada6e53 | 20 | }); |
| 60e980e | 21 | |
| 60e980e | 22 | // .file-explorer-tree lives outside #file-pane (see the comment on |
| 60e980e | 23 | // .file-explorer in repos.css) so htmx swaps of #file-pane never re-render |
| 60e980e | 24 | // it — clicking a file never rebuilds the tree. That's good for keeping |
| 60e980e | 25 | // scroll position/expanded folders, but it also means the .active-file |
| 60e980e | 26 | // class painted at page-load time never moves on its own. Move it here, |
| 60e980e | 27 | // synchronously on click, instead of waiting on htmx's response. |
| 60e980e | 28 | document.addEventListener("click", (e) => { |
| 60e980e | 29 | const link = e.target.closest(".file-explorer-tree a"); |
| 60e980e | 30 | if (!link) return; |
| 60e980e | 31 | const tree = link.closest(".file-explorer-tree"); |
| 60e980e | 32 | const current = tree.querySelector("a.active-file"); |
| 60e980e | 33 | if (current) current.classList.remove("active-file"); |
| 60e980e | 34 | link.classList.add("active-file"); |
| 60e980e | 35 | }); |
| 8be9c52 | 36 | |
| 8be9c52 | 37 | // Copies the git clone command shown in the repo header (see .clone-cmd in |
| 8be9c52 | 38 | // repos.css) and flips the button to a checkmark for a bit as feedback, |
| 8be9c52 | 39 | // instead of a tooltip/toast that would need its own positioning logic. |
| 8be9c52 | 40 | document.addEventListener("click", (e) => { |
| 8be9c52 | 41 | const btn = e.target.closest(".copy-btn"); |
| 8be9c52 | 42 | if (!btn) return; |
| 8be9c52 | 43 | navigator.clipboard.writeText(btn.dataset.clone).then(() => { |
| 8be9c52 | 44 | clearTimeout(btn._copyResetTimer); |
| 8be9c52 | 45 | btn.classList.add("copied"); |
| 8be9c52 | 46 | btn._copyResetTimer = setTimeout(() => btn.classList.remove("copied"), 1500); |
| 8be9c52 | 47 | }); |
| 8be9c52 | 48 | }); |