diff --git a/eslint.config.cjs b/eslint.config.cjs index 5a1c52e39..42748c817 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -223,4 +223,37 @@ module.exports = [ "no-console": "off", }, }, + // Website browser scripts — run in the browser, not Node.js + { + files: ["website/src/scripts/**/*.js"], + languageOptions: { + parserOptions: { + ecmaVersion: 2024, + sourceType: "module", + }, + globals: { + // Browser globals + document: "readonly", + window: "readonly", + navigator: "readonly", + localStorage: "readonly", + sessionStorage: "readonly", + CustomEvent: "readonly", + Event: "readonly", + IntersectionObserver: "readonly", + MutationObserver: "readonly", + requestAnimationFrame: "readonly", + clearTimeout: "readonly", + setTimeout: "readonly", + console: "readonly", + }, + }, + plugins: { prettier }, + rules: { + "prettier/prettier": "warn", + "no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], + "no-console": "off", + "no-empty": ["error", { allowEmptyCatch: true }], + }, + }, ]; diff --git a/website/src/components/Header.astro b/website/src/components/Header.astro new file mode 100644 index 000000000..6f04c793c --- /dev/null +++ b/website/src/components/Header.astro @@ -0,0 +1,675 @@ +--- +import { CATEGORIES } from '../lib/catalogue'; + +const base = import.meta.env.BASE_URL; +const pathname = Astro.url.pathname; + +const isActive = (path: string): boolean => { + const baseClean = base.replace(/\/$/, ''); + const rawNorm = pathname.replace(/\/$/, ''); + const norm = baseClean && rawNorm.startsWith(baseClean) ? rawNorm.slice(baseClean.length) : rawNorm; + const p = path.replace(/\/$/, ''); + return norm === p || norm.startsWith(p + '/'); +}; + +/* Icon path data (Heroicons-style, 24×24, 1.6 stroke) */ +const icons = { + chevronDown: ``, + search: ``, + robot: ``, + book: ``, + chat: ``, + sparkles: ``, + shield: ``, + workflow: ``, + puzzle: ``, + wrench: ``, + bolt: ``, + download: ``, + layers: ``, + github: ``, + external: ``, + menu: ``, + close: ``, +}; + +const catIconMap: Record = { + agents: icons.robot, instructions: icons.book, prompts: icons.chat, + skills: icons.sparkles, hooks: icons.shield, workflows: icons.workflow, + plugins: icons.puzzle, tools: icons.wrench, +}; + +function svg(paths: string, size = 20) { + return `${paths}`; +} + +const resources = [ + { href: `${base}onboarding/`, icon: icons.bolt, label: 'Onboarding journey' }, + { href: `${base}getting-started/`, icon: icons.download, label: 'Getting started' }, + { href: `${base}why/`, icon: icons.layers, label: 'Why this exists' }, + { href: `${base}glossary/`, icon: icons.book, label: 'Glossary' }, + { href: `${base}references/`, icon: icons.github, label: 'References' }, +]; +--- + + + + + + + + + + + + + + + diff --git a/website/src/layouts/AwesomeGithubLayout.astro b/website/src/layouts/AwesomeGithubLayout.astro index 1e079fed6..998091cb6 100644 --- a/website/src/layouts/AwesomeGithubLayout.astro +++ b/website/src/layouts/AwesomeGithubLayout.astro @@ -1,21 +1,10 @@ --- import "../styles/global.css"; import "../styles/awesome-github.css"; -import AwesomeGithubNav from "../components/AwesomeGithub/AwesomeGithubNav.astro"; +import Header from "../components/Header.astro"; import AwesomeGithubFooter from "../components/AwesomeGithub/AwesomeGithubFooter.astro"; -import { ITEMS } from "../lib/catalogue"; const base = import.meta.env.BASE_URL; -const pathname = Astro.url.pathname.replace(base, "/"); - -const searchItems = ITEMS.map((i) => ({ - id: i.id, - slug: i.slug, - cat: i.cat, - name: i.name, - description: i.description, - tags: i.tags, -})); const { title = "Awesome GitHub", @@ -38,33 +27,11 @@ const themeInitScript = `(function(){try{var s='ag-theme',t=localStorage.getItem Skip to main content - +
- diff --git a/website/src/scripts/header.js b/website/src/scripts/header.js new file mode 100644 index 000000000..e5284b5b7 --- /dev/null +++ b/website/src/scripts/header.js @@ -0,0 +1,174 @@ +/* header.js — burger, drawer, dropdowns, scroll-shrink */ + +document.addEventListener("DOMContentLoaded", () => { + /* ── Scroll shrink ────────────────────────────── */ + const nav = document.querySelector(".nav"); + if (nav && "IntersectionObserver" in window) { + const sentinel = document.createElement("div"); + sentinel.style.cssText = + "position:absolute;top:0;height:1px;width:1px;pointer-events:none"; + document.body.prepend(sentinel); + const observer = new IntersectionObserver( + ([e]) => nav.classList.toggle("nav-shrunk", !e.isIntersecting), + { rootMargin: "-60px 0px 0px 0px", threshold: 0 }, + ); + observer.observe(sentinel); + } + + /* ── Dropdowns ───────────────────────────────── */ + document.querySelectorAll(".nav-dropdown-trigger").forEach((trigger) => { + const parent = trigger.closest(".nav-dropdown-wrap"); + const dropdown = parent?.querySelector(".nav-dropdown"); + if (!parent || !dropdown) return; + + trigger.addEventListener("click", (e) => { + e.stopPropagation(); + const isOpen = parent.classList.contains("open"); + + document.querySelectorAll(".nav-dropdown-wrap.open").forEach((d) => { + d.classList.remove("open"); + d.querySelector(".nav-dropdown-trigger")?.setAttribute( + "aria-expanded", + "false", + ); + }); + + if (!isOpen) { + parent.classList.add("open"); + trigger.setAttribute("aria-expanded", "true"); + } else { + trigger.setAttribute("aria-expanded", "false"); + } + }); + + trigger.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + parent.classList.remove("open"); + trigger.setAttribute("aria-expanded", "false"); + trigger.focus(); + } + }); + + parent.addEventListener("focusout", (e) => { + if (!parent.contains(e.relatedTarget)) { + parent.classList.remove("open"); + trigger.setAttribute("aria-expanded", "false"); + } + }); + }); + + document.addEventListener("click", () => { + document.querySelectorAll(".nav-dropdown-wrap.open").forEach((d) => { + d.classList.remove("open"); + d.querySelector(".nav-dropdown-trigger")?.setAttribute( + "aria-expanded", + "false", + ); + }); + }); + + /* ── Burger + Drawer ────────────────────────── */ + const burger = document.getElementById("burger-btn"); + const drawer = document.getElementById("nav-drawer"); + const drawerClose = document.getElementById("drawer-close"); + const drawerScrim = document.getElementById("drawer-scrim"); + + function openDrawer() { + document.body.classList.add("drawer-open"); + burger?.setAttribute("aria-expanded", "true"); + burger?.setAttribute("aria-label", "Close menu"); + drawer?.setAttribute("aria-hidden", "false"); + drawer?.removeAttribute("inert"); + Array.from(document.body.children).forEach((child) => { + if (child.id !== "nav-drawer" && child.id !== "drawer-scrim") { + child.setAttribute("inert", ""); + } + }); + drawerClose?.focus(); + } + function closeDrawer() { + document.body.classList.remove("drawer-open"); + burger?.setAttribute("aria-expanded", "false"); + burger?.setAttribute("aria-label", "Open menu"); + drawer?.setAttribute("aria-hidden", "true"); + drawer?.setAttribute("inert", ""); + Array.from(document.body.children).forEach((child) => { + if (child.id !== "nav-drawer" && child.id !== "drawer-scrim") { + child.removeAttribute("inert"); + } + }); + burger?.focus(); + } + + burger?.addEventListener("click", openDrawer); + drawerClose?.addEventListener("click", closeDrawer); + drawerScrim?.addEventListener("click", closeDrawer); + + window.matchMedia("(min-width: 1025px)").addEventListener("change", (e) => { + if (e.matches && document.body.classList.contains("drawer-open")) + closeDrawer(); + }); + + document.addEventListener("keydown", (e) => { + if (e.key === "Escape" && document.body.classList.contains("drawer-open")) { + closeDrawer(); + } + }); + + /* ── Branch toggle ──────────────────────────── */ + const BRANCH_KEY = "ag-branch"; + const branchBtns = document.querySelectorAll(".branch-btn"); + + const savedBranch = (() => { + try { + return localStorage.getItem(BRANCH_KEY) || "main"; + } catch (e) { + return "main"; + } + })(); + + branchBtns.forEach((btn) => { + const b = btn.dataset.branch; + btn.setAttribute("aria-pressed", b === savedBranch ? "true" : "false"); + btn.addEventListener("click", () => { + try { + localStorage.setItem(BRANCH_KEY, b); + } catch (e) {} + branchBtns.forEach((x) => + x.setAttribute( + "aria-pressed", + x.dataset.branch === b ? "true" : "false", + ), + ); + document.dispatchEvent( + new CustomEvent("ag:branch-changed", { detail: { branch: b } }), + ); + }); + }); + + /* ── Search palette wiring ──────────────────── */ + document + .getElementById("search-btn-mobile") + ?.addEventListener("click", () => { + document.dispatchEvent(new CustomEvent("ag:open-search")); + }); + document.querySelector(".search-trigger")?.addEventListener("click", () => { + document.dispatchEvent(new CustomEvent("ag:open-search")); + }); + + /* ── ⌘K / Ctrl+K ───────────────────────────── */ + document.addEventListener("keydown", (e) => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { + e.preventDefault(); + document.dispatchEvent(new CustomEvent("ag:open-search")); + } + }); + + /* ── Keyboard hint (Mac vs PC) ──────────────── */ + const isMac = + typeof navigator !== "undefined" && + /Mac|iPhone|iPod|iPad/i.test(navigator.userAgent); + document.querySelectorAll(".search-kbd-hint").forEach((el) => { + el.textContent = isMac ? "⌘K" : "Ctrl K"; + }); +}); diff --git a/website/src/scripts/theme-toggle.js b/website/src/scripts/theme-toggle.js new file mode 100644 index 000000000..7d46f9e2d --- /dev/null +++ b/website/src/scripts/theme-toggle.js @@ -0,0 +1,56 @@ +/* theme-toggle.js + Handles all .theme-toggle-btn buttons on the page. + Moon = currently light mode (click to go dark) + Sun = currently dark mode (click to go light) +*/ + +const SVG_MOON = ` + +`; + +const SVG_SUN = ` + + +`; + +function getTheme() { + return document.documentElement.getAttribute("data-theme") || "light"; +} + +function updateAllIcons() { + const isDark = getTheme() === "dark"; + document.querySelectorAll(".theme-toggle-btn").forEach((btn) => { + btn.innerHTML = isDark ? SVG_SUN : SVG_MOON; + btn.setAttribute("aria-pressed", isDark ? "true" : "false"); + btn.setAttribute( + "aria-label", + isDark ? "Switch to light theme" : "Switch to dark theme", + ); + }); +} + +function toggleTheme() { + const next = getTheme() === "dark" ? "light" : "dark"; + document.documentElement.setAttribute("data-theme", next); + document.documentElement.style.colorScheme = next; + try { + localStorage.setItem("ag-theme", next); + } catch (e) {} + updateAllIcons(); + document.dispatchEvent(new CustomEvent("theme-changed")); +} + +function initThemeToggle() { + updateAllIcons(); + document + .querySelectorAll(".theme-toggle-btn") + .forEach((btn) => btn.addEventListener("click", toggleTheme)); +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initThemeToggle); +} else { + initThemeToggle(); +}