┌─ ASTRO ────────────────────────────────────────────────────────────────────┐
│ --- │
│ import Pre from "../Pre/Pre.astro"; │
│ import NavBar from "../NavBar/NavBar.astro"; │
│ import ThemeToggle from "../ThemeToggle/ThemeToggle.astro"; │
│ import { rule, ruleD } from "../../lib/ascii"; │
│ import { FRAME_W, MOBILE_FRAME_W } from "../../lib/config"; │
│ import { │
│ MASTHEAD_LABEL, │
│ MASTHEAD_SEARCH_WIDE, │
│ MASTHEAD_SEARCH_NARROW, │
│ MASTHEAD_TOGGLE_GAP, │
│ mastheadPad, │
│ } from "./TopHeader"; │
│ import "./TopHeader.scss"; │
│ │
│ const padW = mastheadPad(FRAME_W, MASTHEAD_SEARCH_WIDE); │
│ const padN = mastheadPad(MOBILE_FRAME_W, MASTHEAD_SEARCH_NARROW); │
│ const toggleGap = " ".repeat(MASTHEAD_TOGGLE_GAP); │
│ const PLACEHOLDER_WIDE = "_".repeat(20); │
│ const PLACEHOLDER_NARROW = "_".repeat(18); │
│ --- │
│ <div class="frame-wide top-header"> │
│ <div class="masthead"> │
│ <Pre><a href="/" class="masthead-home">{MASTHEAD_LABEL}</a>{padW}<span class=" │
│ masthead-search">[ / search the index <input class="masthead-search-input" type=" │
│ text" aria-label="Search notes" autocomplete="off" spellcheck="false" placeholder= │
│ {PLACEHOLDER_WIDE} maxlength="20" data-search-input data-cells="20" /> ]</span>{to │
│ ggleGap}<ThemeToggle /></Pre> │
│ <ul class="masthead-search-results" hidden data-search-results></ul> │
│ </div> │
│ <Pre>{rule(FRAME_W)}</Pre> │
│ <NavBar width={FRAME_W} /> │
│ <Pre>{ruleD(FRAME_W)}</Pre> │
│ </div> │
│ <div class="frame-narrow top-header"> │
│ <div class="masthead"> │
│ <Pre><a href="/" class="masthead-home">{MASTHEAD_LABEL}</a>{padN}<span class=" │
│ masthead-search">[ / search <input class="masthead-search-input" type="text" aria │
│ -label="Search notes" autocomplete="off" spellcheck="false" placeholder={PLACEHOLD │
│ ER_NARROW} maxlength="18" data-search-input data-cells="18" /> ]</span>{toggleGap} │
│ <ThemeToggle /></Pre> │
│ <ul class="masthead-search-results" hidden data-search-results></ul> │
│ </div> │
│ <Pre>{rule(MOBILE_FRAME_W)}</Pre> │
│ <NavBar width={MOBILE_FRAME_W} gap={1} lang={false} /> │
│ <Pre>{ruleD(MOBILE_FRAME_W)}</Pre> │
│ </div> │
│ │
│ <script> │
│ import { searchIndex, type SearchItem } from "../../lib/search"; │
│ │
│ let data: SearchItem[] | null = null; │
│ │
│ async function ensureLoaded() { │
│ if (data) return; │
│ const res = await fetch("/search.json"); │
│ data = await res.json(); │
│ } │
│ │
│ function getResultsList(input: HTMLInputElement) { │
│ return input.closest(".masthead")?.querySelector<HTMLUListElement>("[data-sear │
│ ch-results]") ?? null; │
│ } │
│ │
│ function activeIndexFor(list: HTMLUListElement) { │
│ const items = Array.from(list.querySelectorAll("li")); │
│ return items.findIndex((el) => el.classList.contains("active")); │
│ } │
│ │
│ function setActive(list: HTMLUListElement, idx: number) { │
│ const items = Array.from(list.querySelectorAll<HTMLLIElement>("li")); │
│ items.forEach((el, i) => el.classList.toggle("active", i === idx)); │
│ if (idx >= 0 && items[idx]) items[idx].scrollIntoView({ block: "nearest" }); │
│ } │
│ │
│ function close(list: HTMLUListElement) { │
│ list.hidden = true; │
│ list.innerHTML = ""; │
│ } │
│ │
│ function render(list: HTMLUListElement, hits: SearchItem[]) { │
│ if (!hits.length) { close(list); return; } │
│ list.hidden = false; │
│ list.innerHTML = hits │
│ .map((item, i) => { │
│ const safe = (s: string) => s.replace(/&/g, "&").replace(/</g, "<") │
│ .replace(/>/g, ">"); │
│ return `<li${i === 0 ? ' class="active"' : ""}><a href="${safe(item.url)}" │
│ >${safe(item.note)} - ${safe(item.title)}</a></li>`; │
│ }) │
│ .join(""); │
│ } │
│ │
│ async function onInput(e: Event) { │
│ const input = (e.target as HTMLElement).closest<HTMLInputElement>("[data-searc │
│ h-input]"); │
│ if (!input) return; │
│ const list = getResultsList(input); │
│ if (!list) return; │
│ const q = input.value.trim(); │
│ if (!q) { close(list); return; } │
│ await ensureLoaded(); │
│ render(list, searchIndex(data!, q)); │
│ } │
│ │
│ function onKeydown(e: KeyboardEvent) { │
│ const target = e.target as HTMLElement; │
│ const input = target.closest?.<HTMLInputElement>("[data-search-input]"); │
│ if (!input) { │
│ // Global "/" focuses the visible search input. │
│ if (e.key === "/" && !["INPUT", "TEXTAREA"].includes(target.tagName)) { │
│ const visible = Array.from(document.querySelectorAll<HTMLInputElement>("[d │
│ ata-search-input]")) │
│ .find((el) => el.offsetParent !== null); │
│ if (visible) { │
│ e.preventDefault(); │
│ visible.focus(); │
│ visible.select(); │
│ } │
│ } │
│ return; │
│ } │
│ const list = getResultsList(input); │
│ if (!list || list.hidden) return; │
│ const items = Array.from(list.querySelectorAll<HTMLLIElement>("li")); │
│ const idx = activeIndexFor(list); │
│ if (e.key === "ArrowDown") { │
│ e.preventDefault(); │
│ setActive(list, Math.min(items.length - 1, idx + 1)); │
│ } else if (e.key === "ArrowUp") { │
│ e.preventDefault(); │
│ setActive(list, Math.max(0, idx - 1)); │
│ } else if (e.key === "Enter") { │
│ const link = items[Math.max(0, idx)]?.querySelector("a"); │
│ if (link) { e.preventDefault(); window.location.href = link.href; } │
│ } else if (e.key === "Escape") { │
│ e.preventDefault(); │
│ input.value = ""; │
│ close(list); │
│ input.blur(); │
│ } │
│ } │
│ │
│ function onDocClick(e: MouseEvent) { │
│ const target = e.target as HTMLElement; │
│ if (target.closest(".masthead")) return; │
│ document.querySelectorAll<HTMLUListElement>("[data-search-results]").forEach(c │
│ lose); │
│ } │
│ │
│ document.addEventListener("input", onInput); │
│ document.addEventListener("keydown", onKeydown); │
│ document.addEventListener("click", onDocClick); │
│ │
│ // Tag badges link to /?q=<tag>; prefill the search and show results. │
│ const q = new URLSearchParams(window.location.search).get("q"); │
│ if (q) { │
│ const visible = Array.from(document.querySelectorAll<HTMLInputElement>("[data- │
│ search-input]")) │
│ .find((el) => el.offsetParent !== null); │
│ if (visible) { │
│ visible.value = q.slice(0, Number(visible.dataset.cells) || 20); │
│ visible.dispatchEvent(new Event("input", { bubbles: true })); │
│ visible.focus(); │
│ } │
│ } │
│ </script> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ASTRO ──────────────────────────────┐
│ --- │
│ import Pre from "../Pre/Pre.astro"; │
│ import NavBar from "../NavBar/NavBar.astro"; │
│ import ThemeToggle from "../ThemeToggle/Them │
│ eToggle.astro"; │
│ import { rule, ruleD } from "../../lib/ascii │
│ "; │
│ import { FRAME_W, MOBILE_FRAME_W } from "../ │
│ ../lib/config"; │
│ import { │
│ MASTHEAD_LABEL, │
│ MASTHEAD_SEARCH_WIDE, │
│ MASTHEAD_SEARCH_NARROW, │
│ MASTHEAD_TOGGLE_GAP, │
│ mastheadPad, │
│ } from "./TopHeader"; │
│ import "./TopHeader.scss"; │
│ │
│ const padW = mastheadPad(FRAME_W, MASTHEAD_S │
│ EARCH_WIDE); │
│ const padN = mastheadPad(MOBILE_FRAME_W, MAS │
│ THEAD_SEARCH_NARROW); │
│ const toggleGap = " ".repeat(MASTHEAD_TOGGLE │
│ _GAP); │
│ const PLACEHOLDER_WIDE = "_".repeat(20); │
│ const PLACEHOLDER_NARROW = "_".repeat(18); │
│ --- │
│ <div class="frame-wide top-header"> │
│ <div class="masthead"> │
│ <Pre><a href="/" class="masthead-home">{ │
│ MASTHEAD_LABEL}</a>{padW}<span class="masthe │
│ ad-search">[ / search the index <input clas │
│ s="masthead-search-input" type="text" aria-l │
│ abel="Search notes" autocomplete="off" spell │
│ check="false" placeholder={PLACEHOLDER_WIDE} │
│ maxlength="20" data-search-input data-cells │
│ ="20" /> ]</span>{toggleGap}<ThemeToggle />< │
│ /Pre> │
│ <ul class="masthead-search-results" hidd │
│ en data-search-results></ul> │
│ </div> │
│ <Pre>{rule(FRAME_W)}</Pre> │
│ <NavBar width={FRAME_W} /> │
│ <Pre>{ruleD(FRAME_W)}</Pre> │
│ </div> │
│ <div class="frame-narrow top-header"> │
│ <div class="masthead"> │
│ <Pre><a href="/" class="masthead-home">{ │
│ MASTHEAD_LABEL}</a>{padN}<span class="masthe │
│ ad-search">[ / search <input class="masthea │
│ d-search-input" type="text" aria-label="Sear │
│ ch notes" autocomplete="off" spellcheck="fal │
│ se" placeholder={PLACEHOLDER_NARROW} maxleng │
│ th="18" data-search-input data-cells="18" /> │
│ ]</span>{toggleGap}<ThemeToggle /></Pre> │
│ <ul class="masthead-search-results" hidd │
│ en data-search-results></ul> │
│ </div> │
│ <Pre>{rule(MOBILE_FRAME_W)}</Pre> │
│ <NavBar width={MOBILE_FRAME_W} gap={1} lan │
│ g={false} /> │
│ <Pre>{ruleD(MOBILE_FRAME_W)}</Pre> │
│ </div> │
│ │
│ <script> │
│ import { searchIndex, type SearchItem } fr │
│ om "../../lib/search"; │
│ │
│ let data: SearchItem[] | null = null; │
│ │
│ async function ensureLoaded() { │
│ if (data) return; │
│ const res = await fetch("/search.json"); │
│ data = await res.json(); │
│ } │
│ │
│ function getResultsList(input: HTMLInputEl │
│ ement) { │
│ return input.closest(".masthead")?.query │
│ Selector<HTMLUListElement>("[data-search-res │
│ ults]") ?? null; │
│ } │
│ │
│ function activeIndexFor(list: HTMLUListEle │
│ ment) { │
│ const items = Array.from(list.querySelec │
│ torAll("li")); │
│ return items.findIndex((el) => el.classL │
│ ist.contains("active")); │
│ } │
│ │
│ function setActive(list: HTMLUListElement, │
│ idx: number) { │
│ const items = Array.from(list.querySelec │
│ torAll<HTMLLIElement>("li")); │
│ items.forEach((el, i) => el.classList.to │
│ ggle("active", i === idx)); │
│ if (idx >= 0 && items[idx]) items[idx].s │
│ crollIntoView({ block: "nearest" }); │
│ } │
│ │
│ function close(list: HTMLUListElement) { │
│ list.hidden = true; │
│ list.innerHTML = ""; │
│ } │
│ │
│ function render(list: HTMLUListElement, hi │
│ ts: SearchItem[]) { │
│ if (!hits.length) { close(list); return; │
│ } │
│ list.hidden = false; │
│ list.innerHTML = hits │
│ .map((item, i) => { │
│ const safe = (s: string) => s.replac │
│ e(/&/g, "&").replace(/</g, "<").repla │
│ ce(/>/g, ">"); │
│ return `<li${i === 0 ? ' class="acti │
│ ve"' : ""}><a href="${safe(item.url)}">${saf │
│ e(item.note)} - ${safe(item.title)}</a></li> │
│ `; │
│ }) │
│ .join(""); │
│ } │
│ │
│ async function onInput(e: Event) { │
│ const input = (e.target as HTMLElement). │
│ closest<HTMLInputElement>("[data-search-inpu │
│ t]"); │
│ if (!input) return; │
│ const list = getResultsList(input); │
│ if (!list) return; │
│ const q = input.value.trim(); │
│ if (!q) { close(list); return; } │
│ await ensureLoaded(); │
│ render(list, searchIndex(data!, q)); │
│ } │
│ │
│ function onKeydown(e: KeyboardEvent) { │
│ const target = e.target as HTMLElement; │
│ const input = target.closest?.<HTMLInput │
│ Element>("[data-search-input]"); │
│ if (!input) { │
│ // Global "/" focuses the visible sear │
│ ch input. │
│ if (e.key === "/" && !["INPUT", "TEXTA │
│ REA"].includes(target.tagName)) { │
│ const visible = Array.from(document. │
│ querySelectorAll<HTMLInputElement>("[data-se │
│ arch-input]")) │
│ .find((el) => el.offsetParent !== │
│ null); │
│ if (visible) { │
│ e.preventDefault(); │
│ visible.focus(); │
│ visible.select(); │
│ } │
│ } │
│ return; │
│ } │
│ const list = getResultsList(input); │
│ if (!list || list.hidden) return; │
│ const items = Array.from(list.querySelec │
│ torAll<HTMLLIElement>("li")); │
│ const idx = activeIndexFor(list); │
│ if (e.key === "ArrowDown") { │
│ e.preventDefault(); │
│ setActive(list, Math.min(items.length │
│ - 1, idx + 1)); │
│ } else if (e.key === "ArrowUp") { │
│ e.preventDefault(); │
│ setActive(list, Math.max(0, idx - 1)); │
│ } else if (e.key === "Enter") { │
│ const link = items[Math.max(0, idx)]?. │
│ querySelector("a"); │
│ if (link) { e.preventDefault(); window │
│ .location.href = link.href; } │
│ } else if (e.key === "Escape") { │
│ e.preventDefault(); │
│ input.value = ""; │
│ close(list); │
│ input.blur(); │
│ } │
│ } │
│ │
│ function onDocClick(e: MouseEvent) { │
│ const target = e.target as HTMLElement; │
│ if (target.closest(".masthead")) return; │
│ document.querySelectorAll<HTMLUListEleme │
│ nt>("[data-search-results]").forEach(close); │
│ } │
│ │
│ document.addEventListener("input", onInput │
│ ); │
│ document.addEventListener("keydown", onKey │
│ down); │
│ document.addEventListener("click", onDocCl │
│ ick); │
│ │
│ // Tag badges link to /?q=<tag>; prefill t │
│ he search and show results. │
│ const q = new URLSearchParams(window.locat │
│ ion.search).get("q"); │
│ if (q) { │
│ const visible = Array.from(document.quer │
│ ySelectorAll<HTMLInputElement>("[data-search │
│ -input]")) │
│ .find((el) => el.offsetParent !== null │
│ ); │
│ if (visible) { │
│ visible.value = q.slice(0, Number(visi │
│ ble.dataset.cells) || 20); │
│ visible.dispatchEvent(new Event("input │
│ ", { bubbles: true })); │
│ visible.focus(); │
│ } │
│ } │
│ </script> │
└──────────────────────────────────────────────┘